Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel ae6656e0ec Merge pull request #386 from treyg/global-theming-2.4
Global theming support
2024-01-02 07:30:01 -05:00
Trey Gordon 7e9eae0ef2 style: change to neutral to handle new themes 2023-12-29 12:29:10 -05:00
Trey Gordon 6b28abc405 feat: add new theming options 2023-12-29 12:28:43 -05:00
16 changed files with 628 additions and 479 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
NEXTAUTH_SECRET=very_sensitive_secret NEXTAUTH_SECRET=very_sensitive_secret
NEXTAUTH_URL=http://localhost:3000/api/v1/auth NEXTAUTH_URL=http://localhost:3000
# Manual installation database settings # Manual installation database settings
DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
+1 -1
View File
@@ -9,7 +9,7 @@ WORKDIR /data
COPY ./package.json ./yarn.lock ./playwright.config.ts ./ COPY ./package.json ./yarn.lock ./playwright.config.ts ./
# Increase timeout to pass github actions arm64 build # Increase timeout to pass github actions arm64 build
RUN --mount=type=cache,sharing=locked,target=/usr/local/share/.cache/yarn yarn install --network-timeout 10000000 RUN yarn install --network-timeout 10000000
RUN npx playwright install-deps && \ RUN npx playwright install-deps && \
apt-get clean && \ apt-get clean && \
+1 -3
View File
@@ -17,9 +17,7 @@
## Intro & motivation ## Intro & motivation
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.** **Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.** The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly. Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly.
@@ -69,13 +69,11 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
const isReady = () => { const isReady = () => {
return ( return (
collectionOwner.archiveAsScreenshot ===
(link && link.pdf && link.pdf !== "pending") &&
collectionOwner.archiveAsPDF ===
(link && link.pdf && link.pdf !== "pending") &&
link && link &&
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
link.readable && link.readable &&
link.readable !== "pending" link.readable !== "pending"
); );
+6 -6
View File
@@ -26,13 +26,13 @@ export default function Navbar() {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const handleToggle = () => { const handleToggle = () => {
if (settings.theme === "dark") { const [colorTheme, mode] = (settings.theme || "default-light").split('-');
updateSettings({ theme: "light" }); const newMode = mode === "dark" ? "light" : "dark";
} else { const newTheme = `${colorTheme}-${newMode}`;
updateSettings({ theme: "dark" }); updateSettings({ theme: newTheme });
}
}; };
useEffect(() => { useEffect(() => {
setSidebar(false); setSidebar(false);
}, [width]); }, [width]);
@@ -143,7 +143,7 @@ export default function Navbar() {
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
Switch to {settings.theme === "light" ? "Dark" : "Light"} Switch to {(settings.theme || "default-light").endsWith("-dark") ? "Light" : "Dark"}
</div> </div>
</li> </li>
<li> <li>
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
export default function SettingsSidebar({ className }: { className?: string }) { export default function SettingsSidebar({ className }: { className?: string }) {
const LINKWARDEN_VERSION = "v2.4.9"; const LINKWARDEN_VERSION = "v2.4.7";
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
+29 -22
View File
@@ -6,32 +6,39 @@ type Props = {
}; };
export default function ToggleDarkMode({ className }: Props) { export default function ToggleDarkMode({ className }: Props) {
const { settings, updateSettings } = useLocalSettingsStore(); const { updateSettings } = useLocalSettingsStore();
const [theme, setTheme] = useState('default-light');
const [theme, setTheme] = useState(localStorage.getItem("theme"));
const handleToggle = (e: any) => {
setTheme(e.target.checked ? "dark" : "light");
};
useEffect(() => { useEffect(() => {
updateSettings({ theme: theme as string }); const storedTheme = localStorage.getItem("theme");
}, [theme]); if (storedTheme) {
setTheme(storedTheme);
} else {
// Default theme if not set in localStorage
localStorage.setItem("theme", "default-light");
setTheme("default-light");
}
console.log("Initial theme from localStorage:", storedTheme || "default-light");
}, []);
const handleToggle = () => {
const [currentColorTheme, currentMode] = theme.split('-');
const newMode = currentMode === 'light' ? 'dark' : 'light';
const newTheme = `${currentColorTheme}-${newMode}`;
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
console.log("New theme set:", newTheme);
};
const isDarkMode = theme.endsWith('-dark');
return ( return (
<div <div className="tooltip tooltip-bottom" data-tip={`Switch to ${isDarkMode ? "Light" : "Dark"}`}>
className="tooltip tooltip-bottom" <label className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}>
data-tip={`Switch to ${settings.theme === "light" ? "Dark" : "Light"}`} <input type="checkbox" onChange={handleToggle} className="theme-controller" checked={isDarkMode} />
>
<label
className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}
>
<input
type="checkbox"
onChange={handleToggle}
className="theme-controller"
checked={localStorage.getItem("theme") === "light" ? false : true}
/>
<i className="bi-sun-fill text-xl swap-on"></i> <i className="bi-sun-fill text-xl swap-on"></i>
<i className="bi-moon-fill text-xl swap-off"></i> <i className="bi-moon-fill text-xl swap-off"></i>
</label> </label>
+1 -2
View File
@@ -43,8 +43,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
? await validateUrlSize(link.url) ? await validateUrlSize(link.url)
: undefined; : undefined;
if (validatedUrl === null) if (validatedUrl === null) throw "File is too large to be stored.";
throw "Something went wrong while retrieving the file size.";
const contentType = validatedUrl?.get("content-type"); const contentType = validatedUrl?.get("content-type");
let linkType = "url"; let linkType = "url";
+6
View File
@@ -67,6 +67,12 @@ export default async function postLink(
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined; const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
if (validatedUrl === null)
return {
response: "Something went wrong while retrieving the file size.",
status: 400,
};
const contentType = validatedUrl?.get("content-type"); const contentType = validatedUrl?.get("content-type");
let linkType = "url"; let linkType = "url";
let imageExtension = "png"; let imageExtension = "png";
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "linkwarden", "name": "linkwarden",
"version": "2.4.9", "version": "2.4.7",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git", "repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>", "author": "Daniel31X13 <daniel31x13@gmail.com>",
+13 -1
View File
@@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import "@/styles/globals.css"; import "@/styles/globals.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
@@ -14,6 +14,18 @@ export default function App({
}: AppProps<{ }: AppProps<{
session: Session; session: Session;
}>) { }>) {
useEffect(() => {
let theme = localStorage.getItem("theme");
if (!theme || !theme.includes("-")) {
theme = "default-dark"; // Default theme
localStorage.setItem("theme", theme);
}
document.documentElement.setAttribute('data-theme', theme);
}, []);
return ( return (
<SessionProvider <SessionProvider
session={pageProps.session} session={pageProps.session}
+4 -2
View File
@@ -104,8 +104,10 @@ export default function Index() {
className="h-[60rem] p-5 flex gap-3 flex-col" className="h-[60rem] p-5 flex gap-3 flex-col"
style={{ style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${ backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6" (settings.theme || "default-light").endsWith("-dark") ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`, } 13rem, ${
(settings.theme || "default-light").endsWith("-dark") ? "#171717" : "#ffffff"
} 100%)`,
}} }}
> >
{activeCollection && ( {activeCollection && (
+2 -2
View File
@@ -155,7 +155,7 @@ export default function Dashboard() {
</div> </div>
<Link <Link
href="/links" href="/links"
className="flex items-center text-sm text-black/75 dark:text-white/75 gap-2 cursor-pointer" className="flex items-center text-sm text-neutral gap-2 cursor-pointer"
> >
View All View All
<i className="bi-chevron-right text-sm"></i> <i className="bi-chevron-right text-sm"></i>
@@ -264,7 +264,7 @@ export default function Dashboard() {
</div> </div>
<Link <Link
href="/links/pinned" href="/links/pinned"
className="flex items-center text-sm text-black/75 dark:text-white/75 gap-2 cursor-pointer" className="flex items-center text-sm text-neutral gap-2 cursor-pointer"
> >
View All View All
<i className="bi-chevron-right text-sm "></i> <i className="bi-chevron-right text-sm "></i>
+58 -39
View File
@@ -1,16 +1,16 @@
import SettingsLayout from "@/layouts/SettingsLayout"; import SettingsLayout from "@/layouts/SettingsLayout";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global"; import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import React from "react";
import useLocalSettingsStore from "@/store/localSettings"; import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() { export default function Appearance() {
const { updateSettings } = useLocalSettingsStore(); const { updateSettings } = useLocalSettingsStore();
const { account, updateAccount } = useAccountStore();
const submit = async () => { const submit = async () => {
setSubmitLoader(true); setSubmitLoader(true);
const load = toast.loading("Applying..."); const load = toast.loading("Applying...");
const response = await updateAccount({ const response = await updateAccount({
@@ -18,17 +18,12 @@ export default function Appearance() {
}); });
toast.dismiss(load); toast.dismiss(load);
if (response.ok) { if (response.ok) {
toast.success("Settings Applied!"); toast.success("Settings Applied!");
} else toast.error(response.data as string); } else toast.error(response.data as string);
setSubmitLoader(false); setSubmitLoader(false);
}; };
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const { account, updateAccount } = useAccountStore();
const [user, setUser] = useState<AccountSettings>( const [user, setUser] = useState<AccountSettings>(
!objectIsEmpty(account) !objectIsEmpty(account)
? account ? account
@@ -48,6 +43,9 @@ export default function Appearance() {
} as unknown as AccountSettings) } as unknown as AccountSettings)
); );
// Combine colorTheme and mode into a single state
const [theme, setTheme] = useState(localStorage.getItem("theme") || "default-dark");
function objectIsEmpty(obj: object) { function objectIsEmpty(obj: object) {
return Object.keys(obj).length === 0; return Object.keys(obj).length === 0;
} }
@@ -56,51 +54,72 @@ export default function Appearance() {
if (!objectIsEmpty(account)) setUser({ ...account }); if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]); }, [account]);
const handleThemeChange = (newThemePart: string, isColorTheme: boolean) => {
const currentTheme = localStorage.getItem("theme") || "default-light";
const [currentColorTheme, currentMode] = currentTheme.split('-');
const newTheme = isColorTheme ? `${newThemePart}-${currentMode}` : `${currentColorTheme}-${newThemePart}`;
localStorage.setItem("theme", newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
updateSettings({ theme: newTheme });
// Update the theme state
setTheme(newTheme);
};
return ( return (
<SettingsLayout> <SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Appearance</p> <p className="capitalize text-3xl font-thin inline">Appearance</p>
<div className="divider my-3"></div> <div className="divider my-3"></div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div> <div>
<p className="mb-3">Select Theme</p> <p className="mb-3">Select Mode</p>
<div className="flex gap-3 w-full"> <div className="grid grid-cols-2 gap-3">
<div {["light", "dark"].map((modeOption) => (
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 ${ <button
localStorage.getItem("theme") === "dark" key={modeOption}
? "dark:outline-primary text-primary" onClick={() => handleThemeChange(modeOption, false)}
: "text-white" className={`w-full text-center h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none ${theme.endsWith(modeOption) ? "ring-2 ring-primary" : "ring-2 ring-neutral"}`}
}`}
onClick={() => updateSettings({ theme: "dark" })}
> >
<i className="bi-moon-fill text-6xl"></i> {modeOption === 'light' ?
<p className="ml-2 text-2xl">Dark</p> <i className={`bi-sun-fill text-6xl ${theme.endsWith(modeOption) ? "text-primary" : ""}`}></i> :
<i className={`bi-moon-fill text-6xl ${theme.endsWith(modeOption) ? "text-primary" : ""}`}></i>}
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */} <p className="ml-2 text-2xl">{modeOption.charAt(0).toUpperCase() + modeOption.slice(1)}</p>
</div> </button>
<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> </div>
{/* <SubmitButton <div>
<p className="mb-3">Select Color Theme</p>
<div className="grid grid-cols-4 gap-3">
{["default", "red", "green", "orange"].map((colorTheme) => (
<button
key={colorTheme}
onClick={() => handleThemeChange(colorTheme, true)}
className={`w-full text-center h-36 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none ${theme.startsWith(colorTheme) ? "ring-2 ring-primary" : "ring-2 ring-neutral"}`}
>
{colorTheme.charAt(0).toUpperCase() + colorTheme.slice(1)}
</button>
))}
</div>
</div>
<button
onClick={submit} onClick={submit}
loading={submitLoader} disabled={submitLoader}
label="Save" className="mt-2 mx-auto lg:mx-0 bg-primary text-white rounded-md px-4 py-2"
className="mt-2 mx-auto lg:mx-0" >
/> */} {submitLoader ? "Saving..." : "Save"}
</button>
</div> </div>
</SettingsLayout> </SettingsLayout>
); );
} }
+21 -16
View File
@@ -18,34 +18,39 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
viewMode: "", viewMode: "",
}, },
updateSettings: async (newSettings) => { updateSettings: async (newSettings) => {
if ( if (newSettings.theme) {
newSettings.theme &&
newSettings.theme !== localStorage.getItem("theme")
) {
localStorage.setItem("theme", newSettings.theme); localStorage.setItem("theme", newSettings.theme);
document.documentElement.setAttribute('data-theme', newSettings.theme);
const localTheme = localStorage.getItem("theme") || ""; if (newSettings.theme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
document.querySelector("html")?.setAttribute("data-theme", localTheme); } else {
document.documentElement.classList.remove("dark");
}
} }
if ( if (newSettings.viewMode) {
newSettings.viewMode &&
newSettings.viewMode !== localStorage.getItem("viewMode")
) {
localStorage.setItem("viewMode", newSettings.viewMode); localStorage.setItem("viewMode", newSettings.viewMode);
// const localTheme = localStorage.getItem("viewMode") || "";
} }
set((state) => ({ settings: { ...state.settings, ...newSettings } })); set((state) => ({ settings: { ...state.settings, ...newSettings } }));
}, },
setSettings: async () => { setSettings: async () => {
if (!localStorage.getItem("theme")) { let theme = localStorage.getItem("theme");
localStorage.setItem("theme", "dark"); if (!theme || !theme.includes("-")) {
theme = "default-dark"; // Default theme
localStorage.setItem("theme", theme);
} }
const localTheme = localStorage.getItem("theme") || ""; const localTheme = theme;
document.documentElement.setAttribute('data-theme', localTheme);
if (localTheme.endsWith("-dark")) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
set((state) => ({ set((state) => ({
settings: { ...state.settings, theme: localTheme }, settings: { ...state.settings, theme: localTheme },
+105 -2
View File
@@ -5,7 +5,7 @@ module.exports = {
daisyui: { daisyui: {
themes: [ themes: [
{ {
light: { "default-light": {
primary: "#0369a1", primary: "#0369a1",
secondary: "#0891b2", secondary: "#0891b2",
accent: "#6d28d9", accent: "#6d28d9",
@@ -21,7 +21,7 @@ module.exports = {
}, },
}, },
{ {
dark: { "default-dark": {
primary: "#7dd3fc", primary: "#7dd3fc",
secondary: "#22d3ee", secondary: "#22d3ee",
accent: "#6d28d9", accent: "#6d28d9",
@@ -36,6 +36,108 @@ module.exports = {
error: "#f1293c", error: "#f1293c",
}, },
}, },
// Red Light Theme
{
"red-light": {
primary: "#ef4444",
secondary: "#dc2626",
accent: "#6d28d9",
neutral: "#6b7280",
"neutral-content": "#d1d5db",
"base-100": "#ffffff",
"base-200": "#f3f4f6",
"base-content": "#0a0a0a",
info: "#a5f3fc",
success: "#22c55e",
warning: "#facc15",
error: "#dc2626",
},
},
// Red Dark Theme
{
"red-dark": {
primary: "#ef4444",
secondary: "#dc2626",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Green Light Theme
{
"green-light": {
primary: "#22c55e",
secondary: "#16a34a",
accent: "#6d28d9",
neutral: "#6b7280",
"neutral-content": "#d1d5db",
"base-100": "#ffffff",
"base-200": "#f3f4f6",
"base-content": "#0a0a0a",
info: "#a5f3fc",
success: "#22c55e",
warning: "#facc15",
error: "#dc2626",
},
},
// Green Dark Theme
{
"green-dark": {
primary: "#22c55e",
secondary: "#16a34a",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Orange Light Theme
{
"orange-light": {
primary: "#f97316",
secondary: "#ea580c",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
// Orange Dark Theme
{
"orange-dark": {
primary: "#f97316",
secondary: "#ea580c",
accent: "#6d28d9",
neutral: "#9ca3af",
"neutral-content": "#404040",
"base-100": "#171717",
"base-200": "#262626",
"base-content": "#fafafa",
info: "#009ee4",
success: "#00b17d",
warning: "#eac700",
error: "#f1293c",
},
},
], ],
}, },
darkMode: ["class", '[data-theme="dark"]'], darkMode: ["class", '[data-theme="dark"]'],
@@ -54,3 +156,4 @@ module.exports = {
}), }),
], ],
}; };