Compare commits

..

7 Commits

Author SHA1 Message Date
11da2335cd fix: use sort=latest for home page books and series queries
All checks were successful
Build, Push & Deploy / deploy (push) Successful in 6m48s
Use the API's sort parameter to explicitly request most recently added
items. Simplify latest series fetch by using /series?sort=latest instead
of N+1 calls per library.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:45:57 +01:00
feceb61e30 fix: increase header logo text size on mobile for better readability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:34:32 +01:00
701a02b55c fix: prevent iOS auto-zoom on input focus by overriding Tailwind text-sm
Move the 16px font-size rule outside @layer base and scope it to iOS
via @supports (-webkit-touch-callout: none) so it takes priority over
Tailwind utility classes without affecting desktop rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:32:54 +01:00
b2664cce08 fix: reset zoom on orientation change in reader to prevent iOS auto-zoom
Temporarily inject maximum-scale=1 into viewport meta tag on orientation
change to cancel the automatic zoom iOS Safari applies, then restore
it to keep pinch-zoom available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:29:54 +01:00
ff44a781c8 fix: add tolerance threshold for zoom detection to prevent swipe breakage
After pinch-zoom then de-zoom, visualViewport.scale may not return
exactly to 1.0, blocking swipe navigation. Use 1.05 threshold instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:26:05 +01:00
d535f9f28e fix: respect RTL direction for reader arrow buttons and swipe navigation
Arrow buttons now swap next/previous in RTL mode. Swipe navigation
receives isRTL from parent state instead of creating its own independent
copy, so toggling direction takes effect immediately without reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:24:11 +01:00
2174579cc1 fix: hide "mark as unread" button on unread books for Stripstream provider
Return null for readProgress when Stripstream book status is "unread"
with no current page, aligning behavior with Komga provider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:19:20 +01:00
7 changed files with 50 additions and 42 deletions

View File

@@ -51,7 +51,7 @@ export function Header({
<div className="mr-2 flex items-center md:mr-4"> <div className="mr-2 flex items-center md:mr-4">
<a className="mr-2 flex items-center md:mr-6" href="/"> <a className="mr-2 flex items-center md:mr-6" href="/">
<span className="inline-flex flex-col leading-none"> <span className="inline-flex flex-col leading-none">
<span className="bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500 bg-clip-text text-sm font-bold tracking-[0.06em] text-transparent sm:text-lg sm:tracking-[0.08em]"> <span className="bg-gradient-to-r from-primary via-cyan-500 to-fuchsia-500 bg-clip-text text-base font-bold tracking-[0.06em] text-transparent sm:text-lg sm:tracking-[0.08em]">
StripStream StripStream
</span> </span>
<span className="mt-1 hidden text-[10px] font-medium uppercase tracking-[0.22em] text-foreground/70 sm:inline"> <span className="mt-1 hidden text-[10px] font-medium uppercase tracking-[0.22em] text-foreground/70 sm:inline">

View File

@@ -76,13 +76,30 @@ export function PhotoswipeReader({ book, pages, onClose, nextBook }: BookReaderP
onPreviousPage: handlePreviousPage, onPreviousPage: handlePreviousPage,
onNextPage: handleNextPage, onNextPage: handleNextPage,
pswpRef, pswpRef,
isRTL,
}); });
// Activer le zoom dans le reader en enlevant la classe no-pinch-zoom // Activer le zoom dans le reader en enlevant la classe no-pinch-zoom
// et reset le zoom lors des changements d'orientation (iOS applique un zoom automatique)
useEffect(() => { useEffect(() => {
document.body.classList.remove("no-pinch-zoom"); document.body.classList.remove("no-pinch-zoom");
const handleOrientationChange = () => {
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
const original = viewport.getAttribute("content") || "";
viewport.setAttribute("content", original + ", maximum-scale=1");
// Restaurer après que iOS ait appliqué le nouveau layout
requestAnimationFrame(() => {
viewport.setAttribute("content", original);
});
}
};
window.addEventListener("orientationchange", handleOrientationChange);
return () => { return () => {
window.removeEventListener("orientationchange", handleOrientationChange);
document.body.classList.add("no-pinch-zoom"); document.body.classList.add("no-pinch-zoom");
}; };
}, []); }, []);

View File

@@ -173,7 +173,7 @@ export const ControlButtons = ({
icon={ChevronLeft} icon={ChevronLeft}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onPreviousPage(); direction === "rtl" ? onNextPage() : onPreviousPage();
}} }}
tooltip={t("reader.controls.previousPage")} tooltip={t("reader.controls.previousPage")}
iconClassName="h-8 w-8" iconClassName="h-8 w-8"
@@ -193,7 +193,7 @@ export const ControlButtons = ({
icon={ChevronRight} icon={ChevronRight}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onNextPage(); direction === "rtl" ? onPreviousPage() : onNextPage();
}} }}
tooltip={t("reader.controls.nextPage")} tooltip={t("reader.controls.nextPage")}
iconClassName="h-8 w-8" iconClassName="h-8 w-8"

View File

@@ -1,31 +1,27 @@
import { useCallback, useRef, useEffect } from "react"; import { useCallback, useRef, useEffect } from "react";
import { useReadingDirection } from "./useReadingDirection";
interface UseTouchNavigationProps { interface UseTouchNavigationProps {
onPreviousPage: () => void; onPreviousPage: () => void;
onNextPage: () => void; onNextPage: () => void;
pswpRef: React.MutableRefObject<unknown>; pswpRef: React.MutableRefObject<unknown>;
isRTL: boolean;
} }
export function useTouchNavigation({ export function useTouchNavigation({
onPreviousPage, onPreviousPage,
onNextPage, onNextPage,
pswpRef, pswpRef,
isRTL,
}: UseTouchNavigationProps) { }: UseTouchNavigationProps) {
const { isRTL } = useReadingDirection();
const touchStartXRef = useRef<number | null>(null); const touchStartXRef = useRef<number | null>(null);
const touchStartYRef = useRef<number | null>(null); const touchStartYRef = useRef<number | null>(null);
const isPinchingRef = useRef(false); const isPinchingRef = useRef(false);
// Helper pour vérifier si la page est zoomée (zoom natif du navigateur) // Helper pour vérifier si la page est zoomée (zoom natif du navigateur)
const isZoomed = useCallback(() => { const isZoomed = useCallback(() => {
// Utiliser visualViewport.scale pour détecter le zoom natif
// Si scale > 1, la page est zoomée
if (window.visualViewport) { if (window.visualViewport) {
return window.visualViewport.scale > 1; return window.visualViewport.scale > 1.05;
} }
// Fallback pour les navigateurs qui ne supportent pas visualViewport
// Comparer la taille de la fenêtre avec la taille réelle
return window.innerWidth !== window.screen.width; return window.innerWidth !== window.screen.width;
}, []); }, []);

View File

@@ -34,7 +34,10 @@ export class StripstreamAdapter {
volume: book.volume ?? null, volume: book.volume ?? null,
pageCount: book.page_count ?? 0, pageCount: book.page_count ?? 0,
thumbnailUrl: `/api/stripstream/images/books/${book.id}/thumbnail`, thumbnailUrl: `/api/stripstream/images/books/${book.id}/thumbnail`,
readProgress: { readProgress:
book.reading_status === "unread" && !book.reading_current_page
? null
: {
page: book.reading_current_page ?? null, page: book.reading_current_page ?? null,
completed: book.reading_status === "read", completed: book.reading_status === "read",
lastReadAt: book.reading_last_read_at ?? null, lastReadAt: book.reading_last_read_at ?? null,
@@ -52,7 +55,10 @@ export class StripstreamAdapter {
volume: book.volume ?? null, volume: book.volume ?? null,
pageCount: book.page_count ?? 0, pageCount: book.page_count ?? 0,
thumbnailUrl: `/api/stripstream/images/books/${book.id}/thumbnail`, thumbnailUrl: `/api/stripstream/images/books/${book.id}/thumbnail`,
readProgress: { readProgress:
book.reading_status === "unread" && !book.reading_current_page
? null
: {
page: book.reading_current_page ?? null, page: book.reading_current_page ?? null,
completed: book.reading_status === "read", completed: book.reading_status === "read",
lastReadAt: book.reading_last_read_at ?? null, lastReadAt: book.reading_last_read_at ?? null,

View File

@@ -222,11 +222,11 @@ export class StripstreamProvider implements IMediaProvider {
async getHomeData(): Promise<HomeData> { async getHomeData(): Promise<HomeData> {
const homeOpts = { revalidate: CACHE_TTL_MED, tags: [HOME_CACHE_TAG] }; const homeOpts = { revalidate: CACHE_TTL_MED, tags: [HOME_CACHE_TAG] };
const [ongoingBooksResult, ongoingSeriesResult, booksPage, libraries] = await Promise.allSettled([ const [ongoingBooksResult, ongoingSeriesResult, booksPage, latestSeriesResult] = await Promise.allSettled([
this.client.fetch<StripstreamBookItem[]>("books/ongoing", { limit: "20" }, homeOpts), this.client.fetch<StripstreamBookItem[]>("books/ongoing", { limit: "20" }, homeOpts),
this.client.fetch<StripstreamSeriesItem[]>("series/ongoing", { limit: "10" }, homeOpts), this.client.fetch<StripstreamSeriesItem[]>("series/ongoing", { limit: "10" }, homeOpts),
this.client.fetch<StripstreamBooksPage>("books", { limit: "10" }, homeOpts), this.client.fetch<StripstreamBooksPage>("books", { sort: "latest", limit: "10" }, homeOpts),
this.client.fetch<StripstreamLibraryResponse[]>("libraries", undefined, { revalidate: CACHE_TTL_LONG, tags: [HOME_CACHE_TAG] }), this.client.fetch<StripstreamSeriesPage>("series", { sort: "latest", limit: "10" }, homeOpts),
]); ]);
// /books/ongoing returns both currently reading and next unread per series // /books/ongoing returns both currently reading and next unread per series
@@ -242,23 +242,9 @@ export class StripstreamProvider implements IMediaProvider {
? booksPage.value.items.map(StripstreamAdapter.toNormalizedBook) ? booksPage.value.items.map(StripstreamAdapter.toNormalizedBook)
: []; : [];
let latestSeries: NormalizedSeries[] = []; const latestSeries = latestSeriesResult.status === "fulfilled"
if (libraries.status === "fulfilled" && libraries.value.length > 0) { ? latestSeriesResult.value.items.map(StripstreamAdapter.toNormalizedSeries)
const allSeriesResults = await Promise.allSettled( : [];
libraries.value.map((lib) =>
this.client.fetch<StripstreamSeriesPage>(
`libraries/${lib.id}/series`,
{ limit: "10" },
homeOpts
)
)
);
latestSeries = allSeriesResults
.filter((r): r is PromiseFulfilledResult<StripstreamSeriesPage> => r.status === "fulfilled")
.flatMap((r) => r.value.items)
.map(StripstreamAdapter.toNormalizedSeries)
.slice(0, 10);
}
return { return {
ongoing: ongoingSeries, ongoing: ongoingSeries,

View File

@@ -118,11 +118,14 @@ body.no-pinch-zoom * {
font-family: var(--font-ui); font-family: var(--font-ui);
} }
/* Empêche le zoom automatique iOS sur les inputs */ }
/* Empêche le zoom automatique iOS sur les inputs (hors @layer pour surcharger text-sm) */
@supports (-webkit-touch-callout: none) {
input, input,
textarea, textarea,
select { select {
font-size: 16px; font-size: 16px !important;
} }
} }