mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-09 03:18:04 +00:00
feat(browser-extension): folder level x bookmarks import with project selection (#495)
Feature : Import folder level x bookmarks [Screen Recording 2025-10-17 at 1.37.52 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/user-attachments/thumbnails/15cd60ff-856e-4f29-8897-74ae3c869c87.mov" />](https://app.graphite.dev/user-attachments/video/15cd60ff-856e-4f29-8897-74ae3c869c87.mov)
This commit is contained in:
parent
5a001c639a
commit
809eaa0d49
8 changed files with 573 additions and 18 deletions
|
|
@ -1,4 +1,9 @@
|
|||
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
|
||||
import {
|
||||
getDefaultProject,
|
||||
saveMemory,
|
||||
searchMemories,
|
||||
fetchProjects,
|
||||
} from "../utils/api"
|
||||
import {
|
||||
CONTAINER_TAGS,
|
||||
CONTEXT_MENU_IDS,
|
||||
|
|
@ -169,6 +174,9 @@ export default defineBackground(() => {
|
|||
// Handle Twitter import request
|
||||
if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) {
|
||||
const importConfig: TwitterImportConfig = {
|
||||
isFolderImport: message.isFolderImport,
|
||||
bookmarkCollectionId: message.bookmarkCollectionId,
|
||||
selectedProject: message.selectedProject,
|
||||
onProgress: sendMessageToCurrentTab,
|
||||
onComplete: sendImportDoneMessage,
|
||||
onError: async (error: Error) => {
|
||||
|
|
@ -249,6 +257,21 @@ export default defineBackground(() => {
|
|||
})()
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.action === MESSAGE_TYPES.FETCH_PROJECTS) {
|
||||
;(async () => {
|
||||
try {
|
||||
const projects = await fetchProjects()
|
||||
sendResponse({ success: true, data: projects })
|
||||
} catch (error) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
}
|
||||
})()
|
||||
return true
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,26 @@ import {
|
|||
POSTHOG_EVENT_KEY,
|
||||
} from "../../utils/constants"
|
||||
import { trackEvent } from "../../utils/posthog"
|
||||
import { createTwitterImportButton, DOMUtils } from "../../utils/ui-components"
|
||||
import {
|
||||
createTwitterImportButton,
|
||||
createProjectSelectionModal,
|
||||
DOMUtils,
|
||||
} from "../../utils/ui-components"
|
||||
|
||||
async function loadSpaceGroteskFonts(): Promise<void> {
|
||||
if (document.getElementById("supermemory-modal-styles")) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const style = document.createElement("style")
|
||||
style.id = "supermemory-modal-styles"
|
||||
style.textContent = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap');
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
await document.fonts.ready
|
||||
}
|
||||
|
||||
export function initializeTwitter() {
|
||||
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
|
||||
|
|
@ -16,6 +35,7 @@ export function initializeTwitter() {
|
|||
if (window.location.pathname === "/i/bookmarks") {
|
||||
setTimeout(() => {
|
||||
addTwitterImportButton()
|
||||
addTwitterImportButtonForFolders()
|
||||
}, 2000)
|
||||
} else {
|
||||
// Remove button if not on bookmarks page
|
||||
|
|
@ -26,7 +46,6 @@ export function initializeTwitter() {
|
|||
}
|
||||
|
||||
function addTwitterImportButton() {
|
||||
// Only show the import button on the bookmarks page
|
||||
if (window.location.pathname !== "/i/bookmarks") {
|
||||
return
|
||||
}
|
||||
|
|
@ -51,6 +70,46 @@ function addTwitterImportButton() {
|
|||
document.body.appendChild(button)
|
||||
}
|
||||
|
||||
function addTwitterImportButtonForFolders() {
|
||||
if (window.location.pathname !== "/i/bookmarks") {
|
||||
return
|
||||
}
|
||||
|
||||
const targetElements = document.querySelectorAll(
|
||||
".css-175oi2r.r-1wtj0ep.r-16x9es5.r-1mmae3n.r-o7ynqc.r-6416eg.r-1ny4l3l.r-1loqt21",
|
||||
)
|
||||
|
||||
targetElements.forEach((element) => {
|
||||
addButtonToElement(element as HTMLElement)
|
||||
})
|
||||
}
|
||||
|
||||
function addButtonToElement(element: HTMLElement) {
|
||||
if (element.querySelector("[data-supermemory-button]")) {
|
||||
return
|
||||
}
|
||||
|
||||
loadSpaceGroteskFonts()
|
||||
|
||||
const button = createSaveTweetElement(async () => {
|
||||
const url = element.getAttribute("href")
|
||||
const bookmarkCollectionId = url?.split("/").pop()
|
||||
console.log("Bookmark collection ID:", bookmarkCollectionId)
|
||||
if (bookmarkCollectionId) {
|
||||
await showProjectSelectionModal(bookmarkCollectionId)
|
||||
}
|
||||
})
|
||||
|
||||
button.setAttribute("data-supermemory-button", "true")
|
||||
|
||||
element.appendChild(button)
|
||||
element.style.flexDirection = "row"
|
||||
element.style.alignItems = "center"
|
||||
element.style.justifyContent = "center"
|
||||
element.style.gap = "10px"
|
||||
element.style.padding = "10px"
|
||||
}
|
||||
|
||||
export function updateTwitterImportUI(message: {
|
||||
type: string
|
||||
importedMessage?: string
|
||||
|
|
@ -94,9 +153,120 @@ export function handleTwitterNavigation() {
|
|||
|
||||
if (window.location.pathname === "/i/bookmarks") {
|
||||
addTwitterImportButton()
|
||||
addTwitterImportButtonForFolders()
|
||||
} else {
|
||||
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
|
||||
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
|
||||
}
|
||||
document.querySelectorAll("[data-supermemory-button]").forEach((button) => {
|
||||
button.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the project selection modal for folder imports
|
||||
* @param bookmarkCollectionId - The ID of the bookmark collection to import
|
||||
*/
|
||||
async function showProjectSelectionModal(bookmarkCollectionId: string) {
|
||||
try {
|
||||
const modal = createProjectSelectionModal(
|
||||
[],
|
||||
async (selectedProject) => {
|
||||
modal.remove()
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
|
||||
isFolderImport: true,
|
||||
bookmarkCollectionId: bookmarkCollectionId,
|
||||
selectedProject: selectedProject,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error importing bookmarks:", error)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
modal.remove()
|
||||
},
|
||||
)
|
||||
|
||||
document.body.appendChild(modal)
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: MESSAGE_TYPES.FETCH_PROJECTS,
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
const projects = response.data
|
||||
|
||||
if (projects.length === 0) {
|
||||
console.warn("No projects available for import")
|
||||
updateModalWithProjects(modal, [])
|
||||
} else {
|
||||
updateModalWithProjects(modal, projects)
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch projects:", response.error)
|
||||
updateModalWithProjects(modal, [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching projects:", error)
|
||||
updateModalWithProjects(modal, [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error showing project selection modal:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the modal with fetched projects
|
||||
* @param modal - The modal element
|
||||
* @param projects - Array of projects to populate the dropdown
|
||||
*/
|
||||
function updateModalWithProjects(
|
||||
modal: HTMLElement,
|
||||
projects: Array<{ id: string; name: string; containerTag: string }>,
|
||||
) {
|
||||
const select = modal.querySelector("#project-select") as HTMLSelectElement
|
||||
if (!select) return
|
||||
|
||||
while (select.children.length > 1) {
|
||||
select.removeChild(select.children[1])
|
||||
}
|
||||
|
||||
if (projects.length === 0) {
|
||||
const noProjectsOption = document.createElement("option")
|
||||
noProjectsOption.value = ""
|
||||
noProjectsOption.textContent = "No projects available"
|
||||
noProjectsOption.disabled = true
|
||||
select.appendChild(noProjectsOption)
|
||||
|
||||
const importButton = modal.querySelector(
|
||||
"button:last-child",
|
||||
) as HTMLButtonElement
|
||||
if (importButton) {
|
||||
importButton.disabled = true
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s ease;
|
||||
`
|
||||
}
|
||||
} else {
|
||||
projects.forEach((project) => {
|
||||
const option = document.createElement("option")
|
||||
option.value = project.id
|
||||
option.textContent = project.name
|
||||
option.dataset.containerTag = project.containerTag
|
||||
select.appendChild(option)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const ELEMENT_IDS = {
|
|||
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
|
||||
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
|
||||
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
|
||||
PROJECT_SELECTION_MODAL: "sm-project-selection-modal",
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
@ -81,6 +82,7 @@ export const MESSAGE_TYPES = {
|
|||
IMPORT_DONE: "sm-import-done",
|
||||
GET_RELATED_MEMORIES: "sm-get-related-memories",
|
||||
CAPTURE_PROMPT: "sm-capture-prompt",
|
||||
FETCH_PROJECTS: "sm-fetch-projects",
|
||||
} as const
|
||||
|
||||
export const CONTEXT_MENU_IDS = {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
* Twitter Bookmarks Import Module
|
||||
* Handles the import process for Twitter bookmarks
|
||||
*/
|
||||
|
||||
import { saveAllTweets } from "./api"
|
||||
import type { MemoryPayload } from "./types"
|
||||
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
|
||||
import {
|
||||
BOOKMARKS_URL,
|
||||
BOOKMARK_COLLECTION_URL,
|
||||
buildRequestVariables,
|
||||
buildBookmarkCollectionVariables,
|
||||
extractNextCursor,
|
||||
getAllTweets,
|
||||
type TwitterAPIResponse,
|
||||
|
|
@ -18,6 +20,13 @@ export type ImportProgressCallback = (message: string) => Promise<void>
|
|||
export type ImportCompleteCallback = (totalImported: number) => Promise<void>
|
||||
|
||||
export interface TwitterImportConfig {
|
||||
isFolderImport?: boolean
|
||||
bookmarkCollectionId?: string
|
||||
selectedProject?: {
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
}
|
||||
onProgress: ImportProgressCallback
|
||||
onComplete: ImportCompleteCallback
|
||||
onError: (error: Error) => Promise<void>
|
||||
|
|
@ -81,7 +90,11 @@ export class TwitterImporter {
|
|||
* @param cursor - Pagination cursor for Twitter API
|
||||
* @param totalImported - Number of tweets imported so far
|
||||
*/
|
||||
private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> {
|
||||
private async batchImportAll(
|
||||
cursor = "",
|
||||
totalImported = 0,
|
||||
uniqueGroupId = "twitter_bookmarks",
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Use a local variable to track imported count
|
||||
let importedCount = totalImported
|
||||
|
|
@ -99,10 +112,19 @@ export class TwitterImporter {
|
|||
const headers = createTwitterAPIHeaders(tokens)
|
||||
|
||||
// Build API request with pagination
|
||||
const variables = buildRequestVariables(cursor)
|
||||
const variables =
|
||||
this.config.isFolderImport && this.config.bookmarkCollectionId
|
||||
? buildBookmarkCollectionVariables(this.config.bookmarkCollectionId)
|
||||
: buildRequestVariables(cursor)
|
||||
const urlWithCursor = cursor
|
||||
? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
|
||||
: BOOKMARKS_URL
|
||||
? `${
|
||||
this.config.isFolderImport && this.config.bookmarkCollectionId
|
||||
? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
|
||||
: BOOKMARKS_URL
|
||||
}&variables=${encodeURIComponent(JSON.stringify(variables))}`
|
||||
: this.config.isFolderImport && this.config.bookmarkCollectionId
|
||||
? `${BOOKMARK_COLLECTION_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
|
||||
: `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
|
||||
|
||||
const response = await fetch(urlWithCursor, {
|
||||
method: "GET",
|
||||
|
|
@ -140,14 +162,20 @@ export class TwitterImporter {
|
|||
retweets: tweet.retweet_count || 0,
|
||||
sm_internal_group_id: uniqueGroupId,
|
||||
}
|
||||
const containerTag =
|
||||
this.config.selectedProject?.containerTag ||
|
||||
"sm_project_twitter_bookmarks"
|
||||
|
||||
documents.push({
|
||||
containerTags: ["sm_project_twitter_bookmarks"],
|
||||
containerTags: [containerTag],
|
||||
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
|
||||
metadata,
|
||||
customId: tweet.id_str,
|
||||
})
|
||||
importedCount++
|
||||
await this.config.onProgress(`Imported ${importedCount} tweets, so far...`)
|
||||
await this.config.onProgress(
|
||||
`Imported ${importedCount} tweets, so far...`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error importing tweet:", error)
|
||||
}
|
||||
|
|
@ -167,13 +195,15 @@ export class TwitterImporter {
|
|||
|
||||
// Handle pagination
|
||||
const instructions =
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions
|
||||
const nextCursor = extractNextCursor(instructions || [])
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions ||
|
||||
data.data?.bookmark_collection_timeline?.timeline?.instructions ||
|
||||
[]
|
||||
const nextCursor = extractNextCursor(instructions)
|
||||
|
||||
console.log("Next cursor:", nextCursor)
|
||||
console.log("Tweets length:", tweets.length)
|
||||
|
||||
if (nextCursor && tweets.length > 0) {
|
||||
if (nextCursor && tweets.length > 0 && !this.config.isFolderImport) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
|
||||
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,19 @@ export interface Tweet {
|
|||
|
||||
export interface TwitterAPIResponse {
|
||||
data: {
|
||||
bookmark_timeline_v2: {
|
||||
bookmark_timeline_v2?: {
|
||||
timeline: {
|
||||
instructions: Array<{
|
||||
type: string
|
||||
entries?: Array<{
|
||||
entryId: string
|
||||
sortIndex: string
|
||||
content: Record<string, unknown>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
}
|
||||
bookmark_collection_timeline?: {
|
||||
timeline: {
|
||||
instructions: Array<{
|
||||
type: string
|
||||
|
|
@ -167,8 +179,49 @@ export const TWITTER_API_FEATURES = {
|
|||
verified_phone_label_enabled: true,
|
||||
}
|
||||
|
||||
// Twitter API features configuration for BookmarkFolderTimeline
|
||||
export const TWITTER_BOOKMARK_FOLDER_FEATURES = {
|
||||
rweb_video_screen_enabled: false,
|
||||
payments_enabled: false,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: false,
|
||||
rweb_tipjar_consumption_enabled: true,
|
||||
verified_phone_label_enabled: false,
|
||||
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: true,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
responsive_web_grok_share_attachment_enabled: true,
|
||||
articles_preview_enabled: true,
|
||||
responsive_web_edit_tweet_api_enabled: true,
|
||||
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||
view_counts_everywhere_api_enabled: true,
|
||||
longform_notetweets_consumption_enabled: true,
|
||||
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||
tweet_awards_web_tipping_enabled: false,
|
||||
responsive_web_grok_show_grok_translated_post: true,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||
freedom_of_speech_not_reach_fetch_enabled: true,
|
||||
standardized_nudges_misinfo: true,
|
||||
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
}
|
||||
|
||||
export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`
|
||||
|
||||
export const BOOKMARK_COLLECTION_URL = `https://x.com/i/api/graphql/I8Y9ni1dqP-ZSpwxqJQ--Q/BookmarkFolderTimeline?features=${encodeURIComponent(JSON.stringify(TWITTER_BOOKMARK_FOLDER_FEATURES))}`
|
||||
|
||||
/**
|
||||
* Transform raw Twitter API response data into standardized Tweet format
|
||||
*/
|
||||
|
|
@ -264,7 +317,9 @@ export function getAllTweets(data: TwitterAPIResponse): Tweet[] {
|
|||
|
||||
try {
|
||||
const instructions =
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions || []
|
||||
data.data?.bookmark_timeline_v2?.timeline?.instructions ||
|
||||
data.data?.bookmark_collection_timeline?.timeline?.instructions ||
|
||||
[]
|
||||
|
||||
for (const instruction of instructions) {
|
||||
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
|
||||
|
|
@ -375,3 +430,13 @@ export function buildRequestVariables(cursor?: string, count = 100) {
|
|||
|
||||
return variables
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Twitter API request variables for bookmark collection
|
||||
*/
|
||||
export function buildBookmarkCollectionVariables(bookmarkCollectionId: string) {
|
||||
return {
|
||||
bookmark_collection_id: bookmarkCollectionId,
|
||||
includePromotedContent: true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export type ToastState = "loading" | "success" | "error"
|
|||
* Message types for extension communication
|
||||
*/
|
||||
export interface ExtensionMessage {
|
||||
isFolderImport?: boolean
|
||||
bookmarkCollectionId?: string
|
||||
action?: string
|
||||
type?: string
|
||||
data?: unknown
|
||||
|
|
@ -18,6 +20,11 @@ export interface ExtensionMessage {
|
|||
importedMessage?: string
|
||||
totalImported?: number
|
||||
actionSource?: string
|
||||
selectedProject?: {
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
|
|
@ -214,7 +215,6 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
|
|||
iconButton.style.cssText = `
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
opacity: 0.7;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
|
@ -378,6 +378,264 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
|
|||
return iconButton
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a project selection modal for Twitter folder imports
|
||||
* @param projects - Array of available projects
|
||||
* @param onImport - Callback when import is clicked with selected project
|
||||
* @param onClose - Callback when modal is closed
|
||||
* @returns HTMLElement - The modal element
|
||||
*/
|
||||
export function createProjectSelectionModal(
|
||||
projects: Array<{ id: string; name: string; containerTag: string }>,
|
||||
onImport: (project: {
|
||||
id: string
|
||||
name: string
|
||||
containerTag: string
|
||||
}) => void,
|
||||
onClose: () => void,
|
||||
): HTMLElement {
|
||||
const modal = document.createElement("div")
|
||||
modal.id = "sm-project-selection-modal"
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2147483648;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`
|
||||
|
||||
const dialog = document.createElement("div")
|
||||
dialog.style.cssText = `
|
||||
background: #05070A;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(5, 7, 10, 0.2);
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const header = document.createElement("div")
|
||||
header.style.cssText = `
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const iconUrl = browser.runtime.getURL("/icon-16.png")
|
||||
header.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #ffffff; display: flex; align-items: center; gap: 8px;">
|
||||
<img src="${iconUrl}" width="20" height="20" alt="Supermemory" style="border-radius: 4px;" />
|
||||
Import to Supermemory
|
||||
</h3>
|
||||
<p style="margin: 0; font-size: 14px; font-weight: 400; color: #ffffff; opacity: 0.7;">
|
||||
The project you want to import your bookmarks to.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
const form = document.createElement("div")
|
||||
form.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const selectContainer = document.createElement("div")
|
||||
selectContainer.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const label = document.createElement("label")
|
||||
label.style.cssText = `
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
`
|
||||
label.textContent = "Select Project to import"
|
||||
|
||||
const select = document.createElement("select")
|
||||
select.id = "project-select"
|
||||
select.style.cssText = `
|
||||
padding: 12px 40px 12px 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
background: rgba(91, 126, 245, 0.04);
|
||||
box-shadow: -1px -1px 1px 0 rgba(82, 89, 102, 0.08) inset, 2px 2px 1px 0 rgba(0, 0, 0, 0.50) inset;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
background-size: 16px;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`
|
||||
select.addEventListener("focus", () => {
|
||||
select.style.borderColor = "#1A88FF"
|
||||
})
|
||||
select.addEventListener("blur", () => {
|
||||
select.style.borderColor = "#374151"
|
||||
})
|
||||
|
||||
// Add default option
|
||||
const defaultOption = document.createElement("option")
|
||||
defaultOption.value = ""
|
||||
defaultOption.textContent = "Choose a project..."
|
||||
defaultOption.disabled = true
|
||||
defaultOption.selected = true
|
||||
select.appendChild(defaultOption)
|
||||
|
||||
// Add project options
|
||||
projects.forEach((project) => {
|
||||
const option = document.createElement("option")
|
||||
option.value = project.id
|
||||
option.textContent = project.name
|
||||
option.dataset.containerTag = project.containerTag
|
||||
select.appendChild(option)
|
||||
})
|
||||
|
||||
const buttonContainer = document.createElement("div")
|
||||
buttonContainer.style.cssText = `
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const cancelButton = document.createElement("button")
|
||||
cancelButton.textContent = "Cancel"
|
||||
cancelButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: #05070A;
|
||||
`
|
||||
cancelButton.addEventListener("mouseenter", () => {
|
||||
cancelButton.style.backgroundColor = "#f9fafb"
|
||||
cancelButton.style.color = "#05070A"
|
||||
})
|
||||
cancelButton.addEventListener("mouseleave", () => {
|
||||
cancelButton.style.backgroundColor = "#05070A"
|
||||
cancelButton.style.color = "#ffffff"
|
||||
})
|
||||
|
||||
const importButton = document.createElement("button")
|
||||
importButton.textContent = "Import"
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s ease;
|
||||
`
|
||||
importButton.disabled = true
|
||||
|
||||
// Handle project selection
|
||||
select.addEventListener("change", () => {
|
||||
const selectedOption = select.options[select.selectedIndex]
|
||||
if (selectedOption.value) {
|
||||
importButton.disabled = false
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(203deg, #0FF0D2 -49.88%, #5BD3FB -33.14%, #1E0FF0 81.81%);
|
||||
box-shadow: 1px 1px 2px 1px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
`
|
||||
} else {
|
||||
importButton.disabled = true
|
||||
importButton.style.cssText = `
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s ease;
|
||||
`
|
||||
}
|
||||
})
|
||||
|
||||
// Handle import button click
|
||||
importButton.addEventListener("click", () => {
|
||||
const selectedOption = select.options[select.selectedIndex]
|
||||
if (selectedOption.value) {
|
||||
const selectedProject = {
|
||||
id: selectedOption.value,
|
||||
name: selectedOption.textContent,
|
||||
containerTag: selectedOption.dataset.containerTag || "",
|
||||
}
|
||||
onImport(selectedProject)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle cancel button click
|
||||
cancelButton.addEventListener("click", onClose)
|
||||
|
||||
// Handle overlay click to close
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
|
||||
// Handle escape key
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
// Clean up event listener when modal is removed
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!document.contains(modal)) {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
|
||||
selectContainer.appendChild(label)
|
||||
selectContainer.appendChild(select)
|
||||
form.appendChild(selectContainer)
|
||||
buttonContainer.appendChild(cancelButton)
|
||||
buttonContainer.appendChild(importButton)
|
||||
form.appendChild(buttonContainer)
|
||||
|
||||
dialog.appendChild(header)
|
||||
dialog.appendChild(form)
|
||||
modal.appendChild(dialog)
|
||||
|
||||
return modal
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions for DOM manipulation
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default defineConfig({
|
|||
manifest: {
|
||||
name: "supermemory",
|
||||
homepage_url: "https://supermemory.ai",
|
||||
version: "6.0.003",
|
||||
version: "6.0.1",
|
||||
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
|
||||
host_permissions: [
|
||||
"*://x.com/*",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue