diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-06-05 00:37:02 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-06-11 01:13:31 +1000 |
| commit | 81a0570c1b64891f286ee86d34d6f77090d525e3 (patch) | |
| tree | 1c98593ddec2eb64029f4fabe024cb5323050bb1 | |
| parent | 03055cb1b262a2b9a0516ad3aa523e503edeb36b (diff) | |
Deleted server-specific files
49 files changed, 0 insertions, 4062 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index e3d2b85..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-ef": { - "version": "8.0.23", - "commands": [ - "dotnet-ef" - ], - "rollForward": false - } - } -}
\ No newline at end of file diff --git a/App.razor b/App.razor deleted file mode 100644 index b4e47c9..0000000 --- a/App.razor +++ /dev/null @@ -1,19 +0,0 @@ -<CascadingAuthenticationState> - <Router AppAssembly="@typeof(App).Assembly"> - <Found Context="routeData"> - <AuthorizeRouteView - RouteData="@routeData" - DefaultLayout="@typeof(MainLayout)"> - <NotAuthorized> - <RedirectLogin/> - </NotAuthorized> - </AuthorizeRouteView> - </Found> - <NotFound> - <PageTitle>Not found</PageTitle> - <LayoutView Layout="@typeof(MainLayout)"> - <p role="alert">Sorry, there's nothing at this address.</p> - </LayoutView> - </NotFound> - </Router> -</CascadingAuthenticationState> diff --git a/Controllers/ApiFeedController.cs b/Controllers/ApiFeedController.cs deleted file mode 100644 index 068cc17..0000000 --- a/Controllers/ApiFeedController.cs +++ /dev/null @@ -1,25 +0,0 @@ -using HyperBooru.ApiModels; -using HyperBooru.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Controllers; - -[ApiController] -[Authorize] -[Route("/api/feed")] -public class ApiFeedController : Controller { - private IFeedService feedService; - - public ApiFeedController(IDbContextFactory<HBContext> dbFactory, IFeedService feedService) => - this.feedService = feedService; - - [HttpPost] - public IActionResult FetchChunkAsync([FromBody] FeedRequest feedRequest) { - if(feedRequest.Count > 1000) - throw new ApiModels.ArgumentException("Total number of requested items exceeds maximum"); - - return Ok(feedService.LoadChunk(feedRequest).Select(m => m.Guid).ToArray()); - } -} diff --git a/Controllers/ApiMediaController.cs b/Controllers/ApiMediaController.cs deleted file mode 100644 index bb6c81e..0000000 --- a/Controllers/ApiMediaController.cs +++ /dev/null @@ -1,227 +0,0 @@ -using HyperBooru.ApiModels; -using HyperBooru.Services; -using HyperBooru.Util; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System.Text.Json; - -namespace HyperBooru.Controllers; - -[ApiController] -[Authorize] -[Route("/api/media")] -public class ApiMediaController : Controller { - private IDbContextFactory<HBContext> dbFactory; - private IMediaService mediaService; - - public ApiMediaController(IDbContextFactory<HBContext> dbFactory, IMediaService mediaService) { - this.dbFactory = dbFactory; - this.mediaService = mediaService; - } - - [HttpGet("{mediaId}")] - public async Task<IActionResult> Get([FromRoute] Guid mediaId) { - using var db = dbFactory.CreateDbContext(); - - var media = await db.Media.FirstOrDefaultAsync(m => m.Guid == mediaId); - - if(media is null) - throw new ObjectNotFoundException([ mediaId ]); - - return Ok((ApiModels.Media) media); - } - - [HttpGet("{mediaId}/files")] - public async Task<IActionResult> GetUploadedFiles([FromRoute] Guid mediaId) { - using var db = dbFactory.CreateDbContext(); - - var media = await db.Media - .Include(m => m.UploadedFiles) - .FirstOrDefaultAsync(m => m.Guid == mediaId); - - if(media is null) - throw new ObjectNotFoundException([ mediaId ]); - - return Ok(media.UploadedFiles.Select(uf => (ApiModels.UploadedFile) uf).ToArray()); - } - - [HttpPatch] - public async Task<IActionResult> UpdateMedia([FromBody] ApiModels.Media updatedMedia) { - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var media = await db.Media.FirstOrDefaultAsync(m => m.Guid == updatedMedia.MediaId); - if(media is null) - throw new ObjectNotFoundException([ updatedMedia.MediaId ]); - - media.ShortDescription = updatedMedia.ShortDescription?.NullIfEmpty(); - media.LongDescription = updatedMedia.LongDescription?.NullIfEmpty(); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(); - } - - [HttpPost] - public IActionResult Upload() { - if(Request.Form.Files.Count == 0) - throw new ApiModels.ArgumentException("No files"); - if(Request.Form.Files.Count > 1) - throw new ApiModels.ArgumentException("More than one file supplied"); - - var metadataString = Request.Form.Files - .First() - .Headers["X-HyperBooru-Metadata"] - .ElementAtOrDefault(0); - - MediaUploadRequest? metadata = metadataString is null ? null : - JsonSerializer.Deserialize<MediaUploadRequest>(metadataString); - - var formFile = Request.Form.Files.First(); - - var media = mediaService.Create( - formFile.OpenReadStream(), - formFile.FileName, - metadata?.Checksum, - metadata?.LastAccessTime, - metadata?.LastWriteTime, - metadata?.CreateTime, - metadata?.Path.NullIfEmpty(), - metadata?.PathType, - metadata?.Tags); - - return Ok((ApiModels.Media) media); - } - - [HttpDelete("{mediaId}")] - public void Delete([FromRoute] Guid mediaId) => - mediaService.Delete(mediaId); - - [HttpGet("{mediaId}/tags")] - public async Task<IActionResult> GetMediaTagsAsync([FromRoute] Guid mediaId) { - using var db = dbFactory.CreateDbContext(); - - var media = await db.Media - .Include(m => m.Tags) - .ThenInclude(t => t.TagDefinition) - .ThenInclude(td => td.ImplicitTags) - .FirstOrDefaultAsync(m => m.Guid == mediaId); - if(media is null) - throw new ObjectNotFoundException([ mediaId ]); - - return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); - } - - [HttpPatch("{mediaId}/tags")] - public async Task<IActionResult> AddTagsToMediaAsync( - [FromRoute] Guid mediaId, - [FromBody] Guid[] tagIds) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var missing = new List<Guid>(); - - var media = await db.Media - .Include(m => m.Tags) - .ThenInclude(t => t.TagDefinition) - .ThenInclude(td => td.ImplicitTags) - .FirstOrDefaultAsync(m => m.Guid == mediaId); - if(media is null) - missing.Add(mediaId); - - tagIds = tagIds.Distinct().ToArray(); - - var tags = await db.TagDefinitions - .Where(td => tagIds.Contains(td.Guid)) - .ToArrayAsync(); - - missing.AddRange(tagIds.Except(tags.Select(td => td.Guid))); - if(missing.Any()) - throw new ObjectNotFoundException(missing); - - media.Tags.AddRange(tags - .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid)) - .Select(td => new Tag() { TagDefinition = td })); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); - } - - [HttpPut("{mediaId}/tags")] - public async Task<IActionResult> ReplaceMediaTagsAsync( - [FromRoute] Guid mediaId, - [FromBody] Guid[] tagIds) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var missing = new List<Guid>(); - - var media = await db.Media - .Include(m => m.Tags) - .ThenInclude(t => t.TagDefinition) - .ThenInclude(td => td.ImplicitTags) - .FirstOrDefaultAsync(m => m.Guid == mediaId); - if(media is null) - missing.Add(mediaId); - - tagIds = tagIds.Distinct().Order().ToArray(); - var tags = await db.TagDefinitions - .Where(td => tagIds.Contains(td.Guid)) - .ToArrayAsync(); - - missing.AddRange(tagIds.Except(tags.Select(td => td.Guid))); - if(missing.Any()) - throw new ObjectNotFoundException(missing); - - media.Tags.AddRange(tags - .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid)) - .Select(td => new Tag() { TagDefinition = td })); - - db.Tags.RemoveRange( - media.Tags.Where(t => !tagIds.Contains(t.TagDefinition.Guid))); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); - } - - [HttpPatch("{mediaId}/tags/delete")] - public async Task<IActionResult> DeleteTagsFromMediaAsync( - [FromRoute] Guid mediaId, - [FromBody] Guid[] tagIds) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var missing = new List<Guid>(); - - var media = await db.Media - .Include(m => m.Tags) - .ThenInclude(t => t.TagDefinition) - .ThenInclude(td => td.ImplicitTags) - .FirstOrDefaultAsync(m => m.Guid == mediaId); - if(media is null) - missing.Add(mediaId); - - tagIds = tagIds.Distinct().Order().ToArray(); - - missing.AddRange(tagIds.Except(media.Tags.Select(t => t.TagDefinition.Guid))); - if(missing.Any()) - throw new ObjectNotFoundException(missing); - - db.Tags.RemoveRange( - media.Tags.Where(t => tagIds.Contains(t.TagDefinition.Guid))); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(media.Tags.Select(t => (ApiModels.TagDefinition) t.TagDefinition).ToArray()); - } -} diff --git a/Controllers/ApiStatisticsController.cs b/Controllers/ApiStatisticsController.cs deleted file mode 100644 index 3acd1d5..0000000 --- a/Controllers/ApiStatisticsController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using HyperBooru.ApiModels; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Controllers; - -[ApiController] -[Route("/api/stats")] -public class ApiStatisticsController : Controller { - private IDbContextFactory<HBContext> dbFactory; - - public ApiStatisticsController(IDbContextFactory<HBContext> dbFactory) => - this.dbFactory = dbFactory; - - [HttpGet("ingest")] - public async Task<IActionResult> GetIngestStatistics() { - using var db = dbFactory.CreateDbContext(); - - return Ok(new IngestStatistics() { - TotalMediaCount = db.Media.Count(), - UntaggedMediaCount = db.Media - .Where(m => m.Tags.Any(t => t.TagDefinition.ObjectId == (int) HBObjectId.IngestTag)) - .Count(), - }); - } -} diff --git a/Controllers/ApiTagController.cs b/Controllers/ApiTagController.cs deleted file mode 100644 index d1e49ee..0000000 --- a/Controllers/ApiTagController.cs +++ /dev/null @@ -1,256 +0,0 @@ -using HyperBooru.ApiModels; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Controllers; - -[ApiController] -[Authorize] -[Route("/api/tag")] -public class ApiTagController : Controller { - private IDbContextFactory<HBContext> dbFactory; - - public ApiTagController(IDbContextFactory<HBContext> dbFactory) => - this.dbFactory = dbFactory; - - [HttpGet("definition")] - public async Task<IActionResult> GetAllTagDefinitionsAsync() { - using var db = dbFactory.CreateDbContext(); - - var definitions = await db.TagDefinitions - .Include(td => td.ImplicitTags) - .Select(td => (ApiModels.TagDefinition)td) - .ToArrayAsync(); - - return Ok(definitions); - } - - [HttpGet("definition/{tagDefinitionId}")] - public async Task<IActionResult> GetTagDefinitionAsync([FromRoute] Guid tagDefinitionId) { - using var db = dbFactory.CreateDbContext(); - - var tagDefinition = await db.TagDefinitions - .Include(td => td.ImplicitTags) - .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - - if(tagDefinition is null) - throw new ObjectNotFoundException([ tagDefinitionId ]); - - return Ok(tagDefinition); - } - - [HttpPost("definition")] - public async Task<IActionResult> CreateTagDefinitionAsync([FromBody] TagCreateRequest request) { - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var nameExists = db.TagDefinitions.Any(td => td.Name == request.Name); - var aliasExists = - request.Alias is not null && db.TagDefinitions.Any(td => td.Alias == request.Alias); - - if(nameExists || aliasExists) - throw new TagDuplicateException(nameExists, aliasExists); - - List<TagDefinition> implicitTags = new(); - if(request.ImplicitTags is not null) { - implicitTags = await db.TagDefinitions - .Where(td => request.ImplicitTags.Distinct().Contains(td.Guid)) - .ToListAsync(); - } - - var tagDefinition = new TagDefinition { - Source = TagSource.UserTag, - Namespace = request.Namespace, - Name = request.Name, - Alias = request.Alias, - ImplicitTags = implicitTags - }; - - db.TagDefinitions.Add(tagDefinition); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok((ApiModels.TagDefinition) tagDefinition); - } - - [HttpDelete("definition/{tagDefinitionId}")] - public async Task<IActionResult> DeleteTagDefinitionAsync([FromRoute] Guid tagDefinitionId) { - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var tagDefinition = await db.TagDefinitions - .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - - if(tagDefinition is null) - throw new ObjectNotFoundException([ tagDefinitionId ]); - - if(tagDefinition.ObjectId < 0) - throw new ApiModels.ArgumentException("Cannot delete built-in tag definition"); - - db.TagDefinitions.Remove(tagDefinition); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(); - } - - [HttpPatch("definition/{tagDefinitionId}")] - public async Task<IActionResult> UpdateTagDefinitionAsync( - [FromRoute] Guid tagDefinitionId, - [FromBody] TagUpdateRequest request) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var tagDefinition = await db.TagDefinitions - .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - - if(tagDefinition is null) - throw new ObjectNotFoundException([ tagDefinitionId ]); - - if(tagDefinition.ObjectId < 0) - throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); - - var nameExists = - request.Name is not null && db.TagDefinitions.Any(td => td.Name == request.Name); - var aliasExists = - request.Alias is not null && db.TagDefinitions.Any(td => td.Alias == request.Alias); - - if(nameExists || aliasExists) - throw new TagDuplicateException(nameExists, aliasExists); - - tagDefinition.Namespace = request.Namespace ?? tagDefinition.Namespace; - tagDefinition.Name = request.Name ?? tagDefinition.Name; - tagDefinition.Alias = request.Alias ?? tagDefinition.Alias; - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok((ApiModels.TagDefinition) tagDefinition); - } - - [HttpPatch("definition/{tagDefinitionId}/implicit")] - public async Task<IActionResult> AddImplicitTagsAsync( - [FromRoute] Guid tagDefinitionId, - [FromBody] Guid[] implicitTagIds) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var missing = new List<Guid>(); - - var tagDefinition = await db.TagDefinitions - .Include(td => td.ImplicitTags) - .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - - if(tagDefinition is null) - missing.Add(tagDefinitionId); - - implicitTagIds = implicitTagIds.Distinct().ToArray(); - - var implicitTags = db.TagDefinitions - .Where(td => implicitTagIds.Contains(td.Guid)) - .ToArray(); - - missing.AddRange(implicitTagIds.Except(implicitTags.Select(td => td.Guid))); - if(missing.Any()) - throw new ObjectNotFoundException(missing); - - if(tagDefinition!.ObjectId < 0) - throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); - - tagDefinition.ImplicitTags.AddRange( - implicitTags.ExceptBy(tagDefinition.ImplicitTags.Select(td => td.Guid), td => td.Guid)); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(); - } - - [HttpPut("definition/{tagDefinitionId}/implicit")] - public async Task<IActionResult> ReplaceImplicitTagsAsync( - [FromRoute] Guid tagDefinitionId, - [FromBody] Guid[] implicitTagIds) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var missing = new List<Guid>(); - - var tagDefinition = await db.TagDefinitions - .Include(td => td.ImplicitTags) - .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - - if(tagDefinition is null) - missing.Add(tagDefinitionId); - - implicitTagIds = implicitTagIds.Distinct().ToArray(); - - var implicitTags = db.TagDefinitions - .Where(td => implicitTagIds.Contains(td.Guid)) - .ToArray(); - - missing.AddRange(implicitTagIds.Except(implicitTags.Select(td => td.Guid))); - if(missing.Any()) - throw new ObjectNotFoundException(missing); - - if(tagDefinition!.ObjectId < 0) - throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); - - tagDefinition.ImplicitTags.AddRange( - implicitTags.ExceptBy(tagDefinition.ImplicitTags.Select(td => td.Guid), td => td.Guid)); - - var toRemove = tagDefinition.ImplicitTags - .Where(td => !implicitTags.Select(td => td.Guid).Contains(td.Guid)) - .ToArray(); - - foreach(var td in toRemove) - tagDefinition.ImplicitTags.Remove(td); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(); - } - - [HttpPost("definition/{tagDefinitionId}/implicit/delete")] - public async Task<IActionResult> DeleteImplicitTagAsync( - [FromRoute] Guid tagDefinitionId, - [FromBody] Guid[] implicitTagIds) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = await db.Database.BeginTransactionAsync(); - - var tagDefinition = await db.TagDefinitions - .Include(td => td.ImplicitTags) - .FirstOrDefaultAsync(td => td.Guid == tagDefinitionId); - - if(tagDefinition is null) - throw new ObjectNotFoundException([ tagDefinitionId ]); - - if(tagDefinition.ObjectId < 0) - throw new ApiModels.ArgumentException("Cannot update built-in tag definition"); - - implicitTagIds = implicitTagIds.Distinct().ToArray(); - - var missingTags = implicitTagIds.Except(tagDefinition.ImplicitTags.Select(td => td.Guid)); - if(missingTags.Any()) - throw new ObjectNotFoundException(missingTags); - - var toRemove = tagDefinition.ImplicitTags - .Where(td => !implicitTagIds.Contains(td.Guid)) - .ToArray(); - - foreach(var td in toRemove) - tagDefinition.ImplicitTags.Remove(td); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok(); - } -} diff --git a/Controllers/ApiUserController.cs b/Controllers/ApiUserController.cs deleted file mode 100644 index 3230218..0000000 --- a/Controllers/ApiUserController.cs +++ /dev/null @@ -1,113 +0,0 @@ -using HyperBooru.ApiModels; -using HyperBooru.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Controllers; - -[ApiController] -[Authorize] -[Route("/api/user")] -public class ApiUserController : Controller { - private IDbContextFactory<HBContext> dbFactory; - - public ApiUserController(IDbContextFactory<HBContext> dbFactory) => - this.dbFactory = dbFactory; - - [HttpGet] - public async Task<IActionResult> GetAllUsersAsync() { - using var db = dbFactory.CreateDbContext(); - - return Ok(await db.Users - .Select(u => (ApiModels.User) u) - .ToArrayAsync()); - } - - [HttpGet("{userId}")] - public async Task<IActionResult> GetUserAsync([FromRoute] Guid userId) { - using var db = dbFactory.CreateDbContext(); - - var user = await db.Users - .FirstOrDefaultAsync(u => u.Guid == userId); - - if(user is null) - throw new ObjectNotFoundException([ userId ]); - - return Ok((ApiModels.User) user); - } - - [HttpPost] - public async Task<IActionResult> CreateUserAsync([FromBody] ApiModels.UserCreateRequest request) { - using var db = dbFactory.CreateDbContext(); - - using var transaction = await db.Database.BeginTransactionAsync(); - - if(await db.Users.AnyAsync(u => u.Username == request.Username)) - throw new ApiModels.ArgumentException("Username already exists"); - - var user = new User() { - Username = request.Username, - PasswordHash = UserService.HashPassword(request.Password) - }; - - db.Users.Add(user); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok((ApiModels.User) user); - } - - [HttpPatch("{userId}")] - public async Task<IActionResult> UpdateUserAsync( - [FromRoute] Guid userId, - [FromBody] ApiModels.UserUpdateRequest request) { - - using var db = dbFactory.CreateDbContext(); - - using var transaction = await db.Database.BeginTransactionAsync(); - - var user = await db.Users.FirstOrDefaultAsync(u => u.Guid == userId); - if(user is null) - throw new ObjectNotFoundException([ userId ]); - - if(request.Username is not null) { - if(string.IsNullOrWhiteSpace(request.Username)) - throw new ApiModels.ArgumentException("Username cannot be empty"); - user.Username = request.Username; - } - - if(request.Password is not null) { - if(string.IsNullOrWhiteSpace(request.Password)) - throw new ApiModels.ArgumentException("Password cannot be empty"); - user.PasswordHash = UserService.HashPassword(request.Password); - } - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok((ApiModels.User) user); - } - - [HttpDelete("{userId}")] - public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid userId) { - if(userId == HBContext.AdminUser) - throw new ApiModels.ArgumentException("Cannot delete the admin user"); - - using var db = dbFactory.CreateDbContext(); - - using var transaction = await db.Database.BeginTransactionAsync(); - - var user = await db.Users.FirstOrDefaultAsync(u => u.Guid == userId); - if(user is null) - throw new ObjectNotFoundException([ userId ]); - - db.Users.Remove(user); - - await db.SaveChangesAsync(); - await transaction.CommitAsync(); - - return Ok((ApiModels.User) user); - } -} diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs deleted file mode 100644 index c93f0d5..0000000 --- a/Controllers/LoginController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using HyperBooru.Services; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; - -namespace HyperBooru.Controllers; - -[ApiController] -[Route("/")] -public class LoginController : Controller { - private IHttpContextAccessor httpContextAccessor; - - public LoginController(IHttpContextAccessor httpContextAccessor) => - this.httpContextAccessor = httpContextAccessor; - - [HttpPost("Login")] - public async Task<IActionResult> Login( - [FromForm] string username, - [FromForm] string password, - HBContext db) { - - var user = db.Users.FirstOrDefault(u => u.Username == username); - if(user is null) - return StatusCode(403); - - var hash = UserService.HashPassword(password); - if(hash != user.PasswordHash) - return StatusCode(403); - - var claims = new Claim[] { - new Claim(ClaimTypes.Name, user.Username), - new Claim("ObjectId", user.ObjectId.ToString()) - }; - - var claimsIdentity = new ClaimsIdentity( - claims, - CookieAuthenticationDefaults.AuthenticationScheme); - - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - await httpContextAccessor.HttpContext!.SignInAsync(claimsPrincipal); - return Ok(); - } - - [HttpPost("Logout")] - public async Task Logout() => - await httpContextAccessor.HttpContext!.SignOutAsync(); -} diff --git a/Controllers/MediaController.cs b/Controllers/MediaController.cs deleted file mode 100644 index 248765a..0000000 --- a/Controllers/MediaController.cs +++ /dev/null @@ -1,145 +0,0 @@ -using HyperBooru.ApiModels; -using HyperBooru.Services; -using HyperBooru.Util; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Controllers; - -[ApiController] -[Authorize] -[Route("/media")] -public class MediaController : Controller { - private IHttpContextAccessor httpContextAccessor; - private IMediaService mediaService; - private IConfigService config; - private HBContext db; - - private readonly string[] FormatPriority = [ - "image/webp", - "image/png" - ]; - - public MediaController( - IHttpContextAccessor httpContextAccessor, - IMediaService mediaService, - IConfigService config, - HBContext db) { - - this.httpContextAccessor = httpContextAccessor; - this.mediaService = mediaService; - this.config = config; - this.db = db; - } - - [HttpGet("{mediaId}")] - public IActionResult Fetch([FromRoute] Guid mediaId) { - var media = db.Media - .Include(m => m.CurrentUploadedFile) - .First(m => m.Guid == mediaId); - if(media is null) - throw new ObjectNotFoundException([ mediaId ]); - - // Check if the requested media item is a HEIC image and if it is, convert it - // otherwise, return the original file content, unaltered - if(media.CurrentUploadedFile!.MimeType == "image/heic") { - // If the media needs to be converted, check the HTTP request for allowed - // media formats, and convert to the best available format or WebP otherwise - var allowedTypes = httpContextAccessor - .HttpContext? - .Request - .GetTypedHeaders().Accept.Select(h => h.MediaType.ToString()) ?? Array.Empty<string>(); - - var format = FormatPriority.FirstOrDefault(f => allowedTypes.Contains(f)) ?? "image/webp"; - - var fs = mediaService.GetConverted(media, format); - - return new FileStreamResult(fs, format); - } else { - var fs = System.IO.File.OpenRead(mediaService.GetPath(media)); - return new FileStreamResult(fs, media.CurrentUploadedFile!.MimeType); - } - } - - [HttpGet("thumb/{mediaId}")] - public IActionResult Thumbnail( - [FromRoute] Guid mediaId, - [FromQuery(Name = "w")] int? width, - [FromQuery(Name = "h")] int? height) { - - var thumb = mediaService.GetThumbnail(mediaId, width, height); - return new FileStreamResult(thumb, "image/jpeg"); - } - - [HttpDelete("{mediaId}")] - public void Delete([FromRoute] Guid mediaId) { - mediaService.Delete(mediaId); - } - - [HttpPost] - public IActionResult Upload() { - if(Request.Form.Files.Count == 0) - throw new ApiModels.ArgumentException("No files"); - - Media media = new(); - - foreach(var formFile in Request.Form.Files) { - // Parse timestamps from headers - DateTime? lastAccessTime = - formFile.Headers["X-HyperBooru-LastAccessTime"] - .ElementAtOrDefault(0)? - .TryParseDateTimeUtc(); - DateTime? lastWriteTime = - formFile.Headers["X-HyperBooru-LastWriteTime"] - .ElementAtOrDefault(0)? - .TryParseDateTimeUtc(); - DateTime? createTime = - formFile.Headers["X-HyperBooru-CreateTime"] - .ElementAtOrDefault(0)? - .TryParseDateTimeUtc(); - - // Parse original path from headers - string? path = - formFile.Headers["X-HyperBooru-Path"] - .ElementAtOrDefault(0); - - object? pathType = null; - string? pathTypeString = - formFile.Headers["X-HyperBooru-PathType"] - .ElementAtOrDefault(0); - Enum.TryParse(typeof(PathType), pathTypeString, true, out pathType); - - // Parse tag IDs from headers - Guid[]? tagIds = formFile.Headers["X-HyperBooru-Tags"] - .ElementAtOrDefault(0)? - .Split(',') - .Select(t => Guid.Parse(t)) - .ToArray(); - - media = mediaService.Create( - formFile.OpenReadStream(), - formFile.FileName, - formFile.Headers["X-HyperBooru-Checksum"] - .ElementAtOrDefault(0), - lastAccessTime, - lastWriteTime, - createTime, - path, - (PathType?) pathType, - tagIds); - - // Return the GUID of the new media object if requested - bool returnMetadataParsed = bool.TryParse( - formFile.Headers["X-HyperBooru-ReturnMediaId"], out var returnMetadata); - - if(returnMetadataParsed && returnMetadata) - return Content(media.Guid.ToString()); - } - - if(Request.Form.Files.Count == 1) - return Redirect($"/ViewMedia?m={media.Guid}"); - else - return Redirect($"/Gallery"); - } -}
\ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7769bf4..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:f061e5a7532b36fa1d1b684857fe1f504ba92115b9934f154643266613c44c62 AS build -WORKDIR /App/Server - -COPY Server /App/Server -COPY ApiModels /App/ApiModels -RUN dotnet restore -RUN dotnet publish -o out - -FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:ccdca44cd4f256d50187f920dc8ccc2a9ea7a8a4597ac1d51e08fddb2e3b3205 -RUN apt update -RUN apt install -y imagemagick tesseract-ocr tesseract-ocr-eng -RUN apt clean -RUN rm -rf /var/lib/apt/lists/* -WORKDIR /App -COPY --from=build /App/Server/out . -ENTRYPOINT [ "dotnet", "HyperBooru.dll" ] diff --git a/ExceptionMiddleware.cs b/ExceptionMiddleware.cs deleted file mode 100644 index ba4049a..0000000 --- a/ExceptionMiddleware.cs +++ /dev/null @@ -1,64 +0,0 @@ -using HyperBooru.ApiModels; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace HyperBooru; - -// Middleware class to intercept API controller exceptions and -// return said exceptions to API clients as serialized JSON objects -public sealed class ExceptionMiddleware { - private RequestDelegate next; - - public ExceptionMiddleware(RequestDelegate next) => - this.next = next; - - public async Task Invoke(HttpContext context) { - try { - await next(context); - } catch(HBException e) { - context.Response.ContentType = "application/json"; - context.Response.StatusCode = - e.GetType().GetCustomAttribute<ExceptionStatusCodeAttribute>()?.StatusCode ?? - StatusCodes.Status500InternalServerError; - - context.Response.Clear(); - - await context.Response.WriteAsJsonAsync(e); - } catch(Exception) { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Response.ContentType = "application/json"; - - context.Response.Clear(); - - await context.Response.WriteAsJsonAsync(new ServerException()); - } - } -} - -// This class is needed as the JSON serializer often fails to serialize -// members of the native 'Exception' class -public sealed class ExceptionJsonResolver : DefaultJsonTypeInfoResolver { - public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) { - var info = base.GetTypeInfo(type, options); - - if(!typeof(Exception).IsAssignableFrom(type)) - return info; - - string[] excludedProps = [ - "data", - "hResult", - "helpLink", - "innerException", - "source", - "stackTrace", - "targetSite" - ]; - - foreach(var p in info.Properties.Where(p => excludedProps.Contains(p.Name))) - p.ShouldSerialize = (_, _) => false; - - return info; - } -} diff --git a/HBContext.cs b/HBContext.cs deleted file mode 100644 index 766c0a3..0000000 --- a/HBContext.cs +++ /dev/null @@ -1,85 +0,0 @@ -using HyperBooru.ApiModels; -using HyperBooru.Services; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru; - -enum HBObjectId { - NsfwTag = -1, - IngestTag = -2, - AdminUser = -3 -} - -public class HBContext : DbContext { - public static readonly Guid NsfwTag = new("EBDAD4F8-455A-4351-8017-1D4854D6FA38"); - public static readonly Guid IngestTag = new("EA212801-5BCC-4C0E-814F-FB9D30DB58BC"); - public static readonly Guid AdminUser = new("4FA948F4-7C45-4F81-BB6B-E417491E6C96"); - - public DbSet<HBObject> Objects { get; set; } - public DbSet<User> Users { get; set; } - public DbSet<TagDefinition> TagDefinitions { get; set; } - public DbSet<Tag> Tags { get; set; } - public DbSet<Media> Media { get; set; } - public DbSet<UploadedFile> UploadedFiles { get; set; } - public DbSet<OcrData> OcrData { get; set; } - - private IConfigService config; - - public HBContext(DbContextOptions<HBContext> options, IConfigService config) : base(options) => - this.config = config; - - protected override void OnConfiguring(DbContextOptionsBuilder options) { - options.UseNpgsql(config.DbConnectionString); - - #if DEBUG - options.EnableSensitiveDataLogging(); - #endif - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) { - // Don't use shared tables for inherited types - modelBuilder.Entity<HBObject>().ToTable("Objects"); - modelBuilder.Entity<TagDefinition>().ToTable("TagDefinitions"); - modelBuilder.Entity<Tag>().ToTable("Tags"); - modelBuilder.Entity<Media>().ToTable("Media"); - modelBuilder.Entity<UploadedFile>().ToTable("UploadedFiles"); - - // Seed internal tag definitions - // These should NEVER change - modelBuilder.Entity<TagDefinition>().HasData(new TagDefinition[] { - new() { - ObjectId = (int) HBObjectId.NsfwTag, - Guid = NsfwTag, - Source = TagSource.Internal, - Name = "nsfw" - }, - new() { - ObjectId = (int) HBObjectId.IngestTag, - Guid = IngestTag, - Source = TagSource.Internal, - Name = "ingest" - } - }); - - // Seed initial admin user - modelBuilder.Entity<User>().HasData(new User[] { - new() { - ObjectId = (int) HBObjectId.AdminUser, - Guid = AdminUser, - Username = "admin", - PasswordHash = UserService.HashPassword("admin") - } - }); - - // Some complex relationships cannot be inferred and require - // additional configuration, as seen below. - modelBuilder.Entity<TagDefinition>() - .HasMany(e => e.ImplicitTags) - .WithMany(); - - modelBuilder.Entity<Media>() - .HasOne(m => m.CurrentUploadedFile) - .WithOne() - .HasForeignKey<Media>("CurrentUploadedFileId"); - } -}
\ No newline at end of file diff --git a/HBObject.cs b/HBObject.cs deleted file mode 100644 index 8001ea3..0000000 --- a/HBObject.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace HyperBooru; - -[Index(nameof(Guid))] -public class HBObject { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int ObjectId { get; set; } - public Guid Guid { get; set; } = Guid.NewGuid(); - public virtual List<Tag> Tags { get; set; } = new(); -}
\ No newline at end of file diff --git a/Media.cs b/Media.cs deleted file mode 100644 index 2ff9e63..0000000 --- a/Media.cs +++ /dev/null @@ -1,92 +0,0 @@ -using HyperBooru.ApiModels; -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace HyperBooru; - -public class Media : HBObject { - public string? ShortDescription { get; set; } - public string? LongDescription { get; set; } - public virtual OcrData? OcrData { get; set; } - public virtual UploadedFile? CurrentUploadedFile { get; set; } - public virtual List<UploadedFile> UploadedFiles { get; set; } = new(); - - public bool IsIngest => Tags - .Select(t => t.TagDefinitionId) - .Contains((int) HBObjectId.IngestTag); - - public string? DisplayName { - get { - if(ShortDescription is not null) - return ShortDescription; - - return UploadedFiles - .OrderBy(f => f.UploadTime) - .First()?.Filename ?? Guid.ToString().ToUpper(); - } - } - - public static explicit operator ApiModels.Media(Media media) => - new() { - MediaId = media.Guid, - ShortDescription = media.ShortDescription, - LongDescription = media.LongDescription - }; -} - -public class UploadedFile : HBObject { - public string Checksum { get; set; } - public bool ChecksumVerified { get; set; } = false; - public string? Filename { get; set; } - public long Length { get; set; } - public string MimeType { get; set; } - public int? Width { get; set; } - public int? Height { get; set; } - public DateTime UploadTime { get; set; } = DateTime.UtcNow; - public DateTime? LastAccessTime { get; set; } - public DateTime? LastWriteTime { get; set; } - public DateTime? CreateTime { get; set; } - public string? Path { get; set; } - public PathType? PathType { get; set; } - public virtual Media Media { get; set; } - - public static explicit operator ApiModels.UploadedFile(UploadedFile uploadedFile) => - new() { - MediaId = uploadedFile.Media.Guid, - UploadedFileId = uploadedFile.Guid, - Checksum = uploadedFile.Checksum, - ChecksumVerified = uploadedFile.ChecksumVerified, - Filename = uploadedFile.Filename, - Length = uploadedFile.Length, - MimeType = uploadedFile.MimeType, - Width = uploadedFile.Width, - Height = uploadedFile.Height, - UploadTime = uploadedFile.UploadTime, - LastAccessTime = uploadedFile.LastAccessTime, - LastWriteTime = uploadedFile.LastWriteTime, - CreateTime = uploadedFile.CreateTime, - Path = uploadedFile.Path, - PathType = (ApiModels.PathType?) uploadedFile.PathType - }; -} - -public class OcrData { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int OcrDataId { get; set; } - [ForeignKey("ObjectId")] - public int MediaId { get; set; } - public string Text { get; set; } - public string SearchableText { get; set; } - public DateTime Timestamp { get; set; } - public virtual Media Media { get; set; } - - public static explicit operator ApiModels.OcrData(OcrData ocrData) => - new() { - MediaId = ocrData.Media.Guid, - Text = ocrData.Text, - SearchableText = ocrData.SearchableText, - Timestamp = ocrData.Timestamp - }; -}
\ No newline at end of file diff --git a/Migrations/20260131125650_InitialMigration.Designer.cs b/Migrations/20260131125650_InitialMigration.Designer.cs deleted file mode 100644 index 2e4a05e..0000000 --- a/Migrations/20260131125650_InitialMigration.Designer.cs +++ /dev/null @@ -1,362 +0,0 @@ -// <auto-generated /> -using System; -using HyperBooru; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace HyperBooru.Migrations -{ - [DbContext(typeof(HBContext))] - [Migration("20260131125650_InitialMigration")] - partial class InitialMigration - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.23") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("HyperBooru.HBObject", b => - { - b.Property<int>("ObjectId") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ObjectId")); - - b.Property<Guid>("Guid") - .HasColumnType("uuid"); - - b.HasKey("ObjectId"); - - b.HasIndex("Guid"); - - b.ToTable("Objects", (string)null); - - b.UseTptMappingStrategy(); - }); - - modelBuilder.Entity("HyperBooru.OcrData", b => - { - b.Property<int>("OcrDataId") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("OcrDataId")); - - b.Property<int>("MediaId") - .HasColumnType("integer"); - - b.Property<string>("SearchableText") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Text") - .IsRequired() - .HasColumnType("text"); - - b.Property<DateTime>("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.HasKey("OcrDataId"); - - b.HasIndex("MediaId") - .IsUnique(); - - b.ToTable("OcrData"); - }); - - modelBuilder.Entity("TagDefinitionTagDefinition", b => - { - b.Property<int>("ImplicitTagsObjectId") - .HasColumnType("integer"); - - b.Property<int>("TagDefinitionObjectId") - .HasColumnType("integer"); - - b.HasKey("ImplicitTagsObjectId", "TagDefinitionObjectId"); - - b.HasIndex("TagDefinitionObjectId"); - - b.ToTable("TagDefinitionTagDefinition"); - }); - - modelBuilder.Entity("HyperBooru.Media", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<int?>("CurrentUploadedFileId") - .HasColumnType("integer"); - - b.Property<string>("LongDescription") - .HasColumnType("text"); - - b.Property<string>("ShortDescription") - .HasColumnType("text"); - - b.HasIndex("CurrentUploadedFileId") - .IsUnique(); - - b.ToTable("Media", (string)null); - }); - - modelBuilder.Entity("HyperBooru.Tag", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<DateTime>("CreateTime") - .HasColumnType("timestamp with time zone"); - - b.Property<int>("TagDefinitionId") - .HasColumnType("integer"); - - b.Property<int>("TargetObjectId") - .HasColumnType("integer"); - - b.HasIndex("TagDefinitionId"); - - b.HasIndex("TargetObjectId"); - - b.ToTable("Tags", (string)null); - }); - - modelBuilder.Entity("HyperBooru.TagDefinition", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<string>("Alias") - .HasColumnType("text"); - - b.Property<string>("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Namespace") - .HasColumnType("text"); - - b.Property<int>("Source") - .HasColumnType("integer"); - - b.ToTable("TagDefinitions", (string)null); - - b.HasData( - new - { - ObjectId = -1, - Guid = new Guid("ebdad4f8-455a-4351-8017-1d4854d6fa38"), - Name = "nsfw", - Source = 0 - }, - new - { - ObjectId = -2, - Guid = new Guid("ea212801-5bcc-4c0e-814f-fb9d30db58bc"), - Name = "ingest", - Source = 0 - }); - }); - - modelBuilder.Entity("HyperBooru.UploadedFile", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<string>("Checksum") - .IsRequired() - .HasColumnType("text"); - - b.Property<bool>("ChecksumVerified") - .HasColumnType("boolean"); - - b.Property<DateTime?>("CreateTime") - .HasColumnType("timestamp with time zone"); - - b.Property<string>("Filename") - .HasColumnType("text"); - - b.Property<int?>("Height") - .HasColumnType("integer"); - - b.Property<DateTime?>("LastAccessTime") - .HasColumnType("timestamp with time zone"); - - b.Property<DateTime?>("LastWriteTime") - .HasColumnType("timestamp with time zone"); - - b.Property<long>("Length") - .HasColumnType("bigint"); - - b.Property<int>("MediaObjectId") - .HasColumnType("integer"); - - b.Property<string>("MimeType") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Path") - .HasColumnType("text"); - - b.Property<int?>("PathType") - .HasColumnType("integer"); - - b.Property<DateTime>("UploadTime") - .HasColumnType("timestamp with time zone"); - - b.Property<int?>("Width") - .HasColumnType("integer"); - - b.HasIndex("MediaObjectId"); - - b.ToTable("UploadedFiles", (string)null); - }); - - modelBuilder.Entity("HyperBooru.User", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<string>("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Username") - .IsRequired() - .HasColumnType("text"); - - b.HasIndex("Username"); - - b.ToTable("Users"); - - b.HasData( - new - { - ObjectId = -3, - Guid = new Guid("4fa948f4-7c45-4f81-bb6b-e417491e6c96"), - PasswordHash = "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw==", - Username = "admin" - }); - }); - - modelBuilder.Entity("HyperBooru.OcrData", b => - { - b.HasOne("HyperBooru.Media", "Media") - .WithOne("OcrData") - .HasForeignKey("HyperBooru.OcrData", "MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("TagDefinitionTagDefinition", b => - { - b.HasOne("HyperBooru.TagDefinition", null) - .WithMany() - .HasForeignKey("ImplicitTagsObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.TagDefinition", null) - .WithMany() - .HasForeignKey("TagDefinitionObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("HyperBooru.Media", b => - { - b.HasOne("HyperBooru.UploadedFile", "CurrentUploadedFile") - .WithOne() - .HasForeignKey("HyperBooru.Media", "CurrentUploadedFileId"); - - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.Media", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CurrentUploadedFile"); - }); - - modelBuilder.Entity("HyperBooru.Tag", b => - { - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.Tag", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.TagDefinition", "TagDefinition") - .WithMany() - .HasForeignKey("TagDefinitionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.HBObject", "Target") - .WithMany("Tags") - .HasForeignKey("TargetObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("TagDefinition"); - - b.Navigation("Target"); - }); - - modelBuilder.Entity("HyperBooru.TagDefinition", b => - { - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.TagDefinition", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("HyperBooru.UploadedFile", b => - { - b.HasOne("HyperBooru.Media", "Media") - .WithMany("UploadedFiles") - .HasForeignKey("MediaObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.UploadedFile", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("HyperBooru.User", b => - { - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.User", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("HyperBooru.HBObject", b => - { - b.Navigation("Tags"); - }); - - modelBuilder.Entity("HyperBooru.Media", b => - { - b.Navigation("OcrData"); - - b.Navigation("UploadedFiles"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20260131125650_InitialMigration.cs b/Migrations/20260131125650_InitialMigration.cs deleted file mode 100644 index a1a7d8f..0000000 --- a/Migrations/20260131125650_InitialMigration.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace HyperBooru.Migrations -{ - /// <inheritdoc /> - public partial class InitialMigration : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Objects", - columns: table => new - { - ObjectId = table.Column<int>(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Guid = table.Column<Guid>(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Objects", x => x.ObjectId); - }); - - migrationBuilder.CreateTable( - name: "TagDefinitions", - columns: table => new - { - ObjectId = table.Column<int>(type: "integer", nullable: false), - Source = table.Column<int>(type: "integer", nullable: false), - Namespace = table.Column<string>(type: "text", nullable: true), - Name = table.Column<string>(type: "text", nullable: false), - Alias = table.Column<string>(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TagDefinitions", x => x.ObjectId); - table.ForeignKey( - name: "FK_TagDefinitions_Objects_ObjectId", - column: x => x.ObjectId, - principalTable: "Objects", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - ObjectId = table.Column<int>(type: "integer", nullable: false), - Username = table.Column<string>(type: "text", nullable: false), - PasswordHash = table.Column<string>(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.ObjectId); - table.ForeignKey( - name: "FK_Users_Objects_ObjectId", - column: x => x.ObjectId, - principalTable: "Objects", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "TagDefinitionTagDefinition", - columns: table => new - { - ImplicitTagsObjectId = table.Column<int>(type: "integer", nullable: false), - TagDefinitionObjectId = table.Column<int>(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TagDefinitionTagDefinition", x => new { x.ImplicitTagsObjectId, x.TagDefinitionObjectId }); - table.ForeignKey( - name: "FK_TagDefinitionTagDefinition_TagDefinitions_ImplicitTagsObjec~", - column: x => x.ImplicitTagsObjectId, - principalTable: "TagDefinitions", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_TagDefinitionTagDefinition_TagDefinitions_TagDefinitionObje~", - column: x => x.TagDefinitionObjectId, - principalTable: "TagDefinitions", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Tags", - columns: table => new - { - ObjectId = table.Column<int>(type: "integer", nullable: false), - TagDefinitionId = table.Column<int>(type: "integer", nullable: false), - CreateTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), - TargetObjectId = table.Column<int>(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tags", x => x.ObjectId); - table.ForeignKey( - name: "FK_Tags_Objects_ObjectId", - column: x => x.ObjectId, - principalTable: "Objects", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Tags_Objects_TargetObjectId", - column: x => x.TargetObjectId, - principalTable: "Objects", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Tags_TagDefinitions_TagDefinitionId", - column: x => x.TagDefinitionId, - principalTable: "TagDefinitions", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Media", - columns: table => new - { - ObjectId = table.Column<int>(type: "integer", nullable: false), - ShortDescription = table.Column<string>(type: "text", nullable: true), - LongDescription = table.Column<string>(type: "text", nullable: true), - CurrentUploadedFileId = table.Column<int>(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Media", x => x.ObjectId); - table.ForeignKey( - name: "FK_Media_Objects_ObjectId", - column: x => x.ObjectId, - principalTable: "Objects", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "OcrData", - columns: table => new - { - OcrDataId = table.Column<int>(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - MediaId = table.Column<int>(type: "integer", nullable: false), - Text = table.Column<string>(type: "text", nullable: false), - SearchableText = table.Column<string>(type: "text", nullable: false), - Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_OcrData", x => x.OcrDataId); - table.ForeignKey( - name: "FK_OcrData_Media_MediaId", - column: x => x.MediaId, - principalTable: "Media", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UploadedFiles", - columns: table => new - { - ObjectId = table.Column<int>(type: "integer", nullable: false), - Checksum = table.Column<string>(type: "text", nullable: false), - ChecksumVerified = table.Column<bool>(type: "boolean", nullable: false), - Filename = table.Column<string>(type: "text", nullable: true), - Length = table.Column<long>(type: "bigint", nullable: false), - MimeType = table.Column<string>(type: "text", nullable: false), - Width = table.Column<int>(type: "integer", nullable: true), - Height = table.Column<int>(type: "integer", nullable: true), - UploadTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), - LastAccessTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), - LastWriteTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), - CreateTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), - Path = table.Column<string>(type: "text", nullable: true), - PathType = table.Column<int>(type: "integer", nullable: true), - MediaObjectId = table.Column<int>(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UploadedFiles", x => x.ObjectId); - table.ForeignKey( - name: "FK_UploadedFiles_Media_MediaObjectId", - column: x => x.MediaObjectId, - principalTable: "Media", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_UploadedFiles_Objects_ObjectId", - column: x => x.ObjectId, - principalTable: "Objects", - principalColumn: "ObjectId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.InsertData( - table: "Objects", - columns: new[] { "ObjectId", "Guid" }, - values: new object[,] - { - { -3, new Guid("4fa948f4-7c45-4f81-bb6b-e417491e6c96") }, - { -2, new Guid("ea212801-5bcc-4c0e-814f-fb9d30db58bc") }, - { -1, new Guid("ebdad4f8-455a-4351-8017-1d4854d6fa38") } - }); - - migrationBuilder.InsertData( - table: "TagDefinitions", - columns: new[] { "ObjectId", "Alias", "Name", "Namespace", "Source" }, - values: new object[,] - { - { -2, null, "ingest", null, 0 }, - { -1, null, "nsfw", null, 0 } - }); - - migrationBuilder.InsertData( - table: "Users", - columns: new[] { "ObjectId", "PasswordHash", "Username" }, - values: new object[] { -3, "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw==", "admin" }); - - migrationBuilder.CreateIndex( - name: "IX_Media_CurrentUploadedFileId", - table: "Media", - column: "CurrentUploadedFileId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Objects_Guid", - table: "Objects", - column: "Guid"); - - migrationBuilder.CreateIndex( - name: "IX_OcrData_MediaId", - table: "OcrData", - column: "MediaId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_TagDefinitionTagDefinition_TagDefinitionObjectId", - table: "TagDefinitionTagDefinition", - column: "TagDefinitionObjectId"); - - migrationBuilder.CreateIndex( - name: "IX_Tags_TagDefinitionId", - table: "Tags", - column: "TagDefinitionId"); - - migrationBuilder.CreateIndex( - name: "IX_Tags_TargetObjectId", - table: "Tags", - column: "TargetObjectId"); - - migrationBuilder.CreateIndex( - name: "IX_UploadedFiles_MediaObjectId", - table: "UploadedFiles", - column: "MediaObjectId"); - - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - table: "Users", - column: "Username"); - - migrationBuilder.AddForeignKey( - name: "FK_Media_UploadedFiles_CurrentUploadedFileId", - table: "Media", - column: "CurrentUploadedFileId", - principalTable: "UploadedFiles", - principalColumn: "ObjectId"); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Media_Objects_ObjectId", - table: "Media"); - - migrationBuilder.DropForeignKey( - name: "FK_UploadedFiles_Objects_ObjectId", - table: "UploadedFiles"); - - migrationBuilder.DropForeignKey( - name: "FK_Media_UploadedFiles_CurrentUploadedFileId", - table: "Media"); - - migrationBuilder.DropTable( - name: "OcrData"); - - migrationBuilder.DropTable( - name: "TagDefinitionTagDefinition"); - - migrationBuilder.DropTable( - name: "Tags"); - - migrationBuilder.DropTable( - name: "Users"); - - migrationBuilder.DropTable( - name: "TagDefinitions"); - - migrationBuilder.DropTable( - name: "Objects"); - - migrationBuilder.DropTable( - name: "UploadedFiles"); - - migrationBuilder.DropTable( - name: "Media"); - } - } -} diff --git a/Migrations/HBContextModelSnapshot.cs b/Migrations/HBContextModelSnapshot.cs deleted file mode 100644 index 422037f..0000000 --- a/Migrations/HBContextModelSnapshot.cs +++ /dev/null @@ -1,359 +0,0 @@ -// <auto-generated /> -using System; -using HyperBooru; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace HyperBooru.Migrations -{ - [DbContext(typeof(HBContext))] - partial class HBContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.23") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("HyperBooru.HBObject", b => - { - b.Property<int>("ObjectId") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ObjectId")); - - b.Property<Guid>("Guid") - .HasColumnType("uuid"); - - b.HasKey("ObjectId"); - - b.HasIndex("Guid"); - - b.ToTable("Objects", (string)null); - - b.UseTptMappingStrategy(); - }); - - modelBuilder.Entity("HyperBooru.OcrData", b => - { - b.Property<int>("OcrDataId") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("OcrDataId")); - - b.Property<int>("MediaId") - .HasColumnType("integer"); - - b.Property<string>("SearchableText") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Text") - .IsRequired() - .HasColumnType("text"); - - b.Property<DateTime>("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.HasKey("OcrDataId"); - - b.HasIndex("MediaId") - .IsUnique(); - - b.ToTable("OcrData"); - }); - - modelBuilder.Entity("TagDefinitionTagDefinition", b => - { - b.Property<int>("ImplicitTagsObjectId") - .HasColumnType("integer"); - - b.Property<int>("TagDefinitionObjectId") - .HasColumnType("integer"); - - b.HasKey("ImplicitTagsObjectId", "TagDefinitionObjectId"); - - b.HasIndex("TagDefinitionObjectId"); - - b.ToTable("TagDefinitionTagDefinition"); - }); - - modelBuilder.Entity("HyperBooru.Media", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<int?>("CurrentUploadedFileId") - .HasColumnType("integer"); - - b.Property<string>("LongDescription") - .HasColumnType("text"); - - b.Property<string>("ShortDescription") - .HasColumnType("text"); - - b.HasIndex("CurrentUploadedFileId") - .IsUnique(); - - b.ToTable("Media", (string)null); - }); - - modelBuilder.Entity("HyperBooru.Tag", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<DateTime>("CreateTime") - .HasColumnType("timestamp with time zone"); - - b.Property<int>("TagDefinitionId") - .HasColumnType("integer"); - - b.Property<int>("TargetObjectId") - .HasColumnType("integer"); - - b.HasIndex("TagDefinitionId"); - - b.HasIndex("TargetObjectId"); - - b.ToTable("Tags", (string)null); - }); - - modelBuilder.Entity("HyperBooru.TagDefinition", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<string>("Alias") - .HasColumnType("text"); - - b.Property<string>("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Namespace") - .HasColumnType("text"); - - b.Property<int>("Source") - .HasColumnType("integer"); - - b.ToTable("TagDefinitions", (string)null); - - b.HasData( - new - { - ObjectId = -1, - Guid = new Guid("ebdad4f8-455a-4351-8017-1d4854d6fa38"), - Name = "nsfw", - Source = 0 - }, - new - { - ObjectId = -2, - Guid = new Guid("ea212801-5bcc-4c0e-814f-fb9d30db58bc"), - Name = "ingest", - Source = 0 - }); - }); - - modelBuilder.Entity("HyperBooru.UploadedFile", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<string>("Checksum") - .IsRequired() - .HasColumnType("text"); - - b.Property<bool>("ChecksumVerified") - .HasColumnType("boolean"); - - b.Property<DateTime?>("CreateTime") - .HasColumnType("timestamp with time zone"); - - b.Property<string>("Filename") - .HasColumnType("text"); - - b.Property<int?>("Height") - .HasColumnType("integer"); - - b.Property<DateTime?>("LastAccessTime") - .HasColumnType("timestamp with time zone"); - - b.Property<DateTime?>("LastWriteTime") - .HasColumnType("timestamp with time zone"); - - b.Property<long>("Length") - .HasColumnType("bigint"); - - b.Property<int>("MediaObjectId") - .HasColumnType("integer"); - - b.Property<string>("MimeType") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Path") - .HasColumnType("text"); - - b.Property<int?>("PathType") - .HasColumnType("integer"); - - b.Property<DateTime>("UploadTime") - .HasColumnType("timestamp with time zone"); - - b.Property<int?>("Width") - .HasColumnType("integer"); - - b.HasIndex("MediaObjectId"); - - b.ToTable("UploadedFiles", (string)null); - }); - - modelBuilder.Entity("HyperBooru.User", b => - { - b.HasBaseType("HyperBooru.HBObject"); - - b.Property<string>("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property<string>("Username") - .IsRequired() - .HasColumnType("text"); - - b.HasIndex("Username"); - - b.ToTable("Users"); - - b.HasData( - new - { - ObjectId = -3, - Guid = new Guid("4fa948f4-7c45-4f81-bb6b-e417491e6c96"), - PasswordHash = "P4geAuE2yX/PDRHuJSq74FF5vO782rWz5c0LAQPR8m45DEYAONhu1wYnAn60PSNyjocqEBdnCeKCJfK3sKyuWw==", - Username = "admin" - }); - }); - - modelBuilder.Entity("HyperBooru.OcrData", b => - { - b.HasOne("HyperBooru.Media", "Media") - .WithOne("OcrData") - .HasForeignKey("HyperBooru.OcrData", "MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("TagDefinitionTagDefinition", b => - { - b.HasOne("HyperBooru.TagDefinition", null) - .WithMany() - .HasForeignKey("ImplicitTagsObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.TagDefinition", null) - .WithMany() - .HasForeignKey("TagDefinitionObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("HyperBooru.Media", b => - { - b.HasOne("HyperBooru.UploadedFile", "CurrentUploadedFile") - .WithOne() - .HasForeignKey("HyperBooru.Media", "CurrentUploadedFileId"); - - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.Media", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CurrentUploadedFile"); - }); - - modelBuilder.Entity("HyperBooru.Tag", b => - { - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.Tag", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.TagDefinition", "TagDefinition") - .WithMany() - .HasForeignKey("TagDefinitionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.HBObject", "Target") - .WithMany("Tags") - .HasForeignKey("TargetObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("TagDefinition"); - - b.Navigation("Target"); - }); - - modelBuilder.Entity("HyperBooru.TagDefinition", b => - { - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.TagDefinition", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("HyperBooru.UploadedFile", b => - { - b.HasOne("HyperBooru.Media", "Media") - .WithMany("UploadedFiles") - .HasForeignKey("MediaObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.UploadedFile", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Media"); - }); - - modelBuilder.Entity("HyperBooru.User", b => - { - b.HasOne("HyperBooru.HBObject", null) - .WithOne() - .HasForeignKey("HyperBooru.User", "ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("HyperBooru.HBObject", b => - { - b.Navigation("Tags"); - }); - - modelBuilder.Entity("HyperBooru.Media", b => - { - b.Navigation("OcrData"); - - b.Navigation("UploadedFiles"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pages/_Host.cshtml b/Pages/_Host.cshtml deleted file mode 100644 index 28ff24c..0000000 --- a/Pages/_Host.cshtml +++ /dev/null @@ -1,37 +0,0 @@ -@page "/" -@using Microsoft.AspNetCore.Components.Web -@namespace HyperBooru.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> - <base href="~/" /> - <link href="css/site.css" rel="stylesheet" /> - <link href="/styles/global.css" rel="stylesheet" /> - <link href="/favicon.ico" rel="icon" /> - <link href="/manifest.webmanifest" rel="manifest" /> - <script type="text/javascript" src="/js/dialog.js"></script> - <script type="text/javascript" src="/js/keyboard.js"></script> - <script type="text/javascript" src="/js/mobile.js"></script> - <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" /> -</head> -<body> - <component type="typeof(App)" render-mode="ServerPrerendered" /> - - <div id="blazor-error-ui"> - <environment include="Staging,Production"> - An error has occurred. This application may no longer respond until reloaded. - </environment> - <environment include="Development"> - An unhandled exception has occurred. See browser dev tools for details. - </environment> - <a href="" class="reload">Reload</a> - <a class="dismiss">🗙</a> - </div> - - <script src="_framework/blazor.server.js"></script> -</body> -</html> diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json deleted file mode 100644 index 9f4966c..0000000 --- a/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "profiles": { - "WSL": { - "commandName": "WSL2", - "launchBrowser": true, - "launchUrl": "https://localhost:7132", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_URLS": "https://localhost:7132;http://localhost:5186" - }, - "distributionName": "" - }, - "HyperBooru": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7132;http://localhost:5186" - } - }, - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:1922", - "sslPort": 44354 - } - } -}
\ No newline at end of file diff --git a/Services/ConfigService.cs b/Services/ConfigService.cs deleted file mode 100644 index ac1f155..0000000 --- a/Services/ConfigService.cs +++ /dev/null @@ -1,72 +0,0 @@ -using HyperBooru.ApiModels; - -namespace HyperBooru.Services; - -public interface IConfigService { - public string DataPath { get; } - public string KeyPath { get; } - public string DbConnectionString { get; } - public string MediaBasePath { get; } - public string ThumbnailBasePath { get; } - public string ConvertedMediaBasePath { get; } - public bool EnableOcr { get; } -} - -public class ConfigService : IConfigService { - private IConfiguration config; - - private const string AppName = "HyperBooru"; - - public string DataPath { - get { - #if DEBUG - return "Data"; - #else - string? path = config["DataPath"]; - if(path is not null) - return path; - - switch(Environment.OSVersion.Platform) { - case PlatformID.Win32NT: - return Path.Join( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - AppName); - case PlatformID.Unix: - return $"/var/lib/{AppName.ToLower()}"; - default: - throw new NotImplementedException( - $"Unknown Operating System: {Environment.OSVersion.Platform}"); - } - #endif - } - } - - public string KeyPath => - Path.Join(DataPath, "keys"); - - public string DbConnectionString => - config.GetConnectionString("DefaultConnection") ?? - throw new HBException("Unable to get default connection string"); - - public string MediaBasePath => - Path.Join(DataPath, "media"); - - public string ThumbnailBasePath => - Path.Join(DataPath, "thumb"); - - public string ConvertedMediaBasePath => - Path.Join(DataPath, "converted"); - - public bool EnableOcr => - bool.TryParse(config["DisableOcr"], out bool x) ? !x : true; - - public ConfigService(IConfiguration config) { - this.config = config; - InitDirectoryStructure(); - } - - private void InitDirectoryStructure() { - Directory.CreateDirectory(DataPath); - Directory.CreateDirectory(MediaBasePath); - } -}
\ No newline at end of file diff --git a/Services/FeedService.cs b/Services/FeedService.cs deleted file mode 100644 index 3744e73..0000000 --- a/Services/FeedService.cs +++ /dev/null @@ -1,212 +0,0 @@ -using HyperBooru.ApiModels; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Services; - -public interface IFeedService { - public Media[] LoadChunk( - bool selectIngest, - bool includeNsfw, - Media? key = null, - int count = 50, - SortOrder sortOrder = SortOrder.ObjectId); - - public Media[] LoadChunk( - bool selectIngest, - bool includeNsfw, - string query, - Media? key = null, - int count = 50, - SortOrder sortOrder = SortOrder.ObjectId); - - public Media[] LoadChunk( - bool selectIngest, - bool includeNsfw, - Guid tagId, - Media? key = null, - int count = 50, - SortOrder sortOrder = SortOrder.ObjectId); - - public Media[] LoadChunk(FeedRequest feedRequest); -} - -public class FeedService : IFeedService { - private IDbContextFactory<HBContext> dbFactory; - - public FeedService(IDbContextFactory<HBContext> dbFactory) => - this.dbFactory = dbFactory; - - public Media[] LoadChunk( - bool selectIngest, - bool includeNsfw, - Media? continuationToken, - int count, - SortOrder sortOrder) => LoadChunkInternal( - selectIngest, includeNsfw, null, null, continuationToken?.Guid, count, sortOrder); - - public Media[] LoadChunk( - bool selectIngest, - bool includeNsfw, - string query, - Media? continuationToken, - int count, - SortOrder sortOrder) => LoadChunkInternal( - selectIngest, includeNsfw, query, null, continuationToken?.Guid, count, sortOrder); - - public Media[] LoadChunk( - bool selectIngest, - bool includeNsfw, - Guid tagId, - Media? continuationToken, - int count, - SortOrder sortOrder) => LoadChunkInternal( - selectIngest, includeNsfw, null, tagId, continuationToken?.Guid, count, sortOrder); - - public Media[] LoadChunk(FeedRequest feedRequest) { - switch(feedRequest) { - case FeedSearchRequest searchRequest: - return LoadChunkInternal( - selectIngest: searchRequest.SelectIngest, - includeNsfw: searchRequest.IncludeNsfw, - query: searchRequest.Query, - tagId: null, - continuationToken: searchRequest.ContinuationToken, - count: searchRequest.Count, - sortOrder: searchRequest.SortOrder); - case FeedTagRequest tagRequest: - return LoadChunkInternal( - selectIngest: tagRequest.SelectIngest, - includeNsfw: tagRequest.IncludeNsfw, - query: null, - tagId: tagRequest.TagId, - continuationToken: tagRequest.ContinuationToken, - count: tagRequest.Count, - sortOrder: tagRequest.SortOrder); - default: - return LoadChunkInternal( - selectIngest: feedRequest.SelectIngest, - includeNsfw: feedRequest.IncludeNsfw, - query: null, - tagId: null, - continuationToken: feedRequest.ContinuationToken, - count: feedRequest.Count, - sortOrder: feedRequest.SortOrder); - } - } - - private Media[] LoadChunkInternal( - bool selectIngest, - bool includeNsfw, - string? query, - Guid? tagId, - Guid? continuationToken, - int count, - SortOrder sortOrder) { - - if(selectIngest && !includeNsfw) - return Array.Empty<Media>(); - - using var db = dbFactory.CreateDbContext(); - - IQueryable<Media> media = db.Media - .AsSingleQuery() - .AsNoTracking() - .Include(m => m.Tags) - .Include(m => m.CurrentUploadedFile); - - if(!includeNsfw) - media = media - .Where(m => !TagsThatImply(db, HBContext.NsfwTag) - .Intersect(m.Tags.Select(t => t.TagDefinitionId)) - .Any()); - - if(selectIngest) { - media = media - .Where(m => m.Tags - .Select(t => t.TagDefinitionId) - .Contains((int) HBObjectId.IngestTag)); - } else { - media = media - .Where(m => !m.Tags - .Select(t => t.TagDefinitionId) - .Contains((int) HBObjectId.IngestTag)); - } - - if(query is not null) { - media = Search(media, query); - } else if(tagId is not null) { - media = media - .Where(m => TagsThatImply(db, (Guid) tagId) - .Intersect(m.Tags.Select(t => t.TagDefinitionId)) - .Any()); - } - - if(continuationToken is not null) - media = media - .Where(m => m.ObjectId > db.Media.First(m => m.Guid == continuationToken).ObjectId); - - switch(sortOrder) { - case SortOrder.ObjectId: - media = media.OrderBy(m => m.ObjectId); - break; - case SortOrder.LastWriteTime: - media = media.OrderBy(m => m.CurrentUploadedFile!.LastWriteTime); - break; - case SortOrder.Random: - media = media.OrderBy(m => EF.Functions.Random()); - break; - } - - return media - .Take(count) - .ToArray(); - } - - private static IQueryable<Media> Search(IQueryable<Media> media, string query) { - // TODO: search implicit tags as well - - query = query.ToLower().Trim(); - - return media - .Where(m => - (m.ShortDescription != null && m.ShortDescription.ToLower().Contains(query)) || - (m.LongDescription != null && m.LongDescription.ToLower().Contains(query)) || - (m.UploadedFiles.Any(uf => uf.Filename != null && uf.Filename.ToLower().Contains(query))) || - (m.OcrData != null && m.OcrData.SearchableText.ToLower().Contains(query)) || - (m.Tags.Any(t => t.TagDefinition.Name.ToLower().Contains(query)))); - } - - private static IQueryable<int> TagsThatImply(HBContext db, Guid tagId) => - db.Database.SqlQueryRaw<int>(""" - WITH RECURSIVE basetag AS ( - SELECT "ObjectId" FROM "Objects" WHERE "Guid" = {0} - ), - impliedtags AS ( - SELECT - "TagDefinitionObjectId" - FROM - "TagDefinitionTagDefinition" - INNER JOIN - basetag - ON - "ImplicitTagsObjectId" = basetag."ObjectId" - UNION - SELECT - "TagDefinitionTagDefinition"."TagDefinitionObjectId" - FROM - "TagDefinitionTagDefinition" - INNER JOIN - impliedtags - ON - impliedtags."TagDefinitionObjectId" = "TagDefinitionTagDefinition"."ImplicitTagsObjectId" - ) - SELECT DISTINCT - "TagDefinitionObjectId" AS "Value" - FROM impliedtags - UNION - SELECT - "ObjectId" AS "Value" - FROM - basetag - """, tagId); -} diff --git a/Services/MediaService.cs b/Services/MediaService.cs deleted file mode 100644 index 2d1533c..0000000 --- a/Services/MediaService.cs +++ /dev/null @@ -1,400 +0,0 @@ -using HyperBooru.ApiModels; -using ImageMagick; -using Microsoft.EntityFrameworkCore; -using MimeDetective; -using MimeDetective.Definitions; -using System.Security.Cryptography; -using System.Text.RegularExpressions; - -namespace HyperBooru.Services; - -public interface IMediaService { - public void SetDescription( - Media media, - string? shortDescription, - string? longDescription); - - public void SetIngest(Media media, bool ingest); - - public Media Create( - Stream fileData, - string fileName, - string? checksum = null, - DateTime? lastAccessTime = null, - DateTime? lastWriteTime = null, - DateTime? createTime = null, - string? path = null, - PathType? pathType = null, - Guid[]? tagIds = null); - - public void Delete(Guid media); - public void Delete(Media media); - public void DeleteThumbnails(Guid media); - public void DeleteThumbnails(Media media); - public Stream GetThumbnail(Guid media, int? width, int? height); - public Stream GetThumbnail(Media media, int? width, int? height); - public Stream GetConverted(Guid mediaId, string mimeType = "image/png"); - public Stream GetConverted(Media media, string mimeType = "image/png"); - public string GetPath(Guid media); - public string GetPath(Media media); - -} - -public class MediaService : IMediaService { - private readonly Dictionary<string,MagickFormat> FormatMap = new() { - ["image/jpeg"] = MagickFormat.Jpeg, - ["image/jpg"] = MagickFormat.Jpg, - ["image/png"] = MagickFormat.Png, - ["image/webp"] = MagickFormat.WebP - }; - - private IDbContextFactory<HBContext> dbFactory; - private IConfigService config; - - private IContentInspector inspector; - - public MediaService(IDbContextFactory<HBContext> dbFactory, - IConfigService config) { - - this.dbFactory = dbFactory; - this.config = config; - - ContentInspectorBuilder inspectorBuilder = new() { - Definitions = - DefaultDefinitions.FileTypes.Images.All() - .Union(DefaultDefinitions.FileTypes.Video.All()) - .ToList() - }; - - inspector = inspectorBuilder.Build(); - } - - public void SetIngest(Media media, bool ingest) { - using var db = dbFactory.CreateDbContext(); - media = db.Media - .Include(m => m.Tags) - .ThenInclude(t => t.TagDefinition) - .First(m => m.Guid == media.Guid); - var ingestTag = db.TagDefinitions - .First(td => td.Guid == HBContext.IngestTag); - - if(ingest) { - if(!media.Tags.Select(t => t.TagDefinition.Guid).Contains(HBContext.IngestTag)) - media.Tags.Add(new(ingestTag)); - } else { - media.Tags.RemoveAll(t => t.TagDefinition.Guid == HBContext.IngestTag); - } - - db.SaveChanges(); - } - - public void SetDescription( - Media media, - string? shortDescription, - string? longDescription) { - - using var db = dbFactory.CreateDbContext(); - var m = db.Media.First(m => m.Guid == media.Guid); - - shortDescription = shortDescription?.Trim(); - longDescription = longDescription?.Trim(); - - if(string.IsNullOrEmpty(shortDescription)) - shortDescription = null; - if(string.IsNullOrEmpty(longDescription)) - longDescription = null; - - m.ShortDescription = shortDescription; - m.LongDescription = longDescription; - - db.SaveChanges(); - } - - public Media Create( - Stream fileData, - string fileName, - string? checksum = null, - DateTime? lastAccessTime = null, - DateTime? lastWriteTime = null, - DateTime? createTime = null, - string? path = null, - PathType? pathType = null, - Guid[]? tagIds = null) { - - using var db = dbFactory.CreateDbContext(); - using var transaction = db.Database.BeginTransaction(); - - if(fileData.Length == 0) - throw new MediaCreateException("File is empty"); - - // Calculate the checksum using the in-memory file contents - var hash = BitConverter - .ToString(MD5.Create().ComputeHash(fileData)) - .Replace("-", "") - .ToLower(); - - if(checksum is not null && hash != checksum.ToLower()) - throw new MediaCreateException("Checksum does not match"); - - // Determine the MIME type - fileData.Seek(0, SeekOrigin.Begin); - var defs = inspector.Inspect(fileData); - var mime = defs.ByMimeType().FirstOrDefault()?.MimeType; - if(mime is null) - throw new MediaCreateException("Unsupported file type"); - - // Read the image with ImageMagick to determine the width and height - fileData.Seek(0, SeekOrigin.Begin); - using var magickImage = new MagickImage(fileData); - - var media = db.Media - .Include(m => m.UploadedFiles) - .Include(m => m.Tags) - .FirstOrDefault(m => m.UploadedFiles.Any(uf => uf.Checksum == hash)); - - var fileRecord = new UploadedFile() { - Filename = fileName, - Length = fileData.Length, - Checksum = hash, - ChecksumVerified = checksum is not null, - MimeType = mime, - Width = (int) magickImage.Width, - Height = (int) magickImage.Height, - UploadTime = DateTime.UtcNow, - LastAccessTime = lastAccessTime, - LastWriteTime = lastWriteTime, - CreateTime = createTime, - Path = pathType is null ? null : path, - PathType = pathType - }; - - var tags = Array.Empty<TagDefinition>(); - if(tagIds is not null) { - tagIds = tagIds.Distinct().ToArray(); - - tags = db.TagDefinitions - .Where(td => tagIds.Contains(td.Guid)) - .ToArray(); - - if(tags.Count() < tagIds.Count()) { - var badIds = tagIds - .Where(x => !tags.Select(td => td.Guid).Contains(x)) - .Order(); - - throw new MediaCreateException( - $"Non-existent tags specified: {string.Join(", ", badIds)}"); - } - } - - if(media is null) { - var ingestTagDef = db.TagDefinitions - .First(td => td.Guid == HBContext.IngestTag); - - media = new() { - UploadedFiles = new() { - fileRecord - }, - Tags = tags is null ? [ new() { TagDefinition = ingestTagDef } ] : tags - .Select(td => new Tag() { TagDefinition = td }) - .ToList() - }; - - using var newFile = File.Create(GetPath(media)); - - fileData.Seek(0, SeekOrigin.Begin); - fileData.CopyTo(newFile); - newFile.Flush(); - - db.Media.Add(media); - db.SaveChanges(); - media.CurrentUploadedFile = fileRecord; - db.SaveChanges(); - } else { - var fileHashes = media.UploadedFiles - .Select(uf => GetUploadedFileHash(uf)); - // Only add the uploaded file record if it contains new information - if(!fileHashes.Contains(GetUploadedFileHash(fileRecord))) - media.UploadedFiles.Add(fileRecord); - // Add new tags if needed - var missingTags = tags - .Where(td => !media.Tags.Select(t => t.TagDefinition.Guid).Contains(td.Guid)); - media.Tags.AddRange(missingTags.Select(td => new Tag() { TagDefinition = td })); - db.Update(media); - db.SaveChanges(); - } - - transaction.Commit(); - - return media; - } - - public void Delete(Guid media) { - using var db = dbFactory.CreateDbContext(); - var m = db.Media.First(m => m.Guid == media); - - var path = Path.Join( - config.MediaBasePath, - m.Guid.ToString().Substring(0, 2), - m.Guid.ToString().Substring(2, 2), - m.Guid.ToString()); - - try { - var fileInfo = new FileInfo(path); - fileInfo.Delete(); - fileInfo.Directory?.Delete(); - fileInfo.Directory?.Parent?.Delete(); - } catch(IOException) {} - - try { - DeleteThumbnails(media); - } catch {} - - db.Media.Remove(m); - db.SaveChanges(); - } - - public void Delete(Media media) => - Delete(media.Guid); - - public void DeleteThumbnails(Guid media) { - var dir = new DirectoryInfo(Path.Join( - config.ThumbnailBasePath, - media.ToString().Substring(0, 2), - media.ToString().Substring(2, 2))); - - var pattern = new Regex($"^{media}-[0-9]+-[0-9]+$"); - var toDelete = dir.GetFiles() - .Where(f => pattern.IsMatch(f.Name)) - .ToList(); - - List<Exception> exceptions = new(); - - foreach(var file in toDelete) { - try { - file.Delete(); - } catch(Exception e) { - exceptions.Add(e); - } - } - - try { - dir.Delete(); - dir.Parent?.Delete(); - } catch(Exception e) { - exceptions.Add(e); - } - - // TODO: wrap the AggregateException in a ThumbnailException - if(exceptions.Count() > 1) - throw new AggregateException(exceptions); - } - - public void DeleteThumbnails(Media media) => - DeleteThumbnails(media.Guid); - - public Stream GetThumbnail(Guid mediaId, int? width, int? height) { - if(width is null && height is null) - throw new ThumbnailException( - "Both width and height cannot be null!", - mediaId); - - var thumbPath = GetThumbnailPath(mediaId, width, height); - - if(File.Exists(thumbPath)) - return System.IO.File.OpenRead(thumbPath); - - if(!File.Exists(GetPath(mediaId))) - throw new ObjectNotFoundException([ mediaId ]); - - using var image = new MagickImage(GetPath(mediaId)); - - if(width > image.Width || height > image.Height) { - width = (int) image.Width; - height = (int) image.Height; - } - - image.Thumbnail((uint) (width ?? -1), (uint) (height ?? -1)); - image.Write(thumbPath, MagickFormat.Jpeg); - - return System.IO.File.OpenRead(thumbPath); - } - - public Stream GetConverted(Guid mediaId, string mimeType) { - if(!FormatMap.TryGetValue(mimeType, out var format)) - throw new MediaException($"Cannot convert to unknown format ({mimeType})", mediaId); - - var convertedPath = GetConvertedPath(mediaId, mimeType); - - if(File.Exists(convertedPath)) - return System.IO.File.OpenRead(convertedPath); - - if(!File.Exists(GetPath(mediaId))) - throw new ObjectNotFoundException([ mediaId ]); - - using var image = new MagickImage(GetPath(mediaId)); - image.Write(convertedPath, format); - - return System.IO.File.OpenRead(convertedPath); - } - - public Stream GetThumbnail(Media media, int? width, int? height) => - GetThumbnail(media.Guid, width, height); - - public Stream GetConverted(Media media, string mimeType) => - GetConverted(media.Guid, mimeType); - - public string GetPath(Guid mediaId) { - var fileInfo = new FileInfo( - Path.Join( - config.MediaBasePath, - mediaId.ToString().Substring(0, 2), - mediaId.ToString().Substring(2, 2), - mediaId.ToString())); - - Directory.CreateDirectory(fileInfo.Directory!.FullName); - return fileInfo.FullName; - } - - public string GetThumbnailPath(Guid mediaId, int? width, int? height) { - if(width is null && height is null) - throw new ThumbnailException( - "Both width and height cannot be null!", - mediaId); - - var fileInfo = new FileInfo(Path.Join( - config.ThumbnailBasePath, - mediaId.ToString().Substring(0, 2), - mediaId.ToString().Substring(2, 2), - $"{mediaId.ToString()}-{(width ?? 0)}-{(height ?? 0)}")); - - Directory.CreateDirectory(fileInfo.Directory!.FullName); - return fileInfo.FullName; - } - - public string GetConvertedPath(Guid mediaId, string mimeType) { - var fileInfo = new FileInfo(Path.Join( - config.ConvertedMediaBasePath, - mediaId.ToString().Substring(0, 2), - mediaId.ToString().Substring(2, 2), - $"{mediaId.ToString()}-{mimeType.Split('/')[1]}")); - - Directory.CreateDirectory(fileInfo.Directory!.FullName); - return fileInfo.FullName; - } - - public string GetPath(Media media) => - GetPath(media.Guid); - - public string GetThumbnailPath(Media media, int? width, int? height) => - GetThumbnailPath(media.Guid, width, height); - - public string GetConvertedPath(Media media, string mimeType) => - GetConvertedPath(media.Guid, mimeType); - - private int GetUploadedFileHash(UploadedFile uf) => ( - uf.CreateTime, - uf.LastWriteTime, - uf.Filename, - uf.Length, - uf.Checksum).GetHashCode(); -} diff --git a/Services/OcrService.cs b/Services/OcrService.cs deleted file mode 100644 index d43db2e..0000000 --- a/Services/OcrService.cs +++ /dev/null @@ -1,128 +0,0 @@ -using HyperBooru.Util; -using Microsoft.EntityFrameworkCore; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using Tesseract; - -namespace HyperBooru.Services; - -public class OcrService : IHostedService { - private readonly string[] InvalidMimeTypes = [ "image/heic", "image/webp" ]; - - private readonly TimeSpan ProcessInterval = TimeSpan.FromMinutes(30); - private readonly TimeSpan StartupDelay = TimeSpan.FromSeconds(30); - - private readonly Regex SpaceRegex = new(@"[^0-9a-z]+", RegexOptions.Compiled); - - private Task? task; - private CancellationTokenSource cts = new(); - - private Timer timer; - - private IConfigService configService; - private IServiceScopeFactory scopeFactory; - private ILogger<OcrService> logger; - private IDbContextFactory<HBContext> dbFactory; - - public OcrService( - IConfigService configService, - IServiceScopeFactory scopeFactory, - ILogger<OcrService> logger, - IDbContextFactory<HBContext> dbFactory) { - - this.configService = configService; - this.scopeFactory = scopeFactory; - this.logger = logger; - this.dbFactory = dbFactory; - - timer = new((object? state) => { - if(task is not null && !task.IsCompleted) - return; - cts = new(); - task = ProcessAllAsync(cts.Token); - }); - } - - public Task StartAsync(CancellationToken ct) { - if(configService.EnableOcr) { - logger.LogInformation("Service starting..."); - timer.Change(StartupDelay, ProcessInterval); - } - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken ct) { - logger.LogInformation("Service stopping..."); - timer.Change(Timeout.Infinite, Timeout.Infinite); - cts.Cancel(); - return Task.CompletedTask; - } - - async Task ProcessAllAsync(CancellationToken ct) { - using var scope = scopeFactory.CreateScope(); - var mediaService = scope.ServiceProvider - .GetRequiredService<IMediaService>(); - - using var db = dbFactory.CreateDbContext(); - Guid[] guids = db.Media - .AsNoTracking() - .Include(m => m.CurrentUploadedFile) - .Include(m => m.OcrData) - .Where(m => m.OcrData == null) - .Where(m => m.CurrentUploadedFile!.MimeType.Contains("image/")) - .Where(m => !InvalidMimeTypes.Contains(m.CurrentUploadedFile!.MimeType)) - .Select(m => m.Guid) - .ToArray(); - db.Dispose(); - - logger.LogInformation($"Performing OCR pass on {guids.Count()} media items"); - - var factory = new TaskFactory(new LimitedConcurrencyTaskScheduler()); - var tasks = new List<Task>(); - - var stopwatch = Stopwatch.StartNew(); - - foreach(var guid in guids) - tasks.Add(factory.StartNew(() => Process(guid, mediaService), ct)); - - await Task.WhenAll(tasks); - stopwatch.Stop(); - - var time = stopwatch.Elapsed.ToStringHumanReadable(); - logger.LogInformation( - $"Performed OCR pass on {guids.Count()} media items in {time}"); - } - - private void Process(Guid media, IMediaService mediaService) { - logger.LogDebug($"Performing OCR on media item {media}"); - - using var db = dbFactory.CreateDbContext(); - var m = db.Media - .Include(m => m.OcrData) - .First(m => m.Guid == media); - - OcrData o = m.OcrData ?? new(); - - using var engine = new TesseractEngine("tessdata", "eng", EngineMode.Default); - using var image = Pix.LoadFromFile(mediaService.GetPath(m)); - engine.SetVariable("debug_file", NullFile); - - o.Timestamp = DateTime.UtcNow; - o.Text = engine.Process(image).GetText().Trim(); - o.SearchableText = SpaceRegex.Replace(o.Text.ToLower(), " ").Trim(); - - m.OcrData = o; - db.SaveChanges(); - } - - private string NullFile { - get { - if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "NUL"; - else - return "/dev/null"; - } - } -} diff --git a/Services/TagService.cs b/Services/TagService.cs deleted file mode 100644 index f7b91dc..0000000 --- a/Services/TagService.cs +++ /dev/null @@ -1,305 +0,0 @@ -using HyperBooru.ApiModels; -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru.Services; - -public interface ITagService { - public void AddTag(Guid obj, Guid tagDef); - public void AddTag(HBObject obj, TagDefinition tagDef); - public void RemoveTag(Guid obj, Guid tagDef); - public void RemoveTag(HBObject obj, TagDefinition tagDef); - public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs); - public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs); - public void AddImplicitTag(Guid tagDef, Guid implicitTagDef); - public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); - public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef); - public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); - public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null); - public void DeleteTagDefinition(Guid tagDef); - public void DeleteTagDefinition(TagDefinition tagDef); - public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null); - public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null); - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj); - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj); - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef); - public TagDefinition[] TagsThatImply(Guid tagDef); - public TagDefinition[] TagsThatImply(TagDefinition tagDef); -} - -public class TagService : ITagService { - private IDbContextFactory<HBContext> dbFactory; - - public TagService(IDbContextFactory<HBContext> dbFactory) => - this.dbFactory = dbFactory; - - public void AddTag(Guid obj, Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions.First(td => td.Guid == tagDef); - - db.Objects - .Include(o => o.Tags) - .ThenInclude(t => t.TagDefinition) - .Where(o => !o.Tags.Select(t => t.TagDefinition.Guid).Contains(tagDef)) - .FirstOrDefault(o => o.Guid == obj)? - .Tags - .Add(new(tag)); - - db.SaveChanges(); - } - - public void AddTag(HBObject obj, TagDefinition tagDef) => - AddTag(obj.Guid, tagDef.Guid); - - public void RemoveTag(Guid obj, Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - db.Objects - .Include(o => o.Tags) - .ThenInclude(t => t.TagDefinition) - .First(o => o.Guid == obj) - .Tags - .RemoveAll(t => t.TagDefinition.Guid == tagDef); - - db.SaveChanges(); - } - - public void RemoveTag(HBObject obj, TagDefinition tagDef) => - RemoveTag(obj.Guid, tagDef.Guid); - - public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs) { - using var db = dbFactory.CreateDbContext(); - using var transaction = db.Database.BeginTransaction(); - - var tag = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef); - - tag.ImplicitTags.RemoveAll(td => !implicitTagDefs.Contains(td.Guid)); - tag.ImplicitTags.AddRange( - db.TagDefinitions - .Where(td => implicitTagDefs.Contains(td.Guid)) - .Where(td => !tag.ImplicitTags - .Select(td => td.Guid) - .Contains(td.Guid))); - - db.SaveChanges(); - transaction.Commit(); - } - - public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs) => - SetImplicitTags(tagDef.Guid, implicitTagDefs.Select(td => td.Guid).ToArray()); - - public void AddImplicitTag(Guid tagDef, Guid implicitTagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef); - var implicitTag = db.TagDefinitions.First(td => td.Guid == implicitTagDef); - - tag.ImplicitTags.Add(implicitTag); - db.SaveChanges(); - } - - public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) => - AddImplicitTag(tagDef, implicitTagDef); - - public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef); - - tag.ImplicitTags.RemoveAll(td => td.Guid == implicitTagDef); - db.SaveChanges(); - } - - public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) => - RemoveImplicitTag(tagDef, implicitTagDef); - - public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null) { - using var db = dbFactory.CreateDbContext(); - - if(string.IsNullOrEmpty(@namespace)) - @namespace = null; - if(string.IsNullOrEmpty(alias)) - alias = null; - - // Remove leading and trailing whitespace - name = name.Trim(); - @namespace = @namespace?.Trim(); - alias = alias?.Trim(); - - TagDefinition tagDef = new() { - Source = TagSource.UserTag, - Namespace = @namespace, - Name = name, - Alias = alias - }; - - bool nameExists = db.TagDefinitions.Any(td => td.Name.ToLower() == name.ToLower()); - bool aliasExists = false; - if(alias is not null) - aliasExists = db.TagDefinitions - .Where(td => td.Alias != null) - .Any(td => td.Alias!.ToLower() == alias.ToLower()); - if(nameExists || aliasExists) - throw new TagDuplicateException(nameExists, aliasExists); - - if(!db.TagDefinitions.Contains(tagDef)) - db.TagDefinitions.Add(tagDef); - db.SaveChanges(); - } - - public void DeleteTagDefinition(Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tag = db.TagDefinitions.First(td => td.Guid == tagDef); - - using var transaction = db.Database.BeginTransaction(); - - db.Tags.RemoveRange( - db.Tags - .Include(t => t.TagDefinition) - .Where(t => t.TagDefinition.Guid == tagDef)); - db.TagDefinitions.Remove(tag); - db.SaveChanges(); - - transaction.Commit(); - } - - public void DeleteTagDefinition(TagDefinition tagDef) => - DeleteTagDefinition(tagDef.Guid); - - public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null) { - using var db = dbFactory.CreateDbContext(); - - if(string.IsNullOrEmpty(@namespace)) - @namespace = null; - if(string.IsNullOrEmpty(alias)) - alias = null; - - // Remove leading and trailing whitespace - name = name.Trim(); - @namespace = @namespace?.Trim(); - alias = alias?.Trim(); - - var tag = db.TagDefinitions.First(td => td.Guid == tagDef); - - TagDefinition? nameExisting = db.TagDefinitions.FirstOrDefault(td => td.Name.ToLower() == name.ToLower()); - TagDefinition? aliasExisting = null; - if(alias is not null) - aliasExisting = db.TagDefinitions - .Where(td => td.Alias != null) - .FirstOrDefault(td => td.Alias!.ToLower() == alias.ToLower()); - bool nameExists = nameExisting is not null && nameExisting != tag; - bool aliasExists = aliasExisting is not null && aliasExisting != tag; - if(nameExists || aliasExists) - throw new TagDuplicateException(nameExists, aliasExists); - - tag.Name = name; - tag.Namespace = @namespace; - tag.Alias = alias; - - db.SaveChanges(); - } - - public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null) => - UpdateTagDefinition(tagDef.Guid, name, @namespace, alias); - - private (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(IEnumerable<TagDefinition> tagDefs) { - using var db = dbFactory.CreateDbContext(); - - var tagGuids = tagDefs - .Select(td => td.Guid) - .ToArray(); - - // Query all tag definitions - var allTags = db.TagDefinitions - .Include(td => td.ImplicitTags) - .ToArray(); - - var tags = new List<TagDefinition>( - allTags.IntersectBy( - tagGuids, - td => td.Guid)); - - while(true) { - var toAdd = tags - .SelectMany(td => td.ImplicitTags) - .ExceptBy(tags.Select(td => td.Guid), td => td.Guid) - .ToArray(); - - if(toAdd.Count() == 0) - break; - - tags.AddRange(toAdd); - } - - return tags - .Select(td => new ValueTuple<TagDefinition, bool>(td, !tagGuids.Contains(td.Guid))) - .ToArray(); - } - - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj) { - using var db = dbFactory.CreateDbContext(); - - // Query a list of tag GUIDs for this object - var tags = db.Objects - .Include(o => o.Tags) - .ThenInclude(t => t.TagDefinition) - .First(o => o.Guid == obj) - .Tags - .Select(t => t.TagDefinition) - .ToArray(); - - return GetAllTags(tags); - } - - - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj) => - GetAllTags(obj.Guid); - - public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tags = db.TagDefinitions - .Include(td => td.ImplicitTags) - .First(td => td.Guid == tagDef.Guid) - .ImplicitTags - .ToArray(); - - return GetAllTags(tags); - } - - public TagDefinition[] TagsThatImply(Guid tagDef) { - using var db = dbFactory.CreateDbContext(); - - var tagDefs = db.TagDefinitions - .Include(td => td.ImplicitTags) - .ToArray(); - - var tags = new List<TagDefinition>() { - db.TagDefinitions.First(td => td.Guid == tagDef) - }; - - while(true) { - var toAdd = tagDefs - .Where(td => td.ImplicitTags.Select(it => it.Guid).Intersect(tags.Select(td => td.Guid)).Any()) - .ExceptBy(tags.Select(td => td.Guid), td => td.Guid) - .ToArray(); - - if(toAdd.Count() == 0) - break; - - tags.AddRange(toAdd); - } - - return tags.ToArray(); - } - - public TagDefinition[] TagsThatImply(TagDefinition tagDef) => - TagsThatImply(tagDef.Guid); -} diff --git a/Services/UserService.cs b/Services/UserService.cs deleted file mode 100644 index 9e79dc6..0000000 --- a/Services/UserService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Cryptography.KeyDerivation; - -namespace HyperBooru.Services; - -public interface IUserService { - public UserSessionState UserSessionState { get; } -} - -public class UserService : IUserService { - public UserSessionState UserSessionState => - globalUserService.GetSessionState(httpContext.Session.Id); - - private IHttpContextAccessor httpContextAccessor; - private IGlobalUserService globalUserService; - - private HttpContext httpContext => - httpContextAccessor.HttpContext!; - - public UserService( - IHttpContextAccessor httpContextAccessor, - IGlobalUserService globalUserService) { - - this.httpContextAccessor = httpContextAccessor; - this.globalUserService = globalUserService; - - // HTTP context session states are discarded if no values - // are set. Set a dummy value so that the session state - // will not be discarded later when we actually need it. - httpContext.Session.SetInt32("Persist", 1); - } - - public static string HashPassword(string password) => - Convert.ToBase64String( - KeyDerivation.Pbkdf2( - password, - Array.Empty<byte>(), - KeyDerivationPrf.HMACSHA512, - 100_000, - 512 / 8)); -} - -public interface IGlobalUserService { - public UserSessionState GetSessionState(string id); -} - -public class GlobalUserService : IGlobalUserService { - // TODO: prune this list periodically - private Dictionary<string, UserSessionState> sessionStates = new(); - - public UserSessionState GetSessionState(string id) { - sessionStates.TryGetValue(id, out var state); - - if(state is null) { - state = new(); - sessionStates[id] = state; - } - - return state; - } -} - -public record UserSessionState { - public event UserSessionStateChange OnStateChange; - - public bool ShowNsfw { - get => showNsfw; - set { - showNsfw = value; - OnStateChange.Invoke(this); - } - } - - private bool showNsfw = false; -} - -public delegate void UserSessionStateChange(UserSessionState sessionState); @@ -1,34 +0,0 @@ -using HyperBooru.ApiModels; -using System.ComponentModel.DataAnnotations.Schema; - -namespace HyperBooru; - -public class TagDefinition : HBObject { - public TagSource Source { get; set; } = TagSource.Internal; - public string? Namespace { get; set; } - public string Name { get; set; } - public string? Alias { get; set; } - public virtual List<TagDefinition> ImplicitTags { get; set; } = new(); - - public static explicit operator ApiModels.TagDefinition(TagDefinition tagDefinition) => new() { - TagDefinitionId = tagDefinition.Guid, - Source = tagDefinition.Source, - Namespace = tagDefinition.Namespace, - Name = tagDefinition.Name, - Alias = tagDefinition.Alias, - ImplicitTags = tagDefinition.ImplicitTags.Select(td => td.Guid).ToArray() - }; -} - -public class Tag : HBObject { - [ForeignKey("ObjectId")] - public int TagDefinitionId { get; set; } - public virtual TagDefinition TagDefinition { get; set; } - public DateTime CreateTime { get; set; } = DateTime.UtcNow; - public virtual HBObject Target { get; set; } - - public Tag() {} - - public Tag(TagDefinition tagDef) => - this.TagDefinition = tagDef; -} diff --git a/Todo.md b/Todo.md deleted file mode 100644 index 612f60e..0000000 --- a/Todo.md +++ /dev/null @@ -1,45 +0,0 @@ -# WASM - - [ ] Sanitize controller inputs for empty strings - - [ ] Audit and fix namespaces (e.g. HyperBooru.Controllers -> HyperBooru.Server.Controllers) - -# Bugs - - [X] Images in the gallery on mobile can easily exceed screen width - - [X] Images smaller than the requested thumbnail size aren't delivered - - [X] Autocorrect needs to be disabled on inputs such as username, tag name, etc - - [X] Mobile menu does not automatically hide upon page navigation - - [X] Input not focused - - [ ] Setting implicit tags removes builtin tags - - [X] UserService listeners don't seem to be removed after disposal - - [X] Cancelling tag creation creates the tag anyway - - [ ] Prevent marking tagging complete unless there are actually user tags - - [X] Media upload not deduping media - - [ ] Can't delete media - -# Short-term Features - - [ ] Ability to set password (at least via API) - - [ ] PowerShell uploading with initial tagging - - [ ] Proper thumbnail generation - - [ ] Video support - - [ ] User/security support - - [X] Record in UploadedFiles whether the checksums was verified at upload time - - [X] Record in Media which UploadedFile actually holds the current content - -# Long-term Features - - [ ] Redirect to last page after login - - [ ] Enlarge image view in ViewMedia - - [ ] Periodic thumbnail scrubbing - - [ ] Loading animations - - [ ] Keyboard shortcuts - - [ ] Find source - - [ ] Collections - - [ ] Search memes by audio (for some reason) - - [ ] Jump into ingest feed at random point - - [ ] Rating system - - [ ] Instantaneous OCR processing when media is uploaded - - [ ] OCR status reporting on admin page - - [ ] Dynamically update OCR data on ViewMedia page - - [ ] Image deduplication by visual similarity - - [ ] Audit log - - [ ] Journaled operations - - [ ] Confirmation dialog before enabling NSFW mode - - [ ] Upload progress bars diff --git a/User.cs b/User.cs deleted file mode 100644 index 87384d2..0000000 --- a/User.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace HyperBooru; - -[Index(nameof(Username))] -public class User : HBObject { - public string Username { get; set; } - public string PasswordHash { get; set; } - - public static explicit operator ApiModels.User(User user) => - new() { - UserId = user.Guid, - Username = user.Username - }; -} diff --git a/tessdata/eng.traineddata b/tessdata/eng.traineddata Binary files differdeleted file mode 100644 index 176dc32..0000000 --- a/tessdata/eng.traineddata +++ /dev/null diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css deleted file mode 100644 index 21f9a94..0000000 --- a/wwwroot/css/site.css +++ /dev/null @@ -1,28 +0,0 @@ -#blazor-error-ui { - background: #555; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - -#blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 3.5rem; - top: 0.5rem; -} - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - color: white; - padding: 1rem 1rem 1rem 3.7rem; -} - -.blazor-error-boundary::after { - content: "An error has occurred." -} diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico Binary files differdeleted file mode 100644 index a1be4cc..0000000 --- a/wwwroot/favicon.ico +++ /dev/null diff --git a/wwwroot/icon-192.png b/wwwroot/icon-192.png Binary files differdeleted file mode 100644 index 28ce06d..0000000 --- a/wwwroot/icon-192.png +++ /dev/null diff --git a/wwwroot/icon-512.png b/wwwroot/icon-512.png Binary files differdeleted file mode 100644 index 8c28696..0000000 --- a/wwwroot/icon-512.png +++ /dev/null diff --git a/wwwroot/images/book.svg b/wwwroot/images/book.svg deleted file mode 100644 index 6cdfc79..0000000 --- a/wwwroot/images/book.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg width="800px" height="800px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.096"/> -
<g id="SVGRepo_iconCarrier"> <path d="M22 16.7399V4.66994C22 3.46994 21.02 2.57994 19.83 2.67994H19.77C17.67 2.85994 14.48 3.92994 12.7 5.04994L12.53 5.15994C12.24 5.33994 11.76 5.33994 11.47 5.15994L11.22 5.00994C9.44 3.89994 6.26 2.83994 4.16 2.66994C2.97 2.56994 2 3.46994 2 4.65994V16.7399C2 17.6999 2.78 18.5999 3.74 18.7199L4.03 18.7599C6.2 19.0499 9.55 20.1499 11.47 21.1999L11.51 21.2199C11.78 21.3699 12.21 21.3699 12.47 21.2199C14.39 20.1599 17.75 19.0499 19.93 18.7599L20.26 18.7199C21.22 18.5999 22 17.6999 22 16.7399Z" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 5.48999V20.49" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> <path d="M7.75 8.48999H5.5" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.5 11.49H5.5" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/images/checkmark.svg b/wwwroot/images/checkmark.svg deleted file mode 100644 index 5e55d9e..0000000 --- a/wwwroot/images/checkmark.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg width="800px" height="800px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff" stroke-width="0.00024000000000000003"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.048"/> -
<g id="SVGRepo_iconCarrier"> <path d="M17.8086 9.70558C18.1983 9.31421 18.1969 8.68105 17.8056 8.29137C17.4142 7.90169 16.781 7.90305 16.3913 8.29442L10.6215 14.0892L7.30211 10.816C6.90886 10.4283 6.27571 10.4327 5.88793 10.8259C5.50015 11.2192 5.50459 11.8524 5.89784 12.2401L9.92581 16.212C10.3177 16.5985 10.9482 16.5956 11.3366 16.2056L17.8086 9.70558Z" fill="#ffffff"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z" fill="#ffffff"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/images/cross.svg b/wwwroot/images/cross.svg deleted file mode 100644 index 0c37363..0000000 --- a/wwwroot/images/cross.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg width="800px" height="800px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff" stroke-width="0.00024000000000000003"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.096"/> -
<g id="SVGRepo_iconCarrier"> <path d="M7.75716 7.75736C8.14768 7.36683 8.78084 7.36683 9.17137 7.75736L11.9998 10.5858L14.8283 7.75736C15.2188 7.36684 15.852 7.36684 16.2425 7.75736C16.6331 8.14789 16.6331 8.78105 16.2425 9.17158L13.4141 12L16.2424 14.8284C16.633 15.2189 16.633 15.8521 16.2424 16.2426C15.8519 16.6332 15.2187 16.6332 14.8282 16.2426L11.9998 13.4143L9.17146 16.2426C8.78094 16.6332 8.14777 16.6332 7.75725 16.2426C7.36672 15.8521 7.36672 15.219 7.75725 14.8284L10.5856 12L7.75716 9.17157C7.36663 8.78104 7.36663 8.14788 7.75716 7.75736Z" fill="#ffffff"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3Z" fill="#ffffff"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/images/edit.svg b/wwwroot/images/edit.svg deleted file mode 100644 index d4c6ec4..0000000 --- a/wwwroot/images/edit.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 32.00 32.00" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff" stroke-width="0.00032"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.064"/> -
<g id="SVGRepo_iconCarrier"> <title>alt-square-pencil</title> <path d="M0 26.016v-20q0-2.496 1.76-4.256t4.256-1.76h14.688l-4.032 4h-10.656q-0.832 0-1.44 0.608t-0.576 1.408v20q0 0.832 0.576 1.408t1.44 0.576h20q0.8 0 1.408-0.576t0.576-1.408v-10.688l4-4v14.688q0 2.496-1.76 4.224t-4.224 1.76h-20q-2.496 0-4.256-1.76t-1.76-4.224zM6.016 26.016l2.112-7.84 12.256-12.192 5.728 5.568-12.32 12.288zM22.112 4.256l3.072-3.072q1.152-1.184 2.816-1.184t2.816 1.184 1.184 2.816-1.184 2.848l-2.976 2.976z"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/images/info.svg b/wwwroot/images/info.svg deleted file mode 100644 index b194f05..0000000 --- a/wwwroot/images/info.svg +++ /dev/null @@ -1,63 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> - -<svg - width="256" - height="255.99988" - viewBox="-2 -2 24 23.999989" - fill="none" - stroke="#ffffff" - stroke-width="0.0002" - version="1.1" - id="svg7" - sodipodi:docname="info-svgrepo-com.svg" - inkscape:version="1.1 (c68e22c387, 2021-05-23)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> - <defs - id="defs11" /> - <sodipodi:namedview - id="namedview9" - pagecolor="#505050" - bordercolor="#ffffff" - borderopacity="1" - inkscape:pageshadow="0" - inkscape:pageopacity="0" - inkscape:pagecheckerboard="1" - showgrid="false" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" - inkscape:zoom="3.25" - inkscape:cx="96" - inkscape:cy="120.76923" - inkscape:window-width="1920" - inkscape:window-height="1017" - inkscape:window-x="1272" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg7" /> - <g - id="SVGRepo_bgCarrier" - stroke-width="0" - transform="translate(-2.9999,-2.9999039)" /> - <g - id="SVGRepo_tracerCarrier" - stroke-linecap="round" - stroke-linejoin="round" - stroke="#cccccc" - stroke-width="0.08" - transform="translate(-2.9999,-2.9999039)" /> - <g - id="SVGRepo_iconCarrier" - transform="matrix(1.3333185,0,0,1.3333185,-3.3331852,-3.3331904)"> - <path - fill="#ffffff" - fill-rule="evenodd" - d="M 10,3 A 7,7 0 1 0 10,17 7,7 0 0 0 10,3 Z M 1,10 A 9,9 0 1 1 19,10 9,9 0 0 1 1,10 Z M 9,6 a 1,1 0 0 1 1,-1 h 0.01 a 1,1 0 1 1 0,2 H 10 A 1,1 0 0 1 9,6 Z m 0.01,8 a 1,1 0 1 0 2,0 V 9 a 1,1 0 1 0 -2,0 z" - id="path4" /> - </g> -</svg> diff --git a/wwwroot/images/loginbg.webp b/wwwroot/images/loginbg.webp Binary files differdeleted file mode 100644 index 759e666..0000000 --- a/wwwroot/images/loginbg.webp +++ /dev/null diff --git a/wwwroot/images/photo.svg b/wwwroot/images/photo.svg deleted file mode 100644 index 486c360..0000000 --- a/wwwroot/images/photo.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 32.00 32.00" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff" stroke-width="0.00032"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.128"/> -
<g id="SVGRepo_iconCarrier"> <title>image</title> <path d="M0 26.016q0 2.496 1.76 4.224t4.256 1.76h20q2.464 0 4.224-1.76t1.76-4.224v-20q0-2.496-1.76-4.256t-4.224-1.76h-20q-2.496 0-4.256 1.76t-1.76 4.256v20zM4 26.016v-20q0-0.832 0.576-1.408t1.44-0.608h20q0.8 0 1.408 0.608t0.576 1.408v20q0 0.832-0.576 1.408t-1.408 0.576h-20q-0.832 0-1.44-0.576t-0.576-1.408zM6.016 24q0 0.832 0.576 1.44t1.408 0.576h16q0.832 0 1.408-0.576t0.608-1.44v-0.928q-0.224-0.448-1.12-2.688t-1.6-3.584-1.28-2.112q-0.544-0.576-1.12-0.608t-1.152 0.384-1.152 1.12-1.184 1.568-1.152 1.696-1.152 1.6-1.088 1.184-1.088 0.448q-0.576 0-1.664-1.44-0.16-0.192-0.48-0.608-1.12-1.504-1.6-1.824-0.768-0.512-1.184 0.352-0.224 0.512-0.928 2.24t-1.056 2.56v0.64zM6.016 9.024q0 1.248 0.864 2.112t2.112 0.864 2.144-0.864 0.864-2.112-0.864-2.144-2.144-0.864-2.112 0.864-0.864 2.144z"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/images/tag.svg b/wwwroot/images/tag.svg deleted file mode 100644 index 3eb8843..0000000 --- a/wwwroot/images/tag.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg width="800px" height="800px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.096"/> -
<g id="SVGRepo_iconCarrier"> <path d="M7.0498 7.0498H7.0598M10.5118 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V10.5118C3 11.2455 3 11.6124 3.08289 11.9577C3.15638 12.2638 3.27759 12.5564 3.44208 12.8249C3.6276 13.1276 3.88703 13.387 4.40589 13.9059L9.10589 18.6059C10.2939 19.7939 10.888 20.388 11.5729 20.6105C12.1755 20.8063 12.8245 20.8063 13.4271 20.6105C14.112 20.388 14.7061 19.7939 15.8941 18.6059L18.6059 15.8941C19.7939 14.7061 20.388 14.112 20.6105 13.4271C20.8063 12.8245 20.8063 12.1755 20.6105 11.5729C20.388 10.888 19.7939 10.2939 18.6059 9.10589L13.9059 4.40589C13.387 3.88703 13.1276 3.6276 12.8249 3.44208C12.5564 3.27759 12.2638 3.15638 11.9577 3.08289C11.6124 3 11.2455 3 10.5118 3ZM7.5498 7.0498C7.5498 7.32595 7.32595 7.5498 7.0498 7.5498C6.77366 7.5498 6.5498 7.32595 6.5498 7.0498C6.5498 6.77366 6.77366 6.5498 7.0498 6.5498C7.32595 6.5498 7.5498 6.77366 7.5498 7.0498Z" stroke="#ffffff" stroke-width="1.9200000000000004" stroke-linecap="round" stroke-linejoin="round"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/images/trash.svg b/wwwroot/images/trash.svg deleted file mode 100644 index 18ff9c1..0000000 --- a/wwwroot/images/trash.svg +++ /dev/null @@ -1,7 +0,0 @@ -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --> -<svg width="800px" height="800px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg"> -
<g id="SVGRepo_bgCarrier" stroke-width="0"/> -
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.192"/> -
<g id="SVGRepo_iconCarrier"> <path d="M10 12L14 16M14 12L10 16M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6M18 6V16.2C18 17.8802 18 18.7202 17.673 19.362C17.3854 19.9265 16.9265 20.3854 16.362 20.673C15.7202 21 14.8802 21 13.2 21H10.8C9.11984 21 8.27976 21 7.63803 20.673C7.07354 20.3854 6.6146 19.9265 6.32698 19.362C6 18.7202 6 17.8802 6 16.2V6" stroke="#ffffff" stroke-width="1.56" stroke-linecap="round" stroke-linejoin="round"/> </g> -
</svg>
\ No newline at end of file diff --git a/wwwroot/js/dialog.js b/wwwroot/js/dialog.js deleted file mode 100644 index 418962f..0000000 --- a/wwwroot/js/dialog.js +++ /dev/null @@ -1,78 +0,0 @@ -function dialogMouseDown(e) { - bumpDialog(e.currentTarget); -} - -function dialogTitleMouseDown(e) { - e = e || window.event; - e.preventDefault(); - var element = e.currentTarget.parentElement; - var ds = element.dataset; - ds.lastX = e.clientX; - ds.lastY = e.clientY; - - window.dragDialog = element; - document.onmouseup = dragMouseUp; - document.onmousemove = dragMouseMove; -} - -function dragMouseUp() { - window.dragDialog = null; - document.onmouseup = null; - document.onmousemove = null; -} - -function dragMouseMove(e) { - e = e || window.event; - e.preventDefault(); - var element = window.dragDialog; - var ds = element.dataset; - deltaX = ds.lastX - e.clientX; - deltaY = ds.lastY - e.clientY; - ds.lastX = e.clientX; - ds.lastY = e.clientY; - element.style.left = (element.offsetLeft - deltaX) + 'px'; - element.style.top = (element.offsetTop - deltaY) + 'px'; -} - -function setDialogVisibility(element, visible) { - if(visible) { - element.style.left = null; - element.style.top = null; - element.style.opacity = 1; - element.style.visibility = 'visible'; - bumpDialog(element); - - var input = element.querySelector('input[type="text"]'); - if(input) { - setTimeout(() => input.focus(), 100); - } - } else { - element.style.opacity = 0; - element.style.visibility = 'hidden'; - } -} - -function bumpDialog(element) { - var dialogs = Array - .from(document.querySelectorAll('div.dialog')) - .map(e => ({ zIndex: parseInt(e.style.zIndex), element: e })) - .sort((a, b) => a.zIndex - b.zIndex) - .map(d => d.element) - .filter(e => e != element); - - dialogs.push(element); - - var z = 900; - for(var d of dialogs) - d.style.zIndex = z++; -} - -function dialogAddObjectReference(element, dialogObject) { - if(!window.dialogObjects) - window.dialogObjects = [] - - window.dialogObjects.push({ - element: element, - dialogObject: dialogObject - }); -} diff --git a/wwwroot/js/keyboard.js b/wwwroot/js/keyboard.js deleted file mode 100644 index 8b46639..0000000 --- a/wwwroot/js/keyboard.js +++ /dev/null @@ -1,57 +0,0 @@ -async function keyDownHandler(e) { - function isDialogChild(e) { - while(e = e.parentElement) - if(e.tagName == 'DIV' && e.classList.contains('dialog')) - return true; - return false; - } - - var tag = document.activeElement.tagName; - if((tag == 'INPUT' || (tag == 'TEXTAREA' && e.ctrlKey)) && e.key == 'Enter') { - var element = document.activeElement; - while(element = element.parentElement) { - if(element.tagName == 'FORM') { - element - .querySelectorAll('input,textarea') - .forEach(e => e.dispatchEvent(new Event('change'))); - element.requestSubmit(); - e.preventDefault(); - return; - } - } - } - - if((tag == 'INPUT' || tag == 'TEXTAREA') && e.key != 'Escape') - return; - - var element = Array.from(document.querySelectorAll('div.dialog')) - .filter(e => e.style.visibility == 'visible') - .map(e => ({ element: e, zIndex: parseInt(e.style.zIndex) })) - .sort((a, b) => b.zIndex - a.zIndex) - .map(e => e.element)[0]; - - if(element) { - await window.dialogObjects - .find(d => d.element == element) - .dialogObject - .invokeMethodAsync('KeyHandler', e.key); - e.preventDefault(); - return; - } - - var button = Array.from(document.getElementsByTagName('button')) - .filter(b => typeof(b.dataset.keyboardShortcut) == 'string') - .filter(b => !isDialogChild(b)) - .find(b => b.dataset.keyboardShortcut == e.key); - - if(!e.ctrlKey && button) { - button.click(); - e.preventDefault(); - return; - } - - if(typeof pageKeyDownHandler == 'function') - pageKeyDownHandler(e); -} - -window.onload = () => document.onkeydown = keyDownHandler;
\ No newline at end of file diff --git a/wwwroot/js/mobile.js b/wwwroot/js/mobile.js deleted file mode 100644 index 0af11cc..0000000 --- a/wwwroot/js/mobile.js +++ /dev/null @@ -1,7 +0,0 @@ -function hideMobileMenu() { - document.getElementsByTagName('body')[0].classList.remove('mobile-menu-visible'); -} - -function toggleMobileMenu() { - document.getElementsByTagName('body')[0].classList.toggle('mobile-menu-visible'); -} diff --git a/wwwroot/loginbg.webm b/wwwroot/loginbg.webm Binary files differdeleted file mode 100644 index 139ed0d..0000000 --- a/wwwroot/loginbg.webm +++ /dev/null diff --git a/wwwroot/manifest.webmanifest b/wwwroot/manifest.webmanifest deleted file mode 100644 index f150f98..0000000 --- a/wwwroot/manifest.webmanifest +++ /dev/null @@ -1,6 +0,0 @@ -{ - "icons": [ - { "src": "/icon-192.png", "type": "images/png", "sizes": "192x192" }, - { "src": "/icon-512.png", "type": "images/png", "sizes": "512x512" } - ] -}
\ No newline at end of file diff --git a/wwwroot/styles/data-table.css b/wwwroot/styles/data-table.css deleted file mode 100644 index 994d625..0000000 --- a/wwwroot/styles/data-table.css +++ /dev/null @@ -1,21 +0,0 @@ -table.data-table { - border-collapse: collapse; - width: 100%; -} - -table.data-table > tr > th { - border-bottom: 1px solid white; - padding: 4px; -} - -table.data-table > tr > td { - padding: 4px; -} - -table.data-table > tr:nth-child(2n) { - background: rgba(255, 255, 255, 0.1); -} - -table.data-table > tr > td:not(:last-child) { - border-right: 1px solid white; -} diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css deleted file mode 100644 index 9de9fc1..0000000 --- a/wwwroot/styles/global.css +++ /dev/null @@ -1,214 +0,0 @@ -@import url('data-table.css'); - -:root { - --col-accent-pri: #0aa; - --col-accent-pri-hl: #0cc; - --col-error-pri: #ffaa00; - --col-checksum-verified-pri: #8dff76; - --col-bg: #222; - --col-dialog-bg: #333; - --col-navbar-bg: var(--col-accent-pri); - --col-button-pri: var(--col-accent-pri); - --col-button-pri-hl: var(--col-accent-pri-hl); - --col-button-disabled: #777; - --col-button-disabled-bg: #444; - --col-button-sec: #555; - --col-button-sec-hl: #777; - --col-button-sec-disabled: #555; - --col-button-sec-disabled-bg: #000; - --col-button-warning: #ff4848; - --col-button-warning-hl: #ff9999; - --col-hr: #888; - --col-scrollbar: #666; - --col-scrollbar-hover: #aaaaaa; - --col-switch-bg: var(--col-bg); - --col-switch-fg: #fff; - --col-switch-bg-hl: var(--col-accent-pri); - --col-progbar-fg: var(--col-accent-pri); - --col-progbar-bg: #777; - - --size-default-gap: 30px; -} - -::selection { - background: var(--col-accent-pri); -} - -body { - background: var(--col-bg); - color: white; - display: flex; - flex-direction: column; - font-family: 'Trebuchet MS', 'Lucida Sans Unicode'; - height: 100dvh; - margin: 0; - overflow: hidden; - width: 100dvw; -} - -a { - color: var(--col-accent-pri); - text-decoration: none; -} - -@media (hover: hover) { - a:hover { - filter: brightness(1.5); - } -} - -a::selection { - background: var(--col-accent-pri); - color: #fff; -} - -a.nondecorated { - color: #fff; -} - -@media (hover: hover) { - a.nondecorated:hover { - color: #999; - } -} - -code { - background: #222; - border-radius: 10px; - box-sizing: border-box; - font-family: 'Lucida Console'; - font-size: 8pt; - overflow-y: auto; - padding: 20px; - white-space: pre-line; -} - -button, input[type=submit] { - align-items: center; - background: var(--col-button-pri); - border-radius: 10px; - border: none; - box-sizing: border-box; - color: white; - cursor: pointer; - display: flex; - height: 30px; - margin: 10px 5px 0 5px; - padding: 0 9px 0 9px; - user-select: none; -} - -button:disabled { - color: var(--col-button-disabled) !important; - background: var(--col-button-disabled-bg) !important; -} - -button.warning { - background: var(--col-button-warning); -} - -button > img { - height: 15px; - margin-right: 5px; - width: 15px; -} - -@media (hover: none) and (pointer: coarse) { - button > :not(:first-child) { - display: none; - } - - button > img { - height: 20px; - margin-right: 0; - padding: 8px; - width: 20px; - } -} - -@media (hover: hover) { - button.warning:hover { - background: var(--col-button-warning-hl); - } -} - -button.warning:active { - color: var(--col-button-warning); - background: white; -} - -button.secondary { - background: var(--col-button-sec); -} - -@media (hover: hover) { - button.secondary:hover { - background: var(--col-button-sec-hl); - } -} - -button.secondary:active { - background: white; - color: var(--col-button-sec); -} - -button.secondary:disabled { - color: var(--col-button-sec-disabled) !important; - background: var(--col-button-sec-disabled-bg) !important; -} - -@media (hover: hover) { - button:hover, input[type=submit]:hover { - background: var(--col-button-pri-hl); - } -} - -button:active, input[type=submit]:active { - background: white; - color: var(--col-button-pri); -} - -input, textarea { - background: rgba(0, 0, 0, 0); - border-radius: 5px; - border: 1px solid #aaa; - box-sizing: border-box; - color: white; - margin-bottom: 10px; -} - -input { - height: 25px !important; -} - -/* disable hotkey underlines on mobile devices */ -@media (hover: none) and (pointer: coarse) { - button > u { - text-decoration: none !important; - } -} - -/* necessary for use inside flex containers */ -hr { - width: 100%; -} - -::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -::-webkit-scrollbar-thumb { - background: var(--col-scrollbar); - border-radius: 10px; -} - -@media (hover: hover) { - ::-webkit-scrollbar-thumb:hover { - background: var(--col-scrollbar-hover); - } -} - -::-webkit-scrollbar-corner { - opacity: 0; -} |
