diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 3a22dfa91..d6c0d4516 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -172,8 +172,13 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: try: mimetype = _mime_for_path(fullpath) if os.path.exists(fullpath): + if fullpath.endswith(".css"): + # make changes to css files immediately reflected in the webview + max_age = 10 + else: + max_age = 60 * 60 return flask.send_file( - fullpath, mimetype=mimetype, conditional=True, max_age=60 * 60 # type: ignore[call-arg] + fullpath, mimetype=mimetype, conditional=True, max_age=max_age # type: ignore[call-arg] ) else: print(f"Not found: {path}") diff --git a/ts/reviewer/css.ts b/ts/reviewer/css.ts new file mode 100644 index 000000000..3829d71b4 --- /dev/null +++ b/ts/reviewer/css.ts @@ -0,0 +1,49 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +type CssElementType = HTMLStyleElement | HTMLLinkElement; + +const preloadCssClassName = "preload-css"; +const template = document.createElement("template"); + +export async function maybePreloadExternalCss(html: string): Promise { + clearPreloadedCss(); + template.innerHTML = html; + const externalCssElements = extractExternalCssElements(template.content); + if (externalCssElements.length) { + await Promise.race([ + Promise.all(externalCssElements.map(injectAndLoadCss)), + new Promise((r) => setTimeout(r, 500)), + ]); + } +} + +function clearPreloadedCss(): void { + [...document.head.getElementsByClassName(preloadCssClassName)].forEach((css) => + css.remove(), + ); +} + +function extractExternalCssElements(fragment: DocumentFragment): CssElementType[] { + return ( + [...fragment.querySelectorAll("style, link")].filter( + (css) => + (css instanceof HTMLStyleElement && + css.innerHTML.includes("@import")) || + (css instanceof HTMLLinkElement && css.rel === "stylesheet"), + ) + ); +} + +function injectAndLoadCss(css: CssElementType): Promise { + return new Promise((resolve) => { + css.classList.add(preloadCssClassName); + + // this prevents the css from affecting the page rendering + css.media = "print"; + + css.addEventListener("load", () => resolve()); + css.addEventListener("error", () => resolve()); + document.head.appendChild(css); + }); +} diff --git a/ts/reviewer/index.ts b/ts/reviewer/index.ts index b2b335515..5aca4410e 100644 --- a/ts/reviewer/index.ts +++ b/ts/reviewer/index.ts @@ -21,6 +21,7 @@ globalThis.anki.mutateNextCardStates = mutateNextCardStates; import { bridgeCommand } from "../lib/bridgecommand"; import { allImagesLoaded, preloadAnswerImages } from "./images"; +import { maybePreloadExternalCss } from "./css"; declare const MathJax: any; type Callback = () => void | Promise; @@ -113,6 +114,9 @@ export async function _updateQA( const qa = document.getElementById("qa")!; + // prevent flash of unstyled content when external css used + await maybePreloadExternalCss(html); + qa.style.opacity = "0"; try {