aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/lib')
-rw-r--r--apps/web/lib/clientConfig.tsx3
-rw-r--r--apps/web/lib/exportBookmarks.ts51
-rw-r--r--apps/web/lib/hooks/bookmark-search.ts9
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json42
-rw-r--r--apps/web/lib/i18n/locales/en_US/translation.json41
-rw-r--r--apps/web/lib/importBookmarkParser.ts32
-rw-r--r--apps/web/lib/userSettings.tsx34
7 files changed, 185 insertions, 27 deletions
diff --git a/apps/web/lib/clientConfig.tsx b/apps/web/lib/clientConfig.tsx
index ef8e0815..03089e49 100644
--- a/apps/web/lib/clientConfig.tsx
+++ b/apps/web/lib/clientConfig.tsx
@@ -3,12 +3,15 @@ import { createContext, useContext } from "react";
import type { ClientConfig } from "@karakeep/shared/config";
export const ClientConfigCtx = createContext<ClientConfig>({
+ publicUrl: "",
+ publicApiUrl: "",
demoMode: undefined,
auth: {
disableSignups: false,
disablePasswordAuth: false,
},
inference: {
+ isConfigured: false,
inferredTagLang: "english",
},
serverVersion: undefined,
diff --git a/apps/web/lib/exportBookmarks.ts b/apps/web/lib/exportBookmarks.ts
index 45db104f..5dc26e78 100644
--- a/apps/web/lib/exportBookmarks.ts
+++ b/apps/web/lib/exportBookmarks.ts
@@ -19,6 +19,7 @@ export const zExportBookmarkSchema = z.object({
])
.nullable(),
note: z.string().nullable(),
+ archived: z.boolean().optional().default(false),
});
export const zExportSchema = z.object({
@@ -56,5 +57,55 @@ export function toExportFormat(
tags: bookmark.tags.map((t) => t.name),
content,
note: bookmark.note ?? null,
+ archived: bookmark.archived,
};
}
+
+export function toNetscapeFormat(bookmarks: ZBookmark[]): string {
+ const header = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<H1>Bookmarks</H1>
+<DL><p>`;
+
+ const footer = `</DL><p>`;
+
+ const bookmarkEntries = bookmarks
+ .map((bookmark) => {
+ if (bookmark.content?.type !== BookmarkTypes.LINK) {
+ return "";
+ }
+ const addDate = bookmark.createdAt
+ ? `ADD_DATE="${Math.floor(bookmark.createdAt.getTime() / 1000)}"`
+ : "";
+
+ const tagNames = bookmark.tags.map((t) => t.name).join(",");
+ const tags = tagNames.length > 0 ? `TAGS="${tagNames}"` : "";
+
+ const encodedUrl = encodeURI(bookmark.content.url);
+ const displayTitle = bookmark.title ?? bookmark.content.url;
+ const encodedTitle = escapeHtml(displayTitle);
+
+ return ` <DT><A HREF="${encodedUrl}" ${addDate} ${tags}>${encodedTitle}</A>`;
+ })
+ .filter(Boolean)
+ .join("\n");
+
+ return `${header}\n${bookmarkEntries}\n${footer}`;
+}
+
+function escapeHtml(input: string): string {
+ const escapeMap: Record<string, string> = {
+ "&": "&amp;",
+ "'": "&#x27;",
+ "`": "&#x60;",
+ '"': "&quot;",
+ "<": "&lt;",
+ ">": "&gt;",
+ };
+
+ return input.replace(/[&'`"<>]/g, (match) => escapeMap[match] || "");
+}
diff --git a/apps/web/lib/hooks/bookmark-search.ts b/apps/web/lib/hooks/bookmark-search.ts
index 1bccd280..b6af94ee 100644
--- a/apps/web/lib/hooks/bookmark-search.ts
+++ b/apps/web/lib/hooks/bookmark-search.ts
@@ -6,6 +6,11 @@ import { keepPreviousData } from "@tanstack/react-query";
import { parseSearchQuery } from "@karakeep/shared/searchQueryParser";
+export function useIsSearchPage() {
+ const pathname = usePathname();
+ return pathname.startsWith("/dashboard/search");
+}
+
function useSearchQuery() {
const searchParams = useSearchParams();
const searchQuery = decodeURIComponent(searchParams.get("q") ?? "");
@@ -17,8 +22,8 @@ function useSearchQuery() {
export function useDoBookmarkSearch() {
const router = useRouter();
const { searchQuery, parsedSearchQuery } = useSearchQuery();
+ const isInSearchPage = useIsSearchPage();
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | undefined>();
- const pathname = usePathname();
useEffect(() => {
return () => {
@@ -49,7 +54,7 @@ export function useDoBookmarkSearch() {
debounceSearch,
searchQuery,
parsedSearchQuery,
- isInSearchPage: pathname.startsWith("/dashboard/search"),
+ isInSearchPage,
};
}
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
index 71dc93ef..3ad4a25e 100644
--- a/apps/web/lib/i18n/locales/en/translation.json
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -51,6 +51,7 @@
"favorite": "Favorite",
"unfavorite": "Unfavorite",
"delete": "Delete",
+ "toggle_show_archived": "Show Archived",
"refresh": "Refresh",
"recrawl": "Recrawl",
"download_full_page_archive": "Download Full Page Archive",
@@ -79,6 +80,7 @@
"ignore": "Ignore",
"sort": {
"title": "Sort",
+ "relevant_first": "Most Relevant First",
"newest_first": "Newest First",
"oldest_first": "Oldest First"
}
@@ -97,7 +99,20 @@
"new_password": "New Password",
"confirm_new_password": "Confirm New Password",
"options": "Options",
- "interface_lang": "Interface Language"
+ "interface_lang": "Interface Language",
+ "user_settings": {
+ "user_settings_updated": "User settings have been updated!",
+ "bookmark_click_action": {
+ "title": "Bookmark Click Action",
+ "open_external_url": "Open Original URL",
+ "open_bookmark_details": "Open Bookmark Details"
+ },
+ "archive_display_behaviour": {
+ "title": "Archived Bookmarks",
+ "show": "Show archived bookmarks in tags and lists",
+ "hide": "Hide archived bookmarks in tags and lists"
+ }
+ }
},
"ai": {
"ai_settings": "AI Settings",
@@ -114,7 +129,9 @@
},
"feeds": {
"rss_subscriptions": "RSS Subscriptions",
- "add_a_subscription": "Add a Subscription"
+ "add_a_subscription": "Add a Subscription",
+ "feed_enabled": "RSS Feed enabled",
+ "feed_disabled": "RSS Feed disabled"
},
"webhooks": {
"webhooks": "Webhooks",
@@ -246,6 +263,8 @@
"without_inference": "Without Inference",
"regenerate_ai_tags_for_failed_bookmarks_only": "Regenerate AI Tags for Failed Bookmarks Only",
"regenerate_ai_tags_for_all_bookmarks": "Regenerate AI Tags for All Bookmarks",
+ "regenerate_ai_summaries_for_failed_bookmarks_only": "Regenerate AI Summaries for Failed Bookmarks Only",
+ "regenerate_ai_summaries_for_all_bookmarks": "Regenerate AI Summaries for All Bookmarks",
"reindex_all_bookmarks": "Reindex All Bookmarks",
"compact_assets": "Compact Assets",
"reprocess_assets_fix_mode": "Reprocess Assets (Fix Mode)"
@@ -271,6 +290,7 @@
"favourites": "Favourites",
"new_list": "New List",
"edit_list": "Edit List",
+ "share_list": "Share List",
"new_nested_list": "New Nested List",
"merge_list": "Merge List",
"destination_list": "Destination List",
@@ -283,7 +303,17 @@
"smart_list": "Smart List",
"search_query": "Search Query",
"search_query_help": "Learn more about the search query language.",
- "description": "Description (Optional)"
+ "description": "Description (Optional)",
+ "rss": {
+ "title": "RSS Feed",
+ "description": "Enable an RSS feed for this list",
+ "feed_url": "RSS Feed URL"
+ },
+ "public_list": {
+ "title": "Public List",
+ "description": "Allow others to view this list",
+ "share_link": "Share Link"
+ }
},
"tags": {
"all_tags": "All Tags",
@@ -338,7 +368,11 @@
"preview": {
"view_original": "View Original",
"cached_content": "Cached Content",
- "reader_view": "Reader View"
+ "reader_view": "Reader View",
+ "tabs": {
+ "content": "Content",
+ "details": "Details"
+ }
},
"editor": {
"quickly_focus": "You can quickly focus on this field by pressing ⌘ + E",
diff --git a/apps/web/lib/i18n/locales/en_US/translation.json b/apps/web/lib/i18n/locales/en_US/translation.json
index 8783cfc0..a80ecb84 100644
--- a/apps/web/lib/i18n/locales/en_US/translation.json
+++ b/apps/web/lib/i18n/locales/en_US/translation.json
@@ -149,10 +149,10 @@
"align_right": "Right Align"
},
"quickly_focus": "You can quickly focus on this field by pressing ⌘ + E",
- "multiple_urls_dialog_title": "Importing URLs as separate Bookmarks?",
+ "multiple_urls_dialog_title": "Importing URLs as separate bookmarks?",
"multiple_urls_dialog_desc": "The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?",
- "import_as_text": "Import as Text Bookmark",
- "import_as_separate_bookmarks": "Import as separate Bookmarks",
+ "import_as_text": "Import as text bookmark",
+ "import_as_separate_bookmarks": "Import as separate bookmarks",
"placeholder": "Paste a link or an image, write a note, or drag and drop an image in here…",
"placeholder_v2": "Paste a link, write a note, or drop an image…",
"new_item": "NEW ITEM",
@@ -205,7 +205,8 @@
"title": "Events",
"crawled": "Crawled",
"created": "Created",
- "edited": "Edited"
+ "edited": "Edited",
+ "deleted": "Deleted"
},
"auth_token": "Auth Token",
"delete_webhook": "Delete Webhook",
@@ -266,7 +267,7 @@
"rule_has_been_created": "Rule's been created!",
"rule_has_been_updated": "Rule has been updated!",
"rule_has_been_deleted": "Rule's been deleted!",
- "no_rules_created_yet": "No rules created yet, dude",
+ "no_rules_created_yet": "No rules have been created yet",
"create_your_first_rule": "Create your first rule to automate your workflow",
"conditions_types": {
"always": "Always",
@@ -381,7 +382,7 @@
"created_on_or_before": "Created on or Before",
"not_created_on_or_before": "Not Created on or Before",
"created_within": "Created Within",
- "created_earlier_than": "Created Earlier Than",
+ "created_earlier_than": "Created earlier than",
"day_s": " Day(s)",
"week_s": " Week(s)",
"month_s": " Month(s)",
@@ -390,17 +391,17 @@
"week_s_ago": " Week(s) Ago",
"month_s_ago": " Month(s) Ago",
"year_s_ago": " Year(s) Ago",
- "url_contains": "URL Contains",
+ "url_contains": "URL contains",
"is_in_list": "Is In List",
"is_not_in_list": "Is not In List",
"has_tag": "Has Tag",
- "does_not_have_tag": "Does Not Have Tag",
- "full_text_search": "Full Text Search",
+ "does_not_have_tag": "Does not have tag",
+ "full_text_search": "Full text search",
"type_is": "Type is",
"type_is_not": "Type is not",
- "url_does_not_contain": "URL Does Not Contain",
- "is_from_feed": "Is from RSS Feed",
- "is_not_from_feed": "Is not from RSS Feed",
+ "url_does_not_contain": "URL does not contain",
+ "is_from_feed": "Is from RSS feed",
+ "is_not_from_feed": "Is not from RSS feed",
"and": "And",
"or": "Or"
},
@@ -411,18 +412,18 @@
},
"toasts": {
"bookmarks": {
- "deleted": "The bookmark has been deleted!",
- "refetch": "Re-fetch has been added to the queue!",
+ "deleted": "The bookmark has been deleted",
+ "refetch": "Re-fetch has been added to the queue",
"full_page_archive": "Full Page Archive creation has been triggered",
"delete_from_list": "The bookmark has been deleted from the list",
- "clipboard_copied": "Link has been added to your clipboard!",
- "updated": "The bookmark has been updated!"
+ "clipboard_copied": "Link has been added to your clipboard",
+ "updated": "The bookmark has been updated"
},
"lists": {
- "created": "List has been created!",
- "updated": "List has been updated!",
- "merged": "List has been merged!",
- "deleted": "List has been deleted!"
+ "created": "List has been created",
+ "updated": "List has been updated",
+ "merged": "List has been merged",
+ "deleted": "List has been deleted"
}
},
"dialogs": {
diff --git a/apps/web/lib/importBookmarkParser.ts b/apps/web/lib/importBookmarkParser.ts
index a97e4da9..2e354ffe 100644
--- a/apps/web/lib/importBookmarkParser.ts
+++ b/apps/web/lib/importBookmarkParser.ts
@@ -15,6 +15,8 @@ export interface ParsedBookmark {
tags: string[];
addDate?: number;
notes?: string;
+ archived?: boolean;
+ paths: string[][];
}
export async function parseNetscapeBookmarkFile(
@@ -41,11 +43,24 @@ export async function parseNetscapeBookmarkFile(
/* empty */
}
const url = $a.attr("href");
+
+ // Build folder path by traversing up the hierarchy
+ const path: string[] = [];
+ let current = $a.parent();
+ while (current && current.length > 0) {
+ const h3 = current.find("> h3").first();
+ if (h3.length > 0) {
+ path.unshift(h3.text());
+ }
+ current = current.parent();
+ }
+
return {
title: $a.text(),
content: url ? { type: BookmarkTypes.LINK as const, url } : undefined,
tags,
addDate: typeof addDate === "undefined" ? undefined : parseInt(addDate),
+ paths: [path],
};
})
.get();
@@ -64,6 +79,7 @@ export async function parsePocketBookmarkFile(
url: string;
time_added: string;
tags: string;
+ status?: string;
}[];
return records.map((record) => {
@@ -72,6 +88,8 @@ export async function parsePocketBookmarkFile(
content: { type: BookmarkTypes.LINK as const, url: record.url },
tags: record.tags.length > 0 ? record.tags.split("|") : [],
addDate: parseInt(record.time_added),
+ archived: record.status === "archive",
+ paths: [], // TODO
};
});
}
@@ -107,6 +125,8 @@ export async function parseKarakeepBookmarkFile(
tags: bookmark.tags,
addDate: bookmark.createdAt,
notes: bookmark.note ?? undefined,
+ archived: bookmark.archived,
+ paths: [], // TODO
};
});
}
@@ -121,6 +141,7 @@ export async function parseOmnivoreBookmarkFile(
url: z.string(),
labels: z.array(z.string()),
savedAt: z.coerce.date(),
+ state: z.string().optional(),
}),
);
@@ -137,6 +158,8 @@ export async function parseOmnivoreBookmarkFile(
content: { type: BookmarkTypes.LINK as const, url: bookmark.url },
tags: bookmark.labels,
addDate: bookmark.savedAt.getTime() / 1000,
+ archived: bookmark.state === "Archived",
+ paths: [],
};
});
}
@@ -173,6 +196,7 @@ export async function parseLinkwardenBookmarkFile(
content: { type: BookmarkTypes.LINK as const, url: bookmark.url },
tags: bookmark.tags.map((tag) => tag.name),
addDate: bookmark.createdAt.getTime() / 1000,
+ paths: [], // TODO
}));
});
}
@@ -213,6 +237,7 @@ export async function parseTabSessionManagerStateFile(
content: { type: BookmarkTypes.LINK as const, url: tab.url },
tags: [],
addDate: tab.lastAccessed,
+ paths: [], // Tab Session Manager doesn't have folders
})),
);
}
@@ -230,7 +255,8 @@ export function deduplicateBookmarks(
const existing = deduplicatedBookmarksMap.get(url)!;
// Merge tags
existing.tags = [...new Set([...existing.tags, ...bookmark.tags])];
- // Keep earliest date
+ // Merge paths
+ existing.paths = [...existing.paths, ...bookmark.paths];
const existingDate = existing.addDate ?? Infinity;
const newDate = bookmark.addDate ?? Infinity;
if (newDate < existingDate) {
@@ -242,6 +268,10 @@ export function deduplicateBookmarks(
} else if (bookmark.notes) {
existing.notes = bookmark.notes;
}
+ // For archived status, prefer archived if either is archived
+ if (bookmark.archived === true) {
+ existing.archived = true;
+ }
// Title: keep existing one for simplicity
} else {
deduplicatedBookmarksMap.set(url, bookmark);
diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx
new file mode 100644
index 00000000..1590f727
--- /dev/null
+++ b/apps/web/lib/userSettings.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+import { ZUserSettings } from "@karakeep/shared/types/users";
+
+import { api } from "./trpc";
+
+export const UserSettingsContext = createContext<ZUserSettings>({
+ bookmarkClickAction: "open_original_link",
+ archiveDisplayBehaviour: "show",
+});
+
+export function UserSettingsContextProvider({
+ userSettings,
+ children,
+}: {
+ userSettings: ZUserSettings;
+ children: React.ReactNode;
+}) {
+ const { data } = api.users.settings.useQuery(undefined, {
+ initialData: userSettings,
+ });
+
+ return (
+ <UserSettingsContext.Provider value={data}>
+ {children}
+ </UserSettingsContext.Provider>
+ );
+}
+
+export function useUserSettings() {
+ return useContext(UserSettingsContext);
+}