inject sakura css into cached html
This commit is contained in:
@@ -1,8 +1,17 @@
|
|||||||
import type { User } from "@markone/core/user"
|
import type { User } from "@markone/core/user"
|
||||||
import type { Bookmark } from "@markone/core/bookmark"
|
import { Readability } from "@mozilla/readability"
|
||||||
|
import { JSDOM } from "jsdom"
|
||||||
import { db } from "~/database.ts"
|
import { db } from "~/database.ts"
|
||||||
import { DEMO_BOOKMARKS } from "./demo-bookmarks.ts"
|
import { DEMO_BOOKMARKS } from "./demo-bookmarks.ts"
|
||||||
|
|
||||||
|
class LinkUnreachable {}
|
||||||
|
class UnsupportedLink {}
|
||||||
|
|
||||||
|
interface CachedPage {
|
||||||
|
title: string
|
||||||
|
readableHtml: string
|
||||||
|
}
|
||||||
|
|
||||||
function insertDemoBookmarks(user: User) {
|
function insertDemoBookmarks(user: User) {
|
||||||
const query = db.query(`
|
const query = db.query(`
|
||||||
INSERT OR IGNORE INTO bookmarks (id, user_id, kind, title, url)
|
INSERT OR IGNORE INTO bookmarks (id, user_id, kind, title, url)
|
||||||
@@ -32,4 +41,49 @@ function findBookmarkHtml(id: string, user: User): string | null {
|
|||||||
return content_html
|
return content_html
|
||||||
}
|
}
|
||||||
|
|
||||||
export { insertDemoBookmarks, findBookmarkHtml }
|
async function cacheWebsite(url: string): Promise<CachedPage | null> {
|
||||||
|
const websiteText = await fetch(url)
|
||||||
|
.catch(() => {
|
||||||
|
throw new LinkUnreachable()
|
||||||
|
})
|
||||||
|
.then((res) => res.text())
|
||||||
|
.catch(() => {
|
||||||
|
throw new UnsupportedLink()
|
||||||
|
})
|
||||||
|
|
||||||
|
const dom = new JSDOM(websiteText, { url })
|
||||||
|
const reader = new Readability(dom.window.document)
|
||||||
|
const article = reader.parse()
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (article.content) {
|
||||||
|
const newDom = new JSDOM(article.content, { url })
|
||||||
|
const doc = newDom.window.document
|
||||||
|
|
||||||
|
const lightStyleLink = doc.createElement("link")
|
||||||
|
lightStyleLink.rel = "stylesheet"
|
||||||
|
lightStyleLink.href = "/reader-styles/sakura.css"
|
||||||
|
lightStyleLink.media = "screen"
|
||||||
|
|
||||||
|
const darkStyleLink = doc.createElement("link")
|
||||||
|
darkStyleLink.rel = "stylesheet"
|
||||||
|
darkStyleLink.href = "/reader-styles/sakura-dark.css"
|
||||||
|
darkStyleLink.media = "screen and (prefers-color-scheme: dark)"
|
||||||
|
|
||||||
|
doc.head.appendChild(lightStyleLink)
|
||||||
|
doc.head.appendChild(darkStyleLink)
|
||||||
|
|
||||||
|
article.content = newDom.serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: article.title || "Untitled",
|
||||||
|
readableHtml: article.content || "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { insertDemoBookmarks, findBookmarkHtml, cacheWebsite }
|
||||||
|
export { LinkUnreachable, UnsupportedLink }
|
||||||
|
@@ -5,9 +5,7 @@ import { ulid } from "ulid"
|
|||||||
import { db } from "~/database.ts"
|
import { db } from "~/database.ts"
|
||||||
import { HttpError } from "~/error.ts"
|
import { HttpError } from "~/error.ts"
|
||||||
import type { User } from "~/user/user.ts"
|
import type { User } from "~/user/user.ts"
|
||||||
import { JSDOM } from "jsdom"
|
import { LinkUnreachable, UnsupportedLink, findBookmarkHtml, cacheWebsite } from "./bookmark.ts"
|
||||||
import { Readability } from "@mozilla/readability"
|
|
||||||
import { findBookmarkHtml } from "./bookmark.ts"
|
|
||||||
|
|
||||||
const BOOKMARK_PAGINATION_LIMIT = 100
|
const BOOKMARK_PAGINATION_LIMIT = 100
|
||||||
|
|
||||||
@@ -66,17 +64,19 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
|
|||||||
throw new HttpError(400)
|
throw new HttpError(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const websiteResponse = await fetch(body.url).catch(() => {
|
const cachedWebsite = await cacheWebsite(body.url).catch((error) => {
|
||||||
if (body.force) {
|
if (error instanceof LinkUnreachable) {
|
||||||
return null
|
if (body.force) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
throw new HttpError(400, "LinkUnreachable")
|
||||||
}
|
}
|
||||||
throw new HttpError(400, "WebsiteUnreachable")
|
if (error instanceof UnsupportedLink) {
|
||||||
|
throw new HttpError(400, "UnsupportedLink")
|
||||||
|
}
|
||||||
|
console.error(error)
|
||||||
|
throw new HttpError(500)
|
||||||
})
|
})
|
||||||
const websiteText = websiteResponse
|
|
||||||
? await websiteResponse.text().catch(() => {
|
|
||||||
throw new HttpError(400, "UnsupportedWebsite")
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
|
|
||||||
const bookmark: LinkBookmark = {
|
const bookmark: LinkBookmark = {
|
||||||
kind: "link",
|
kind: "link",
|
||||||
@@ -88,24 +88,8 @@ async function addBookmark(request: Bun.BunRequest<"/api/bookmarks">, user: User
|
|||||||
|
|
||||||
if (body.title) {
|
if (body.title) {
|
||||||
bookmark.title = body.title
|
bookmark.title = body.title
|
||||||
}
|
} else if (cachedWebsite?.title) {
|
||||||
|
bookmark.title = cachedWebsite.title
|
||||||
let contentHtml: string
|
|
||||||
if (websiteText) {
|
|
||||||
const dom = new JSDOM(websiteText, {
|
|
||||||
url: body.url,
|
|
||||||
})
|
|
||||||
const reader = new Readability(dom.window.document)
|
|
||||||
const article = reader.parse()
|
|
||||||
if (!bookmark.title) {
|
|
||||||
bookmark.title = article?.title || "Untitled"
|
|
||||||
}
|
|
||||||
contentHtml = article?.content || ""
|
|
||||||
} else {
|
|
||||||
contentHtml = ""
|
|
||||||
if (!bookmark.title) {
|
|
||||||
bookmark.title = "Untitled"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = db.query(`
|
const query = db.query(`
|
||||||
@@ -118,7 +102,7 @@ VALUES ($id, $userId, $kind, $title, $url, $html)
|
|||||||
kind: bookmark.kind,
|
kind: bookmark.kind,
|
||||||
title: bookmark.title,
|
title: bookmark.title,
|
||||||
url: bookmark.url,
|
url: bookmark.url,
|
||||||
html: contentHtml,
|
html: cachedWebsite?.readableHtml ?? "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const insertTagQuery = db.query(`
|
const insertTagQuery = db.query(`
|
||||||
|
@@ -16,8 +16,8 @@ body {
|
|||||||
line-height: 1.618;
|
line-height: 1.618;
|
||||||
max-width: 38em;
|
max-width: 38em;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: #c9c9c9;
|
color: #e7e5e4;
|
||||||
background-color: #222222;
|
background-color: #1c1917;
|
||||||
padding: 13px;
|
padding: 13px;
|
||||||
}
|
}
|
||||||
|
|
@@ -1,14 +1,15 @@
|
|||||||
/* Sakura.css v1.5.0
|
/* Sakura.css v1.5.0
|
||||||
* ================
|
* ================
|
||||||
* Minimal css theme.
|
* Minimal css theme.
|
||||||
|
* Colors have been slightly modified to match the rest of the website.
|
||||||
* Project: https://github.com/oxalorg/sakura/
|
* Project: https://github.com/oxalorg/sakura/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
html {
|
html {
|
||||||
font-size: 62.5%;
|
font-size: 62.5%;
|
||||||
font-family:
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
Arial, "Noto Sans", sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -16,8 +17,8 @@ body {
|
|||||||
line-height: 1.618;
|
line-height: 1.618;
|
||||||
max-width: 38em;
|
max-width: 38em;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: #4a4a4a;
|
color: #292524;
|
||||||
background-color: #f9f9f9;
|
background-color: #f5f5f4;
|
||||||
padding: 13px;
|
padding: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,9 +39,8 @@ h4,
|
|||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
font-family:
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
Arial, "Noto Sans", sans-serif;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
@@ -12,8 +12,6 @@ import { FormField } from "~/components/form-field"
|
|||||||
import { LoadingSpinner } from "~/components/loading-spinner"
|
import { LoadingSpinner } from "~/components/loading-spinner"
|
||||||
import { useMnemonics } from "~/hooks/use-mnemonics"
|
import { useMnemonics } from "~/hooks/use-mnemonics"
|
||||||
import { useLogOut } from "~/auth"
|
import { useLogOut } from "~/auth"
|
||||||
import sakuraCssSrc from "~/reader-styles/sakura.css?url"
|
|
||||||
import sakuraDarkCssSrc from "~/reader-styles/sakura-dark.css?url"
|
|
||||||
|
|
||||||
const LAYOUT_MODE = {
|
const LAYOUT_MODE = {
|
||||||
popup: "popup",
|
popup: "popup",
|
||||||
@@ -153,8 +151,8 @@ function Main({ children }: React.PropsWithChildren) {
|
|||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"grid flex justify-center relative w-full",
|
"relative w-full",
|
||||||
isPreviewOpened && layoutMode === LAYOUT_MODE.sideBySide ? "grid-cols-3" : "grid-cols-1",
|
isPreviewOpened && layoutMode === LAYOUT_MODE.sideBySide ? "grid grid-cols-3" : "flex justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -166,7 +164,12 @@ function BookmarkListPane() {
|
|||||||
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("flex flex-col py-16", { "md:flex-row lg:py-32 justify-center ": !isBookmarkPreviewOpened })}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col py-16",
|
||||||
|
isBookmarkPreviewOpened ? "w-full" : "container max-w-3xl md:flex-row lg:py-32",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<header className="mb-4 md:mb-0 md:mr-16 text-start">
|
<header className="mb-4 md:mb-0 md:mr-16 text-start">
|
||||||
<h1 className={clsx("font-bold text-start", { "mb-4": isBookmarkPreviewOpened })}>
|
<h1 className={clsx("font-bold text-start", { "mb-4": isBookmarkPreviewOpened })}>
|
||||||
<span
|
<span
|
||||||
@@ -179,7 +182,9 @@ function BookmarkListPane() {
|
|||||||
YOUR BOOKMARKS
|
YOUR BOOKMARKS
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<BookmarkListSection />
|
<div className="flex-1">
|
||||||
|
<BookmarkListSection />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -393,7 +398,7 @@ function BookmarkListSection() {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case "pending":
|
case "pending":
|
||||||
return (
|
return (
|
||||||
<p className="container max-w-2xl">
|
<p>
|
||||||
Loading
|
Loading
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</p>
|
</p>
|
||||||
@@ -468,7 +473,7 @@ function BookmarkList({ bookmarks }: { bookmarks: LinkBookmark[] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col container max-w-2xl -mt-2">
|
<div className="flex flex-col -mt-2">
|
||||||
{bookmarks.length === 0 ? (
|
{bookmarks.length === 0 ? (
|
||||||
<p className="mt-2">You have not saved any bookmark!</p>
|
<p className="mt-2">You have not saved any bookmark!</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -495,15 +500,15 @@ function BookmarkPreview() {
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BookmarkPreviewFrame />
|
<BookmarkPreviewContent />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BookmarkPreviewFrame() {
|
function BookmarkPreviewContent() {
|
||||||
const selectedBookmarkId = useBookmarkPageStore((state) => state.selectedBookmarkId)
|
const selectedBookmarkId = useBookmarkPageStore((state) => state.selectedBookmarkId)
|
||||||
const actionBarHeight = useBookmarkPageStore((state) => state.actionBarHeight)
|
const actionBarHeight = useBookmarkPageStore((state) => state.actionBarHeight)
|
||||||
const { data, status } = useAuthenticatedQuery(["bookmarks", selectedBookmarkId], () =>
|
const { data, status } = useAuthenticatedQuery(["bookmarks", `${selectedBookmarkId}.html`], () =>
|
||||||
fetchApi(`/bookmark/${selectedBookmarkId}`, {
|
fetchApi(`/bookmark/${selectedBookmarkId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "text/html",
|
Accept: "text/html",
|
||||||
@@ -511,23 +516,6 @@ function BookmarkPreviewFrame() {
|
|||||||
}).then((res) => res.text()),
|
}).then((res) => res.text()),
|
||||||
)
|
)
|
||||||
|
|
||||||
function injectCss(event: React.SyntheticEvent<HTMLIFrameElement, Element>) {
|
|
||||||
const lightCssLink = document.createElement("link")
|
|
||||||
lightCssLink.rel = "stylesheet"
|
|
||||||
lightCssLink.href = sakuraCssSrc
|
|
||||||
lightCssLink.media = "screen"
|
|
||||||
|
|
||||||
const darkCssLink = document.createElement("link")
|
|
||||||
darkCssLink.rel = "stylesheet"
|
|
||||||
darkCssLink.href = sakuraDarkCssSrc
|
|
||||||
darkCssLink.media = "screen and (prefers-color-scheme: dark)"
|
|
||||||
|
|
||||||
if (event.currentTarget.contentDocument) {
|
|
||||||
event.currentTarget.contentDocument.head.appendChild(lightCssLink)
|
|
||||||
event.currentTarget.contentDocument.head.appendChild(darkCssLink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "pending":
|
case "pending":
|
||||||
return (
|
return (
|
||||||
@@ -537,7 +525,13 @@ function BookmarkPreviewFrame() {
|
|||||||
)
|
)
|
||||||
case "success":
|
case "success":
|
||||||
return (
|
return (
|
||||||
<iframe className="w-full h-full" style={{ paddingBottom: actionBarHeight }} srcDoc={data} onLoad={injectCss} />
|
<iframe
|
||||||
|
key="preview-iframe"
|
||||||
|
title="asd"
|
||||||
|
className={clsx("w-full h-full")}
|
||||||
|
style={{ paddingBottom: actionBarHeight }}
|
||||||
|
srcDoc={data}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
36
packages/web/src/hooks/use-color-scheme.ts
Normal file
36
packages/web/src/hooks/use-color-scheme.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
enum ColorScheme {
|
||||||
|
Light = "Light",
|
||||||
|
Dark = "Dark",
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialColorScheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches
|
||||||
|
? ColorScheme.Dark
|
||||||
|
: ColorScheme.Light
|
||||||
|
|
||||||
|
function useColorScheme() {
|
||||||
|
const [colorScheme, setColorScheme] = useState(initialColorScheme)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.matchMedia) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMediaChange(this: MediaQueryList) {
|
||||||
|
setColorScheme(this.matches ? ColorScheme.Dark : ColorScheme.Light)
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
onMediaChange.call(l)
|
||||||
|
l.addEventListener("change", onMediaChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
l.removeEventListener("change", onMediaChange)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return colorScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ColorScheme, useColorScheme }
|
@@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
Reference in New Issue
Block a user