adjustible archive formats + finalized settings page
This commit is contained in:
parent
19467f243f
commit
ca3ce7e3de
|
@ -239,6 +239,16 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex text-black dark:text-white gap-1">
|
<div className="flex text-black dark:text-white gap-1">
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload("png")}
|
||||||
|
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/api/archives/${link.collectionId}/${link.id}.png`}
|
href={`/api/archives/${link.collectionId}/${link.id}.png`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -249,16 +259,6 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
|
||||||
className="w-5 h-5 text-sky-500 dark:text-sky-500"
|
className="w-5 h-5 text-sky-500 dark:text-sky-500"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div
|
|
||||||
onClick={() => handleDownload("png")}
|
|
||||||
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCloudArrowDown}
|
|
||||||
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -272,6 +272,16 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex text-black dark:text-white gap-1">
|
<div className="flex text-black dark:text-white gap-1">
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload("pdf")}
|
||||||
|
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/api/archives/${link.collectionId}/${link.id}.pdf`}
|
href={`/api/archives/${link.collectionId}/${link.id}.pdf`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -282,16 +292,6 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
|
||||||
className="w-5 h-5 text-sky-500 dark:text-sky-500"
|
className="w-5 h-5 text-sky-500 dark:text-sky-500"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div
|
|
||||||
onClick={() => handleDownload("pdf")}
|
|
||||||
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCloudArrowDown}
|
|
||||||
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -301,7 +301,9 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) {
|
||||||
<FontAwesomeIcon icon={faGlobe} className="w-6 h-6" />
|
<FontAwesomeIcon icon={faGlobe} className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-black dark:text-white">Archive.org Snapshot</p>
|
<p className="text-black dark:text-white">
|
||||||
|
Latest archive.org Snapshot
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -94,14 +94,7 @@ export default function Navbar() {
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
onClick: () => {
|
href: "/settings/account",
|
||||||
setModal({
|
|
||||||
modal: "ACCOUNT",
|
|
||||||
state: true,
|
|
||||||
active: account,
|
|
||||||
});
|
|
||||||
setProfileDropdown(!profileDropdown);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `Switch to ${theme === "light" ? "Dark" : "Light"}`,
|
name: `Switch to ${theme === "light" ? "Dark" : "Light"}`,
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
import { Page, chromium, devices } from "playwright";
|
import { chromium, devices } from "playwright";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import createFile from "@/lib/api/storage/createFile";
|
import createFile from "@/lib/api/storage/createFile";
|
||||||
import sendToWayback from "./sendToWayback";
|
import sendToWayback from "./sendToWayback";
|
||||||
|
|
||||||
export default async function archive(linkId: number, url: string) {
|
export default async function archive(
|
||||||
|
linkId: number,
|
||||||
|
url: string,
|
||||||
|
userId: number
|
||||||
|
) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user?.archiveAsWaybackMachine) sendToWayback(url);
|
||||||
|
|
||||||
|
if (user?.archiveAsPDF || user?.archiveAsScreenshot) {
|
||||||
const browser = await chromium.launch();
|
const browser = await chromium.launch();
|
||||||
const context = await browser.newContext(devices["Desktop Chrome"]);
|
const context = await browser.newContext(devices["Desktop Chrome"]);
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
sendToWayback(url);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(url, { waitUntil: "domcontentloaded" });
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
|
@ -25,12 +36,7 @@ export default async function archive(linkId: number, url: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (linkExists) {
|
if (linkExists) {
|
||||||
const pdf = await page.pdf({
|
if (user.archiveAsScreenshot) {
|
||||||
width: "1366px",
|
|
||||||
height: "1931px",
|
|
||||||
printBackground: true,
|
|
||||||
margin: { top: "15px", bottom: "15px" },
|
|
||||||
});
|
|
||||||
const screenshot = await page.screenshot({
|
const screenshot = await page.screenshot({
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
});
|
});
|
||||||
|
@ -39,12 +45,22 @@ export default async function archive(linkId: number, url: string) {
|
||||||
data: screenshot,
|
data: screenshot,
|
||||||
filePath: `archives/${linkExists.collectionId}/${linkId}.png`,
|
filePath: `archives/${linkExists.collectionId}/${linkId}.png`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.archiveAsPDF) {
|
||||||
|
const pdf = await page.pdf({
|
||||||
|
width: "1366px",
|
||||||
|
height: "1931px",
|
||||||
|
printBackground: true,
|
||||||
|
margin: { top: "15px", bottom: "15px" },
|
||||||
|
});
|
||||||
|
|
||||||
createFile({
|
createFile({
|
||||||
data: pdf,
|
data: pdf,
|
||||||
filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`,
|
filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -52,6 +68,7 @@ export default async function archive(linkId: number, url: string) {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => {
|
const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => {
|
||||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default async function postLink(
|
||||||
|
|
||||||
createFolder({ filePath: `archives/${newLink.collectionId}` });
|
createFolder({ filePath: `archives/${newLink.collectionId}` });
|
||||||
|
|
||||||
archive(newLink.id, newLink.url);
|
archive(newLink.id, newLink.url, userId);
|
||||||
|
|
||||||
return { response: newLink, status: 200 };
|
return { response: newLink, status: 200 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,9 @@ export default async function updateUser(
|
||||||
username: user.username.toLowerCase(),
|
username: user.username.toLowerCase(),
|
||||||
email: user.email?.toLowerCase(),
|
email: user.email?.toLowerCase(),
|
||||||
isPrivate: user.isPrivate,
|
isPrivate: user.isPrivate,
|
||||||
|
archiveAsScreenshot: user.archiveAsScreenshot,
|
||||||
|
archiveAsPDF: user.archiveAsPDF,
|
||||||
|
archiveAsWaybackMachine: user.archiveAsWaybackMachine,
|
||||||
password:
|
password:
|
||||||
user.newPassword && user.newPassword !== ""
|
user.newPassword && user.newPassword !== ""
|
||||||
? newHashedPassword
|
? newHashedPassword
|
||||||
|
|
|
@ -1,34 +1,37 @@
|
||||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export default function appearance() {
|
export default function appearance() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
if (theme === "dark") {
|
|
||||||
setTheme("light");
|
|
||||||
} else {
|
|
||||||
setTheme("dark");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
|
<p className="mb-3 text-sm">Select Theme</p>
|
||||||
<div className="flex gap-3 w-full">
|
<div className="flex gap-3 w-full">
|
||||||
<div
|
<div
|
||||||
className="w-full text-center border-solid border-sky-100 border dark:border-neutral-700 h-40 rounded-md flex items-center justify-center cursor-pointer select-none bg-black text-white"
|
className={`w-full text-center outline-solid outline-sky-100 outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
|
||||||
|
theme === "dark"
|
||||||
|
? "dark:outline-sky-500 text-sky-500"
|
||||||
|
: "text-white"
|
||||||
|
}`}
|
||||||
onClick={() => setTheme("dark")}
|
onClick={() => setTheme("dark")}
|
||||||
>
|
>
|
||||||
<p>Dark Theme</p>
|
<FontAwesomeIcon icon={faMoon} className="w-1/2 h-1/2" />
|
||||||
|
<p className="text-2xl">Dark Theme</p>
|
||||||
|
|
||||||
{/* <hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" /> */}
|
{/* <hr className="my-3 outline-1 outline-sky-100 dark:outline-neutral-700" /> */}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-full text-center border-solid border-sky-100 border dark:border-neutral-700 h-40 rounded-md flex items-center justify-center cursor-pointer select-none bg-white text-black"
|
className={`w-full text-center outline-solid outline-sky-100 outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
|
||||||
|
theme === "light" ? "outline-sky-500 text-sky-500" : "text-black"
|
||||||
|
}`}
|
||||||
onClick={() => setTheme("light")}
|
onClick={() => setTheme("light")}
|
||||||
>
|
>
|
||||||
<p>Light Theme</p>
|
<FontAwesomeIcon icon={faSun} className="w-1/2 h-1/2" />
|
||||||
{/* <hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" /> */}
|
<p className="text-2xl">Light Theme</p>
|
||||||
|
{/* <hr className="my-3 outline-1 outline-sky-100 dark:outline-neutral-700" /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
|
|
|
@ -1,10 +1,88 @@
|
||||||
|
import Checkbox from "@/components/Checkbox";
|
||||||
|
import SubmitButton from "@/components/SubmitButton";
|
||||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||||
import React from "react";
|
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() {
|
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 (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<div>archive</div>
|
<p>Formats to Archive 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"
|
||||||
|
className="mt-2 mx-auto lg:mx-0"
|
||||||
|
/>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "archiveAsPDF" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN "archiveAsScreenshot" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN "archiveAsWaybackMachine" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -54,6 +54,10 @@ model User {
|
||||||
|
|
||||||
pinnedLinks Link[]
|
pinnedLinks Link[]
|
||||||
|
|
||||||
|
archiveAsScreenshot Boolean @default(true)
|
||||||
|
archiveAsPDF Boolean @default(true)
|
||||||
|
archiveAsWaybackMachine Boolean @default(false)
|
||||||
|
|
||||||
collectionsJoined UsersAndCollections[]
|
collectionsJoined UsersAndCollections[]
|
||||||
isPrivate Boolean @default(false)
|
isPrivate Boolean @default(false)
|
||||||
whitelistedUsers WhitelistedUser[]
|
whitelistedUsers WhitelistedUser[]
|
||||||
|
|
Ŝarĝante…
Reference in New Issue