├── src ├── background │ ├── index.ts │ └── messages │ │ ├── fetch-bookmark-data.ts │ │ ├── add-bookmark.ts │ │ ├── sign-in.ts │ │ ├── validate-grimoire-api-url.ts │ │ └── fetch-categories-tags.ts ├── contents │ ├── index.ts │ └── get-webpage-content.ts ├── style.css ├── shared │ ├── enums.ts │ ├── utils │ │ ├── show-toast.ts │ │ ├── clear-url.util.ts │ │ └── debug-logs.ts │ ├── handlers │ │ ├── handle-theme-change.handler.ts │ │ ├── handle-grimoire-api-check.handler.ts │ │ ├── handle-sign-in.handler.ts │ │ └── on-add-bookmark.handler.ts │ ├── components │ │ ├── TagsInput.component.svelte │ │ ├── StatusNotConnected.svelte │ │ └── Navbar.component.svelte │ ├── helpers │ │ └── validate-grimoire-api-url.ts │ ├── types │ │ └── add-bookmark.type.ts │ └── stores.ts └── popup.svelte ├── .github └── FUNDING.yml ├── assets ├── icon.png ├── favicon-32x32.png ├── icon-192x192.png ├── screenshot_1280_800.png ├── chrome-web-store-button.png └── firefox-addons-button.webp ├── svelte.config.js ├── postcss.config.js ├── .prettierrc ├── tailwind.config.js ├── tsconfig.json ├── .gitignore ├── .prettierrc.mjs ├── PRIVACY.md ├── workflows └── submit.yml ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── README.md /src/background/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contents/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [goniszewski] 2 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/shared/enums.ts: -------------------------------------------------------------------------------- 1 | export enum themes { 2 | light = 'fantasy', 3 | dark = 'dracula' 4 | } 5 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goniszewski/grimoire-web-extension/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goniszewski/grimoire-web-extension/HEAD/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goniszewski/grimoire-web-extension/HEAD/assets/icon-192x192.png -------------------------------------------------------------------------------- /assets/screenshot_1280_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goniszewski/grimoire-web-extension/HEAD/assets/screenshot_1280_800.png -------------------------------------------------------------------------------- /assets/chrome-web-store-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goniszewski/grimoire-web-extension/HEAD/assets/chrome-web-store-button.png -------------------------------------------------------------------------------- /assets/firefox-addons-button.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goniszewski/grimoire-web-extension/HEAD/assets/firefox-addons-button.webp -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | const sveltePreprocess = require("svelte-preprocess") 2 | 3 | module.exports = { 4 | preprocess: sveltePreprocess() 5 | } -------------------------------------------------------------------------------- /src/shared/utils/show-toast.ts: -------------------------------------------------------------------------------- 1 | import toast, { Toaster } from 'svelte-french-toast/dist/'; 2 | 3 | export const showToast = toast; 4 | export const ToastNode = Toaster; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('postcss').ProcessOptions} 3 | */ 4 | module.exports = { 5 | plugins: { 6 | tailwindcss: {}, 7 | autoprefixer: {} 8 | } 9 | } -------------------------------------------------------------------------------- /src/shared/utils/clear-url.util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clear the updated URL 3 | */ 4 | export function clearUrl(url: string) { 5 | const newUrl = new URL(url); 6 | newUrl.search = ''; 7 | newUrl.hash = ''; 8 | 9 | return newUrl.toString(); 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/handlers/handle-theme-change.handler.ts: -------------------------------------------------------------------------------- 1 | import { themes } from '~shared/enums'; 2 | 3 | export function handleThemeChange(storage: any, theme: keyof typeof themes) { 4 | document.documentElement.setAttribute('data-theme', themes[theme]); 5 | storage.set('theme', theme); 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: 'jit', 4 | darkMode: 'class', 5 | content: ['./src/**/*.svelte'], 6 | plugins: [require('@tailwindcss/typography'), require('daisyui')], 7 | daisyui: { 8 | themes: ['fantasy', 'dracula'] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plasmo/templates/tsconfig.base", 3 | "exclude": ["node_modules"], 4 | "include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx", "./**/*.svelte"], 5 | "compilerOptions": { 6 | "paths": { 7 | "~*": ["./src/*"] 8 | }, 9 | "baseUrl": "." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/messages/fetch-bookmark-data.ts: -------------------------------------------------------------------------------- 1 | import { type PlasmoMessaging } from '@plasmohq/messaging'; 2 | 3 | export {}; 4 | 5 | const handler: PlasmoMessaging.MessageHandler = async (req, res) => { 6 | const content = { 7 | ...req.body 8 | }; 9 | 10 | res.send({ 11 | content 12 | }); 13 | }; 14 | 15 | export default handler; 16 | -------------------------------------------------------------------------------- /src/shared/handlers/handle-grimoire-api-check.handler.ts: -------------------------------------------------------------------------------- 1 | import { validateGrimoireApiUrl } from '~shared/helpers/validate-grimoire-api-url'; 2 | 3 | export async function handleGrimoireApiCheck(grimoireApiUrl: string, $status: any) { 4 | const isGrimoireApiReachable = await validateGrimoireApiUrl(grimoireApiUrl); 5 | 6 | $status = { 7 | ...$status, 8 | isGrimoireApiReachable 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/components/TagsInput.component.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /src/shared/helpers/validate-grimoire-api-url.ts: -------------------------------------------------------------------------------- 1 | import { sendToBackground } from '@plasmohq/messaging'; 2 | 3 | export async function validateGrimoireApiUrl(url: string) { 4 | const healthCheck = await sendToBackground< 5 | { 6 | grimoireApiUrl: string; 7 | }, 8 | { 9 | valid: boolean; 10 | } 11 | >({ 12 | name: 'validate-grimoire-api-url', 13 | body: { 14 | grimoireApiUrl: url 15 | } 16 | }); 17 | 18 | return healthCheck.valid; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | *.pem 15 | 16 | # debug 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | .pnpm-debug.log* 21 | 22 | # local env files 23 | .env*.local 24 | 25 | out/ 26 | build/ 27 | dist/ 28 | 29 | # plasmo 30 | .plasmo 31 | 32 | # typescript 33 | .tsbuildinfo 34 | -------------------------------------------------------------------------------- /src/shared/types/add-bookmark.type.ts: -------------------------------------------------------------------------------- 1 | export type AddBookmarkRequestBody = { 2 | url: string; 3 | title: string; 4 | description?: string; 5 | author?: string; 6 | content_text?: string; 7 | content_html?: string; 8 | content_type?: string; 9 | content_published_date?: Date | null; 10 | note?: string; 11 | main_image_url?: string; 12 | icon_url?: string; 13 | icon?: string; 14 | importance?: number; 15 | flagged?: boolean; 16 | category: string; 17 | tags?: string[]; 18 | screenshot?: string; 19 | }; 20 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | export default { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "none", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 14 | importOrder: [ 15 | "", // Node.js built-in modules 16 | "", // Imports not matched by other special words or groups. 17 | "", // Empty line 18 | "^@plasmo/(.*)$", 19 | "", 20 | "^@plasmohq/(.*)$", 21 | "", 22 | "^~(.*)$", 23 | "", 24 | "^[./]" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/contents/get-webpage-content.ts: -------------------------------------------------------------------------------- 1 | import { listen } from '@plasmohq/messaging/message'; 2 | import { logger } from '~shared/utils/debug-logs'; 3 | 4 | listen(async (_req, res) => { 5 | logger.debug('background.messages.get-webpage-content', 'Fetching webpage content'); 6 | 7 | try { 8 | const html = window.document.documentElement.innerHTML; 9 | const description = window.document 10 | .querySelector("meta[name='description']") 11 | ?.getAttribute('content'); 12 | 13 | logger.debug('background.messages.get-webpage-content', 'Fetched webpage content', { 14 | html, 15 | description 16 | }); 17 | 18 | return res.send({ 19 | html, 20 | description 21 | }); 22 | } catch (error) { 23 | logger.error( 24 | 'background.messages.get-webpage-content', 25 | 'Error fetching webpage content', 26 | error 27 | ); 28 | 29 | return res.send({ 30 | html: null, 31 | description: null 32 | }); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | ## Our Privacy Policy 2 | 3 | We get it – privacy is important. That's why we've designed this Chrome extension to be as privacy-friendly as possible. 4 | 5 | Here's the deal: 6 | 7 | - **We don't collect any of your personal data.** That means no names, no emails, no browsing history – nothing that could identify you. 8 | - **This extension only talks to your own Grimoire API instance.** It doesn't send any data to third-parties or any mysterious servers. 9 | - **We won't track how you use the extension.** We want you to have full control over your experience. 10 | 11 | ## Keeping it Simple 12 | 13 | We believe privacy policies shouldn't be complicated. If you have any questions, feel free to reach out to us at contact@grimoire.pro. 14 | 15 | ## Changes 16 | 17 | We might update this policy if we make changes to the extension. We'll let you know if that happens! 18 | 19 | ## Contact Us 20 | 21 | If you have any questions about this Privacy Policy, please contact us at contact@grimoire.pro. 22 | -------------------------------------------------------------------------------- /src/shared/stores.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const categories = writable([]); 4 | export const tags = writable([]); 5 | export const currentTab = writable({ 6 | url: '', 7 | title: '', 8 | icon_url: '', 9 | mainImage: '', 10 | contentHtml: '', 11 | description: '', 12 | category: '', 13 | tags: [], 14 | note: '', 15 | importance: 0, 16 | flagged: false 17 | }); 18 | export const updatedUrl = writable(''); 19 | export const credentials = writable({ 20 | emailOrUsername: null, 21 | password: null 22 | }); 23 | export const status = writable({ 24 | isGrimoireApiReachable: true 25 | }); 26 | export const loading = writable({ 27 | isFetchingCategoriesAndTags: false, 28 | isSigningIn: false, 29 | isAddingBookmark: false, 30 | justAddedBookmark: false 31 | }); 32 | 33 | loading.subscribe((value) => { 34 | if (value.justAddedBookmark) { 35 | setTimeout(() => { 36 | loading.update((value) => ({ ...value, isAddingBookmark: false, justAddedBookmark: false })); 37 | }, 1650); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: latest 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build the extension 27 | run: pnpm build 28 | - name: Package the extension into a zip artifact 29 | run: pnpm package 30 | - name: Browser Platform Publish 31 | uses: PlasmoHQ/bpp@v3 32 | with: 33 | keys: ${{ secrets.SUBMIT_KEYS }} 34 | artifact: build/chrome-mv3-prod.zip 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2023 Robert Goniszewski 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/shared/components/StatusNotConnected.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | Grimoire API is not reachable! 11 | 28 |
29 | -------------------------------------------------------------------------------- /src/background/messages/add-bookmark.ts: -------------------------------------------------------------------------------- 1 | import { type PlasmoMessaging } from '@plasmohq/messaging'; 2 | import type { AddBookmarkRequestBody } from '~shared/types/add-bookmark.type'; 3 | import { logger } from '~shared/utils/debug-logs'; 4 | 5 | export const handler: PlasmoMessaging.MessageHandler<{ 6 | grimoireApiUrl: string; 7 | token: string; 8 | bookmark: AddBookmarkRequestBody; 9 | }> = async (req, res) => { 10 | const { grimoireApiUrl, token, bookmark } = req.body; 11 | 12 | logger.debug('background.messages.add-bookmark', 'Adding bookmark', { 13 | grimoireApiUrl, 14 | bookmark 15 | }); 16 | 17 | try { 18 | const response = await fetch(`${grimoireApiUrl}/bookmarks`, { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | Authorization: `Bearer ${token}` 23 | }, 24 | body: JSON.stringify(bookmark) 25 | }).then((data) => data.json()); 26 | 27 | logger.debug('background.messages.add-bookmark', 'Grimoire API response', response); 28 | 29 | res.send({ 30 | bookmark: response?.bookmark 31 | }); 32 | } catch (error) { 33 | logger.error('background.messages.add-bookmark', 'Error adding bookmark', error?.message); 34 | 35 | res.send({ 36 | bookmark: null 37 | }); 38 | } 39 | }; 40 | 41 | export default handler; 42 | -------------------------------------------------------------------------------- /src/background/messages/sign-in.ts: -------------------------------------------------------------------------------- 1 | import { type PlasmoMessaging } from '@plasmohq/messaging'; 2 | import { logger } from '~shared/utils/debug-logs'; 3 | 4 | export {}; 5 | 6 | const handler: PlasmoMessaging.MessageHandler<{ 7 | emailOrUsername: string; 8 | password: string; 9 | grimoireApiUrl: string; 10 | }> = async (req, res) => { 11 | const { emailOrUsername, password, grimoireApiUrl } = req.body; 12 | 13 | logger.debug('background.messages.sign-in', 'Signing in', { 14 | grimoireApiUrl, 15 | emailOrUsername, 16 | password: `${password.slice(0, 2)}***${password.slice(-2)}` 17 | }); 18 | 19 | try { 20 | const response = await fetch(`${grimoireApiUrl}/auth`, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json' 24 | }, 25 | body: JSON.stringify({ 26 | login: emailOrUsername, 27 | password 28 | }) 29 | }).then((res) => res.json()); 30 | 31 | const { token, ...responseWithoutToken } = response; 32 | 33 | if (!response.success) { 34 | logger.error('background.messages.sign-in', 'Error signing in', responseWithoutToken); 35 | 36 | return res.send({ 37 | token: null 38 | }); 39 | } 40 | 41 | return res.send({ 42 | token 43 | }); 44 | } catch (error) { 45 | logger.error('background.messages.sign-in', 'Error signing in', error?.message); 46 | 47 | res.send({ 48 | token: null 49 | }); 50 | } 51 | }; 52 | 53 | export default handler; 54 | -------------------------------------------------------------------------------- /src/background/messages/validate-grimoire-api-url.ts: -------------------------------------------------------------------------------- 1 | import { type PlasmoMessaging } from '@plasmohq/messaging'; 2 | import { logger } from '~shared/utils/debug-logs'; 3 | 4 | export {}; 5 | 6 | const handler: PlasmoMessaging.MessageHandler<{ 7 | grimoireApiUrl: string; 8 | }> = async (req, res) => { 9 | const { grimoireApiUrl } = req.body; 10 | 11 | logger.debug('background.messages.validate-grimoire-api-url', 'Validating Grimoire API URL', { 12 | grimoireApiUrl 13 | }); 14 | 15 | try { 16 | const response = await fetch(`${grimoireApiUrl}/health`, { 17 | method: 'GET', 18 | headers: { 19 | 'Content-Type': 'application/json' 20 | } 21 | }); // todo: it always returns empty object 22 | 23 | response.ok 24 | ? logger.debug( 25 | 'background.messages.validate-grimoire-api-url', 26 | 'Connection to Grimoire API established' 27 | ) 28 | : logger.error( 29 | 'background.messages.validate-grimoire-api-url', 30 | 'Error validating Grimoire API URL', 31 | { 32 | status: response.status, 33 | statusText: response.statusText 34 | } 35 | ); 36 | 37 | res.send({ 38 | valid: response.ok 39 | }); 40 | } catch (error) { 41 | logger.error( 42 | 'background.messages.validate-grimoire-api-url', 43 | 'Error validating Grimoire API URL', 44 | error?.message 45 | ); 46 | res.send({ 47 | valid: false 48 | }); 49 | } 50 | }; 51 | 52 | export default handler; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grimoire-companion", 3 | "displayName": "grimoire companion", 4 | "version": "0.1.3", 5 | "description": "Companion extension for Grimoire.", 6 | "author": "Robert Goniszewski ", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build", 10 | "package": "plasmo package" 11 | }, 12 | "dependencies": { 13 | "@plasmohq/messaging": "^0.6.2", 14 | "@plasmohq/storage": "^1.12.0", 15 | "@tailwindcss/typography": "^0.5.15", 16 | "daisyui": "^4.12.12", 17 | "plasmo": "0.89.3", 18 | "prettier-plugin-svelte": "^3.2.7", 19 | "svelte": "^4.2.19", 20 | "svelte-french-toast": "^1.2.0", 21 | "svelte-preprocess": "^6.0.3" 22 | }, 23 | "devDependencies": { 24 | "@ianvs/prettier-plugin-sort-imports": "4.3.1", 25 | "@types/chrome": "0.0.277", 26 | "@types/node": "22.7.5", 27 | "autoprefixer": "^10.4.20", 28 | "postcss": "^8.4.47", 29 | "prettier": "3.3.3", 30 | "svelte-multiselect": "^10.2.0", 31 | "tailwindcss": "^3.4.13", 32 | "typescript": "5.6.2" 33 | }, 34 | "manifest": { 35 | "host_permissions": [ 36 | "" 37 | ], 38 | "permissions": [ 39 | "activeTab", 40 | "storage" 41 | ], 42 | "browser_specific_settings": { 43 | "gecko": { 44 | "id": "contact@grimoire.pro", 45 | "strict_min_version": "109.0" 46 | } 47 | }, 48 | "content_security_policy": { 49 | "extension_pages": "script-src 'self'; object-src 'self'" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/shared/handlers/handle-sign-in.handler.ts: -------------------------------------------------------------------------------- 1 | import { sendToBackground } from '@plasmohq/messaging'; 2 | import { logger } from '~shared/utils/debug-logs'; 3 | import { showToast } from '~shared/utils/show-toast'; 4 | 5 | export async function handleSignIn( 6 | grimoireApiUrl: string, 7 | emailOrUsername: string, 8 | password: string 9 | ) { 10 | if (!emailOrUsername || !password) { 11 | showToast.error('Please enter your email/username and password'); 12 | 13 | return null; 14 | } 15 | 16 | try { 17 | logger.debug('shared.handlers.handle-sign-in', 'Signing in', { 18 | grimoireApiUrl, 19 | emailOrUsername, 20 | password: `${password.slice(0, 2)}***${password.slice(-2)}` 21 | }); 22 | 23 | const { token } = await sendToBackground< 24 | { 25 | emailOrUsername: string; 26 | password: string; 27 | grimoireApiUrl: string; 28 | }, 29 | { 30 | token: string; 31 | } 32 | >({ 33 | name: 'sign-in', 34 | body: { 35 | emailOrUsername, 36 | password, 37 | grimoireApiUrl 38 | } 39 | }); 40 | 41 | if (!token) { 42 | showToast.error('Error signing in. Are your credentials correct? 🤔'); 43 | 44 | return null; 45 | } 46 | 47 | showToast.success('Signed in successfully! 🎉'); 48 | 49 | return token; 50 | } catch (error) { 51 | logger.error('shared.handlers.handle-sign-in', 'Error signing in', error?.message); 52 | 53 | showToast.error('Error signing in. Are your credentials correct?'); 54 | 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/background/messages/fetch-categories-tags.ts: -------------------------------------------------------------------------------- 1 | import { type PlasmoMessaging } from '@plasmohq/messaging'; 2 | import { logger } from '~shared/utils/debug-logs'; 3 | 4 | export {}; 5 | 6 | const handler: PlasmoMessaging.MessageHandler<{ 7 | grimoireApiUrl: string; 8 | token: string; 9 | }> = async (req, res) => { 10 | const { grimoireApiUrl, token } = req.body; 11 | 12 | logger.debug('background.messages.fetch-categories-tags', 'Fetching categories and tags', { 13 | grimoireApiUrl 14 | }); 15 | 16 | try { 17 | const { categories } = await fetch(`${grimoireApiUrl}/categories`, { 18 | method: 'GET', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | Authorization: `Bearer ${token}` 22 | } 23 | }).then((response) => response.json()); 24 | 25 | if (categories) 26 | logger.debug('background.messages.fetch-categories-tags', 'Fetched categories', categories); 27 | 28 | const { tags } = await fetch(`${grimoireApiUrl}/tags`, { 29 | method: 'GET', 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | Authorization: `Bearer ${token}` 33 | } 34 | }).then((response) => response.json()); 35 | 36 | if (tags) logger.debug('background.messages.fetch-categories-tags', 'Fetched tags', tags); 37 | 38 | res.send({ 39 | categories, 40 | tags 41 | }); 42 | } catch (error) { 43 | logger.error( 44 | 'background.messages.fetch-categories-tags', 45 | 'Error fetching categories and tags', 46 | error 47 | ); 48 | 49 | res.send({ 50 | categories: null, 51 | tags: null 52 | }); 53 | } 54 | }; 55 | 56 | export default handler; 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering contributing to this project! All contributors, big or small, are welcomed. To make the contribution process as smooth as possible, please follow the guidelines below. 4 | 5 | 1. Fork the repository: Start by forking the repository to your own GitHub account. This will create a copy of the repository under your username. 6 | 2. Create a new branch: Clone the forked repository to your local machine and create a new branch for your feature or bug fix. 7 | ```bash 8 | git clone https://github.com/goniszewski/grimoire-web-extension/repository.git 9 | cd grimoire-web-extension 10 | git checkout -b your-branch-name 11 | ``` 12 | 3. Make the changes: Make the necessary changes to the codebase, ensuring that you follow any coding style guidelines mentioned in the project documentation or README file. 13 | 4. Test your changes: Thoroughly test your changes to ensure that they do not break existing functionality and introduce new bugs. 14 | 5. Commit your changes: Once you are satisfied with your modifications, commit them using a descriptive commit message following the rules of [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716). 15 | ```bash 16 | git add . 17 | git commit -m "fix: Your detailed description of your changes." 18 | ``` 19 | 6. Push to your branch: Push your changes to your forked repository on GitHub. 20 | ```bash 21 | git push origin your-branch-name 22 | ``` 23 | 7. Submit a Pull Request: Navigate to the GitHub page of the original project and submit a pull request with a clear description of your changes. 24 | 8. Wait for review: Patiently wait for the maintainers to review your pull request. They might ask for additional information or changes, which you can address by updating your branch and submitting an updated pull request. 25 | 9. Let it spark ✨ Yay, your contribution has been accepted and merged into the project! Thank you for making this project better 🤝 26 | 27 | Thank you for contributing to this project! We appreciate your efforts in making it even better. If you have any questions or need further clarification, feel free to reach out to us. 28 | -------------------------------------------------------------------------------- /src/shared/utils/debug-logs.ts: -------------------------------------------------------------------------------- 1 | const sendRequestToLogger = (level: string, job: string, message: any) => 2 | fetch(process.env.PLASMO_PUBLIC_LOGGER_URL, { 3 | method: 'POST', 4 | headers: { 5 | 'Content-Type': 'application/json', 6 | Authorization: `Basic ${process.env.PLASMO_PUBLIC_LOGGER_BASIC_AUTH}` 7 | }, 8 | body: JSON.stringify({ 9 | level, 10 | job, 11 | log: message 12 | }) 13 | }); 14 | 15 | /** 16 | * Logger 17 | * @description 18 | * This is a simple logger that logs to the console and sends logs to external logger if the `PLASMO_PUBLIC_LOGGER_URL` environment variable is set. 19 | * @example 20 | * import { logger } from '~debug-logs'; 21 | * 22 | * logger.info('job/scope', 'test message', { data: 'test', data2: 'test2'}); 23 | * 24 | */ 25 | export const logger = { 26 | /** 27 | * 28 | * INFO message 29 | * @param job Scope of this log. Example: function name, class name, etc. 30 | * @param message Event message 31 | * @param data Event data (optional) 32 | */ 33 | info: (job: string, message: string, data?: any) => { 34 | console.info(`[INFO] ${job} - ${message}${data ? `: ${JSON.stringify(data)}` : ''}`); 35 | 36 | if (process.env.PLASMO_PUBLIC_LOGGER_URL) { 37 | sendRequestToLogger('info', job, { 38 | message, 39 | data: JSON.stringify(data) 40 | }); 41 | } 42 | }, 43 | /** 44 | * 45 | * ERROR message 46 | * @param job Scope of this log. Example: function name, class name, etc. 47 | * @param message Event message 48 | * @param data Event data (optional) 49 | */ 50 | error: (job: string, message: string, data?: any) => { 51 | console.error(`[ERROR] ${job} - ${message}${data ? `: ${JSON.stringify(data)}` : ''}`); 52 | 53 | if (process.env.PLASMO_PUBLIC_LOGGER_URL) { 54 | sendRequestToLogger('error', job, { 55 | message, 56 | data: JSON.stringify(data) 57 | }); 58 | } 59 | }, 60 | /** 61 | * 62 | * DEBUG message 63 | * @param job Scope of this log. Example: function name, class name, etc. 64 | * @param message Event message 65 | * @param data Event data (optional) 66 | */ 67 | debug: (job: string, message: string, data?: any) => { 68 | console.debug(`[DEBUG] ${job} - ${message}${data ? `: ${JSON.stringify(data)}` : ''}`); 69 | 70 | if (process.env.PLASMO_PUBLIC_LOGGER_URL) { 71 | sendRequestToLogger('debug', job, { 72 | message, 73 | data: JSON.stringify(data) 74 | }); 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/shared/components/Navbar.component.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | grimoire 17 | companion 18 |
19 |
20 | 44 | 58 |
59 |
60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Grimoire Logo 3 |

grimoire companion

4 |

Web extension for Grimoire - a bookmark manager for the wizards 🧙

5 | GitHub License 6 | GitHub Release 7 |
8 |
9 | 10 | Adding bookmarks to Grimoire is now easier than ever! With the Grimoire Companion, you can add bookmarks to your Grimoire account directly from your browser. Just click the extension icon, select the category and tags, and press "Add Bookmark" - it's that simple! 11 | 12 | ## Features 13 | 14 | - add bookmarks to your Grimoire account directly from your browser 🔖 15 | - create new tags as you add bookmarks 🏷️ 16 | - specify the importance and/or flag the bookmark ⭐ 17 | - automatically fetch metadata from the website using the browser's API 🫶 18 | - choose if you want to add a screenshot of the website 📸 19 | 20 |
21 | Grimoire Companion Screenshot 22 |
23 | 24 | ## Where to get it? 25 | 26 | At the moment, _grimoire companion_ is available for download on the following platforms: 27 | 28 | 36 | 37 | ## Development 38 | 39 | ### Prerequisites 40 | 41 | - [Node.js](https://nodejs.org/en/download/) 42 | - [PNPM](https://pnpm.io/installation) 43 | 44 | ### Steps 45 | 46 | ```bash 47 | # Clone the repository 48 | git clone https://github.com/goniszewski/grimoire-web-extension 49 | 50 | # Install the dependencies 51 | pnpm i 52 | 53 | # Run the development version 54 | pnpm dev 55 | 56 | # Build the production version (by default, this will create the Chrome extension for local development in `build/chrome-mv3-prod`) 57 | pnpm build 58 | ``` 59 | 60 | [How to load the extension in Chrome-based browsers](https://docs.plasmo.com/framework#loading-the-extension-in-chrome) 61 | 62 | ## Development 63 | 64 | Check out the [the official Plasmo documentation](https://docs.plasmo.com/) to learn more. 65 | 66 | ## Roadmap 67 | 68 | - [x] Initial release (0.1.0) 🚀 69 | 70 | We're open to suggestions and feature requests! If you have an idea for a feature, please [open an issue](https://github.com/goniszewski/grimoire-web-extension/issues). 71 | 72 | ## Contributing 73 | 74 | If you want to contribute to the project, please read the [contributing guide](CONTRIBUTING.md). 75 | 76 | ## License 77 | 78 | This project is licensed under the [MIT License](LICENSE). 79 | 80 | ## Credits 81 | 82 | Special thanks to: [DaisyUI](https://github.com/saadeghi/daisyui), 83 | [Plasmo](https://docs.plasmo.com), 84 | [Svelte](https://github.com/sveltejs/svelte), 85 | [Svelte French Toast](https://github.com/kbrgl/svelte-french-toast), 86 | [Svelte MultiSelect](https://github.com/janosh/svelte-multiselect), 87 | [Tailwind CSS](https://tailwindcss.com) 88 | -------------------------------------------------------------------------------- /src/shared/handlers/on-add-bookmark.handler.ts: -------------------------------------------------------------------------------- 1 | import { sendToBackground } from '@plasmohq/messaging'; 2 | import { logger } from '~shared/utils/debug-logs'; 3 | import type { AddBookmarkRequestBody } from '~shared/types/add-bookmark.type'; 4 | import { loading } from '~shared/stores'; 5 | import { showToast } from '~shared/utils/show-toast'; 6 | 7 | const updateOnAddBookmarkLoading = (error = false) => { 8 | loading.update((loading) => { 9 | if (error) { 10 | loading.isAddingBookmark = false; 11 | } else { 12 | loading.justAddedBookmark = true; 13 | } 14 | 15 | return loading; 16 | }); 17 | }; 18 | 19 | export async function onAddBookmark( 20 | $currentTab: any, 21 | token: string, 22 | grimoireApiUrl: string, 23 | capturePageScreenshot?: boolean 24 | ) { 25 | logger.debug('onAddBookmark', 'init', $currentTab); 26 | loading.update((loading) => ({ ...loading, isAddingBookmark: true })); 27 | 28 | let screenshot: string = ''; 29 | 30 | const iconIsDataUrl = $currentTab.icon_url.startsWith('data:'); 31 | 32 | try { 33 | if (capturePageScreenshot) { 34 | logger.debug('onAddBookmark', 'Capturing page screenshot'); 35 | await new Promise((resolve) => { 36 | chrome.tabs.captureVisibleTab(function (screenshotDataUrl) { 37 | const screenshotImage = new Image(); 38 | screenshotImage.src = screenshotDataUrl; 39 | screenshotImage.onload = function () { 40 | const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 41 | 42 | logger.debug('onAddBookmark', 'Screenshot loaded', { 43 | width: screenshotImage.width, 44 | height: screenshotImage.height 45 | }); 46 | 47 | const canvas = document.createElement('canvas'); 48 | const ctx = canvas.getContext('2d'); 49 | 50 | ctx.imageSmoothingEnabled = true; 51 | 52 | canvas.height = 800; 53 | canvas.width = (screenshotImage.width / screenshotImage.height) * 800; 54 | ctx.drawImage(screenshotImage, 0, 0, canvas.width, canvas.height); 55 | 56 | logger.debug('onAddBookmark', 'Screenshot resized', { 57 | width: canvas.width, 58 | height: canvas.height 59 | }); 60 | 61 | // Safari doesn't currently support converting to webp :( 62 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL#browser_compatibility 63 | const resizedScreenshotDataUrl = canvas.toDataURL( 64 | isSafari ? 'image/jpeg' : 'image/webp', 65 | 0.8 66 | ); 67 | 68 | logger.debug('onAddBookmark', 'Screenshot converted to data URL'); 69 | 70 | screenshot = resizedScreenshotDataUrl; 71 | 72 | resolve(null); 73 | }; 74 | }); 75 | }); 76 | } 77 | logger.debug('onAddBookmark', 'Bookmark body', { 78 | url: $currentTab.url, 79 | title: $currentTab.title, 80 | icon_url: iconIsDataUrl ? '' : $currentTab.icon_url, 81 | main_image_url: $currentTab.mainImage, 82 | content_html: $currentTab.contentHtml, 83 | description: $currentTab.description, 84 | category: $currentTab.category, 85 | tags: $currentTab.tags, 86 | note: $currentTab.note, 87 | importance: $currentTab.importance, 88 | flagged: $currentTab.flagged, 89 | screenshot, 90 | ...(iconIsDataUrl ? { icon: $currentTab.icon_url } : {}) 91 | }); 92 | 93 | const response = await sendToBackground< 94 | { 95 | token: string; 96 | grimoireApiUrl: string; 97 | bookmark: AddBookmarkRequestBody; 98 | }, 99 | { 100 | bookmark: any; 101 | } 102 | >({ 103 | name: 'add-bookmark', 104 | body: { 105 | token, 106 | grimoireApiUrl, 107 | bookmark: { 108 | url: $currentTab.url, 109 | title: $currentTab.title, 110 | icon_url: iconIsDataUrl ? '' : $currentTab.icon_url, 111 | main_image_url: $currentTab.mainImage, 112 | content_html: $currentTab.contentHtml, 113 | description: $currentTab.description, 114 | category: $currentTab.category, 115 | tags: $currentTab.tags, 116 | note: $currentTab.note, 117 | importance: $currentTab.importance, 118 | flagged: $currentTab.flagged, 119 | screenshot, 120 | ...(iconIsDataUrl ? { icon: $currentTab.icon_url } : {}) 121 | } 122 | } 123 | }); 124 | 125 | if (response.bookmark) { 126 | logger.debug('onAddBookmark', 'Bookmark added', response.bookmark); 127 | } 128 | 129 | updateOnAddBookmarkLoading(!response.bookmark); 130 | } catch (error) { 131 | logger.error('onAddBookmark', 'Error adding bookmark', error?.message); 132 | 133 | updateOnAddBookmarkLoading(true); 134 | showToast.error("Couldn't add bookmark. Please try again."); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/popup.svelte: -------------------------------------------------------------------------------- 1 | 266 | 267 |
268 | {#if missingPermissions} 269 |
270 |

Permissions required

271 | Before we continue, please grant this extension the necessary permissions. 272 |

They will allow us to:

273 |
    274 |
  • connect to your Grimoire instance
  • 275 |
  • take screenshots of webpages
  • 276 |
277 | 278 | 281 |
282 | {:else if !$status.isGrimoireApiReachable} 283 |
284 |

Sign in

285 |

286 | {#if token} 287 | Oh snap! Grimoire API went dark. Please check the URL and try again. 288 | {:else} 289 | First things first! Let's connect to your Grimoire instance! 290 | {/if} 291 |

292 |
293 | Grimoire API URL 294 |
295 | { 301 | if (e.key === 'Enter') { 302 | handleGrimoireApiUrlChange(); 303 | } 304 | }} 305 | /> 306 | {#if !$status.isGrimoireApiReachable} 307 |

Grimoire API is not reachable!

308 | {/if} 309 | 313 |
314 | {:else if !token} 315 |
316 |

Sign in

317 |

Signed out! Please sign in to continue.

318 | 349 | 361 |
362 | {:else if !$categories.length} 363 |
364 | 365 | Fetching categories and tags... 366 | 367 | 368 |
369 | {:else} 370 | 371 |
372 |
375 | 376 | 377 | 378 |
381 | 382 |
383 | URL: 384 |
385 | 391 | 392 | {#if $currentTab.icon_url} 393 |
394 | icon 395 |
396 | {:else} 397 |
398 | 413 |
414 | {/if} 415 |
416 |
417 | 418 |
419 | Title: 420 | 426 |
427 | 428 |
429 | Category: 430 | {#if $categories} 431 | 443 | {/if} 444 |
445 | 446 |
447 | Tags: 448 | tag.name)} selectedTags={currentTab} /> 449 |
450 | 451 |
452 | Note: 453 | 458 |
459 | 460 | 461 |
462 | 463 |
464 | 465 |
466 | ($currentTab.importance = 0)} 473 | /> 474 | ($currentTab.importance = 1)} 480 | /> 481 | ($currentTab.importance = 2)} 487 | /> 488 | ($currentTab.importance = 3)} 494 | /> 495 |
496 |
497 | 498 | 507 |
508 | 509 |
510 | 530 |
531 | 532 | 533 |
534 | 535 |
Show more details
536 |
537 | 538 |
539 | Icon: 540 |
541 | {#if $currentTab.icon_url} 542 |
543 | icon 544 |
545 | {/if} 546 | 552 |
553 |
554 | 555 |
556 | Main Image: 557 | 563 |
564 | 565 |
566 | Description: 567 |