diff options
Diffstat (limited to 'apps/web/lib')
| -rw-r--r-- | apps/web/lib/clientConfig.tsx | 3 | ||||
| -rw-r--r-- | apps/web/lib/exportBookmarks.ts | 51 | ||||
| -rw-r--r-- | apps/web/lib/hooks/bookmark-search.ts | 9 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 42 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en_US/translation.json | 41 | ||||
| -rw-r--r-- | apps/web/lib/importBookmarkParser.ts | 32 | ||||
| -rw-r--r-- | apps/web/lib/userSettings.tsx | 34 |
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> = { + "&": "&", + "'": "'", + "`": "`", + '"': """, + "<": "<", + ">": ">", + }; + + 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); +} |
