From 44d4cae477f5ef7491288aea35eb6f21c8acd913 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Sun, 14 Sep 2025 10:09:00 +0200 Subject: [PATCH 1/8] feat: add bookmark search functionality - Add search mode with real-time filtering - Implement keyboard navigation (arrows, enter, shift+enter) - Integrate with Shiori API /api/bookmarks endpoint - Support opening bookmarks in current/new tabs - Maintain existing add bookmark functionality - Auto-focus search input on popup open --- css/popup.css | 248 ++++++++++++++++++++++++++++- js/background-script.js | 342 ++++++++++++++++++++++------------------ js/popup.js | 292 +++++++++++++++++++++++++++------- manifest.json | 134 ++++++++-------- view/popup.html | 46 ++++-- 5 files changed, 778 insertions(+), 284 deletions(-) diff --git a/css/popup.css b/css/popup.css index 31b183f..443427b 100644 --- a/css/popup.css +++ b/css/popup.css @@ -1 +1,247 @@ -:root{--contentBg:#FFF;--border:#E5E5E5;--color:#232323;--colorLink:#999;--main:#F44336;--errorColor:#F44336;--buttonColor:#FFF;--buttonBg:#292929}@media (prefers-color-scheme:dark){:root:root{--contentBg:#292929;--border:#191919;--color:#FFF;--buttonBg:#444}}*{border-width:0;box-sizing:border-box;font-family:"Source Sans Pro",sans-serif;margin:0;padding:0;text-decoration:none}a{cursor:pointer}body{font-size:16px;background-color:var(--contentBg)}#popup-box{display:grid;width:380px;grid-template-columns:auto 1fr;padding:16px;grid-gap:4px}#popup-box #logo{grid-column:1;grid-row:1 / span 2;width:50px;margin-right:8px;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center}#popup-box #logo img{width:100%}#popup-box #txt-status{color:var(--color);grid-row:1;grid-column:2;font-size:1.5em}#popup-box #menu-box{grid-row:2;grid-column:2;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap}#popup-box #menu-box>*:not(:first-child){margin-left:8px}#popup-box #menu-box p{color:var(--colorLink)}#popup-box #menu-box a{display:block;color:var(--colorLink);font-size:.9em}#popup-box #menu-box a:hover,#popup-box #menu-box a:focus{color:var(--main);text-decoration:underline}#popup-box #input-box{grid-row:3;grid-column:1 / span 2;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;margin-top:12px;margin-bottom:4px}#popup-box #input-box input{color:var(--color);padding:8px;background-color:var(--contentBg);border:1px solid var(--border);min-width:0;font-size:1em;-webkit-box-flex:1;flex:1 0}#popup-box #input-box a{padding:8px 16px;color:var(--buttonColor);background-color:var(--buttonBg);border:1px solid var(--border);border-left-width:0;font-weight:600}#popup-box #input-box a:hover,#popup-box #input-box a:focus{color:var(--main)}#popup-box #input-box #loading-sign{cursor:default}#popup-box #input-box #loading-sign:hover,#popup-box #input-box #loading-sign:focus{color:var(--buttonColor)} \ No newline at end of file +:root { + --contentBg: #FFF; + --border: #E5E5E5; + --color: #232323; + --colorLink: #999; + --main: #F44336; + --errorColor: #F44336; + --buttonColor: #FFF; + --buttonBg: #292929; +} + +@media (prefers-color-scheme: dark) { + :root:root { + --contentBg: #292929; + --border: #191919; + --color: #FFF; + --buttonBg: #444; + } +} + +* { + border-width: 0; + box-sizing: border-box; + font-family: "Source Sans Pro", sans-serif; + margin: 0; + padding: 0; + text-decoration: none; +} + +a { + cursor: pointer; +} + +body { + font-size: 16px; + background-color: var(--contentBg); +} + +#popup-box { + width: 380px; + padding: 16px; +} + +#popup-box #logo { + width: 32px; + height: 32px; + display: inline-block; + vertical-align: top; + margin-right: 8px; +} + +#popup-box #logo img { + width: 100%; + height: 100%; +} + +/* Search mode layout */ +#search-mode { + width: 100%; +} + +#search-input-box { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +#search-input-box input { + color: var(--color); + padding: 8px; + background-color: var(--contentBg); + border: 1px solid var(--border); + min-width: 0; + font-size: 1em; + flex: 1; +} + +#search-input-box a { + padding: 8px 16px; + color: var(--buttonColor); + background-color: var(--buttonBg); + border: 1px solid var(--border); + border-left-width: 0; + font-weight: 600; +} + +#search-input-box a:hover, +#search-input-box a:focus { + color: var(--main); +} + +#search-results { + width: 100%; + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--border); + background-color: var(--contentBg); +} + +#search-loading, +#search-empty { + padding: 16px; + text-align: center; + color: var(--colorLink); + font-size: 0.9em; +} + +#search-loading i { + margin-right: 8px; +} + +#bookmark-list { + display: flex; + flex-direction: column; +} + +.bookmark-item { + padding: 12px; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background-color 0.1s ease; +} + +.bookmark-item:hover, +.bookmark-item.selected { + background-color: var(--border); +} + +.bookmark-item:last-child { + border-bottom: none; +} + +.bookmark-title { + font-weight: 600; + color: var(--color); + margin-bottom: 4px; + font-size: 0.9em; + word-break: break-word; +} + +.bookmark-url { + color: var(--colorLink); + font-size: 0.8em; + margin-bottom: 6px; + word-break: break-all; +} + +.bookmark-tags { + display: flex; + flex-flow: row wrap; + gap: 4px; +} + +.bookmark-tag { + background-color: var(--main); + color: var(--buttonColor); + padding: 2px 6px; + border-radius: 3px; + font-size: 0.7em; + font-weight: 500; +} + +/* Add mode layout */ +#add-mode { + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 4px; + align-items: start; +} + +#add-mode #txt-status { + color: var(--color); + grid-row: 1; + grid-column: 2; + font-size: 1.5em; + margin-top: 4px; +} + +#add-mode #menu-box { + grid-row: 2; + grid-column: 2; + display: flex; + flex-flow: row wrap; +} + +#add-mode #menu-box>*:not(:first-child) { + margin-left: 8px; +} + +#add-mode #menu-box p { + color: var(--colorLink); +} + +#add-mode #menu-box a { + display: block; + color: var(--colorLink); + font-size: 0.9em; +} + +#add-mode #menu-box a:hover, +#add-mode #menu-box a:focus { + color: var(--main); + text-decoration: underline; +} + +#add-mode #input-box { + grid-row: 3; + grid-column: 1 / span 2; + display: flex; + flex-flow: row nowrap; + margin-top: 12px; + margin-bottom: 4px; +} + +#add-mode #input-box input { + color: var(--color); + padding: 8px; + background-color: var(--contentBg); + border: 1px solid var(--border); + min-width: 0; + font-size: 1em; + flex: 1; +} + +#add-mode #input-box a { + padding: 8px 16px; + color: var(--buttonColor); + background-color: var(--buttonBg); + border: 1px solid var(--border); + border-left-width: 0; + font-weight: 600; +} + +#add-mode #input-box a:hover, +#add-mode #input-box a:focus { + color: var(--main); +} + +#add-mode #input-box #loading-sign { + cursor: default; +} + +#add-mode #input-box #loading-sign:hover, +#add-mode #input-box #loading-sign:focus { + color: var(--buttonColor); +} diff --git a/js/background-script.js b/js/background-script.js index b797a7c..4d704c5 100644 --- a/js/background-script.js +++ b/js/background-script.js @@ -1,170 +1,212 @@ async function getCurrentTab() { - const tabs = await browser.tabs.query({ - currentWindow: true, - active: true, - }); + const tabs = await browser.tabs.query({ + currentWindow: true, + active: true, + }); - if (tabs.length < 1) { - throw new Error("No tab available"); - } + if (tabs.length < 1) { + throw new Error("No tab available"); + } - const supportedProtocols = ["https:", "http:", "ftp:", "file:"]; - const activeTab = tabs[0]; - const url = new URL(activeTab.url); + const supportedProtocols = ["https:", "http:", "ftp:", "file:"]; + const activeTab = tabs[0]; + const url = new URL(activeTab.url); - if (!supportedProtocols.includes(url.protocol)) { - throw new Error(`Protocol "${url.protocol}" is not supported`); - } + if (!supportedProtocols.includes(url.protocol)) { + throw new Error(`Protocol "${url.protocol}" is not supported`); + } - return activeTab; + return activeTab; } async function getPageContent(tab) { - try { - const content = await browser.tabs.sendMessage(tab.id, { type: "page-content" }); - return content; - } catch { - return {}; - } + try { + const content = await browser.tabs.sendMessage(tab.id, { type: "page-content" }); + return content; + } catch { + return {}; + } } async function getShioriBookmarkFolder() { - const runtimeUrl = browser.runtime.getURL("/"); - let parentId; - - if (runtimeUrl.startsWith("moz")) { - parentId = "unfiled_____"; - } else if (runtimeUrl.startsWith("chrome")) { - parentId = "2"; - } else { - throw new Error("Extension only supports Firefox and Chrome"); - } - - const children = await browser.bookmarks.getChildren(parentId); - let shiori = children.find((el) => !el.url && el.title === "Shiori"); - - if (!shiori) { - shiori = await browser.bookmarks.create({ - title: "Shiori", - parentId, - }); - } + const runtimeUrl = browser.runtime.getURL("/"); + let parentId; + + if (runtimeUrl.startsWith("moz")) { + parentId = "unfiled_____"; + } else if (runtimeUrl.startsWith("chrome")) { + parentId = "2"; + } else { + throw new Error("Extension only supports Firefox and Chrome"); + } + + const children = await browser.bookmarks.getChildren(parentId); + let shiori = children.find((el) => !el.url && el.title === "Shiori"); + + if (!shiori) { + shiori = await browser.bookmarks.create({ + title: "Shiori", + parentId, + }); + } - return shiori; + return shiori; } async function findLocalBookmark(url) { - const shioriFolder = await getShioriBookmarkFolder(); - const existingBookmarks = await browser.bookmarks.search({ url }); + const shioriFolder = await getShioriBookmarkFolder(); + const existingBookmarks = await browser.bookmarks.search({ url }); - return existingBookmarks.find((book) => book.parentId === shioriFolder.id) || null; + return existingBookmarks.find((book) => book.parentId === shioriFolder.id) || null; } async function saveLocalBookmark(url, title) { - const shioriFolder = await getShioriBookmarkFolder(); - const existingBookmarks = await browser.bookmarks.search({ url }); - - if (!existingBookmarks.some((book) => book.parentId === shioriFolder.id)) { - await browser.bookmarks.create({ - url, - title, - parentId: shioriFolder.id, - }); - } + const shioriFolder = await getShioriBookmarkFolder(); + const existingBookmarks = await browser.bookmarks.search({ url }); + + if (!existingBookmarks.some((book) => book.parentId === shioriFolder.id)) { + await browser.bookmarks.create({ + url, + title, + parentId: shioriFolder.id, + }); + } } async function removeLocalBookmark(url) { - const shioriFolder = await getShioriBookmarkFolder(); - const existingBookmarks = await browser.bookmarks.search({ url }); + const shioriFolder = await getShioriBookmarkFolder(); + const existingBookmarks = await browser.bookmarks.search({ url }); - for (const book of existingBookmarks) { - if (book.parentId === shioriFolder.id) { - await browser.bookmarks.remove(book.id); - } + for (const book of existingBookmarks) { + if (book.parentId === shioriFolder.id) { + await browser.bookmarks.remove(book.id); } + } } async function getExtensionConfig() { - const { token = "", server = "" } = await browser.storage.local.get(); + const { token = "", server = "" } = await browser.storage.local.get(); - if (!token) { - throw new Error("No active session, please login first"); - } + if (!token) { + throw new Error("No active session, please login first"); + } - if (!server) { - throw new Error("Server URL is not specified"); - } + if (!server) { + throw new Error("Server URL is not specified"); + } - return { token, server }; + return { token, server }; } async function openLibraries() { - const config = await getExtensionConfig(); - return browser.tabs.create({ active: true, url: config.server }); + const config = await getExtensionConfig(); + return browser.tabs.create({ active: true, url: config.server }); } async function removeBookmark() { - const tab = await getCurrentTab(); - const config = await getExtensionConfig(); - const srvURL = new URL(config.server); - srvURL.pathname = srvURL.pathname.replace(/\/+$/, '') + '/'; - const apiURL = new URL(`${srvURL}api/bookmarks/ext`).toString(); - - const response = await fetch(apiURL, { - method: "DELETE", - body: JSON.stringify({ url: tab.url }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.token}`, - }, - }); - - if (!response.ok) { - throw new Error(await response.text()); - } - - await removeLocalBookmark(tab.url); + const tab = await getCurrentTab(); + const config = await getExtensionConfig(); + const srvURL = new URL(config.server); + srvURL.pathname = srvURL.pathname.replace(/\/+$/, '') + '/'; + const apiURL = new URL(`${srvURL}api/bookmarks/ext`).toString(); + + const response = await fetch(apiURL, { + method: "DELETE", + body: JSON.stringify({ url: tab.url }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.token}`, + }, + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + await removeLocalBookmark(tab.url); } async function saveBookmark(tags) { - const tab = await getCurrentTab(); - const config = await getExtensionConfig(); - const content = await getPageContent(tab); - const srvURL = new URL(config.server); - srvURL.pathname = srvURL.pathname.replace(/\/+$/, '') + '/'; - const apiURL = new URL(`${srvURL}api/bookmarks/ext`).toString(); - - const response = await fetch(apiURL, { - method: "POST", - body: JSON.stringify({ - url: tab.url, - tags, - html: content.html || "", - }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.token}`, - }, - }); + const tab = await getCurrentTab(); + const config = await getExtensionConfig(); + const content = await getPageContent(tab); + const srvURL = new URL(config.server); + srvURL.pathname = srvURL.pathname.replace(/\/+$/, '') + '/'; + const apiURL = new URL(`${srvURL}api/bookmarks/ext`).toString(); + + const response = await fetch(apiURL, { + method: "POST", + body: JSON.stringify({ + url: tab.url, + tags, + html: content.html || "", + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.token}`, + }, + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + await saveLocalBookmark(tab.url, content.title || tab.title); +} - if (!response.ok) { - throw new Error(await response.text()); - } +async function searchBookmarks(keyword = "", page = 1) { + const config = await getExtensionConfig(); + const srvURL = new URL(config.server); + srvURL.pathname = srvURL.pathname.replace(/\/+$/, '') + '/'; + const apiURL = new URL(`${srvURL}api/bookmarks`); + + if (keyword.trim()) { + apiURL.searchParams.set('keyword', keyword); + } + apiURL.searchParams.set('page', page.toString()); + + const response = await fetch(apiURL.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.token}`, + "X-Shiori-Response-Format": "new", + }, + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + return await response.json(); +} - await saveLocalBookmark(tab.url, content.title || tab.title); +async function openBookmark(url, newTab = true) { + if (newTab) { + await browser.tabs.create({ active: true, url }); + } else { + const tabs = await browser.tabs.query({ currentWindow: true, active: true }); + if (tabs.length > 0) { + await browser.tabs.update(tabs[0].id, { url }); + } + } } browser.runtime.onMessage.addListener((request) => { - switch (request.type) { - case "open-libraries": - return openLibraries(); - case "remove-bookmark": - return removeBookmark(); - case "save-bookmark": - return saveBookmark(request.tags); - default: - return Promise.resolve(); - } + switch (request.type) { + case "open-libraries": + return openLibraries(); + case "remove-bookmark": + return removeBookmark(); + case "save-bookmark": + return saveBookmark(request.tags); + case "search-bookmarks": + return searchBookmarks(request.keyword, request.page); + case "open-bookmark": + return openBookmark(request.url, request.newTab); + default: + return Promise.resolve(); + } }); browser.tabs.onActivated.addListener(() => updateIcon()); @@ -174,41 +216,41 @@ browser.bookmarks.onRemoved.addListener(() => updateIcon()); browser.windows.onFocusChanged.addListener(() => updateIcon()); const bookmarkedIcon = { - 16: "icons/action-bookmarked-16.png", - 32: "icons/action-bookmarked-32.png", - 64: "icons/action-bookmarked-64.png", + 16: "icons/action-bookmarked-16.png", + 32: "icons/action-bookmarked-32.png", + 64: "icons/action-bookmarked-64.png", } const lightModeIcon = { - 16: "icons/action-default-16.png", - 32: "icons/action-default-32.png", - 64: "icons/action-default-64.png", + 16: "icons/action-default-16.png", + 32: "icons/action-default-32.png", + 64: "icons/action-default-64.png", } const darkModeIcon = { - 16: "icons/action-default-dark-16.png", - 32: "icons/action-default-dark-32.png", - 64: "icons/action-default-dark-64.png", + 16: "icons/action-default-dark-16.png", + 32: "icons/action-default-dark-32.png", + 64: "icons/action-default-dark-64.png", } async function updateIcon() { - const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - const defaultIcon = isDarkMode ? darkModeIcon : lightModeIcon; - try { - - const tab = await getCurrentTab(); - const isBookmarked = !!(await findLocalBookmark(tab.url)); - - browser.action.setIcon({ - path: isBookmarked - ? bookmarkedIcon - : defaultIcon, - }); - } catch { - browser.action.setIcon({ - path: defaultIcon, - }); - } + const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + const defaultIcon = isDarkMode ? darkModeIcon : lightModeIcon; + try { + + const tab = await getCurrentTab(); + const isBookmarked = !!(await findLocalBookmark(tab.url)); + + browser.action.setIcon({ + path: isBookmarked + ? bookmarkedIcon + : defaultIcon, + }); + } catch { + browser.action.setIcon({ + path: defaultIcon, + }); + } } browser.runtime.onStartup.addListener(updateIcon); -browser.runtime.onInstalled.addListener(updateIcon); \ No newline at end of file +browser.runtime.onInstalled.addListener(updateIcon); diff --git a/js/popup.js b/js/popup.js index 1abfaa5..ed78324 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,79 +1,257 @@ -// Get DOM -var inputTags = document.getElementById("input-tags"), - btnRemove = document.getElementById("btn-remove"), - btnLibraries = document.getElementById("btn-libraries"), - btnSave = document.getElementById("btn-save"), - loading = document.getElementById("loading-sign"); +// State management +let currentMode = 'search'; // 'search' or 'add' +let searchResults = []; +let selectedIndex = -1; +let searchTimeout; + +// Get DOM elements +const searchMode = document.getElementById("search-mode"); +const addMode = document.getElementById("add-mode"); +const inputSearch = document.getElementById("input-search"); +const inputTags = document.getElementById("input-tags"); +const btnSwitchAdd = document.getElementById("btn-switch-add"); +const btnSwitchSearch = document.getElementById("btn-switch-search"); +const btnRemove = document.getElementById("btn-remove"); +const btnLibraries = document.getElementById("btn-libraries"); +const btnSave = document.getElementById("btn-save"); +const loading = document.getElementById("loading-sign"); +const searchLoading = document.getElementById("search-loading"); +const searchEmpty = document.getElementById("search-empty"); +const bookmarkList = document.getElementById("bookmark-list"); async function showError(err) { - var tabs = await browser.tabs.query({ - currentWindow: true, - active: true, + var tabs = await browser.tabs.query({ + currentWindow: true, + active: true, + }); + + if (tabs.length < 1) { + throw new Error("no tab available"); + } + + if (err instanceof Error) { + err = err.message; + } + + return browser.tabs.sendMessage(tabs[0].id, { + type: "show-error", + message: err, + }); +} + +function switchToMode(mode) { + currentMode = mode; + if (mode === 'search') { + searchMode.style.display = 'block'; + addMode.style.display = 'none'; + // Use requestAnimationFrame for proper DOM rendering + requestAnimationFrame(() => { + setTimeout(() => inputSearch.focus(), 50); }); + loadRecentBookmarks(); + } else { + searchMode.style.display = 'none'; + addMode.style.display = 'block'; + requestAnimationFrame(() => { + setTimeout(() => inputTags.focus(), 50); + }); + } +} + +function renderBookmarks(bookmarks) { + bookmarkList.innerHTML = ''; + searchResults = bookmarks; + selectedIndex = bookmarks.length > 0 ? 0 : -1; + + if (bookmarks.length === 0) { + searchEmpty.style.display = 'block'; + searchLoading.style.display = 'none'; + return; + } + + searchEmpty.style.display = 'none'; + searchLoading.style.display = 'none'; - if (tabs.length < 1) { - throw new Error("no tab available"); + bookmarks.forEach((bookmark, index) => { + const item = document.createElement('div'); + item.className = 'bookmark-item'; + if (index === selectedIndex) { + item.classList.add('selected'); } - if (err instanceof Error) { - err = err.message; + const title = document.createElement('div'); + title.className = 'bookmark-title'; + title.textContent = bookmark.title || bookmark.url; + + const url = document.createElement('div'); + url.className = 'bookmark-url'; + url.textContent = bookmark.url; + + const tagsContainer = document.createElement('div'); + tagsContainer.className = 'bookmark-tags'; + + if (bookmark.tags && bookmark.tags.length > 0) { + bookmark.tags.forEach(tag => { + const tagEl = document.createElement('span'); + tagEl.className = 'bookmark-tag'; + tagEl.textContent = tag.name; + tagsContainer.appendChild(tagEl); + }); } - return browser.tabs.sendMessage(tabs[0].id, { - type: "show-error", - message: err, + item.appendChild(title); + item.appendChild(url); + item.appendChild(tagsContainer); + + item.addEventListener('click', () => openSelectedBookmark(index, true)); + bookmarkList.appendChild(item); + }); +} + +function updateSelection(newIndex) { + if (searchResults.length === 0) return; + + // Remove old selection + const oldSelected = bookmarkList.querySelector('.selected'); + if (oldSelected) { + oldSelected.classList.remove('selected'); + } + + // Update index + selectedIndex = Math.max(0, Math.min(newIndex, searchResults.length - 1)); + + // Add new selection + const bookmarkItems = bookmarkList.querySelectorAll('.bookmark-item'); + if (bookmarkItems[selectedIndex]) { + bookmarkItems[selectedIndex].classList.add('selected'); + bookmarkItems[selectedIndex].scrollIntoView({ block: 'nearest' }); + } +} + +function openSelectedBookmark(index = selectedIndex, newTab = true) { + if (index >= 0 && index < searchResults.length) { + const bookmark = searchResults[index]; + browser.runtime.sendMessage({ + type: "open-bookmark", + url: bookmark.url, + newTab: newTab + }).finally(() => window.close()); + } +} + +async function searchBookmarks(keyword = "") { + searchLoading.style.display = 'block'; + searchEmpty.style.display = 'none'; + + try { + const result = await browser.runtime.sendMessage({ + type: "search-bookmarks", + keyword: keyword, + page: 1 }); + renderBookmarks(result.bookmarks || []); + } catch (err) { + searchLoading.style.display = 'none'; + showError(err); + } } -// Add event handler +async function loadRecentBookmarks() { + await searchBookmarks(""); +} + +// Event handlers for add mode (original functionality) btnRemove.addEventListener("click", (e) => { - // Show loading indicator - btnSave.style.display = "none"; - loading.style.display = "block"; - btnRemove.style.display = "none"; - - browser.runtime.sendMessage({type: "remove-bookmark"}) - .catch(err => showError(err)) - .finally(() => { window.close() }); + btnSave.style.display = "none"; + loading.style.display = "block"; + btnRemove.style.display = "none"; + + browser.runtime.sendMessage({ type: "remove-bookmark" }) + .catch(err => showError(err)) + .finally(() => { window.close() }); }); btnLibraries.addEventListener("click", (e) => { - browser.runtime.sendMessage({type: "open-libraries"}) - .finally(() => { window.close() }); + browser.runtime.sendMessage({ type: "open-libraries" }) + .finally(() => { window.close() }); }); btnSave.addEventListener("click", (e) => { - // Get input value - var tags = inputTags.value - .toLowerCase() - .replace(/\s+/g, " ") - .split(/\s*,\s*/g) - .filter(tag => tag.trim() !== "") - .map(tag => { - return { - name: tag.trim() - }; - }); - - // Show loading indicator - btnSave.style.display = "none"; - loading.style.display = "block"; - - // Send data - var message = { - type: "save-bookmark", - tags: tags, - } + var tags = inputTags.value + .toLowerCase() + .replace(/\s+/g, " ") + .split(/\s*,\s*/g) + .filter(tag => tag.trim() !== "") + .map(tag => { + return { + name: tag.trim() + }; + }); + + btnSave.style.display = "none"; + loading.style.display = "block"; - browser.runtime.sendMessage(message) - .catch(err => showError(err)) - .finally(() => window.close()); + var message = { + type: "save-bookmark", + tags: tags, + } + + browser.runtime.sendMessage(message) + .catch(err => showError(err)) + .finally(() => window.close()); }); inputTags.addEventListener("keyup", (e) => { - // keyCode 13 = "Enter" key on the keyboard - if (event.keyCode === 13) { - event.preventDefault() - btnSave.click() - } -}) + if (event.keyCode === 13) { + event.preventDefault() + btnSave.click() + } +}); + +// Event handlers for search mode +btnSwitchAdd.addEventListener("click", (e) => { + e.preventDefault(); + switchToMode('add'); +}); + +btnSwitchSearch.addEventListener("click", (e) => { + e.preventDefault(); + switchToMode('search'); +}); + +inputSearch.addEventListener("input", (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchBookmarks(e.target.value); + }, 300); +}); + +inputSearch.addEventListener("keydown", (e) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + updateSelection(selectedIndex + 1); + break; + case 'ArrowUp': + e.preventDefault(); + updateSelection(selectedIndex - 1); + break; + case 'Enter': + e.preventDefault(); + if (e.shiftKey) { + openSelectedBookmark(selectedIndex, false); // current tab + } else { + openSelectedBookmark(selectedIndex, true); // new tab + } + break; + case 'Escape': + window.close(); + break; + } +}); + +// Initialize +document.addEventListener("DOMContentLoaded", () => { + // Check if we should start in search mode (when no current tab URL matches bookmarks) + switchToMode('search'); +}); diff --git a/manifest.json b/manifest.json index bf1d1c5..14c3c1a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,66 +1,74 @@ { - "manifest_version": 3, - "version": "1.7.4.0", - "name": "Shiori", - "author": "Go-Shiori Team", - "description": "Web extension for Shiori, a simple bookmark manager.", - "homepage_url": "https://github.com/go-shiori/shiori-web-ext", - "icons": { - "48": "icons/icon-48.png", - "96": "icons/icon-96.png" - }, - "content_security_policy": { - "extension_pages": "default-src 'self'; connect-src 'self' http: https:; style-src 'self' 'unsafe-inline';" - }, - "permissions": [ - "tabs", - "storage", - "bookmarks", - "scripting", - "activeTab" - ], - "host_permissions": [""], - "background": { - "scripts": [ - "js/browser-polyfill.js", - "js/background-script.js" - ] - }, - "content_scripts": [{ - "matches": [""], - "css": ["css/dialog.css"], - "js": [ - "js/browser-polyfill.js", - "js/content-script.js" - ] - }], - "options_ui": { - "page": "view/options.html", - "open_in_tab": false - }, - "action": { - "default_title": "Shiori", - "default_popup": "view/popup.html", - "default_icon": { - "16": "icons/action-default-16.png", - "32": "icons/action-default-32.png", - "64": "icons/action-default-64.png" - } - }, - "commands": { - "_execute_action": { - "suggested_key": { - "default": "Ctrl+Shift+F" - }, - "description": "Show bookmark popup" - } - }, - "browser_specific_settings": { - "gecko": { - "id": "{c6e8bd66-ebb4-4b63-bd29-5ef59c795903}" - } - }, - "omnibox": { - "keyword": "@shiori" + "manifest_version": 3, + "version": "1.7.4.0", + "name": "Shiori", + "author": "Go-Shiori Team", + "description": "Web extension for Shiori, a simple bookmark manager.", + "homepage_url": "https://github.com/go-shiori/shiori-web-ext", + "icons": { + "48": "icons/icon-48.png", + "96": "icons/icon-96.png" + }, + "content_security_policy": { + "extension_pages": "default-src 'self'; connect-src 'self' http: https:; style-src 'self' 'unsafe-inline';" + }, + "permissions": [ + "tabs", + "storage", + "bookmarks", + "scripting", + "activeTab" + ], + "host_permissions": [ + "" + ], + "background": { + "scripts": [ + "js/browser-polyfill.js", + "js/background-script.js" + ] + }, + "content_scripts": [ + { + "matches": [ + "" + ], + "css": [ + "css/dialog.css" + ], + "js": [ + "js/browser-polyfill.js", + "js/content-script.js" + ] } + ], + "options_ui": { + "page": "view/options.html", + "open_in_tab": false + }, + "action": { + "default_title": "Shiori", + "default_popup": "view/popup.html", + "default_icon": { + "16": "icons/action-default-16.png", + "32": "icons/action-default-32.png", + "64": "icons/action-default-64.png" + } + }, + "commands": { + "_execute_action": { + "suggested_key": { + "default": "Ctrl+Shift+K" + }, + "description": "Show bookmark popup" + } + }, + "browser_specific_settings": { + "gecko": { + "id": "{c6e8bd66-ebb4-4b63-bd29-5ef59c795903}" + } + }, + "omnibox": { + "keyword": "@shiori" + } } diff --git a/view/popup.html b/view/popup.html index 0c5ce04..dba0c12 100644 --- a/view/popup.html +++ b/view/popup.html @@ -16,23 +16,43 @@