Compare commits

...

22 Commits

Author SHA1 Message Date
daniel31x13 72266d1cd5 final touch 2024-02-18 11:07:50 -05:00
daniel31x13 7b7b979b20 refactored import and add support for subcollections 2024-02-17 20:08:34 -05:00
Daniel c3c74b8162 Merge pull request #472 from IsaacWise06/fix/imports
Importing sub-collections fix
2024-02-18 04:33:28 +03:30
Isaac Wise 0e60dee47d Subcollections when importing 2024-02-15 11:26:42 -06:00
daniel31x13 c3f72c4be8 minor cleanup 2024-02-14 10:35:59 -05:00
daniel31x13 e3d9912378 fixed the imports 2024-02-10 02:47:58 -05:00
daniel31x13 c78aa2da0d minor improvement 2024-02-08 08:48:22 -05:00
Daniel aef55d65a1 Merge pull request #459 from IsaacWise06/issue/367
feat(links): Allow users to choose what happens when they click a link
2024-02-08 17:15:41 +03:30
daniel31x13 efddd55841 change the checkboxes to radio button 2024-02-08 08:45:14 -05:00
daniel31x13 f7a53d53e2 fix update collection bug 2024-02-08 08:25:45 -05:00
Isaac Wise ef08edf1fb Verify the preference is available 2024-02-08 00:59:17 -06:00
Isaac Wise 39261de45e rename function 2024-02-08 00:44:41 -06:00
Isaac Wise cc915c8a64 Allow users to choose what clicking links opens 2024-02-08 00:42:58 -06:00
daniel31x13 7d9cc1f1f0 added "linksRouteTo" field to the prisma schema 2024-02-07 10:30:09 -05:00
daniel31x13 b06cb7c379 merged the appearance and archive page into preference 2024-02-07 10:20:25 -05:00
Daniel d5bd095827 Merge pull request #456 from IsaacWise06/issue/334
feat(collections): Allow a contributor to pin a link from a collection to their dashboard
2024-02-07 18:19:15 +03:30
daniel31x13 daed2d82f4 minor improvements 2024-02-07 09:48:40 -05:00
Daniel 39e022f87b Merge pull request #457 from linkwarden/feat/sub-collections
Feat/sub collections
2024-02-07 01:17:09 +03:30
Isaac Wise 2d0093172a Allow contributors to pin a link in a shared to a collection to their dashboard 2024-02-04 23:43:59 -06:00
Daniel 34e0115a0f Merge pull request #445 from jan-tee/main
Added env var switch to support screen captures from HTTPS sites with untrusted certificates
2024-02-03 16:36:01 +03:30
Jan T ae3cf104b7 Added environment variable "IGNORE_SSL_ERRORS" to instruct playwright/Chromium to ignore SSL errors; this is useful to support generation of browser screenshots from sources with self-signed certificates or untrusted CAs, but also opens the possibility to index sites with rejected certificates; so it should not be enabled as a default behavior. 2024-01-29 09:49:50 +01:00
daniel31x13 047e156cfb updated version number 2024-01-17 13:02:44 -05:00
21 changed files with 549 additions and 357 deletions
+1
View File
@@ -20,6 +20,7 @@ MAX_LINKS_PER_USER=
ARCHIVE_TAKE_COUNT=
BROWSER_TIMEOUT=
IGNORE_UNAUTHORIZED_CA=
IGNORE_HTTPS_ERRORS=
# AWS S3 Settings
SPACES_KEY=
+2 -4
View File
@@ -14,8 +14,8 @@ import Image from "next/image";
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
import Link from "next/link";
import LinkIcon from "./LinkComponents/LinkIcon";
import LinkGroupedIconURL from "./LinkComponents/LinkGroupedIconURL";
import useOnScreen from "@/hooks/useOnScreen";
import { generateLinkHref } from "@/lib/client/generateLinkHref";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@@ -26,8 +26,6 @@ type Props = {
export default function LinkGrid({
link,
count,
className,
flipDropdown,
}: Props) {
const { collections } = useCollectionStore();
@@ -88,7 +86,7 @@ export default function LinkGrid({
className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative"
>
<Link
href={link.url || ""}
href={generateLinkHref(link)}
target="_blank"
className="rounded-2xl cursor-pointer"
>
@@ -80,22 +80,20 @@ export default function LinkActions({
<i title="More" className="bi-three-dots text-xl" />
</div>
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1 translate-y-10">
{permissions === true ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
pinLink();
}}
>
{link?.pinnedBy && link.pinnedBy[0]
? "Unpin"
: "Pin to Dashboard"}
</div>
</li>
) : undefined}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
pinLink();
}}
>
{link?.pinnedBy && link.pinnedBy[0]
? "Unpin"
: "Pin to Dashboard"}
</div>
</li>
{linkInfo !== undefined && toggleShowInfo ? (
<li>
<div
+2 -2
View File
@@ -18,7 +18,7 @@ type Props = {
className?: string;
};
export default function LinkGrid({ link, count, className }: Props) {
export default function LinkGrid({ link }: Props) {
const { collections } = useCollectionStore();
const { links } = useLinkStore();
@@ -101,7 +101,7 @@ export default function LinkGrid({ link, count, className }: Props) {
</div>
<LinkActions
toggleShowInfo={() => {}}
toggleShowInfo={() => { }}
linkInfo={false}
link={link}
collection={collection}
+6 -9
View File
@@ -12,6 +12,7 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection
import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon";
import Link from "next/link";
import { isPWA } from "@/lib/client/utils";
import { generateLinkHref } from "@/lib/client/generateLinkHref";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@@ -22,12 +23,9 @@ type Props = {
export default function LinkCardCompact({
link,
count,
className,
flipDropdown,
}: Props) {
const { collections } = useCollectionStore();
const { links } = useLinkStore();
let shortendURL;
@@ -58,12 +56,11 @@ export default function LinkCardCompact({
return (
<>
<div
className={`border-neutral-content relative ${
!showInfo && !isPWA() ? "hover:bg-base-300 p-3" : "py-3"
} duration-200 rounded-lg`}
className={`border-neutral-content relative ${!showInfo && !isPWA() ? "hover:bg-base-300 p-3" : "py-3"
} duration-200 rounded-lg`}
>
<Link
href={link.url || ""}
href={generateLinkHref(link)}
target="_blank"
className="flex items-start cursor-pointer"
>
@@ -102,8 +99,8 @@ export default function LinkCardCompact({
collection={collection}
position="top-3 right-3"
flipDropdown={flipDropdown}
// toggleShowInfo={() => setShowInfo(!showInfo)}
// linkInfo={showInfo}
// toggleShowInfo={() => setShowInfo(!showInfo)}
// linkInfo={showInfo}
/>
{showInfo ? (
<div>
+5 -18
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
export default function SettingsSidebar({ className }: { className?: string }) {
const LINKWARDEN_VERSION = "v2.4.8";
const LINKWARDEN_VERSION = "v2.4.9";
const { collections } = useCollectionStore();
@@ -37,30 +37,17 @@ export default function SettingsSidebar({ className }: { className?: string }) {
</div>
</Link>
<Link href="/settings/appearance">
<Link href="/settings/preference">
<div
className={`${
active === `/settings/appearance`
active === `/settings/preference`
? "bg-primary/20"
: "hover:bg-neutral/20"
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
>
<i className="bi-palette text-primary text-2xl"></i>
<i className="bi-sliders text-primary text-2xl"></i>
<p className="truncate w-full pr-7">Appearance</p>
</div>
</Link>
<Link href="/settings/archive">
<div
className={`${
active === `/settings/archive`
? "bg-primary/20"
: "hover:bg-neutral/20"
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
>
<i className="bi-archive text-primary text-2xl"></i>
<p className="truncate w-full pr-7">Archive</p>
<p className="truncate w-full pr-7">Preference</p>
</div>
</Link>
+5 -1
View File
@@ -21,7 +21,11 @@ const BROWSER_TIMEOUT = Number(process.env.BROWSER_TIMEOUT) || 5;
export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
const browser = await chromium.launch();
const context = await browser.newContext(devices["Desktop Chrome"]);
const context = await browser.newContext({
...devices["Desktop Chrome"],
ignoreHTTPSErrors:
process.env.IGNORE_HTTPS_ERRORS === "true",
});
const page = await context.newPage();
const timeoutPromise = new Promise((_, reject) => {
@@ -51,17 +51,18 @@ export default async function updateCollection(
where: {
id: collectionId,
},
data: {
name: data.name.trim(),
description: data.description,
color: data.color,
isPublic: data.isPublic,
parent: {
connect: {
id: data.parentId || undefined,
},
},
parent: data.parentId
? {
connect: {
id: data.parentId,
},
}
: undefined,
members: {
create: data.members.map((e) => ({
user: { connect: { id: e.user.id || e.userId } },
@@ -28,6 +28,36 @@ export default async function updateLinkById(
const unauthorizedSwitchCollection =
!isCollectionOwner && collectionIsAccessible?.id !== data.collection.id;
const canPinPermission = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId
);
// If the user is able to create a link, they can pin it to their dashboard only.
if (canPinPermission) {
const updatedLink = await prisma.link.update({
where: {
id: linkId,
},
data: {
pinnedBy:
data?.pinnedBy && data.pinnedBy[0]
? { connect: { id: userId } }
: { disconnect: { id: userId } },
},
include: {
collection: true,
pinnedBy: isCollectionOwner
? {
where: { id: userId },
select: { id: true },
}
: undefined,
},
});
return { response: updatedLink, status: 200 };
}
// Makes sure collection members (non-owners) cannot move a link to/from a collection.
if (unauthorizedSwitchCollection)
return {
@@ -1,6 +1,7 @@
import { prisma } from "@/lib/api/db";
import createFolder from "@/lib/api/storage/createFolder";
import { JSDOM } from "jsdom";
import { parse, Node, Element, TextNode } from "himalaya";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
@@ -11,6 +12,11 @@ export default async function importFromHTMLFile(
const dom = new JSDOM(rawData);
const document = dom.window.document;
// remove bad tags
document.querySelectorAll("meta").forEach((e) => (e.outerHTML = e.innerHTML));
document.querySelectorAll("META").forEach((e) => (e.outerHTML = e.innerHTML));
document.querySelectorAll("P").forEach((e) => (e.outerHTML = e.innerHTML));
const bookmarks = document.querySelectorAll("A");
const totalImports = bookmarks.length;
@@ -28,94 +34,165 @@ export default async function importFromHTMLFile(
status: 400,
};
const folders = document.querySelectorAll("H3");
const jsonData = parse(document.documentElement.outerHTML);
await prisma
.$transaction(
async () => {
// @ts-ignore
for (const folder of folders) {
const findCollection = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
collections: {
where: {
name: folder.textContent.trim(),
},
},
},
});
const checkIfCollectionExists = findCollection?.collections[0];
let collectionId = findCollection?.collections[0]?.id;
if (!checkIfCollectionExists || !collectionId) {
const newCollection = await prisma.collection.create({
data: {
name: folder.textContent.trim(),
description: "",
color: "#0ea5e9",
isPublic: false,
ownerId: userId,
},
});
createFolder({ filePath: `archives/${newCollection.id}` });
collectionId = newCollection.id;
}
createFolder({ filePath: `archives/${collectionId}` });
const bookmarks = folder.nextElementSibling.querySelectorAll("A");
for (const bookmark of bookmarks) {
await prisma.link.create({
data: {
name: bookmark.textContent.trim(),
url: bookmark.getAttribute("HREF"),
tags: bookmark.getAttribute("TAGS")
? {
connectOrCreate: bookmark
.getAttribute("TAGS")
.split(",")
.map((tag: string) =>
tag
? {
where: {
name_ownerId: {
name: tag.trim(),
ownerId: userId,
},
},
create: {
name: tag.trim(),
owner: {
connect: {
id: userId,
},
},
},
}
: undefined
),
}
: undefined,
description: bookmark.getAttribute("DESCRIPTION")
? bookmark.getAttribute("DESCRIPTION")
: "",
collectionId: collectionId,
createdAt: new Date(),
},
});
}
}
},
{ timeout: 30000 }
)
.catch((err) => console.log(err));
for (const item of jsonData) {
console.log(item);
await processBookmarks(userId, item as Element);
}
return { response: "Success.", status: 200 };
}
async function processBookmarks(
userId: number,
data: Node,
parentCollectionId?: number
) {
if (data.type === "element") {
for (const item of data.children) {
if (item.type === "element" && item.tagName === "dt") {
// process collection or sub-collection
let collectionId;
const collectionName = item.children.find(
(e) => e.type === "element" && e.tagName === "h3"
) as Element;
if (collectionName) {
collectionId = await createCollection(
userId,
(collectionName.children[0] as TextNode).content,
parentCollectionId
);
}
await processBookmarks(
userId,
item,
collectionId || parentCollectionId
);
} else if (item.type === "element" && item.tagName === "a") {
// process link
const linkUrl = item?.attributes.find((e) => e.key === "href")?.value;
const linkName = (
item?.children.find((e) => e.type === "text") as TextNode
)?.content;
const linkTags = item?.attributes
.find((e) => e.key === "tags")
?.value.split(",");
if (linkUrl && parentCollectionId) {
await createLink(
userId,
linkUrl,
parentCollectionId,
linkName,
"",
linkTags
);
} else if (linkUrl) {
// create a collection named "Imported Bookmarks" and add the link to it
const collectionId = await createCollection(userId, "Imports");
await createLink(
userId,
linkUrl,
collectionId,
linkName,
"",
linkTags
);
}
await processBookmarks(userId, item, parentCollectionId);
} else {
// process anything else
await processBookmarks(userId, item, parentCollectionId);
}
}
}
}
const createCollection = async (
userId: number,
collectionName: string,
parentId?: number
) => {
const findCollection = await prisma.collection.findFirst({
where: {
parentId,
name: collectionName,
ownerId: userId,
},
});
if (findCollection) {
return findCollection.id;
}
const collectionId = await prisma.collection.create({
data: {
name: collectionName,
parent: parentId
? {
connect: {
id: parentId,
},
}
: undefined,
owner: {
connect: {
id: userId,
},
},
},
});
createFolder({ filePath: `archives/${collectionId.id}` });
return collectionId.id;
};
const createLink = async (
userId: number,
url: string,
collectionId: number,
name?: string,
description?: string,
tags?: string[]
) => {
await prisma.link.create({
data: {
name: name || "",
url,
description,
collectionId,
tags:
tags && tags[0]
? {
connectOrCreate: tags.map((tag: string) => {
return (
{
where: {
name_ownerId: {
name: tag.trim(),
ownerId: userId,
},
},
create: {
name: tag.trim(),
owner: {
connect: {
id: userId,
},
},
},
} || undefined
);
}),
}
: undefined,
},
});
};
@@ -97,18 +97,18 @@ export default async function updateUserById(
id: { not: userId },
OR: emailEnabled
? [
{
username: data.username.toLowerCase(),
},
{
email: data.email?.toLowerCase(),
},
]
{
username: data.username.toLowerCase(),
},
{
email: data.email?.toLowerCase(),
},
]
: [
{
username: data.username.toLowerCase(),
},
],
{
username: data.username.toLowerCase(),
},
],
},
});
@@ -186,6 +186,7 @@ export default async function updateUserById(
archiveAsScreenshot: data.archiveAsScreenshot,
archiveAsPDF: data.archiveAsPDF,
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
linksRouteTo: data.linksRouteTo,
password:
data.newPassword && data.newPassword !== ""
? newHashedPassword
+29
View File
@@ -0,0 +1,29 @@
import useAccountStore from "@/store/account";
import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { LinksRouteTo } from "@prisma/client";
import { pdfAvailable, readabilityAvailable, screenshotAvailable } from "../shared/getArchiveValidity";
export const generateLinkHref = (link: LinkIncludingShortenedCollectionAndTags): string => {
const { account } = useAccountStore();
// Return the links href based on the account's preference
// If the user's preference is not available, return the original link
switch (account.linksRouteTo) {
case LinksRouteTo.ORIGINAL:
return link.url || '';
case LinksRouteTo.PDF:
if (!pdfAvailable(link)) return link.url || '';
return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`;
case LinksRouteTo.READABLE:
if (!readabilityAvailable(link)) return link.url || '';
return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
case LinksRouteTo.SCREENSHOT:
if (!screenshotAvailable(link)) return link.url || '';
return `/preserved/${link?.id}?format=${link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg}`;
default:
return link.url || '';
}
};
+5 -3
View File
@@ -1,4 +1,6 @@
export function screenshotAvailable(link: any) {
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
export function screenshotAvailable(link: LinkIncludingShortenedCollectionAndTags) {
return (
link &&
link.image &&
@@ -7,13 +9,13 @@ export function screenshotAvailable(link: any) {
);
}
export function pdfAvailable(link: any) {
export function pdfAvailable(link: LinkIncludingShortenedCollectionAndTags) {
return (
link && link.pdf && link.pdf !== "pending" && link.pdf !== "unavailable"
);
}
export function readabilityAvailable(link: any) {
export function readabilityAvailable(link: LinkIncludingShortenedCollectionAndTags) {
return (
link &&
link.readable &&
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "linkwarden",
"version": "2.4.8",
"version": "2.4.9",
"main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>",
@@ -44,6 +44,7 @@
"eslint-config-next": "13.4.9",
"formidable": "^3.5.1",
"framer-motion": "^10.16.4",
"himalaya": "^1.1.0",
"jimp": "^0.22.10",
"jsdom": "^22.1.0",
"lottie-web": "^5.12.2",
-106
View File
@@ -1,106 +0,0 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useState, useEffect } from "react";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import React from "react";
import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() {
const { updateSettings } = useLocalSettingsStore();
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...user,
});
toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
} else toast.error(response.data as string);
setSubmitLoader(false);
};
const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>(
!objectIsEmpty(account)
? account
: ({
// @ts-ignore
id: null,
name: "",
username: "",
email: "",
emailVerified: null,
blurredFavicons: null,
image: "",
isPrivate: true,
// @ts-ignore
createdAt: null,
whitelistedUsers: [],
} as unknown as AccountSettings)
);
function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0;
}
useEffect(() => {
if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]);
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Appearance</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-5">
<div>
<p className="mb-3">Select Theme</p>
<div className="flex gap-3 w-full">
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
localStorage.getItem("theme") === "dark"
? "dark:outline-primary text-primary"
: "text-white"
}`}
onClick={() => updateSettings({ theme: "dark" })}
>
<i className="bi-moon-fill text-6xl"></i>
<p className="ml-2 text-2xl">Dark</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
localStorage.getItem("theme") === "light"
? "outline-primary text-primary"
: "text-black"
}`}
onClick={() => updateSettings({ theme: "light" })}
>
<i className="bi-sun-fill text-6xl"></i>
<p className="ml-2 text-2xl">Light</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
</div>
</div>
{/* <SubmitButton
onClick={submit}
loading={submitLoader}
label="Save Changes"
className="mt-2 mx-auto lg:mx-0"
/> */}
</div>
</SettingsLayout>
);
}
-93
View File
@@ -1,93 +0,0 @@
import Checkbox from "@/components/Checkbox";
import SubmitButton from "@/components/SubmitButton";
import SettingsLayout from "@/layouts/SettingsLayout";
import React, { useEffect, useState } from "react";
import useAccountStore from "@/store/account";
import { toast } from "react-hot-toast";
import { AccountSettings } from "@/types/global";
export default function Archive() {
const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>(account);
const [archiveAsScreenshot, setArchiveAsScreenshot] =
useState<boolean>(false);
const [archiveAsPDF, setArchiveAsPDF] = useState<boolean>(false);
const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
useState<boolean>(false);
useEffect(() => {
setUser({
...account,
archiveAsScreenshot,
archiveAsPDF,
archiveAsWaybackMachine,
});
}, [account, archiveAsScreenshot, archiveAsPDF, archiveAsWaybackMachine]);
function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0;
}
useEffect(() => {
if (!objectIsEmpty(account)) {
setArchiveAsScreenshot(account.archiveAsScreenshot);
setArchiveAsPDF(account.archiveAsPDF);
setArchiveAsWaybackMachine(account.archiveAsWaybackMachine);
}
}, [account]);
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...user,
});
toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
} else toast.error(response.data as string);
setSubmitLoader(false);
};
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Archive Settings</p>
<div className="divider my-3"></div>
<p>Formats to Archive/Preserve webpages:</p>
<div className="p-3">
<Checkbox
label="Screenshot"
state={archiveAsScreenshot}
onClick={() => setArchiveAsScreenshot(!archiveAsScreenshot)}
/>
<Checkbox
label="PDF"
state={archiveAsPDF}
onClick={() => setArchiveAsPDF(!archiveAsPDF)}
/>
<Checkbox
label="Archive.org Snapshot"
state={archiveAsWaybackMachine}
onClick={() => setArchiveAsWaybackMachine(!archiveAsWaybackMachine)}
/>
</div>
<SubmitButton
onClick={submit}
loading={submitLoader}
label="Save Changes"
className="mt-2 w-full sm:w-fit"
/>
</SettingsLayout>
);
}
+225
View File
@@ -0,0 +1,225 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useState, useEffect } from "react";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import React from "react";
import useLocalSettingsStore from "@/store/localSettings";
import Checkbox from "@/components/Checkbox";
import SubmitButton from "@/components/SubmitButton";
import { LinksRouteTo } from "@prisma/client";
export default function Appearance() {
const { updateSettings } = useLocalSettingsStore();
const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>(account);
const [archiveAsScreenshot, setArchiveAsScreenshot] =
useState<boolean>(false);
const [archiveAsPDF, setArchiveAsPDF] = useState<boolean>(false);
const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
useState<boolean>(false);
const [linksRouteTo, setLinksRouteTo] = useState<LinksRouteTo>(
user.linksRouteTo
);
useEffect(() => {
setUser({
...account,
archiveAsScreenshot,
archiveAsPDF,
archiveAsWaybackMachine,
linksRouteTo,
});
}, [
account,
archiveAsScreenshot,
archiveAsPDF,
archiveAsWaybackMachine,
linksRouteTo,
]);
function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0;
}
useEffect(() => {
if (!objectIsEmpty(account)) {
setArchiveAsScreenshot(account.archiveAsScreenshot);
setArchiveAsPDF(account.archiveAsPDF);
setArchiveAsWaybackMachine(account.archiveAsWaybackMachine);
setLinksRouteTo(account.linksRouteTo);
}
}, [account]);
const submit = async () => {
setSubmitLoader(true);
const load = toast.loading("Applying...");
const response = await updateAccount({
...user,
});
toast.dismiss(load);
if (response.ok) {
toast.success("Settings Applied!");
} else toast.error(response.data as string);
setSubmitLoader(false);
};
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Preference</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-5">
<div>
<p className="mb-3">Select Theme</p>
<div className="flex gap-3 w-full">
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
localStorage.getItem("theme") === "dark"
? "dark:outline-primary text-primary"
: "text-white"
}`}
onClick={() => updateSettings({ theme: "dark" })}
>
<i className="bi-moon-fill text-6xl"></i>
<p className="ml-2 text-2xl">Dark</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
<div
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
localStorage.getItem("theme") === "light"
? "outline-primary text-primary"
: "text-black"
}`}
onClick={() => updateSettings({ theme: "light" })}
>
<i className="bi-sun-fill text-6xl"></i>
<p className="ml-2 text-2xl">Light</p>
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
</div>
</div>
<div>
<p className="capitalize text-3xl font-thin inline">
Archive Settings
</p>
<div className="divider my-3"></div>
<p>Formats to Archive/Preserve webpages:</p>
<div className="p-3">
<Checkbox
label="Screenshot"
state={archiveAsScreenshot}
onClick={() => setArchiveAsScreenshot(!archiveAsScreenshot)}
/>
<Checkbox
label="PDF"
state={archiveAsPDF}
onClick={() => setArchiveAsPDF(!archiveAsPDF)}
/>
<Checkbox
label="Archive.org Snapshot"
state={archiveAsWaybackMachine}
onClick={() =>
setArchiveAsWaybackMachine(!archiveAsWaybackMachine)
}
/>
</div>
</div>
<div>
<p className="capitalize text-3xl font-thin inline">Link Settings</p>
<div className="divider my-3"></div>
<p>Clicking on Links should:</p>
<div className="p-3">
<label
className="label cursor-pointer flex gap-2 justify-start w-fit"
tabIndex={0}
role="button"
>
<input
type="radio"
name="link-preference-radio"
className="radio checked:bg-primary"
value="Original"
checked={linksRouteTo === LinksRouteTo.ORIGINAL}
onChange={() => setLinksRouteTo(LinksRouteTo.ORIGINAL)}
/>
<span className="label-text">Open the original content</span>
</label>
<label
className="label cursor-pointer flex gap-2 justify-start w-fit"
tabIndex={0}
role="button"
>
<input
type="radio"
name="link-preference-radio"
className="radio checked:bg-primary"
value="PDF"
checked={linksRouteTo === LinksRouteTo.PDF}
onChange={() => setLinksRouteTo(LinksRouteTo.PDF)}
/>
<span className="label-text">Open PDF, if available</span>
</label>
<label
className="label cursor-pointer flex gap-2 justify-start w-fit"
tabIndex={0}
role="button"
>
<input
type="radio"
name="link-preference-radio"
className="radio checked:bg-primary"
value="Readable"
checked={linksRouteTo === LinksRouteTo.READABLE}
onChange={() => setLinksRouteTo(LinksRouteTo.READABLE)}
/>
<span className="label-text">Open Readable, if available</span>
</label>
<label
className="label cursor-pointer flex gap-2 justify-start w-fit"
tabIndex={0}
role="button"
>
<input
type="radio"
name="link-preference-radio"
className="radio checked:bg-primary"
value="Screenshot"
checked={linksRouteTo === LinksRouteTo.SCREENSHOT}
onChange={() => setLinksRouteTo(LinksRouteTo.SCREENSHOT)}
/>
<span className="label-text">Open Screenshot, if available</span>
</label>
</div>
</div>
<SubmitButton
onClick={submit}
loading={submitLoader}
label="Save Changes"
className="mt-2 w-full sm:w-fit"
/>
</div>
</SettingsLayout>
);
}
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "LinksRouteTo" AS ENUM ('ORIGINAL', 'PDF', 'READABLE', 'SCREENSHOT');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "linksRouteTo" "LinksRouteTo" NOT NULL DEFAULT 'ORIGINAL';
+8
View File
@@ -41,6 +41,7 @@ model User {
whitelistedUsers WhitelistedUser[]
accessTokens AccessToken[]
subscriptions Subscription?
linksRouteTo LinksRouteTo @default(ORIGINAL)
archiveAsScreenshot Boolean @default(true)
archiveAsPDF Boolean @default(true)
archiveAsWaybackMachine Boolean @default(false)
@@ -49,6 +50,13 @@ model User {
updatedAt DateTime @default(now()) @updatedAt
}
enum LinksRouteTo {
ORIGINAL
PDF
READABLE
SCREENSHOT
}
model WhitelistedUser {
id Int @id @default(autoincrement())
username String @default("")
+22
View File
@@ -0,0 +1,22 @@
declare module "himalaya" {
export interface Attribute {
key: string;
value: string;
}
export interface TextNode {
type: "text";
content: string;
}
export type Node = TextNode | Element;
export interface Element {
type: "element";
tagName: string;
attributes: Attribute[];
children: Node[];
}
export function parse(html: string): Node[];
}
+5
View File
@@ -3713,6 +3713,11 @@ hexoid@^1.0.0:
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
himalaya@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/himalaya/-/himalaya-1.1.0.tgz#31724ae9d35714cd7c6f4be94888953f3604606a"
integrity sha512-LLase1dHCRMel68/HZTFft0N0wti0epHr3nNY7ynpLbyZpmrKMQ8YIpiOV77TM97cNpC8Wb2n6f66IRggwdWPw==
hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"