diff --git a/Pages/Folder.cshtml b/Pages/Folder.cshtml
new file mode 100644
index 0000000..7ec3a11
--- /dev/null
+++ b/Pages/Folder.cshtml
@@ -0,0 +1,151 @@
+@page
+@model FolderModel
+@{
+ ViewData["Title"] = Model.Folder?.Title ?? "Folder";
+}
+
+
+
+
+
@Model.Folder?.Title
+ @if (Model.Folder is not null)
+ {
+
@Model.Folder.Date.ToString("yyyy-MM-dd") · @Model.Folder.Author · @((Model.Folder.FileCount ?? 0).ToString()) files
+ }
+ @if (Model.Keywords.Count > 0)
+ {
+
+ @foreach (var tag in Model.Keywords)
+ {
+
@tag
+ }
+
+ }
+
+
Back
+
+
+ @if (Model.Folder is null)
+ {
+
Folder not found.
+ }
+ else if (Model.Images.Count == 0)
+ {
+
No images found.
+ }
+ else
+ {
+
+ @foreach (var image in Model.Images)
+ {
+
+
+
+ }
+
+ }
+
+
+
+
+
+
+
+
![image]()
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/Pages/Folder.cshtml.cs b/Pages/Folder.cshtml.cs
new file mode 100644
index 0000000..d880968
--- /dev/null
+++ b/Pages/Folder.cshtml.cs
@@ -0,0 +1,134 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.EntityFrameworkCore;
+using Sakayaki.Models;
+using Sakayaki.Services;
+
+namespace Sakayaki.Pages;
+
+public class FolderModel(AppDbContext dbContext, ThumbnailService thumbnailService, IConfiguration configuration) : PageModel
+{
+ private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"
+ };
+
+ private readonly AppDbContext _dbContext = dbContext;
+ private readonly ThumbnailService _thumbnailService = thumbnailService;
+ private readonly string? _rootPath = configuration["Fanbox:RootPath"];
+
+ public sealed record ImageItem(string FileName, string Url, string ThumbnailUrl);
+
+ public FanboxFolder? Folder { get; private set; }
+ public IReadOnlyList Keywords { get; private set; } = Array.Empty();
+ public IReadOnlyList Images { get; private set; } = Array.Empty();
+ public string? Keyword { get; private set; }
+ public int? ReturnPage { get; private set; }
+
+ public string BackUrl =>
+ Url.Page("Index", null, new { keyword = Keyword, p = ReturnPage }) ?? Url.Page("Index") ?? "/";
+
+ public async Task OnGetAsync(Guid id, string? keyword, int? p)
+ {
+ Keyword = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
+ ReturnPage = p is > 1 ? p : null;
+
+ Folder = await _dbContext.FanboxFolders.AsNoTracking()
+ .FirstOrDefaultAsync(x => x.Id == id);
+ if (Folder is null)
+ {
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(Folder.Keywords))
+ {
+ Keywords = Folder.Keywords
+ .Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .ToArray();
+ }
+
+ if (string.IsNullOrWhiteSpace(_rootPath))
+ {
+ return;
+ }
+
+ var folderPath = Path.Combine(_rootPath, Folder.FolderName);
+ if (!Directory.Exists(folderPath))
+ {
+ return;
+ }
+
+ var images = Directory.GetFiles(folderPath)
+ .Where(path => ImageExtensions.Contains(Path.GetExtension(path)))
+ .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
+ .Select(path =>
+ {
+ var fileName = Path.GetFileName(path);
+ var url = Url.Page("Folder", "Image", new { id = Folder.Id, file = fileName }) ?? string.Empty;
+ var thumbnailUrl = Url.Page("Folder", "Thumbnail", new { id = Folder.Id, file = fileName }) ?? string.Empty;
+ return new ImageItem(fileName, url, thumbnailUrl);
+ })
+ .ToArray();
+
+ Images = images;
+ }
+
+ public async Task OnGetImageAsync(Guid id, string file)
+ {
+ if (string.IsNullOrWhiteSpace(_rootPath) || string.IsNullOrWhiteSpace(file))
+ return NotFound();
+
+ var safeFile = Path.GetFileName(file);
+ if (string.IsNullOrWhiteSpace(safeFile))
+ return NotFound();
+ if (!ImageExtensions.Contains(Path.GetExtension(safeFile)))
+ return NotFound();
+
+ var folder = await _dbContext.FanboxFolders.AsNoTracking()
+ .FirstOrDefaultAsync(x => x.Id == id);
+ if (folder is null)
+ return NotFound();
+
+ var filePath = Path.Combine(_rootPath, folder.FolderName, safeFile);
+ if (!System.IO.File.Exists(filePath))
+ return NotFound();
+
+ var provider = new FileExtensionContentTypeProvider();
+ if (!provider.TryGetContentType(filePath, out var contentType))
+ contentType = "application/octet-stream";
+
+ return PhysicalFile(filePath, contentType);
+ }
+
+ public async Task OnGetThumbnailAsync(Guid id, string file)
+ {
+ if (string.IsNullOrWhiteSpace(file))
+ return NotFound();
+
+ var safeFile = Path.GetFileName(file);
+ if (string.IsNullOrWhiteSpace(safeFile))
+ return NotFound();
+ if (!ImageExtensions.Contains(Path.GetExtension(safeFile)))
+ return NotFound();
+
+ var folder = await _dbContext.FanboxFolders.AsNoTracking()
+ .FirstOrDefaultAsync(x => x.Id == id);
+ if (folder is null)
+ return NotFound();
+
+ var thumbPath = _thumbnailService.GetThumbnailPath(folder.Id, safeFile);
+ if (string.IsNullOrWhiteSpace(thumbPath))
+ return NotFound();
+
+ if (!System.IO.File.Exists(thumbPath))
+ {
+ await _thumbnailService.EnsureThumbnailAsync(folder.Id, folder.FolderName, safeFile, HttpContext.RequestAborted);
+ }
+
+ if (!System.IO.File.Exists(thumbPath))
+ return NotFound();
+
+ return PhysicalFile(thumbPath, "image/jpeg");
+ }
+}
diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml
index 4c75cf9..a3bc793 100644
--- a/Pages/Index.cshtml
+++ b/Pages/Index.cshtml
@@ -30,7 +30,7 @@
@Model.StatusMessage
}
-
+
@if (Model.Folders.Count == 0)
{
@@ -43,14 +43,10 @@
{
var item = card.Folder;
-
+
}
}
@@ -78,7 +74,7 @@