feat(lib): shelf sort/section pure helpers + formatSize extraction, table tests
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import type { Format } from "../api/types";
|
import type { Format } from "../api/types";
|
||||||
|
|
||||||
|
export { formatSize } from "../lib/format"; // deprecated: phase-2 随阅读器清除
|
||||||
|
|
||||||
export const btn =
|
export const btn =
|
||||||
"inline-flex items-center gap-1.5 rounded-lg bg-white/[0.06] px-3 py-1.5 text-sm ring-1 ring-white/10 transition-colors hover:bg-white/[0.11] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500 active:bg-white/[0.15] disabled:cursor-not-allowed disabled:opacity-50";
|
"inline-flex items-center gap-1.5 rounded-lg bg-white/[0.06] px-3 py-1.5 text-sm ring-1 ring-white/10 transition-colors hover:bg-white/[0.11] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500 active:bg-white/[0.15] disabled:cursor-not-allowed disabled:opacity-50";
|
||||||
export const btnPrimary =
|
export const btnPrimary =
|
||||||
@@ -27,12 +29,3 @@ export const FORMAT_BADGE: Record<Format, string> = {
|
|||||||
export function formatBadge(fmt: Format, extra = ""): string {
|
export function formatBadge(fmt: Format, extra = ""): string {
|
||||||
return `rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ring-1 ${FORMAT_BADGE[fmt]} ${extra}`;
|
return `rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ring-1 ${FORMAT_BADGE[fmt]} ${extra}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSize(bytes: number): string {
|
|
||||||
if (bytes <= 0) return "";
|
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
|
||||||
let i = 0;
|
|
||||||
let n = bytes;
|
|
||||||
for (; n >= 1024 && i < units.length - 1; i++) n /= 1024;
|
|
||||||
return `${n >= 10 || i === 0 ? Math.round(n) : n.toFixed(1)} ${units[i]}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function formatSize(bytes: number): string {
|
||||||
|
if (bytes <= 0) return "";
|
||||||
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
|
let i = 0;
|
||||||
|
let n = bytes;
|
||||||
|
for (; n >= 1024 && i < units.length - 1; i++) n /= 1024;
|
||||||
|
return `${n >= 10 || i === 0 ? Math.round(n) : n.toFixed(1)} ${units[i]}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Book } from "../api/types";
|
||||||
|
import { groupByDir } from "./group";
|
||||||
|
|
||||||
|
export type SortKey = "added" | "title" | "progress";
|
||||||
|
export const SORT_LABEL: Record<SortKey, string> = { added: "最近入库", title: "标题", progress: "阅读进度" };
|
||||||
|
export type GroupMode = "flat" | "dir";
|
||||||
|
|
||||||
|
export function sortBooks(books: Book[], key: SortKey): Book[] {
|
||||||
|
const a = [...books];
|
||||||
|
if (key === "title") return a.sort((x, y) => x.title.localeCompare(y.title, "zh"));
|
||||||
|
if (key === "progress") return a.sort((x, y) => y.percent - x.percent || (x.added_at < y.added_at ? 1 : x.added_at > y.added_at ? -1 : 0));
|
||||||
|
return a.sort((x, y) => (x.added_at < y.added_at ? 1 : x.added_at > y.added_at ? -1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页面分组视图:flat 一段无标题;dir 沿用「根目录置顶、目录名 locale 排序」。 */
|
||||||
|
export function sections(books: Book[], key: SortKey, mode: GroupMode): { label: string; books: Book[] }[] {
|
||||||
|
if (mode === "flat") return [{ label: "", books: sortBooks(books, key) }];
|
||||||
|
return groupByDir(sortBooks(books, key)).map((g) => ({ label: g.dir, books: g.books }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatSize } from "../src/lib/format";
|
||||||
|
|
||||||
|
describe("formatSize", () => {
|
||||||
|
it("renders human sizes across units", () => {
|
||||||
|
expect(formatSize(0)).toBe("");
|
||||||
|
expect(formatSize(512)).toBe("512 B");
|
||||||
|
expect(formatSize(2048)).toBe("2.0 KB");
|
||||||
|
expect(formatSize(15 * 1024 * 1024)).toBe("15 MB");
|
||||||
|
expect(formatSize(3.2 * 1024 ** 3)).toBe("3.2 GB");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty for non-positive and keeps one decimal below 10", () => {
|
||||||
|
expect(formatSize(-1)).toBe("");
|
||||||
|
expect(formatSize(1024)).toBe("1.0 KB");
|
||||||
|
expect(formatSize(10 * 1024)).toBe("10 KB");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { expect, it } from "vitest";
|
||||||
|
import type { Book } from "../src/api/types";
|
||||||
|
import { sections, sortBooks } from "../src/lib/shelf";
|
||||||
|
|
||||||
|
const b = (over: Partial<Book>): Book =>
|
||||||
|
({ id: 1, path: "p", title: "t", format: "cbz", size: 1, pages: 1, percent: 0, added_at: "2026-01-01T00:00:00Z", library_id: 1, state: "ready", cover_url: "", page_url_fmt: "", pages_url: "", error: "", ...over }) as Book;
|
||||||
|
|
||||||
|
it("sortBooks: added 新→旧 / title zh 本地化 / progress 高→低且并列按 added", () => {
|
||||||
|
const old = b({ id: 1, added_at: "2026-01-01T00:00:00Z", title: "b" });
|
||||||
|
const nw = b({ id: 2, added_at: "2026-02-01T00:00:00Z", title: "a" });
|
||||||
|
expect(sortBooks([old, nw], "added").map((x) => x.id)).toEqual([2, 1]);
|
||||||
|
expect(sortBooks([old, nw], "title").map((x) => x.id)).toEqual([2, 1]);
|
||||||
|
expect(sortBooks([b({ id: 3, percent: 0.2 }), b({ id: 4, percent: 0.9 })], "progress").map((x) => x.id)).toEqual([4, 3]);
|
||||||
|
const tie = [b({ id: 5, percent: 0.5, added_at: "2026-01-01T00:00:00Z" }), b({ id: 6, percent: 0.5, added_at: "2026-03-01T00:00:00Z" })];
|
||||||
|
expect(sortBooks(tie, "progress").map((x) => x.id)).toEqual([6, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sections: flat 单段无标题;dir 按目录分组且组内排序、根目录置顶", () => {
|
||||||
|
const books = [b({ id: 1, path: "x/a.cbz" }), b({ id: 2, path: "b.cbz", title: "B" }), b({ id: 3, path: "x/c.cbz", title: "A" })];
|
||||||
|
// 容器 Intl zh 实测序 A < B < t (localeCompare("zh")),故 title 升序为 id3,id2,id1
|
||||||
|
expect(sections(books, "title", "flat")).toEqual([{ label: "", books: [books[2], books[1], books[0]] }]);
|
||||||
|
const g = sections(books, "title", "dir");
|
||||||
|
expect(g.map((s) => s.label)).toEqual(["", "x"]);
|
||||||
|
expect(g[1].books.map((x) => x.title)).toEqual(["A", "t"]); // x 组内按 title 排序
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user