├── public └── .gitkeep ├── src ├── ui │ ├── setup │ │ ├── index.scss │ │ ├── app.vue │ │ ├── index.html │ │ ├── pages │ │ │ ├── install.vue │ │ │ └── update.vue │ │ └── index.ts │ ├── side-panel │ │ ├── index.scss │ │ ├── index.html │ │ ├── app.vue │ │ └── index.ts │ ├── action-popup │ │ ├── index.scss │ │ ├── pages │ │ │ ├── playground.vue │ │ │ └── index.vue │ │ ├── app.vue │ │ ├── index.html │ │ └── index.ts │ ├── devtools-panel │ │ ├── index.scss │ │ ├── pages │ │ │ └── index.vue │ │ ├── index.html │ │ ├── app.vue │ │ └── index.ts │ ├── options-page │ │ ├── index.scss │ │ ├── app.vue │ │ ├── index.html │ │ ├── index.ts │ │ └── pages │ │ │ └── index.vue │ ├── content-script-iframe │ │ ├── index.scss │ │ ├── app.vue │ │ ├── index.html │ │ ├── pages │ │ │ ├── index.vue │ │ │ ├── toolbar.vue │ │ │ └── notebox.vue │ │ └── index.ts │ └── common │ │ └── pages │ │ ├── 404.vue │ │ ├── change-log.vue │ │ ├── about.vue │ │ ├── help.vue │ │ ├── privacy-policy.vue │ │ ├── terms-of-service.vue │ │ └── features.vue ├── assets │ ├── icon-128.png │ └── base.scss ├── utils │ ├── pinia.ts │ ├── notifications.ts │ ├── router │ │ └── index.ts │ └── i18n.ts ├── devtools │ ├── index.html │ └── index.ts ├── offscreen │ ├── index.html │ └── index.ts ├── types │ ├── session.d.ts │ ├── router-meta.d.ts │ ├── vite-env.d.ts │ ├── historymap.ts │ ├── components.d.ts │ ├── typed-router.d.ts │ ├── .eslintrc-auto-import.json │ └── auto-imports.d.ts ├── locales │ ├── zh.json │ └── en.json ├── components │ ├── HistoryMap │ │ ├── layout │ │ │ ├── utils.ts │ │ │ └── compact-tree.ts │ │ └── Index.vue │ ├── Canvas │ │ ├── node-tools │ │ │ ├── ToolSchematise.vue │ │ │ └── ToolRemove.vue │ │ ├── node-toolbars │ │ │ └── HmPageNodeToolbar.vue │ │ └── nodes │ │ │ └── HmPageNode │ │ │ ├── Header.vue │ │ │ └── Index.vue │ ├── Basic │ │ └── ToolbarIcon.vue │ ├── SessionList.vue │ └── Header.vue ├── stores │ ├── options.store.ts │ └── test.store.ts ├── composables │ ├── useTheme.ts │ ├── useLocale.ts │ ├── useSession.ts │ ├── useBrowserStorage.ts │ └── useHistoryMap.ts ├── background │ ├── index.ts │ ├── annotation.ts │ └── controller.ts └── content-script │ ├── index.scss │ ├── select-element.ts │ ├── index.ts │ └── annotation.ts ├── .npmrc ├── .prettierignore ├── eslint.config.mjs ├── screenshots ├── Screenshot_20241225_224236.png ├── Screenshot_20241225_224300.png ├── Screenshot_20241225_224440.png ├── Screenshot_20241225_225109.png └── Screenshot_20241227_000344.png ├── .prettierrc ├── manifest.chrome.config.ts ├── .gitignore ├── tailwind.config.cjs ├── .vscode ├── settings.json └── extensions.json ├── manifest.firefox.config.ts ├── tsconfig.node.json ├── uno.config.ts ├── CHANGELOG.md ├── tsconfig.json ├── define.config.mjs ├── README.md ├── manifest.config.ts ├── vite.chrome.config.ts ├── vite.firefox.config.ts ├── scripts ├── launch.ts └── getInstalledBrowsers.ts ├── package.json └── vite.config.ts /public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/setup/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/side-panel/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/action-popup/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/devtools-panel/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/options-page/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/content-script-iframe/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /src/assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vis4Sense/HistoryMap/HEAD/src/assets/icon-128.png -------------------------------------------------------------------------------- /src/utils/pinia.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | export const pinia = createPinia() 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | auto-imports.d.ts 2 | components.d.ts 3 | pnpm-lock.yaml 4 | yarn.lock 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | 5 | ) 6 | -------------------------------------------------------------------------------- /src/devtools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /src/offscreen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /screenshots/Screenshot_20241225_224236.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vis4Sense/HistoryMap/HEAD/screenshots/Screenshot_20241225_224236.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20241225_224300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vis4Sense/HistoryMap/HEAD/screenshots/Screenshot_20241225_224300.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20241225_224440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vis4Sense/HistoryMap/HEAD/screenshots/Screenshot_20241225_224440.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20241225_225109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vis4Sense/HistoryMap/HEAD/screenshots/Screenshot_20241225_225109.png -------------------------------------------------------------------------------- /screenshots/Screenshot_20241227_000344.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vis4Sense/HistoryMap/HEAD/screenshots/Screenshot_20241227_000344.png -------------------------------------------------------------------------------- /src/ui/content-script-iframe/app.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/base.scss: -------------------------------------------------------------------------------- 1 | // required for vue-devtools 2 | @use '/node_modules/vite-plugin-vue-devtools/src/overlay/devtools-overlay.css'; 3 | 4 | body { 5 | font-size: 1rem; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/session.d.ts: -------------------------------------------------------------------------------- 1 | export interface SessionMetaData { 2 | sessionId: number 3 | time: number 4 | title: string 5 | timeCreated: number 6 | timeUpdated: number 7 | } 8 | -------------------------------------------------------------------------------- /src/ui/action-popup/pages/playground.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/ui/devtools-panel/pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "htmlWhitespaceSensitivity": "ignore", 3 | "quoteProps": "as-needed", 4 | "semi": false, 5 | "singleAttributePerLine": true, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /src/offscreen/index.ts: -------------------------------------------------------------------------------- 1 | // redirect logs to background script 2 | window.console.info = (...data) => { 3 | chrome.runtime.sendMessage({ 4 | type: "CONSOLE_LOG", 5 | data, 6 | }) 7 | } 8 | 9 | console.info("console log from offscreen document") 10 | -------------------------------------------------------------------------------- /manifest.chrome.config.ts: -------------------------------------------------------------------------------- 1 | import { defineManifest } from "@crxjs/vite-plugin" 2 | import ManifestConfig from "./manifest.config" 3 | 4 | // @ts-expect-error ManifestConfig provides all required fields 5 | export default defineManifest((env) => ({ 6 | ...ManifestConfig, 7 | key: env["CHROME_ADDON_KEY"], 8 | })) 9 | -------------------------------------------------------------------------------- /src/utils/notifications.ts: -------------------------------------------------------------------------------- 1 | import { createNotivue } from "notivue" 2 | 3 | import "notivue/notification.css" // Only needed if using built-in notifications 4 | import "notivue/animations.css" // Only needed if using built-in animations 5 | 6 | export const notivue = createNotivue({ 7 | position: "bottom-center", 8 | }) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env 27 | -------------------------------------------------------------------------------- /src/types/router-meta.d.ts: -------------------------------------------------------------------------------- 1 | import "vue-router" 2 | import { RouteNamedMap } from "vue-router/auto-routes" 3 | 4 | export {} 5 | 6 | declare module "vue-router" { 7 | interface RouteMeta { 8 | auth: 9 | | undefined 10 | | boolean 11 | | { 12 | unauthenticatedOnly: true 13 | navigateAuthenticatedTo: keyof RouteNamedMap | "/" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // Put your variables here: 5 | 6 | declare const __VERSION__: string 7 | declare const __NAME__: string 8 | declare const __DISPLAY_NAME__: string 9 | declare const __CHANGELOG__: string 10 | declare const __GIT_COMMIT__: string 11 | declare const __GITHUB_URL__: string 12 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "about": "关于", 4 | "contact": "联系", 5 | "home": "首页" 6 | }, 7 | "notifications": { 8 | "error": "发生错误。", 9 | "success": "成功!", 10 | "warning": "警告!" 11 | }, 12 | "settings": { 13 | "cancelButton": "取消", 14 | "saveButton": "保存", 15 | "title": "设置" 16 | }, 17 | "updated": "已更新", 18 | "welcomeMessage": "欢迎使用我们的Chrome扩展程序!" 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/devtools-panel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | <%= HTML_TITLE %> 10 | 11 | 12 |
13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ui/action-popup/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ui/content-script-iframe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | <%= HTML_TITLE %> 10 | 11 | 12 |
13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ui/devtools-panel/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ui/options-page/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ui/setup/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/HistoryMap/layout/utils.ts: -------------------------------------------------------------------------------- 1 | export function DummyContainer() { 2 | let container: HTMLElement 3 | 4 | function initialise() { 5 | container = document.createElement('div') 6 | container.style.visibility = 'hidden' 7 | document.body.appendChild(container) 8 | } 9 | 10 | initialise() 11 | 12 | return { 13 | node: () => container, 14 | remove: () => container.remove() 15 | } 16 | } -------------------------------------------------------------------------------- /src/ui/common/pages/404.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['src/**/*.{html,vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [ 8 | 'prettier-plugin-tailwindcss', 9 | require('@tailwindcss/typography'), 10 | // require('@tailwindcss/forms'), 11 | require('daisyui'), 12 | ], 13 | daisyui: { 14 | themes: ['light', 'dark'], 15 | logs: false, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import { handleHotUpdate, routes } from 'vue-router/auto-routes' 3 | 4 | // routes.push({ 5 | // path: "/:catchAll(.*)*", 6 | // redirect: "/", 7 | // }) 8 | 9 | export const appRouter = createRouter({ 10 | history: createWebHashHistory(import.meta.env.BASE_URL), 11 | routes, 12 | }) 13 | 14 | if (import.meta.hot) { 15 | handleHotUpdate(appRouter) 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import messages from '@intlify/unplugin-vue-i18n/messages' 2 | import { createI18n } from 'vue-i18n' 3 | 4 | export const i18n = createI18n({ 5 | globalInjection: true, 6 | legacy: false, 7 | locale: 'en', 8 | fallbackLocale: 'en', 9 | messages, 10 | }) 11 | 12 | // restore locale from local storage 13 | 14 | const currentLocale = useBrowserLocalStorage('user-locale', 'en') 15 | 16 | i18n.global.locale.value = currentLocale.value 17 | -------------------------------------------------------------------------------- /src/components/Canvas/node-tools/ToolSchematise.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "about": "About", 4 | "contact": "Contact", 5 | "home": "Home" 6 | }, 7 | "notifications": { 8 | "error": "Error occurred.", 9 | "success": "Success!", 10 | "warning": "Warning!" 11 | }, 12 | "settings": { 13 | "cancelButton": "Cancel", 14 | "saveButton": "Save", 15 | "title": "Settings" 16 | }, 17 | "updated": "Updated", 18 | "welcomeMessage": "Welcome to our Chrome extension!" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Canvas/node-toolbars/HmPageNodeToolbar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/ui/setup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | <%= HTML_TITLE %> 15 | 16 | 17 |
18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ui/action-popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | <%= HTML_TITLE %> 15 | 16 | 17 |
18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ui/options-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | <%= HTML_TITLE %> 15 | 16 | 17 |
18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ui/side-panel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | <%= HTML_TITLE %> 15 | 16 | 17 |
18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ui/setup/pages/install.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/types/historymap.ts: -------------------------------------------------------------------------------- 1 | export interface HmPage { 2 | sessionId: number 3 | id: string // hm-uuid 4 | tabId: number 5 | type: 'hm-page' 6 | timeCreated: number 7 | timeLastActivated: number 8 | pageObj: chrome.tabs.Tab 9 | parentPageId: string | null 10 | isActive: boolean 11 | annotations?: Annotation[] 12 | } 13 | 14 | export interface Annotation { 15 | id: number 16 | selection: string // serialized selection 17 | sourceText: string 18 | highlighted: boolean 19 | tags?: string[] 20 | timeCreated: number 21 | timeUpdated: number 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": ["manifest.json"], 5 | "url": "https://json.schemastore.org/chrome-manifest.json" 6 | } 7 | ], 8 | "typescript.preferences.autoImportFileExcludePatterns": ["vue-router"], 9 | "i18n-ally.localesPaths": [ 10 | "src/locales" 11 | ], 12 | "i18n-ally.keystyle": "nested", 13 | "eslint.experimental.useFlatConfig": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit" 16 | }, 17 | "files.associations": { 18 | "*.css": "postcss" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/stores/options.store.ts: -------------------------------------------------------------------------------- 1 | export const useOptionsStore = defineStore("options", () => { 2 | const { isDark, toggleDark } = useTheme() 3 | 4 | const { data: profile } = useBrowserSyncStorage<{ 5 | name: string 6 | age: number 7 | }>("profile", { 8 | name: "Mario", 9 | age: 24, 10 | }) 11 | 12 | const { data: others } = useBrowserLocalStorage<{ 13 | awesome: boolean 14 | counter: number 15 | }>("options", { 16 | awesome: true, 17 | counter: 0, 18 | }) 19 | 20 | return { 21 | isDark, 22 | toggleDark, 23 | profile, 24 | others, 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "afzalsayed96.icones", 4 | "antfu.goto-alias", 5 | "antfu.iconify", 6 | "antfu.vite", 7 | "bradlc.vscode-tailwindcss", 8 | "csstools.postcss", 9 | "dbaeumer.vscode-eslint", 10 | "editorconfig.editorconfig", 11 | "esbenp.prettier-vscode", 12 | "github.vscode-github-actions", 13 | "mikestead.dotenv", 14 | "mrmlnc.vscode-scss", 15 | "mrniamster.daisyui-snippets", 16 | "mubaidr.vuejs-extension-pack", 17 | "sdras.vue-vscode-snippets", 18 | "usernamehw.errorlens", 19 | "vue.volar" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/content-script-iframe/pages/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /src/stores/test.store.ts: -------------------------------------------------------------------------------- 1 | export const useTestStore = defineStore('app', () => { 2 | const count = useBrowserLocalStorage('count', 0) 3 | const name = useBrowserLocalStorage('name', 'John Doe') 4 | 5 | // You should probably use chrome.storage API instead of localStorage since localStorage history can be cleared by the user. 6 | // See https://developer.chrome.com/docs/extensions/reference/api/storage 7 | 8 | const increment = () => { 9 | count.value++ 10 | } 11 | 12 | const decrement = () => { 13 | count.value-- 14 | } 15 | 16 | return { 17 | count, 18 | name, 19 | increment, 20 | decrement, 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/composables/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { BasicColorSchema } from "@vueuse/core" 2 | import { useBrowserLocalStorage } from "./useBrowserStorage" 3 | 4 | export function useTheme() { 5 | const { data: colorSchema } = useBrowserLocalStorage("mode", "auto") 6 | 7 | const isDark = useDark({ 8 | initialValue: colorSchema, 9 | onChanged(isDark, defaultHandler, mode) { 10 | // load initial value 11 | colorSchema.value = mode 12 | defaultHandler(mode) 13 | document.body.setAttribute("data-theme", mode) 14 | }, 15 | }) 16 | 17 | const toggleDark = useToggle(isDark) 18 | 19 | return { 20 | isDark, 21 | toggleDark, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Canvas/nodes/HmPageNode/Header.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/ui/side-panel/app.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /manifest.firefox.config.ts: -------------------------------------------------------------------------------- 1 | import { defineManifest } from "@crxjs/vite-plugin" 2 | import ManifestConfig from "./manifest.config" 3 | 4 | // @ts-expect-error ManifestConfig provides all required fields 5 | export default defineManifest((env) => ({ 6 | ...ManifestConfig, 7 | browser_specific_settings: { 8 | gecko: { 9 | id: env["FIREFOX_ADDON_ID"], 10 | }, 11 | }, 12 | background: { 13 | scripts: ["src/background/index.ts"], 14 | type: "module", 15 | persistent: false, 16 | }, 17 | permissions: [ 18 | // @ts-expect-error background permission is not supported in Firefox 19 | ...ManifestConfig.permissions.filter( 20 | (permission) => permission !== "background", 21 | ), 22 | ], 23 | })) 24 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { initialiseController } from './controller' 2 | import './annotation' 3 | // import { initialiseExtractor } from './extractor' 4 | 5 | /** open side panel on clicking in toolbar */ 6 | chrome.sidePanel 7 | .setPanelBehavior({ openPanelOnActionClick: true }) 8 | .catch(error => console.error(error)) 9 | 10 | self.onerror = function (message, source, lineno, colno, error) { 11 | console.info(`Error: ${message}`) 12 | console.info(`Source: ${source}`) 13 | console.info(`Line: ${lineno}`) 14 | console.info(`Column: ${colno}`) 15 | console.info(`Error object: ${error}`) 16 | } 17 | 18 | console.info('hello world from background') 19 | 20 | /** initialise controller */ 21 | initialiseController() 22 | // initialiseExtractor() 23 | 24 | export {} 25 | -------------------------------------------------------------------------------- /src/composables/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "vue" 2 | import { useBrowserLocalStorage } from "./useBrowserStorage" 3 | import { i18n } from "@/utils/i18n" // Adjust the import path according to your project structure 4 | 5 | export function useLocale() { 6 | const defaultLocale = "en" 7 | const localeKey = "user-locale" 8 | 9 | // Use the useBrowserLocalStorage composable to persist the locale 10 | const { data: currentLocale } = useBrowserLocalStorage(localeKey, defaultLocale) 11 | 12 | // Initialize the locale from i18n 13 | // currentLocale.value = i18n.global.locale.value 14 | 15 | // Watch for changes in the locale and update i18n 16 | watch(currentLocale, (newLocale) => { 17 | i18n.global.locale.value = newLocale 18 | }) 19 | 20 | return currentLocale 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/options-page/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from "@/utils/i18n" 2 | import { notivue } from "@/utils/notifications" 3 | import { pinia } from "@/utils/pinia" 4 | import { appRouter } from "@/utils/router" 5 | import { createApp } from "vue" 6 | import App from "./app.vue" 7 | import "./index.scss" 8 | 9 | appRouter.addRoute({ 10 | path: "/", 11 | redirect: "/options-page", 12 | }) 13 | 14 | const app = createApp(App).use(i18n).use(notivue).use(pinia).use(appRouter) 15 | 16 | app.mount("#app") 17 | 18 | export default app 19 | 20 | self.onerror = function (message, source, lineno, colno, error) { 21 | console.info("Error: " + message) 22 | console.info("Source: " + source) 23 | console.info("Line: " + lineno) 24 | console.info("Column: " + colno) 25 | console.info("Error object: " + error) 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/devtools-panel/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from "@/utils/i18n" 2 | import { notivue } from "@/utils/notifications" 3 | import { pinia } from "@/utils/pinia" 4 | import { appRouter } from "@/utils/router" 5 | import { createApp } from "vue" 6 | import App from "./app.vue" 7 | import "./index.scss" 8 | 9 | appRouter.addRoute({ 10 | path: "/", 11 | redirect: "/devtools-panel", 12 | }) 13 | 14 | const app = createApp(App).use(i18n).use(notivue).use(pinia).use(appRouter) 15 | 16 | app.mount("#app") 17 | 18 | export default app 19 | 20 | self.onerror = function (message, source, lineno, colno, error) { 21 | console.info("Error: " + message) 22 | console.info("Source: " + source) 23 | console.info("Line: " + lineno) 24 | console.info("Column: " + colno) 25 | console.info("Error object: " + error) 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/setup/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from "@/utils/i18n" 2 | import { notivue } from "@/utils/notifications" 3 | import { pinia } from "@/utils/pinia" 4 | import { appRouter } from "@/utils/router" 5 | import { createApp } from "vue" 6 | import App from "./app.vue" 7 | import "./index.scss" 8 | 9 | appRouter.addRoute({ 10 | path: "/", 11 | alias: "/setup", 12 | redirect: "/setup/install", 13 | }) 14 | 15 | const app = createApp(App).use(i18n).use(notivue).use(pinia).use(appRouter) 16 | 17 | app.mount("#app") 18 | 19 | export default app 20 | 21 | self.onerror = function (message, source, lineno, colno, error) { 22 | console.info("Error: " + message) 23 | console.info("Source: " + source) 24 | console.info("Line: " + lineno) 25 | console.info("Column: " + colno) 26 | console.info("Error object: " + error) 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/common/pages/change-log.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 35 | -------------------------------------------------------------------------------- /src/devtools/index.ts: -------------------------------------------------------------------------------- 1 | self.onerror = function (message, source, lineno, colno, error) { 2 | console.info('Error: ' + message) 3 | console.info('Source: ' + source) 4 | console.info('Line: ' + lineno) 5 | console.info('Column: ' + colno) 6 | console.info('Error object: ' + error) 7 | } 8 | 9 | console.info('hello world from devtools html') 10 | 11 | chrome.devtools.panels.create( 12 | 'My Panel', 13 | chrome.runtime.getURL('src/assets/logo.png'), 14 | chrome.runtime.getURL('src/ui/devtools-panel/index.html'), 15 | function (panel) { 16 | console.info('Panel created', panel) 17 | } 18 | ) 19 | 20 | chrome.devtools.panels.elements.createSidebarPane( 21 | 'My Sidebar', 22 | function (sidebar) { 23 | sidebar.setObject(JSON.stringify({ some_data: 'Some data to show' })) 24 | console.info('Sidebar created', sidebar) 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "strict": true, 5 | "incremental": true, 6 | "composite": true, 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "paths": { 10 | "@/*": ["./src/*"], 11 | "~/*": ["./src/*"], 12 | "src/*": ["./src/*"], 13 | "@assets/*": ["./src/assets/*"] 14 | }, 15 | "resolveJsonModule": true, 16 | "types": [ 17 | "unplugin-icons/types/vue", 18 | "chrome-types", 19 | "@intlify/unplugin-vue-i18n/messages" 20 | ], 21 | "allowSyntheticDefaultImports": true, 22 | "esModuleInterop": true 23 | }, 24 | "include": [ 25 | "package.json", 26 | "vite.config.ts", 27 | "vite.firefox.config.ts", 28 | "manifest.config.ts", 29 | "manifest.firefox.config.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Basic/ToolbarIcon.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetUno, 6 | presetWebFonts, 7 | } from 'unocss' 8 | 9 | export default defineConfig({ 10 | presets: [ 11 | presetUno(), 12 | presetAttributify(), 13 | presetIcons({ 14 | scale: 1.2, 15 | warn: true, 16 | collections: { 17 | 'material-symbols-light': () => import('@iconify-json/material-symbols-light/icons.json').then(i => i.default), 18 | 'mdi': () => import('@iconify-json/mdi/icons.json').then(i => i.default), 19 | 'ph': () => import('@iconify-json/ph/icons.json').then(i => i.default), 20 | }, 21 | }), 22 | presetWebFonts({ 23 | fonts: { 24 | sans: 'DM Sans', 25 | serif: 'DM Serif Display', 26 | mono: 'DM Mono', 27 | }, 28 | }), 29 | ], 30 | theme: { 31 | colors: { }, 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Example update 4 | 5 | - Update README.md 6 | - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies aliquam, nunc nisl ultricies nunc, vitae ultricies. 7 | 8 | - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies aliquam, nunc nisl ultricies nunc, vitae ultricies. 9 | 10 | - [x] Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies aliquam, nunc nisl ultricies nunc, vitae ultricies. 11 | 12 | - [ ] Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies aliquam, nunc nisl ultricies nunc, vitae ultricies. 13 | 14 | | Left columns | Right columns | Center Align | 15 | | ------------ | ------------: | :----------: | 16 | | left foo | right foo | center foo | 17 | | left bar | right bar | center bar | 18 | | left baz | right baz | center baz | 19 | -------------------------------------------------------------------------------- /src/ui/action-popup/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from "@/utils/i18n" 2 | import { notivue } from "@/utils/notifications" 3 | import { pinia } from "@/utils/pinia" 4 | import { appRouter } from "@/utils/router" 5 | import { createApp } from "vue" 6 | import App from "./app.vue" 7 | import "./index.scss" 8 | 9 | appRouter.addRoute({ 10 | path: "/", 11 | redirect: "/action-popup", 12 | }) 13 | 14 | // router.beforeEach((to, from, next) => { 15 | // if (to.path === '/') { 16 | // return next('/action-popup') 17 | // } 18 | 19 | // next() 20 | // }) 21 | 22 | const app = createApp(App).use(i18n).use(notivue).use(pinia).use(appRouter) 23 | 24 | app.mount("#app") 25 | 26 | export default app 27 | 28 | self.onerror = function (message, source, lineno, colno, error) { 29 | console.info("Error: " + message) 30 | console.info("Source: " + source) 31 | console.info("Line: " + lineno) 32 | console.info("Column: " + colno) 33 | console.info("Error object: " + error) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/SessionList.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | -------------------------------------------------------------------------------- /src/ui/side-panel/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '@/utils/i18n' 2 | import { notivue } from '@/utils/notifications' 3 | import { pinia } from '@/utils/pinia' 4 | import { appRouter } from '@/utils/router' 5 | import ElementPlus from 'element-plus' 6 | import { createApp } from 'vue' 7 | import App from './app.vue' 8 | 9 | import './index.scss' 10 | import '@unocss/reset/tailwind.css' 11 | import 'uno.css' 12 | import 'element-plus/dist/index.css' 13 | 14 | appRouter.addRoute({ 15 | path: '/', 16 | redirect: '/side-panel', 17 | }) 18 | 19 | const app = createApp(App) 20 | .use(i18n) 21 | .use(notivue) 22 | .use(pinia) 23 | .use(appRouter) 24 | 25 | app.use(ElementPlus) 26 | app.mount('#app') 27 | 28 | export default app 29 | 30 | self.onerror = function (message, source, lineno, colno, error) { 31 | console.info(`Error: ${message}`) 32 | console.info(`Source: ${source}`) 33 | console.info(`Line: ${lineno}`) 34 | console.info(`Column: ${colno}`) 35 | console.info(`Error object: ${error}`) 36 | } 37 | -------------------------------------------------------------------------------- /src/ui/setup/pages/update.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 42 | -------------------------------------------------------------------------------- /src/ui/common/pages/about.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "incremental": true, 5 | "target": "ES6", 6 | "jsx": "preserve", 7 | "lib": ["ES6", "DOM"], 8 | "useDefineForClassFields": true, 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "paths": { 12 | "@/*": ["./src/*"], 13 | "~/*": ["./src/*"], 14 | "src/*": ["./src/*"], 15 | "@assets/*": ["./src/assets/*"] 16 | }, 17 | "resolveJsonModule": true, 18 | "types": [ 19 | "unplugin-icons/types/vue", 20 | "chrome-types", 21 | "@intlify/unplugin-vue-i18n/messages" 22 | ], 23 | "strict": true, 24 | "noEmit": true, 25 | "esModuleInterop": true, 26 | "isolatedModules": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": [ 30 | "package.json", 31 | "vite.config.ts", 32 | // "manifest.config.ts", 33 | "src/**/*.ts", 34 | "src/**/*.d.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue" 37 | ] 38 | // "references": [ 39 | // { 40 | // "path": "./tsconfig.node.json" 41 | // } 42 | // ] 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/common/pages/help.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /src/content-script/index.scss: -------------------------------------------------------------------------------- 1 | // This file is used to style the iframe that is injected into the page 2 | .historymap-crx-iframe { 3 | border: none; 4 | // border-radius: 2px; 5 | // box-shadow: 6 | // 0 2px 4px rgba(0, 0, 0, 0.2), 7 | // 0 -1px 0px rgba(0, 0, 0, 0.02); 8 | // background-color: #fff; 9 | margin: 0.5rem; 10 | position: fixed; 11 | top: 50%; 12 | right: 0; 13 | // width: min-content; 14 | // height: min-content; 15 | // min-height: 40px; 16 | transform: translateY(-50%); 17 | } 18 | 19 | [hm-clipper-hovered="true"] { 20 | outline: 2px dashed #D18700; 21 | } 22 | 23 | [hm-clipper-selected="true"] { 24 | background-color: rgba($color: #FFB900, $alpha: 0.1); 25 | } 26 | 27 | .historymap-selection-toolbar-iframe { 28 | position: absolute; 29 | transform: translateX(-50%) translateY(-100%); 30 | width: 6rem; 31 | height: 3rem; 32 | } 33 | 34 | .historymap-note-box-iframe { 35 | position: absolute; 36 | width: 10rem; 37 | height: 5rem; 38 | right: 1rem; 39 | } 40 | 41 | .annotate { 42 | text-decoration: underline; 43 | text-decoration-color: #FF7787; 44 | text-decoration-style: dashed; 45 | } 46 | 47 | iframe { 48 | border: none; 49 | } 50 | -------------------------------------------------------------------------------- /define.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs" 2 | import { spawnSync } from "node:child_process" 3 | import packageJson from "./package.json" with { type: "json" } 4 | 5 | // Read CHANGELOG.md file into a string. 6 | const changelog = fs.readFileSync("./CHANGELOG.md", "utf-8") 7 | 8 | // Get the current git commit hash. 9 | const gitCommit = spawnSync("git", ["rev-parse", "--short", "HEAD"]) 10 | .stdout.toString() 11 | .trim() 12 | 13 | const jsn = (value) => JSON.stringify(value) 14 | 15 | // Don't forget to add your added variables to vite-env.d.ts also! 16 | 17 | // These variables are available in your Vue components and will be replaced by their values at build time. 18 | // These will be compiled into your app. Don't store secrets here! 19 | 20 | export const defineViteConfig = { 21 | __VERSION__: jsn(packageJson.version), 22 | __NAME__: jsn(packageJson.name), 23 | __DISPLAY_NAME__: jsn(packageJson.displayName), 24 | __CHANGELOG__: jsn(changelog), 25 | __GIT_COMMIT__: jsn(gitCommit), 26 | __GITHUB_URL__: jsn(packageJson.repository.url), 27 | // Set the HTML title for all pages from package.json so you can use %HTML_TITLE% in your HTML files. 28 | HTML_TITLE: jsn(packageJson.displayName), 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/content-script-iframe/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from '@/utils/i18n' 2 | import { notivue } from '@/utils/notifications' 3 | import { pinia } from '@/utils/pinia' 4 | import { appRouter } from '@/utils/router' 5 | import ElementPlus from 'element-plus' 6 | import { createApp } from 'vue' 7 | import App from './app.vue' 8 | 9 | import './index.scss' 10 | import '@unocss/reset/tailwind.css' 11 | import 'uno.css' 12 | import 'element-plus/dist/index.css' 13 | 14 | appRouter.addRoute({ 15 | path: '/', 16 | redirect: '/content-script-iframe', 17 | }) 18 | 19 | appRouter.addRoute({ 20 | path: '/toolbar', 21 | component: () => import('./pages/toolbar.vue'), 22 | }) 23 | 24 | appRouter.addRoute({ 25 | path: '/notebox', 26 | component: () => import('./pages/notebox.vue'), 27 | }) 28 | 29 | const app = createApp(App).use(i18n).use(notivue).use(pinia).use(appRouter) 30 | 31 | app.use(ElementPlus) 32 | app.mount('#app') 33 | 34 | export default app 35 | 36 | self.onerror = function (message, source, lineno, colno, error) { 37 | console.info(`Error: ${message}`) 38 | console.info(`Source: ${source}`) 39 | console.info(`Line: ${lineno}`) 40 | console.info(`Column: ${colno}`) 41 | console.info(`Error object: ${error}`) 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HistoryMap 2 | 3 | Initialised with [vite-vue3-browser-extension-v3](https://github.com/mubaidr/vite-vue3-browser-extension-v3). 4 | 5 | An ongoing rebuild of HistoryMap ([old branch](https://github.com/Vis4Sense/HistoryMap/tree/mv3)), migrating to [mv3](https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3) and [vue](https://vuejs.org/). 6 | 7 | ## Status 8 | 9 | | Feature | Status | Note | 10 | | --- | --- | --- | 11 | | Capture page history | 🔄 In Progress | MVP done ✅ | 12 | | Curate information (e.g., highlight & tagging) | 🔄 In Progress | Highlight is supported ✅ | 13 | 14 | ## Usage 15 | 16 | 1. Clone the project 17 | 2. run `pnpm i` to install dependencies 18 | 3. run `pnpm build` (or `pnpm dev` to run in develop mode) 19 | 20 | Load the extension (`dist/chrome`) to your browser when using it for the first time. 21 | 22 | ## Reference 23 | 24 | ```bibTeX 25 | @InProceedings{nguyen2016sensemap, 26 | author = {Nguyen, Phong H. and Xu, Kai and Bardill, Andy and Salman, Betul and Herd, Kate and Wong, B.L. William}, 27 | booktitle = {IEEE Conference on Visual Analytics Science and Technology (VAST)}, 28 | title = {SenseMap: Supporting browser-based online sensemaking through analytic provenance}, 29 | year = {2016}, 30 | doi = {10.1109/vast.2016.7883515}, 31 | url = {http://dx.doi.org/10.1109/VAST.2016.7883515}, 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /src/content-script/select-element.ts: -------------------------------------------------------------------------------- 1 | // import { useExtractor } from '@/composables/useExtractor' 2 | 3 | // const { clippingModal } = useExtractor() 4 | 5 | // document.addEventListener('mouseover', (event) => { 6 | // if (clippingModal.value) { 7 | // const target = event.target as HTMLElement 8 | // target.setAttribute('hm-clipper-hovered', 'true') 9 | // } 10 | // }) 11 | 12 | // document.addEventListener('mouseout', (event) => { 13 | // if (clippingModal.value) { 14 | // const target = event.target as HTMLElement 15 | // target.removeAttribute('hm-clipper-hovered') 16 | // } 17 | // }) 18 | 19 | // document.addEventListener('click', (event) => { 20 | // if (clippingModal.value) { 21 | // const target = event.target as HTMLElement 22 | // const selected = target.getAttribute('hm-clipper-selected') 23 | // if (selected) { 24 | // target.removeAttribute('hm-clipper-selected') 25 | // } else { 26 | // target.setAttribute('hm-clipper-selected', 'true') 27 | // } 28 | 29 | // // get all selected elements 30 | // const selectedElements = Array.from(document.querySelectorAll('[hm-clipper-selected]')) 31 | // console.info('selectedElements', selectedElements) 32 | 33 | // chrome.runtime.sendMessage({ 34 | // type: 'selected-elements', 35 | // data: selectedElements.map((element) => element.outerHTML), 36 | // }) 37 | // } 38 | // }) 39 | -------------------------------------------------------------------------------- /src/ui/content-script-iframe/pages/toolbar.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 58 | -------------------------------------------------------------------------------- /src/types/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | BasicToolbarIcon: typeof import('./../components/Basic/ToolbarIcon.vue')['default'] 11 | CanvasNodesHmPageNode: typeof import('./../components/Canvas/nodes/HmPageNode/Index.vue')['default'] 12 | CanvasNodesHmPageNodeHeader: typeof import('./../components/Canvas/nodes/HmPageNode/Header.vue')['default'] 13 | CanvasNodeToolbarsHmPageNodeToolbar: typeof import('./../components/Canvas/node-toolbars/HmPageNodeToolbar.vue')['default'] 14 | CanvasNodeToolsToolRemove: typeof import('./../components/Canvas/node-tools/ToolRemove.vue')['default'] 15 | CanvasNodeToolsToolSchematise: typeof import('./../components/Canvas/node-tools/ToolSchematise.vue')['default'] 16 | Header: typeof import('./../components/Header.vue')['default'] 17 | HistoryMap: typeof import('./../components/HistoryMap/Index.vue')['default'] 18 | IPhListHeart: typeof import('~icons/ph/list-heart')['default'] 19 | IPhPresentationChart: typeof import('~icons/ph/presentation-chart')['default'] 20 | IPhRocketLaunch: typeof import('~icons/ph/rocket-launch')['default'] 21 | RouterLink: typeof import('vue-router')['RouterLink'] 22 | RouterView: typeof import('vue-router')['RouterView'] 23 | SessionList: typeof import('./../components/SessionList.vue')['default'] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /manifest.config.ts: -------------------------------------------------------------------------------- 1 | import { env } from "node:process" 2 | import type { ManifestV3Export } from "@crxjs/vite-plugin" 3 | import packageJson from "./package.json" with { type: "json" } 4 | 5 | const { version, name, description, displayName } = packageJson 6 | // Convert from Semver (example: 0.1.0-beta6) 7 | const [major, minor, patch, label = "0"] = version 8 | // can only contain digits, dots, or dash 9 | .replace(/[^\d.-]+/g, "") 10 | // split into version parts 11 | .split(/[.-]/) 12 | 13 | export default { 14 | name: env.mode === "staging" ? `[INTERNAL] ${name}` : displayName || name, 15 | description, 16 | // up to four numbers separated by dots 17 | version: `${major}.${minor}.${patch}.${label}`, 18 | // semver is OK in "version_name" 19 | version_name: version, 20 | manifest_version: 3, 21 | // key: '', 22 | action: { 23 | default_popup: "src/ui/action-popup/index.html", 24 | }, 25 | background: { 26 | service_worker: "src/background/index.ts", 27 | type: "module", 28 | }, 29 | content_scripts: [ 30 | { 31 | all_frames: false, 32 | js: ["src/content-script/index.ts"], 33 | matches: ["*://*/*"], 34 | run_at: "document_end", 35 | }, 36 | ], 37 | side_panel: { 38 | default_path: "src/ui/side-panel/index.html", 39 | }, 40 | devtools_page: "src/devtools/index.html", 41 | options_page: "src/ui/options-page/index.html", 42 | offline_enabled: true, 43 | host_permissions: [], 44 | permissions: ["storage", "tabs", "background", "sidePanel", "scripting"], 45 | web_accessible_resources: [], 46 | icons: { 47 | 16: "src/assets/icon-128.png", 48 | 24: "src/assets/icon-128.png", 49 | 32: "src/assets/icon-128.png", 50 | 128: "src/assets/icon-128.png", 51 | }, 52 | } as ManifestV3Export 53 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | 59 | 73 | -------------------------------------------------------------------------------- /src/ui/common/pages/privacy-policy.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /src/components/Canvas/node-tools/ToolRemove.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /src/ui/action-popup/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/ui/content-script-iframe/pages/notebox.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 71 | 72 | 78 | -------------------------------------------------------------------------------- /src/ui/common/pages/terms-of-service.vue: -------------------------------------------------------------------------------- 1 | 55 | -------------------------------------------------------------------------------- /src/composables/useSession.ts: -------------------------------------------------------------------------------- 1 | import type { SessionMetaData } from '@/types/session' 2 | 3 | const { data: sessions } = useBrowserLocalStorage('hm-sessions', [] as SessionMetaData[]) 4 | const { data: sessionId } = useBrowserLocalStorage('hm-session-id', -1) 5 | 6 | export function useSession() { 7 | /** define state */ 8 | 9 | // active session 10 | const session = computed(() => sessions.value.find(d => d.sessionId === sessionId.value)) 11 | 12 | const state = { 13 | sessions, 14 | sessionId, 15 | session, 16 | } 17 | 18 | /** utils */ 19 | function newSession(title: string = ''): SessionMetaData { 20 | const id = sessions.value.length 21 | return { 22 | sessionId: id, 23 | time: Date.now(), 24 | title, 25 | timeCreated: Date.now(), 26 | timeUpdated: Date.now(), 27 | } 28 | } 29 | 30 | /** actions */ 31 | function addSession(title: string = '') { 32 | const session_ = newSession(title) 33 | sessions.value = [...sessions.value, session_] 34 | sessionId.value = session_.sessionId 35 | } 36 | 37 | function updateSession(id: number, data: Partial) { 38 | const session_ = sessions.value.find(d => d.sessionId === id) 39 | if (session_) 40 | Object.assign(session_, data) 41 | } 42 | 43 | function switchSession(id: number) { 44 | sessionId.value = id 45 | } 46 | 47 | function switchToDefaultSession() { 48 | switchSession(0) 49 | } 50 | 51 | function switchToLatestSession() { 52 | let latestestId = 0 53 | if (sessions.value.length > 1) { 54 | const latest = sessions.value 55 | .filter(d => d.sessionId !== 0) 56 | .sort((a, b) => b.timeUpdated - a.timeUpdated)[0] 57 | latestestId = latest.sessionId 58 | } 59 | switchSession(latestestId) 60 | } 61 | 62 | /** initialise */ 63 | function initialise() { 64 | // the first session is the background session that captures 65 | // page history when no specific session is active 66 | if (!sessions.value.length) 67 | addSession('Default') 68 | } 69 | 70 | initialise() 71 | 72 | return { 73 | ...state, 74 | addSession, 75 | updateSession, 76 | switchSession, 77 | switchToDefaultSession, 78 | switchToLatestSession, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/content-script/index.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaType } from '@/types/extractor' 2 | import Postmate from 'postmate' 3 | import { onMessage, sendMessage } from 'webext-bridge/content-script' 4 | import { Readability } from '@mozilla/readability' 5 | import TurndownService from 'turndown' 6 | // This import scss file is used to style the iframe that is injected into the page 7 | import './index.scss' 8 | 9 | import './select-element' 10 | import './annotation' 11 | 12 | // const src = chrome.runtime.getURL('src/ui/content-script-iframe/index.html') 13 | 14 | // const handshake = new Postmate({ 15 | // container: document.body, 16 | // url: src, 17 | // classListArray: ['historymap-crx-iframe'], 18 | // }) 19 | 20 | // handshake.then((child) => { 21 | // console.info('postmate connection established') 22 | 23 | // child.on('extract', handleExtract) 24 | // }) 25 | 26 | // function handleExtract(data: { 27 | // schemaType: SchemaType 28 | // }) { 29 | // const sourceText = document.body.textContent || '' 30 | // const data_ = { 31 | // schemaType: data.schemaType, 32 | // sourceText, 33 | // } 34 | // sendMessage('extract', data_, 'background') 35 | // } 36 | 37 | // add listener when document is ready 38 | // document.addEventListener('DOMContentLoaded', () => { 39 | // console.info('document ready') 40 | // sendMessage('content-script-ready', null, 'background') 41 | // }) 42 | 43 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 44 | // console.info('message', message) 45 | if (message.type === 'fetch-content') { 46 | const docClone = document.implementation.createHTMLDocument('Cloned Document') 47 | docClone.body.innerHTML = document.body.innerHTML 48 | const article = new Readability(docClone).parse() 49 | 50 | const turndownService = new TurndownService() 51 | const markdown = turndownService.turndown(article?.content || '') 52 | 53 | sendResponse({ sourceText: markdown }) 54 | } 55 | return true 56 | }) 57 | 58 | self.onerror = function (message, source, lineno, colno, error) { 59 | console.info(`Error: ${message}`) 60 | console.info(`Source: ${source}`) 61 | console.info(`Line: ${lineno}`) 62 | console.info(`Column: ${colno}`) 63 | console.info(`Error object: ${error}`) 64 | } 65 | 66 | console.info('hello world from content-script') 67 | -------------------------------------------------------------------------------- /src/ui/options-page/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 85 | -------------------------------------------------------------------------------- /src/background/annotation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see {@link https://github.com/serversideup/webext-bridge} 3 | * 4 | * document 5 | * @see {@link https://serversideup.net/open-source/webext-bridge/docs} 6 | */ 7 | 8 | import type { Annotation } from '@/types/historymap' 9 | import { useHistoryMap } from '@/composables/useHistoryMap' 10 | import { onMessage } from 'webext-bridge/background' 11 | import { updateActivePage } from './controller' 12 | 13 | const { activePage, addAnnotation, removeHighlight, highlight, addTag, removeTag } = useHistoryMap() 14 | 15 | onMessage('fetch-annotations', () => { 16 | updateActivePage() 17 | return activePage.value?.annotations || [] 18 | }) 19 | 20 | onMessage('highlight', ({ data }) => { 21 | const { id, selection, sourceText } = data as { 22 | id: number 23 | selection: string 24 | sourceText: string 25 | } 26 | if (activePage.value) { 27 | let annotation = activePage.value.annotations?.find(d => d.id === id) || null 28 | if (annotation) { 29 | highlight(activePage.value.id, id) 30 | } 31 | else { 32 | annotation = addAnnotation( 33 | activePage.value.id, 34 | id, 35 | selection, 36 | sourceText, 37 | true, 38 | ) 39 | } 40 | if (annotation) { 41 | return annotation 42 | } 43 | } 44 | return null 45 | }) 46 | 47 | onMessage('dehighlight', ({ data }) => { 48 | if (activePage.value) { 49 | const annotation = data as Annotation 50 | removeHighlight(activePage.value.id, annotation.id) 51 | } 52 | return true 53 | }) 54 | 55 | onMessage('annotate', ({ data }) => { 56 | if (activePage.value) { 57 | const { id, selection, sourceText } = data as { 58 | id: number 59 | selection: string 60 | sourceText: string 61 | } 62 | const annotation = addAnnotation(activePage.value.id, id, selection, sourceText, false) 63 | if (annotation) { 64 | return annotation 65 | } 66 | } 67 | return null 68 | }) 69 | 70 | onMessage('add-tag', ({ data }) => { 71 | // console.log('add-tag', data) 72 | if (activePage.value) { 73 | const { id, tag } = data as { id: number, tag: string } 74 | const annotation = addTag(activePage.value.id, id, tag) 75 | return annotation 76 | } 77 | return null 78 | }) 79 | 80 | onMessage('remove-tag', ({ data }) => { 81 | if (activePage.value) { 82 | const { id, tag } = data as { id: number, tag: string } 83 | const annotation = removeTag(activePage.value.id, id, tag) 84 | return annotation 85 | } 86 | return null 87 | }) 88 | -------------------------------------------------------------------------------- /src/components/HistoryMap/Index.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 91 | 92 | 99 | -------------------------------------------------------------------------------- /src/types/typed-router.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ 5 | // It's recommended to commit this file. 6 | // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. 7 | 8 | declare module 'vue-router/auto-routes' { 9 | import type { 10 | RouteRecordInfo, 11 | ParamValue, 12 | ParamValueOneOrMore, 13 | ParamValueZeroOrMore, 14 | ParamValueZeroOrOne, 15 | } from 'vue-router' 16 | 17 | /** 18 | * Route name map generated by unplugin-vue-router 19 | */ 20 | export interface RouteNamedMap { 21 | '/action-popup/': RouteRecordInfo<'/action-popup/', '/action-popup', Record, Record>, 22 | '/action-popup/playground': RouteRecordInfo<'/action-popup/playground', '/action-popup/playground', Record, Record>, 23 | '/common/404': RouteRecordInfo<'/common/404', '/common/404', Record, Record>, 24 | '/common/about': RouteRecordInfo<'/common/about', '/common/about', Record, Record>, 25 | '/common/change-log': RouteRecordInfo<'/common/change-log', '/common/change-log', Record, Record>, 26 | '/common/features': RouteRecordInfo<'/common/features', '/common/features', Record, Record>, 27 | '/common/help': RouteRecordInfo<'/common/help', '/common/help', Record, Record>, 28 | '/common/privacy-policy': RouteRecordInfo<'/common/privacy-policy', '/common/privacy-policy', Record, Record>, 29 | '/common/terms-of-service': RouteRecordInfo<'/common/terms-of-service', '/common/terms-of-service', Record, Record>, 30 | '/content-script-iframe/': RouteRecordInfo<'/content-script-iframe/', '/content-script-iframe', Record, Record>, 31 | '/content-script-iframe/notebox': RouteRecordInfo<'/content-script-iframe/notebox', '/content-script-iframe/notebox', Record, Record>, 32 | '/content-script-iframe/toolbar': RouteRecordInfo<'/content-script-iframe/toolbar', '/content-script-iframe/toolbar', Record, Record>, 33 | '/devtools-panel/': RouteRecordInfo<'/devtools-panel/', '/devtools-panel', Record, Record>, 34 | '/options-page/': RouteRecordInfo<'/options-page/', '/options-page', Record, Record>, 35 | '/setup/install': RouteRecordInfo<'/setup/install', '/setup/install', Record, Record>, 36 | '/setup/update': RouteRecordInfo<'/setup/update', '/setup/update', Record, Record>, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Canvas/nodes/HmPageNode/Index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 109 | -------------------------------------------------------------------------------- /src/components/HistoryMap/layout/compact-tree.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Layout tree nodes using mxGraph 3 | */ 4 | 5 | import type { HmPage } from '@/types/historymap' 6 | import type { Cell } from '@maxgraph/core' 7 | import type { Edge, Node } from '@vue-flow/core' 8 | import { CompactTreeLayout, Graph } from '@maxgraph/core' 9 | import { DummyContainer } from './utils' 10 | 11 | export function compactTreeLayout({ 12 | levelDistance = 30, 13 | nodeDistance = 5, 14 | } = {}) { 15 | let module: { 16 | run: () => typeof module 17 | close: () => typeof module 18 | nodes: (nodes: Node[]) => typeof module 19 | links: (links: Edge[]) => typeof module 20 | }, 21 | nodes: Node[], 22 | links: Edge[], 23 | roots: Node[], 24 | nodeDict: Record, 25 | dummyContainer: ReturnType, 26 | graph: Graph, 27 | parentNode: Cell, 28 | layout: CompactTreeLayout 29 | 30 | // key functions to bind data 31 | const nodeKey = (d: Node) => d.data!.id 32 | 33 | // function to find parent node 34 | const parent = (d: Node) => nodes.find(n => n.data!.id === d.data!.parentPageId) 35 | 36 | function initialize() { 37 | dummyContainer = DummyContainer() 38 | 39 | graph = new Graph(dummyContainer.node()) 40 | parentNode = graph.getDefaultParent() 41 | layout = new CompactTreeLayout(graph, true) 42 | 43 | layout.useBoundingBox = false 44 | layout.edgeRouting = false 45 | layout.levelDistance = levelDistance 46 | layout.nodeDistance = nodeDistance 47 | 48 | nodeDict = {} 49 | 50 | initializeNodes() 51 | } 52 | 53 | function initializeNodes() { 54 | if (!nodes) 55 | return 56 | 57 | roots = nodes.filter(d => !parent(d)) 58 | 59 | nodes.forEach((d) => { 60 | nodeDict[nodeKey(d)] = graph.insertVertex(parentNode, null, '', 0, 0, d.width as number, d.height as number) 61 | }) 62 | 63 | // virtual root and edges 64 | const virtualRoot = graph.insertVertex(parentNode, null, '', 0, 0, -layout.levelDistance - 10, -layout.levelDistance - 10) 65 | roots.forEach((r) => { 66 | graph.insertEdge(parentNode, null, '', virtualRoot, nodeDict[nodeKey(r)]) 67 | }) 68 | } 69 | 70 | function initializeEdges() { 71 | if (!links) 72 | return 73 | 74 | links.forEach((d) => { 75 | graph.insertEdge(parentNode, null, '', nodeDict[d.source], nodeDict[d.target]) 76 | }) 77 | } 78 | 79 | function setNodeCoordinate() { 80 | nodes.forEach((d) => { 81 | const m = nodeDict[nodeKey(d)].geometry 82 | d.position.x = m?.x ?? 0 83 | d.position.y = m?.y ?? 0 84 | }) 85 | } 86 | 87 | initialize() 88 | 89 | return module = { 90 | run() { 91 | layout.execute(parentNode) 92 | setNodeCoordinate() 93 | return module 94 | }, 95 | 96 | close() { 97 | dummyContainer.remove() 98 | return module 99 | }, 100 | 101 | nodes(_: Node[]) { 102 | return (nodes = _, initializeNodes(), module) 103 | }, 104 | 105 | links(_: Edge[]) { 106 | return (links = _, initializeEdges(), module) 107 | }, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /vite.chrome.config.ts: -------------------------------------------------------------------------------- 1 | import { crx } from "@crxjs/vite-plugin" 2 | import { defineConfig } from "vite" 3 | import zipPack from "vite-plugin-zip-pack" 4 | import manifest from "./manifest.chrome.config" 5 | import packageJson from "./package.json" with { type: "json" } 6 | import ViteConfig from "./vite.config" 7 | import chalk from "chalk" 8 | 9 | const IS_DEV = process.env.NODE_ENV === "development" 10 | const browser = "chrome" 11 | const outDir = "dist" 12 | const browserOutDir = `${outDir}/${browser}` 13 | const outFileName = `${browser}-${packageJson.version}.zip` 14 | 15 | function printDevMessage() { 16 | setTimeout(() => { 17 | console.info("\n") 18 | console.info( 19 | `${chalk.greenBright(`✅ Successfully built for ${browser}.`)}`, 20 | ) 21 | console.info( 22 | chalk.greenBright( 23 | `🚀 To load this extension in Chrome, go to chrome://extensions/, enable "Developer mode", click "Load unpacked", and select the ${browserOutDir} directory.`, 24 | ), 25 | ) 26 | console.info("\n") 27 | }, 50) 28 | } 29 | 30 | function printProdMessage() { 31 | setTimeout(() => { 32 | console.info("\n") 33 | console.info( 34 | `${chalk.greenBright(`✅ Successfully built for ${browser}.`)}`, 35 | ) 36 | console.info( 37 | `${chalk.greenBright(`📦 Zip File for ${browser} is located at ${outDir}/${outFileName}. You can upload this to respective store. `)}`, 38 | ) 39 | console.info( 40 | chalk.greenBright( 41 | ` 🚀 To load this extension in Chrome, go to chrome://extensions/, enable "Developer mode", click "Load unpacked", and select the ${browserOutDir} directory.`, 42 | ), 43 | ) 44 | console.info("\n") 45 | }, 50) 46 | } 47 | 48 | if (!ViteConfig.build) { 49 | ViteConfig.build = {} 50 | } 51 | 52 | if (!ViteConfig.plugins) { 53 | ViteConfig.plugins = [] 54 | } 55 | 56 | ViteConfig.build.outDir = browserOutDir 57 | // ViteConfig.base = IS_DEV 58 | // ? `http://localhost:${ViteConfig.server?.port}/` 59 | // : `/dist/${browser}` 60 | 61 | ViteConfig.plugins.unshift( 62 | crx({ 63 | manifest, 64 | browser, 65 | contentScripts: { 66 | injectCss: true, 67 | }, 68 | }), 69 | ) 70 | 71 | if (IS_DEV) { 72 | ViteConfig.plugins.push({ 73 | name: "vite-plugin-build-message", 74 | enforce: "post", 75 | configureServer(server) { 76 | server.httpServer?.once("listening", () => { 77 | printDevMessage() 78 | }) 79 | }, 80 | closeBundle: { 81 | sequential: true, 82 | handler() { 83 | printDevMessage() 84 | }, 85 | }, 86 | }) 87 | } else { 88 | ViteConfig.plugins.push( 89 | zipPack({ 90 | inDir: browserOutDir, 91 | outDir, 92 | outFileName, 93 | filter: (fileName, filePath, isDirectory) => 94 | !(isDirectory && filePath.includes(".vite")), 95 | }), 96 | ) 97 | 98 | ViteConfig.plugins.push({ 99 | name: "vite-plugin-build-message", 100 | enforce: "post", 101 | closeBundle: { 102 | sequential: true, 103 | handler() { 104 | printProdMessage() 105 | }, 106 | }, 107 | }) 108 | } 109 | 110 | // https://vitejs.dev/config/ 111 | export default defineConfig({ 112 | ...ViteConfig, 113 | }) 114 | -------------------------------------------------------------------------------- /src/composables/useBrowserStorage.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, nextTick } from "vue" 2 | function mergeDeep(defaults: any, source: any): any { 3 | // Merge the default options with the stored options 4 | const output = { ...defaults } // Start with defaults 5 | 6 | Object.keys(defaults).forEach((key) => { 7 | const defaultValue = defaults[key] 8 | const sourceValue = source?.[key] 9 | 10 | if (isObject(defaultValue) && sourceValue != null) { 11 | // Recursively merge nested objects 12 | output[key] = mergeDeep(defaultValue, sourceValue) 13 | } else if (checkType(defaultValue, sourceValue)) { 14 | output[key] = sourceValue 15 | } else { 16 | // If the type is different, use the default value 17 | output[key] = defaultValue 18 | console.log("Type mismatch", key, sourceValue) 19 | } 20 | }) 21 | 22 | return output 23 | } 24 | 25 | function checkType(defaultValue: any, value: any): boolean { 26 | // Check if the value type is the same type as the default value or null 27 | // there are only strings, booleans, nulls and arrays as types left 28 | return (typeof value === typeof defaultValue && Array.isArray(value) == Array.isArray(defaultValue)) || value === null || defaultValue === null 29 | } 30 | function isObject(value: any): boolean { 31 | return value !== null && value instanceof Object && !Array.isArray(value) 32 | } 33 | 34 | export function useBrowserSyncStorage(key: string, defaultValue: T) { 35 | return useBrowserStorage(key, defaultValue, "sync") 36 | } 37 | 38 | export function useBrowserLocalStorage(key: string, defaultValue: T) { 39 | return useBrowserStorage(key, defaultValue, "local") 40 | } 41 | 42 | function useBrowserStorage(key: string, defaultValue: T, storageType: "sync" | "local" = "sync") { 43 | const data = ref(defaultValue) 44 | // Blocking setting storage if it is updating from storage 45 | let isUpdatingFromStorage = true 46 | const defaultIsObject = isObject(defaultValue) 47 | // Initialize storage with the value from chrome.storage 48 | const promise = new Promise((resolve) => { 49 | chrome.storage[storageType].get(key, async (result) => { 50 | if (result?.[key] !== undefined) { 51 | if (defaultIsObject && isObject(result[key])) { 52 | data.value = mergeDeep(defaultValue, result[key]) 53 | } else if (checkType(defaultValue, result[key])) { 54 | data.value = result[key] 55 | } 56 | } 57 | await nextTick() 58 | isUpdatingFromStorage = false 59 | resolve(true) 60 | }) 61 | }) 62 | 63 | // Watch for changes in the storage and update chrome.storage 64 | watch( 65 | data, 66 | (newValue) => { 67 | if (!isUpdatingFromStorage) { 68 | if (checkType(defaultValue, newValue)) { 69 | // chrome.storage[storageType].set({ [key]: toRaw(newValue) }) 70 | chrome.storage[storageType].set({ [key]: JSON.parse(JSON.stringify(newValue)) }) 71 | } else { 72 | console.error("not updating " + key + ": type mismatch") 73 | } 74 | } 75 | }, 76 | { deep: true, flush: "post" }, 77 | ) 78 | // Add the onChanged listener here 79 | chrome.storage[storageType].onChanged.addListener(async function (changes) { 80 | if (changes?.[key]) { 81 | isUpdatingFromStorage = true 82 | const { oldValue, newValue } = changes[key] 83 | data.value = newValue 84 | await nextTick() 85 | isUpdatingFromStorage = false 86 | } 87 | }) 88 | return { data, promise } 89 | } 90 | -------------------------------------------------------------------------------- /vite.firefox.config.ts: -------------------------------------------------------------------------------- 1 | import { crx } from "@crxjs/vite-plugin" 2 | import { defineConfig } from "vite" 3 | import zipPack from "vite-plugin-zip-pack" 4 | import manifest from "./manifest.firefox.config" 5 | import packageJson from "./package.json" with { type: "json" } 6 | import ViteConfig from "./vite.config" 7 | import chalk from "chalk" 8 | 9 | const IS_DEV = process.env.NODE_ENV === "development" 10 | const browser = "firefox" 11 | const outDir = "dist" 12 | const browserOutDir = `${outDir}/${browser}` 13 | const outFileName = `${browser}-${packageJson.version}.zip` 14 | 15 | function printDevMessage() { 16 | setTimeout(() => { 17 | console.info("\n") 18 | console.info( 19 | `${chalk.greenBright(`✅ Successfully built for ${browser}.`)}`, 20 | ) 21 | console.info( 22 | chalk.greenBright( 23 | `🚀 To load this extension in Firefox, go to about:debugging, click "This Firefox", then click "Load Temporary Add-on" and select the extension's manifest file in ${browserOutDir}.`, 24 | ), 25 | ) 26 | console.info("\n") 27 | }, 50) 28 | } 29 | 30 | function printProdMessage() { 31 | setTimeout(() => { 32 | console.info("\n") 33 | console.info( 34 | `${chalk.greenBright(`✅ Successfully built for ${browser}.`)}`, 35 | ) 36 | console.info( 37 | `${chalk.greenBright(`📦 Zip File for ${browser} is located at ${outDir}/${outFileName}. You can upload this to respective store. `)}`, 38 | ) 39 | console.info( 40 | chalk.greenBright( 41 | `🚀 To load this extension in Firefox, go to about:debugging, click "This Firefox", then click "Load Temporary Add-on" and select the extension's manifest file in ${browserOutDir}.`, 42 | ), 43 | ) 44 | console.info("\n") 45 | }, 50) 46 | } 47 | 48 | if (!ViteConfig.build) { 49 | ViteConfig.build = {} 50 | } 51 | 52 | if (!ViteConfig.plugins) { 53 | ViteConfig.plugins = [] 54 | } 55 | 56 | ViteConfig.build.outDir = browserOutDir 57 | 58 | if (IS_DEV) { 59 | ViteConfig.build.minify = false 60 | ViteConfig.build.sourcemap = true 61 | } 62 | 63 | ViteConfig.plugins.unshift( 64 | crx({ 65 | manifest, 66 | browser, 67 | contentScripts: { 68 | injectCss: true, 69 | }, 70 | }), 71 | ) 72 | 73 | if (IS_DEV) { 74 | ViteConfig.plugins.push({ 75 | name: "vite-plugin-build-message", 76 | enforce: "post", 77 | configureServer(server) { 78 | server.httpServer?.once("listening", () => { 79 | printDevMessage() 80 | }) 81 | }, 82 | closeBundle: { 83 | sequential: true, 84 | handler() { 85 | printDevMessage() 86 | }, 87 | }, 88 | }) 89 | } else { 90 | ViteConfig.plugins.push( 91 | zipPack({ 92 | inDir: browserOutDir, 93 | outDir, 94 | outFileName, 95 | filter: (fileName, filePath, isDirectory) => 96 | !(isDirectory && filePath.includes(".vite")), 97 | }), 98 | ) 99 | 100 | ViteConfig.plugins.push({ 101 | name: "vite-plugin-build-message", 102 | enforce: "post", 103 | closeBundle: { 104 | sequential: true, 105 | handler() { 106 | printProdMessage() 107 | }, 108 | }, 109 | }) 110 | } 111 | 112 | // https://vitejs.dev/config/ 113 | export default defineConfig({ 114 | ...ViteConfig, 115 | }) 116 | -------------------------------------------------------------------------------- /scripts/launch.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { exec } from "node:child_process" 4 | import { GetInstalledBrowsers } from "./getInstalledBrowsers" 5 | import concurrently, { type ConcurrentlyCommandInput } from "concurrently" 6 | import { program } from "commander" 7 | 8 | program 9 | .option("-a, --all", "Launch All Supported Browsers", false) 10 | .option("-c, --chrome", "Launch Chrome only", true) 11 | .option("-f, --firefox", "Launch Firefox only", false) 12 | .option("-e, --edge", "Launch Edge only", false) 13 | .option( 14 | "-v, --vite-chrome-config ", 15 | "Path to Vite Chrome config", 16 | "vite.chrome.config.ts" 17 | ) 18 | .option( 19 | "-x, --vite-firefox-config ", 20 | "Path to Vite Firefox config", 21 | "vite.firefox.config.ts" 22 | ) 23 | 24 | program.parse(process.argv) 25 | 26 | const options = program.opts<{ 27 | all: boolean 28 | chrome: boolean 29 | firefox: boolean 30 | edge: boolean 31 | viteChromeConfig: string 32 | viteFirefoxConfig: string 33 | }>() 34 | 35 | async function runViteDev() { 36 | return new Promise((resolve) => { 37 | console.info("Starting Vite dev servers...") 38 | 39 | const viteChrome = exec(`vite dev --config ${options.viteChromeConfig}`) 40 | const viteFirefox = exec(`vite dev --config ${options.viteFirefoxConfig}`) 41 | 42 | viteChrome.stdout?.pipe(process.stdout) 43 | viteChrome.stderr?.pipe(process.stderr) 44 | 45 | viteFirefox.stdout?.pipe(process.stdout) 46 | viteFirefox.stderr?.pipe(process.stderr) 47 | 48 | viteChrome.on("exit", (code) => { 49 | console.info(`Vite Chrome process exited with code ${code}`) 50 | }) 51 | 52 | viteFirefox.on("exit", (code) => { 53 | console.info(`Vite Firefox process exited with code ${code}`) 54 | }) 55 | 56 | setTimeout(() => { 57 | resolve() 58 | }, 1000) 59 | }) 60 | } 61 | 62 | async function launchBrowsers() { 63 | console.info("Detecting installed browsers...") 64 | 65 | const installedBrowsers = GetInstalledBrowsers() 66 | const commands: Array = [] 67 | 68 | if (options.chrome || options.all) { 69 | if (installedBrowsers.Chrome) { 70 | commands.push({ 71 | command: installedBrowsers.Chrome.command, 72 | name: installedBrowsers.Chrome.name, 73 | }) 74 | } else { 75 | console.error("Chrome is not installed.") 76 | } 77 | } 78 | 79 | if (options.firefox || options.all) { 80 | if (installedBrowsers.Firefox) { 81 | commands.push({ 82 | command: installedBrowsers.Firefox.command, 83 | name: installedBrowsers.Firefox.name, 84 | }) 85 | } else { 86 | console.error("Firefox is not installed.") 87 | } 88 | } 89 | 90 | if (options.edge || options.all) { 91 | if (installedBrowsers.Edge) { 92 | commands.push({ 93 | command: installedBrowsers.Edge.command, 94 | name: installedBrowsers.Edge.name, 95 | }) 96 | } else { 97 | console.error("Edge is not installed.") 98 | } 99 | } 100 | 101 | if (commands.length > 0) { 102 | console.info("Launching browsers...") 103 | 104 | concurrently(commands, { 105 | killOthers: ["failure", "success"], 106 | restartTries: 1, 107 | }) 108 | } else { 109 | console.error("No compatible browsers found or selected.") 110 | } 111 | } 112 | 113 | process.on("SIGINT", () => { 114 | console.info("Received SIGINT. Shutting down...") 115 | process.exit() 116 | }) 117 | 118 | process.on("SIGTERM", () => { 119 | console.info("Received SIGTERM. Shutting down...") 120 | process.exit() 121 | }) 122 | 123 | runViteDev().then(launchBrowsers) 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "historymap", 3 | "displayName": "HistoryMap", 4 | "type": "module", 5 | "version": "0.0.1", 6 | "private": true, 7 | "description": "A map of the visited tabs and their links", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/mubaidr/vite-vue3-browser-extension-v3" 11 | }, 12 | "scripts": { 13 | "build": "npm run build:chrome && npm run build:firefox", 14 | "build:chrome": "cross-env NODE_ENV=production vite build -c vite.chrome.config.ts", 15 | "build:firefox": "cross-env NODE_ENV=production vite build -c vite.firefox.config.ts", 16 | "dev": "concurrently \"npm run dev:chrome\" \"npm run dev:firefox\"", 17 | "dev:chrome": "cross-env NODE_ENV=development vite -c vite.chrome.config.ts", 18 | "dev:firefox": "cross-env NODE_ENV=development vite build --mode development --watch -c vite.firefox.config.ts", 19 | "format": "prettier --write .", 20 | "launch": "tsx scripts/launch.ts", 21 | "launch:all": "tsx scripts/launch.ts --all", 22 | "lint": "eslint . --fix --cache", 23 | "lint:manifest": "web-ext lint --pretty", 24 | "typecheck": "vue-tsc --noEmit" 25 | }, 26 | "dependencies": { 27 | "@antfu/eslint-config": "^4.2.1", 28 | "@iconify-json/material-symbols-light": "^1.2.14", 29 | "@maxgraph/core": "^0.15.1", 30 | "@mozilla/readability": "^0.5.0", 31 | "@vue-flow/core": "^1.42.1", 32 | "@vue-flow/node-toolbar": "^1.1.0", 33 | "d3": "^7.9.0", 34 | "element-plus": "^2.9.3", 35 | "graphology": "^0.26.0", 36 | "graphology-layout-force": "^0.2.4", 37 | "graphology-layout-forceatlas2": "^0.10.1", 38 | "lint-staged": "^15.4.3", 39 | "lodash": "^4.17.21", 40 | "marked": "^15.0.6", 41 | "notivue": "^2.4.5", 42 | "pinia": "^2.3.0", 43 | "postmate": "^1.5.2", 44 | "rangy": "^1.3.2", 45 | "sigma": "^3.0.1", 46 | "turndown": "^7.2.0", 47 | "uuid": "^11.0.5", 48 | "vue": "^3.5.13", 49 | "vue-i18n": "^11.0.1", 50 | "vue-router": "^4.5.0", 51 | "webext-bridge": "^6.0.1", 52 | "webextension-polyfill": "^0.12.0" 53 | }, 54 | "devDependencies": { 55 | "@crxjs/vite-plugin": "2.0.0-beta.31", 56 | "@eslint/compat": "^1.2.4", 57 | "@eslint/js": "^9.17.0", 58 | "@iconify-json/carbon": "^1.2.5", 59 | "@iconify-json/lucide": "^1.2.22", 60 | "@iconify-json/mdi": "^1.2.2", 61 | "@iconify-json/ph": "^1.2.2", 62 | "@iconify-json/svg-spinners": "^1.2.2", 63 | "@intlify/unplugin-vue-i18n": "^6.0.3", 64 | "@tailwindcss/forms": "^0.5.10", 65 | "@tailwindcss/typography": "^0.5.16", 66 | "@types/chrome": "^0.0.299", 67 | "@types/d3": "^7.4.3", 68 | "@types/eslint": "^9.6.1", 69 | "@types/eslint__js": "~8.42.3", 70 | "@types/node": "^22.10.5", 71 | "@types/postmate": "^1.5.4", 72 | "@types/rangy": "^0.0.38", 73 | "@types/webextension-polyfill": "^0.12.1", 74 | "@vitejs/plugin-vue": "^5.2.1", 75 | "@vue/compiler-sfc": "^3.5.13", 76 | "@vueuse/core": "^12.3.0", 77 | "autoprefixer": "^10.4.20", 78 | "chalk": "^5.4.1", 79 | "chrome-types": "^0.1.329", 80 | "commander": "^13.0.0", 81 | "concurrently": "^9.1.2", 82 | "cross-env": "^7.0.3", 83 | "daisyui": "^4.12.23", 84 | "dotenv": "^16.4.7", 85 | "eslint": "^9.17.0", 86 | "eslint-plugin-tailwindcss": "^3.18.0", 87 | "eslint-plugin-vue": "^9.32.0", 88 | "get-installed-browsers": "^0.1.7", 89 | "globals": "^15.14.0", 90 | "postcss": "^8.4.49", 91 | "prettier": "^3.4.2", 92 | "prettier-plugin-tailwindcss": "^0.6.9", 93 | "sass": "^1.83.1", 94 | "tailwindcss": "^3.4.17", 95 | "terser": "^5.37.0", 96 | "tsx": "^4.19.2", 97 | "typescript": "^5.7.3", 98 | "typescript-eslint": "^8.19.1", 99 | "unocss": "^65.4.3", 100 | "unplugin-auto-import": "^19.0.0", 101 | "unplugin-icons": "^22.0.0", 102 | "unplugin-imagemin": "^0.6.5", 103 | "unplugin-turbo-console": "^1.11.1", 104 | "unplugin-vue-components": "^28.0.0", 105 | "unplugin-vue-router": "^0.10.9", 106 | "vite": "^6.0.7", 107 | "vite-plugin-html": "^3.2.2", 108 | "vite-plugin-vue-devtools": "^7.7.0", 109 | "vite-plugin-zip-pack": "^1.2.4", 110 | "vue-tsc": "^2.2.0", 111 | "vuefire": "^3.2.1", 112 | "web-ext": "^8.3.0" 113 | }, 114 | "pnpm": { 115 | "overrides": {}, 116 | "peerDependencyRules": { 117 | "allowAny": [], 118 | "allowedDeprecatedVersions": { 119 | "sourcemap-codec": "1.4.8" 120 | }, 121 | "allowedVersions": { 122 | "node-fetch": "*" 123 | }, 124 | "ignoreMissing": [] 125 | } 126 | }, 127 | "overrides": { 128 | "@crxjs/vite-plugin": "$@crxjs/vite-plugin" 129 | }, 130 | "lint-staged": { 131 | "*": "eslint --fix" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs" 2 | import { URL, fileURLToPath } from "node:url" 3 | import vue from "@vitejs/plugin-vue" 4 | import AutoImport from "unplugin-auto-import/vite" 5 | import IconsResolver from "unplugin-icons/resolver" 6 | import Icons from "unplugin-icons/vite" 7 | import Components from "unplugin-vue-components/vite" 8 | import { createHtmlPlugin } from "vite-plugin-html" 9 | import VueRouter from "unplugin-vue-router/vite" 10 | import { defineConfig } from "vite" 11 | // @ts-expect-error commonjs module 12 | import { defineViteConfig as define } from "./define.config.mjs" 13 | import vueDevTools from "vite-plugin-vue-devtools" 14 | import TurboConsole from "unplugin-turbo-console/vite" 15 | import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite" 16 | import { dirname, relative, resolve } from "node:path" 17 | import "dotenv/config" 18 | import UnoCSS from 'unocss/vite' 19 | 20 | const PORT = Number(process.env.PORT || "") || 3303 21 | 22 | function getImmediateDirectories(dirPath: string): string[] { 23 | try { 24 | // Read the directory contents synchronously 25 | const items = fs.readdirSync(dirPath, { withFileTypes: true }) 26 | 27 | // Filter and map to get only directory names 28 | return items 29 | .filter((item): item is fs.Dirent => item.isDirectory()) // Type guard 30 | .map((item) => item.name) 31 | } catch (err) { 32 | throw new Error(`Error reading directories: ${(err as Error).message}`) 33 | } 34 | } 35 | 36 | // https://vitejs.dev/config/ 37 | export default defineConfig({ 38 | resolve: { 39 | alias: { 40 | "@": fileURLToPath(new URL("src", import.meta.url)), 41 | "~": fileURLToPath(new URL("src", import.meta.url)), 42 | src: fileURLToPath(new URL("src", import.meta.url)), 43 | "@assets": fileURLToPath(new URL("src/assets", import.meta.url)), 44 | }, 45 | }, 46 | 47 | css: { 48 | preprocessorOptions: { 49 | scss: { 50 | api: "modern", 51 | // additionalData: `@use "/src/assets/base.scss";`, 52 | additionalData: (content, filePath) => { 53 | // do not include base.scss (tailwind etc) in content-script iframe as it will be affect main page styles 54 | if (filePath.includes("content-script/index.scss")) { 55 | return content 56 | } 57 | 58 | return `@use "/src/assets/base.scss";\n${content}` 59 | }, 60 | }, 61 | }, 62 | }, 63 | 64 | plugins: [ 65 | VueI18nPlugin({ 66 | include: resolve( 67 | dirname(fileURLToPath(import.meta.url)), 68 | "./src/locales/**", 69 | ), 70 | globalSFCScope: true, 71 | compositionOnly: true, 72 | }), 73 | 74 | // vueDevTools(), 75 | 76 | // https://github.com/posva/unplugin-vue-router 77 | VueRouter({ 78 | dts: "src/types/typed-router.d.ts", 79 | routesFolder: getImmediateDirectories("src/ui").map((dir) => { 80 | return { 81 | src: `src/ui/${dir}/pages`, 82 | path: `${dir}/`, 83 | } 84 | }), 85 | }), 86 | 87 | vue(), 88 | 89 | // https://github.com/antfu/unocss 90 | // see uno.config.ts for config 91 | UnoCSS(), 92 | 93 | // imagemin({}), 94 | 95 | TurboConsole(), 96 | 97 | // https://github.com/unplugin/unplugin-auto-import 98 | AutoImport({ 99 | imports: [ 100 | "vue", 101 | "vue-router", 102 | "@vueuse/core", 103 | "pinia", 104 | { 105 | "vue-router/auto": ["definePage"], 106 | }, 107 | { 108 | "vue-i18n": ["useI18n", "t"], 109 | }, 110 | { 111 | "webextension-polyfill": [["*", "browser"]], 112 | }, 113 | { 114 | notivue: ["Notivue", "Notification", ["push", "pushNotification"]], 115 | }, 116 | ], 117 | dts: "src/types/auto-imports.d.ts", 118 | dirs: ["src/composables/**", "src/stores/**", "src/utils/**"], 119 | vueTemplate: true, 120 | viteOptimizeDeps: true, 121 | eslintrc: { 122 | enabled: true, 123 | filepath: "src/types/.eslintrc-auto-import.json", 124 | }, 125 | }), 126 | 127 | // https://github.com/antfu/unplugin-vue-components 128 | Components({ 129 | dirs: ["src/components"], 130 | // generate `components.d.ts` for ts support with Volar 131 | dts: "src/types/components.d.ts", 132 | resolvers: [ 133 | // auto import icons 134 | IconsResolver(), 135 | ], 136 | directoryAsNamespace: true, 137 | globalNamespaces: ["account", "state"], 138 | }), 139 | 140 | // https://github.com/antfu/unplugin-icons 141 | Icons({ 142 | autoInstall: true, 143 | compiler: "vue3", 144 | scale: 1.5, 145 | }), 146 | 147 | // rewrite assets to use relative path 148 | { 149 | name: "assets-rewrite", 150 | enforce: "post", 151 | apply: "build", 152 | transformIndexHtml(html, { path }) { 153 | const assetsPath = relative(dirname(path), "/assets").replace( 154 | /\\/g, 155 | "/", 156 | ) 157 | return html.replace(/"\/assets\//g, `"${assetsPath}/`) 158 | }, 159 | }, 160 | 161 | createHtmlPlugin({ 162 | inject: { 163 | data: define, // Inject all key-value pairs from defineViteConfig 164 | }, 165 | }), 166 | ], 167 | 168 | build: { 169 | manifest: false, 170 | outDir: "dist", 171 | sourcemap: false, 172 | write: true, 173 | rollupOptions: { 174 | // ui or pages that are not specified in manifest file need to be specified here 175 | input: { 176 | setup: "src/ui/setup/index.html", 177 | iframe: "src/ui/content-script-iframe/index.html", 178 | devtoolsPanel: "src/ui/devtools-panel/index.html", 179 | }, 180 | }, 181 | }, 182 | 183 | server: { 184 | port: PORT, 185 | hmr: { 186 | host: "localhost", 187 | clientPort: PORT, 188 | overlay: true, 189 | protocol: "ws", 190 | port: PORT, 191 | }, 192 | origin: `http://localhost:${PORT}`, 193 | }, 194 | 195 | optimizeDeps: { 196 | include: ["vue", "@vueuse/core", "webextension-polyfill"], 197 | exclude: ["vue-demi"], 198 | }, 199 | 200 | define, 201 | }) 202 | -------------------------------------------------------------------------------- /src/ui/common/pages/features.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 149 | -------------------------------------------------------------------------------- /src/composables/useHistoryMap.ts: -------------------------------------------------------------------------------- 1 | import type { Annotation, HmPage } from '~/types/historymap' 2 | import _ from 'lodash' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import { useBrowserLocalStorage } from './useBrowserStorage' 5 | import { useSession } from './useSession' 6 | 7 | function getLinks(pages: HmPage[]) { 8 | return pages 9 | .filter(d => d.parentPageId) 10 | .map(d => ({ source: d.parentPageId!, target: d.id })) 11 | } 12 | 13 | const { data: hmPages } = useBrowserLocalStorage('hm-pages', [] as HmPage[]) 14 | 15 | export function useHistoryMap() { 16 | const { sessionId, session, updateSession } = useSession() 17 | 18 | /** define state */ 19 | const pages = computed(() => hmPages.value.filter(d => d.sessionId === sessionId.value)) 20 | const links = computed(() => getLinks(pages.value)) 21 | 22 | // Active page 23 | const activePage = computed(() => pages.value.find(d => d.isActive)) 24 | 25 | const state = { 26 | pages, 27 | links, 28 | activePage, 29 | } 30 | 31 | /** utilities */ 32 | 33 | function newPage(tab: chrome.tabs.Tab, parentPageId: string | null = null): HmPage | null { 34 | return { 35 | sessionId: sessionId.value, 36 | id: `hm-${uuidv4()}`, 37 | tabId: tab.id!, 38 | type: 'hm-page', 39 | timeCreated: Date.now(), 40 | timeLastActivated: Date.now(), 41 | pageObj: tab, 42 | parentPageId, 43 | isActive: tab.active, 44 | } 45 | } 46 | 47 | function deactivateAllPages() { 48 | hmPages.value.filter(d => d.sessionId === sessionId.value) 49 | .forEach(page => page.isActive = false) 50 | } 51 | 52 | function getPage(id: string) { 53 | return hmPages.value.find(d => d.id === id) 54 | } 55 | 56 | function isEmptyAnnotation(annotation: Annotation) { 57 | if (!annotation.tags) 58 | return true 59 | if (annotation.tags.length === 0) 60 | return true 61 | return false 62 | } 63 | 64 | /** actions */ 65 | 66 | function addPage(tab: chrome.tabs.Tab, parentPageId: string | null = null) { 67 | if (!tab.id) { 68 | console.error('invalid tab id', tab) 69 | return null 70 | } 71 | 72 | const page = newPage(tab, parentPageId) 73 | if (page) { 74 | if (page.isActive) 75 | deactivateAllPages() 76 | if (!session.value?.title && page.pageObj.title) { 77 | updateSession(sessionId.value, { title: page.pageObj.title }) 78 | } 79 | hmPages.value = [...hmPages.value, page] 80 | } 81 | 82 | // console.log('added page', page) 83 | } 84 | 85 | function updatePage(pageId: string, data: Partial) { 86 | const page = hmPages.value.find(d => d.id === pageId) 87 | if (page) { 88 | if (data.isActive) { 89 | deactivateAllPages() 90 | data.timeLastActivated = Date.now() 91 | } 92 | Object.assign(page, data) 93 | } 94 | } 95 | 96 | function removePage(pageId: string, removeChildren = false) { 97 | const page = hmPages.value.find(d => d.id === pageId) 98 | if (page) { 99 | if (removeChildren) { 100 | // remove its children 101 | hmPages.value 102 | .filter(d => d.parentPageId === pageId) 103 | .forEach(d => removePage(d.id, true)) 104 | } 105 | else { 106 | // connect its children to its parent 107 | hmPages.value 108 | .filter(d => d.parentPageId === pageId) 109 | .forEach(d => d.parentPageId = page.parentPageId) 110 | } 111 | 112 | // remove the page 113 | const index = hmPages.value.indexOf(page) 114 | hmPages.value.splice(index, 1) 115 | } 116 | } 117 | 118 | function addAnnotation( 119 | pageId: string, 120 | id: number, 121 | selection: string, 122 | sourceText: string, 123 | highlighted: boolean = false, 124 | ) { 125 | const page = getPage(pageId) 126 | if (!page) 127 | return null 128 | if (!page.annotations) 129 | page.annotations = [] 130 | // const id = _.max(page.annotations.map(d => d.id + 1)) || 0 131 | const annotation = { 132 | id, 133 | selection, 134 | sourceText, 135 | highlighted, 136 | timeCreated: Date.now(), 137 | timeUpdated: Date.now(), 138 | } 139 | page.annotations.push(annotation) 140 | console.log('added annotation', annotation) 141 | return annotation 142 | } 143 | 144 | function highlight(pageId: string, id: number) { 145 | const page = getPage(pageId) 146 | if (!page || !page.annotations) 147 | return 148 | const annotation = page.annotations.find(d => d.id === id) 149 | if (annotation) { 150 | annotation.highlighted = true 151 | } 152 | } 153 | 154 | function removeHighlight( 155 | pageId: string, 156 | id: number, 157 | ) { 158 | const page = getPage(pageId) 159 | if (!page || !page.annotations) 160 | return 161 | const index = page.annotations.findIndex(d => d.id === id) 162 | if (index !== undefined && index >= 0) { 163 | const annotation = page.annotations[index] 164 | if (isEmptyAnnotation(annotation)) { 165 | page.annotations?.splice(index, 1) 166 | } 167 | else { 168 | annotation.highlighted = false 169 | } 170 | } 171 | } 172 | 173 | function addTag(pageId: string, id: number, value: string) { 174 | const page = getPage(pageId) 175 | if (!page) 176 | return 177 | const annotation = page.annotations?.find(d => d.id === id) 178 | if (annotation) { 179 | if (!annotation.tags) 180 | annotation.tags = [] 181 | annotation.tags.push(value) 182 | } 183 | return annotation 184 | } 185 | 186 | function removeTag(pageId: string, id: number, value: string) { 187 | const page = getPage(pageId) 188 | if (!page) 189 | return 190 | const annotation = page.annotations?.find(d => d.id === id) 191 | if (annotation && annotation.tags) { 192 | const index = annotation.tags.indexOf(value) 193 | if (index >= 0) { 194 | annotation.tags.splice(index, 1) 195 | } 196 | } 197 | return annotation 198 | } 199 | 200 | function getAnnotation( 201 | pageId: string, 202 | id: number, 203 | ) { 204 | const page = getPage(pageId) 205 | if (!page) 206 | return null 207 | return page.annotations?.find(d => d.id === id) || null 208 | } 209 | 210 | return { 211 | ...state, 212 | addPage, 213 | updatePage, 214 | removePage, 215 | addAnnotation, 216 | getAnnotation, 217 | highlight, 218 | removeHighlight, 219 | addTag, 220 | removeTag, 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/background/controller.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Controller of history map data, i.e., sessions and pages. 3 | */ 4 | 5 | import type { HmPage } from '@/types/historymap' 6 | import { useHistoryMap } from '@/composables/useHistoryMap' 7 | import { useSession } from '@/composables/useSession' 8 | import _ from 'lodash' 9 | 10 | const { switchToDefaultSession, switchToLatestSession } = useSession() 11 | const { pages, addPage, updatePage, activePage } = useHistoryMap() 12 | 13 | /** 14 | * handle chrome.tabs.onCreated 15 | * 16 | * add a new page to historymap 17 | */ 18 | function tabCreationHandler(tab: chrome.tabs.Tab) { 19 | // if the tab is reopened from historymap 20 | if (activePage.value && activePage.value.tabId === tab.id) { 21 | return 22 | } 23 | 24 | // console.log('tab created', tab) 25 | let parent: HmPage | null = null 26 | if (tab.openerTabId) { 27 | parent = pages.value 28 | .sort((a, b) => b.timeLastActivated - a.timeLastActivated) 29 | .find(page => page.tabId === tab.openerTabId) 30 | || null 31 | } 32 | 33 | addPage(tab, parent?.id || null) 34 | } 35 | 36 | /** 37 | * handle chrome.tabs.onUpdated 38 | * 39 | * possible behaviour: 40 | * - page information updated during loading: update the page object 41 | * - url update due to navigation (within the same tab) 42 | * - capture as go back if the url is previously visited 43 | * - create a new page if the url is new 44 | */ 45 | function tabUpdateHandler(tabId: number, changeInfo: Partial, tab: chrome.tabs.Tab) { 46 | // console.log('tab updated', tabId, changeInfo, tab) 47 | 48 | // the page to update 49 | let page: HmPage | undefined 50 | 51 | // if the tab is loading 52 | if (changeInfo.status === 'loading' && changeInfo.url) { 53 | page = pages.value.find( 54 | page => page.tabId === tabId 55 | && (page.pageObj.status === 'unloaded' 56 | || page.pageObj.pendingUrl === changeInfo.url), 57 | ) 58 | // if there is an unloaded or pending page, update the url 59 | if (page) { 60 | page.pageObj.url = changeInfo.url 61 | page.pageObj.status = 'loading' 62 | updatePage(page.id, { pageObj: page.pageObj }) 63 | } 64 | // go back or create a new page 65 | else { 66 | const prior = pages.value.find( 67 | page => page.tabId === tabId 68 | && page.pageObj.url === changeInfo.url, 69 | ) 70 | if (prior) { 71 | prior.pageObj.status = 'loading' 72 | updatePage(prior.id, { 73 | pageObj: prior.pageObj, 74 | isActive: true, 75 | }) 76 | } 77 | else { 78 | const parent = pages.value 79 | .sort((a, b) => b.timeLastActivated - a.timeLastActivated) 80 | .find(page => page.tabId === tabId) 81 | || null 82 | addPage(tab, parent?.id || null) 83 | } 84 | } 85 | } 86 | 87 | // title or favicon update 88 | if (changeInfo.title || changeInfo.favIconUrl) { 89 | page = pages.value.find( 90 | page => page.tabId === tabId 91 | && page.pageObj.url === tab.url, 92 | ) 93 | if (page) { 94 | page.pageObj.title = tab.title 95 | page.pageObj.favIconUrl = tab.favIconUrl 96 | updatePage(page.id, { pageObj: page.pageObj }) 97 | } 98 | } 99 | 100 | // if a loaded tab is completed 101 | if (changeInfo.status === 'complete') { 102 | page = pages.value.find( 103 | page => page.tabId === tabId 104 | && page.pageObj.status === 'loading', 105 | ) 106 | if (page) { 107 | page.pageObj.status = 'complete' 108 | updatePage(page.id, { pageObj: page.pageObj }) 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * handle chrome.tabs.onActivated 115 | * 116 | * set the activated page as active 117 | */ 118 | function tabActivateHandler(activeInfo: { tabId: number }) { 119 | const pagesIntab = pages.value 120 | .filter(page => page.tabId === activeInfo.tabId) 121 | .sort((a, b) => b.timeLastActivated - a.timeLastActivated) 122 | if (pagesIntab && pagesIntab.length) { 123 | const page = pagesIntab[0] 124 | updatePage(page.id, { 125 | isActive: true, 126 | }) 127 | } 128 | // if page not found, create a new node 129 | else { 130 | chrome.tabs.get(activeInfo.tabId, (tab) => { 131 | if (tab) { 132 | addPage(tab, null) 133 | } 134 | }) 135 | } 136 | } 137 | 138 | /** 139 | * Activate page in response to clicking on historymap 140 | */ 141 | function activatePage(page: HmPage) { 142 | const pageObj = page.pageObj 143 | const url = pageObj.url 144 | 145 | // find the tab with the page url 146 | // not using chrome.tabs.query({ url: url }) because it does not work when the url contains query parameters (?) 147 | chrome.tabs.query({}, (tabs) => { 148 | // find the tab with the page url 149 | const targetTabs = tabs.filter(tab => 150 | tab.id === page.tabId 151 | && tab.url === url, 152 | ) 153 | 154 | if (targetTabs.length > 0) { // if tab found, go back to it 155 | const { id, windowId } = targetTabs[0] 156 | chrome.windows.update(windowId, { focused: true }, () => { 157 | chrome.tabs.update(id!, { active: true }) 158 | }) 159 | } 160 | else { // if tab not found, create a new tab 161 | chrome.tabs.create({ url }, (tab) => { 162 | updatePage(page.id, { 163 | tabId: tab.id, 164 | pageObj: tab, 165 | isActive: true, 166 | }) 167 | chrome.windows.update(tab.windowId, { focused: true }) 168 | }) 169 | } 170 | }) 171 | } 172 | 173 | export function updateActivePage() { 174 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 175 | const tab = tabs[0] 176 | const pagesIntab = pages.value 177 | .filter(page => page.tabId === tab.id) 178 | .sort((a, b) => b.timeLastActivated - a.timeLastActivated) 179 | if (pagesIntab) { 180 | const page = pagesIntab[0] 181 | if (page.id !== activePage.value?.id) { 182 | updatePage(page.id, { 183 | isActive: true, 184 | }) 185 | } 186 | } 187 | }) 188 | } 189 | 190 | export function initialiseController() { 191 | chrome.tabs.onCreated.addListener(tabCreationHandler) 192 | chrome.tabs.onUpdated.addListener(tabUpdateHandler) 193 | chrome.tabs.onActivated.addListener(tabActivateHandler) 194 | 195 | /** switch to default session when historymap is not opened */ 196 | chrome.runtime.onConnect.addListener((port) => { 197 | if (port.name === 'historymap') { 198 | // console.info('connected to historymap') 199 | switchToLatestSession() 200 | port.onDisconnect.addListener(() => { 201 | switchToDefaultSession() 202 | // console.info('switched to default session') 203 | }) 204 | } 205 | }) 206 | 207 | /** listen to activate page message */ 208 | chrome.runtime.onMessage.addListener((message) => { 209 | if (message.type === 'activate-page') { 210 | activatePage(message.data) 211 | } 212 | return true 213 | }) 214 | 215 | // only for debugging 216 | const clearLocalStorage = false 217 | if (clearLocalStorage) { 218 | chrome.storage.local.clear() 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /scripts/getInstalledBrowsers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty */ 2 | import { homedir } from 'os' 3 | import { resolve } from 'path' 4 | import { existsSync } from 'fs' 5 | import { execSync } from 'child_process' 6 | 7 | export interface BrowserPaths { 8 | name: string 9 | type: 'chrome' | 'firefox' | 'safari' | 'other' 10 | path: Record 11 | } 12 | 13 | export interface BrowserPath { 14 | name: string 15 | type: 'chrome' | 'firefox' | 'safari' | 'other' 16 | path: string 17 | } 18 | 19 | interface BrowserInfo { 20 | name: string 21 | type: string 22 | command: string 23 | } 24 | 25 | type BinaryType = 'flatpak' | 'snap' | 'native' 26 | 27 | const getWinPaths = (subdir: string) => { 28 | const sysDrive = process.env['SystemDrive'] || 'C:' 29 | const programFiles = 30 | process.env['ProgramFiles'] || resolve(sysDrive, 'Program Files') 31 | const programFilesX86 = 32 | process.env['ProgramFiles(x86)'] || resolve(sysDrive, 'Program Files (x86)') 33 | const localAppData = 34 | process.env['LocalAppData'] || resolve(homedir(), 'AppData\\Local') 35 | const appData = 36 | process.env['AppData'] || resolve(homedir(), 'AppData\\Roaming') 37 | const knownPaths = [ 38 | resolve(localAppData, subdir), 39 | resolve(appData, subdir), 40 | resolve(programFiles, subdir), 41 | resolve(programFilesX86, subdir), 42 | ] 43 | return knownPaths 44 | } 45 | 46 | const getDarwinPaths = (subdir: string) => { 47 | const home = homedir() 48 | const knownPaths = [ 49 | resolve(home, 'Applications', subdir), 50 | resolve('/Applications', subdir), 51 | ] 52 | return knownPaths 53 | } 54 | 55 | const getLinuxPaths = (subdir: string) => { 56 | return [ 57 | `/usr/bin/${subdir}`, 58 | `/usr/local/bin/${subdir}`, 59 | `/snap/bin/${subdir}`, 60 | `/var/lib/flatpak/exports/bin/${subdir}`, 61 | resolve(homedir(), `.local/share/flatpak/exports/bin/${subdir}`), 62 | ] 63 | } 64 | 65 | const checkBinaryType = (path: string): BinaryType => { 66 | const platform = process.platform 67 | 68 | if (existsSync(path)) return 'native' 69 | 70 | if (platform === 'linux') { 71 | try { 72 | const flatpakResult = execSync(`flatpak list --app | grep ${path}`) 73 | .toString() 74 | .trim() 75 | 76 | if (flatpakResult) return 'flatpak' 77 | } catch {} 78 | 79 | try { 80 | const snapResult = execSync(`snap list | grep ${path}`).toString().trim() 81 | 82 | if (snapResult) return 'snap' 83 | } catch {} 84 | } 85 | 86 | return 'native' 87 | } 88 | 89 | function getCommand(name: string, type: string, path: string) { 90 | const binaryType = checkBinaryType(path) 91 | 92 | switch (binaryType) { 93 | case 'flatpak': 94 | return `flatpak run ${path} --no-input --browser-console --devtools` 95 | 96 | case 'snap': 97 | return `snap run ${path} --no-input --browser-console --devtools` 98 | 99 | case 'native': 100 | if (type === 'chrome') { 101 | return `web-ext run --target chromium --chromium-binary ${path} --source-dir dist/chrome --no-input --browser-console --devtools` 102 | } 103 | if (type === 'firefox') { 104 | return `web-ext run --firefox=${path} --source-dir dist/firefox --no-input --browser-console --devtools` 105 | } 106 | break 107 | 108 | default: 109 | break 110 | } 111 | } 112 | 113 | export function GetInstalledBrowsers(): { [name: string]: BrowserInfo } { 114 | const platform = process.platform 115 | const installedBrowsers: { [name: string]: BrowserInfo } = {} 116 | 117 | for (const browser of Browsers) { 118 | if (!browser.path[platform]) continue 119 | 120 | for (const path of browser.path[platform]) { 121 | const command = getCommand(browser.name, browser.type, path) 122 | 123 | if (!command) { 124 | continue 125 | } 126 | 127 | installedBrowsers[browser.name] = { 128 | name: browser.name, 129 | type: browser.type, 130 | command, 131 | } 132 | } 133 | } 134 | return installedBrowsers 135 | } 136 | 137 | const emptyPlatform = { 138 | aix: [], 139 | android: [], 140 | cygwin: [], 141 | darwin: [], 142 | freebsd: [], 143 | haiku: [], 144 | linux: [], 145 | netbsd: [], 146 | openbsd: [], 147 | sunos: [], 148 | win32: [], 149 | } 150 | export const Browsers: BrowserPaths[] = [ 151 | { 152 | name: 'Arc', 153 | type: 'chrome', 154 | path: { 155 | ...emptyPlatform, 156 | darwin: getDarwinPaths('Arc.app/Contents/MacOS/Arc'), 157 | }, 158 | }, 159 | { 160 | name: 'Brave', 161 | type: 'chrome', 162 | path: { 163 | ...emptyPlatform, 164 | win32: getWinPaths( 165 | 'BraveSoftware\\Brave-Browser\\Application\\brave.exe' 166 | ), 167 | darwin: getDarwinPaths('Brave Browser.app/Contents/MacOS/Brave Browser'), 168 | linux: [ 169 | ...getLinuxPaths('brave-browser'), 170 | ...getLinuxPaths('com.brave.Browser'), 171 | ], 172 | }, 173 | }, 174 | { 175 | name: 'Chrome', 176 | type: 'chrome', 177 | path: { 178 | ...emptyPlatform, 179 | win32: getWinPaths('Google\\Chrome\\Application\\chrome.exe'), 180 | darwin: getDarwinPaths('Google Chrome.app/Contents/MacOS/Google Chrome'), 181 | linux: [ 182 | ...getLinuxPaths('google-chrome'), 183 | ...getLinuxPaths('google-chrome-stable'), 184 | ], 185 | }, 186 | }, 187 | { 188 | name: 'Chrome Beta', 189 | type: 'chrome', 190 | path: { 191 | ...emptyPlatform, 192 | win32: getWinPaths('Google\\Chrome Beta\\Application\\chrome.exe'), 193 | darwin: getDarwinPaths( 194 | 'Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta' 195 | ), 196 | linux: [...getLinuxPaths('google-chrome-beta')], 197 | }, 198 | }, 199 | { 200 | name: 'Chrome Canary', 201 | type: 'chrome', 202 | path: { 203 | ...emptyPlatform, 204 | win32: getWinPaths('Google\\Chrome Canary\\Application\\chrome.exe'), 205 | darwin: getDarwinPaths( 206 | 'Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary' 207 | ), 208 | linux: [...getLinuxPaths('google-chrome-canary')], 209 | }, 210 | }, 211 | { 212 | name: 'Chromium', 213 | type: 'chrome', 214 | path: { 215 | ...emptyPlatform, 216 | win32: getWinPaths('Chromium\\Application\\chrome.exe'), 217 | darwin: getDarwinPaths('Chromium.app/Contents/MacOS/Chromium'), 218 | linux: [ 219 | ...getLinuxPaths('chromium'), 220 | ...getLinuxPaths('org.chromium.Chromium'), 221 | ], 222 | }, 223 | }, 224 | { 225 | name: 'Edge', 226 | type: 'chrome', 227 | path: { 228 | ...emptyPlatform, 229 | win32: getWinPaths('Microsoft\\Edge\\Application\\msedge.exe'), 230 | darwin: getDarwinPaths( 231 | 'Microsoft Edge.app/Contents/MacOS/Microsoft Edge' 232 | ), 233 | linux: [ 234 | ...getLinuxPaths('microsoft-edge'), 235 | ...getLinuxPaths('com.microsoft.Edge'), 236 | ], 237 | }, 238 | }, 239 | { 240 | name: 'Sidekick', 241 | type: 'chrome', 242 | path: { 243 | ...emptyPlatform, 244 | win32: getWinPaths('Sidekick\\Application\\sidekick.exe'), 245 | darwin: getDarwinPaths('Sidekick.app/Contents/MacOS/Sidekick'), 246 | linux: [...getLinuxPaths('sidekick-browser-stable')], 247 | }, 248 | }, 249 | { 250 | name: 'Vivaldi', 251 | type: 'chrome', 252 | path: { 253 | ...emptyPlatform, 254 | win32: getWinPaths('Vivaldi\\Application\\vivaldi.exe'), 255 | darwin: getDarwinPaths('Vivaldi.app/Contents/MacOS/Vivaldi'), 256 | linux: [ 257 | ...getLinuxPaths('vivaldi-stable'), 258 | ...getLinuxPaths('com.vivaldi.Vivaldi'), 259 | ], 260 | }, 261 | }, 262 | { 263 | name: 'Firefox', 264 | type: 'firefox', 265 | path: { 266 | ...emptyPlatform, 267 | win32: getWinPaths('Mozilla Firefox\\firefox.exe'), 268 | darwin: getDarwinPaths('Firefox.app/Contents/MacOS/Firefox'), 269 | linux: [ 270 | ...getLinuxPaths('firefox'), 271 | ...getLinuxPaths('org.mozilla.firefox'), 272 | ], 273 | }, 274 | }, 275 | { 276 | name: 'Firefox Nightly', 277 | type: 'firefox', 278 | path: { 279 | ...emptyPlatform, 280 | win32: getWinPaths('Firefox Nightly\\firefox.exe'), 281 | darwin: getDarwinPaths('Firefox Nightly.app/Contents/MacOS/Firefox'), 282 | linux: [ 283 | ...getLinuxPaths('firefox-nightly'), 284 | ...getLinuxPaths('org.mozilla.firefox-nightly'), 285 | ], 286 | }, 287 | }, 288 | { 289 | name: 'Firefox Developer Edition', 290 | type: 'firefox', 291 | path: { 292 | ...emptyPlatform, 293 | win32: getWinPaths('Firefox Developer Edition\\firefox.exe'), 294 | darwin: getDarwinPaths( 295 | 'Firefox Developer Edition.app/Contents/MacOS/Firefox' 296 | ), 297 | linux: [ 298 | ...getLinuxPaths('firefox-dev'), 299 | ...getLinuxPaths('org.mozilla.firefox.dev'), 300 | ], 301 | }, 302 | }, 303 | { 304 | name: 'Safari', 305 | type: 'safari', 306 | path: { 307 | ...emptyPlatform, 308 | darwin: getDarwinPaths('Safari.app/Contents/MacOS/Safari'), 309 | }, 310 | }, 311 | { 312 | name: 'Safari Technical Preview', 313 | type: 'safari', 314 | path: { 315 | ...emptyPlatform, 316 | darwin: getDarwinPaths( 317 | 'Safari Technical Preview.app/Contents/MacOS/Safari Technical Preview' 318 | ), 319 | }, 320 | }, 321 | { 322 | name: 'Safari beta', 323 | type: 'safari', 324 | path: { 325 | ...emptyPlatform, 326 | darwin: getDarwinPaths('Safari beta.app/Contents/MacOS/Safari beta'), 327 | }, 328 | }, 329 | { 330 | name: 'Orion', 331 | type: 'safari', 332 | path: { 333 | ...emptyPlatform, 334 | darwin: getDarwinPaths('Orion.app/Contents/MacOS/Orion'), 335 | }, 336 | }, 337 | { 338 | name: 'Epiphany', 339 | type: 'safari', 340 | path: { 341 | ...emptyPlatform, 342 | linux: ['epiphany-browser', 'org.gnome.Epiphany'], 343 | }, 344 | }, 345 | { 346 | name: 'Opera', 347 | type: 'chrome', 348 | path: { 349 | ...emptyPlatform, 350 | win32: getWinPaths('Opera\\opera.exe'), 351 | darwin: getDarwinPaths('Opera.app/Contents/MacOS/Opera'), 352 | linux: [...getLinuxPaths('opera'), ...getLinuxPaths('opera-stable')], 353 | }, 354 | }, 355 | { 356 | name: 'IE', 357 | type: 'other', 358 | path: { 359 | ...emptyPlatform, 360 | win32: getWinPaths('Internet Explorer\\iexplore.exe'), 361 | }, 362 | }, 363 | ] 364 | -------------------------------------------------------------------------------- /src/types/.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "DirectiveBinding": true, 7 | "EffectScope": true, 8 | "ExtractDefaultPropTypes": true, 9 | "ExtractPropTypes": true, 10 | "ExtractPublicPropTypes": true, 11 | "InjectionKey": true, 12 | "MaybeRef": true, 13 | "MaybeRefOrGetter": true, 14 | "Notification": true, 15 | "Notivue": true, 16 | "PropType": true, 17 | "Ref": true, 18 | "VNode": true, 19 | "WritableComputedRef": true, 20 | "acceptHMRUpdate": true, 21 | "appRouter": true, 22 | "asyncComputed": true, 23 | "autoResetRef": true, 24 | "browser": true, 25 | "computed": true, 26 | "computedAsync": true, 27 | "computedEager": true, 28 | "computedInject": true, 29 | "computedWithControl": true, 30 | "controlledComputed": true, 31 | "controlledRef": true, 32 | "createApp": true, 33 | "createEventHook": true, 34 | "createGlobalState": true, 35 | "createInjectionState": true, 36 | "createPinia": true, 37 | "createReactiveFn": true, 38 | "createReusableTemplate": true, 39 | "createSharedComposable": true, 40 | "createTemplatePromise": true, 41 | "createUnrefFn": true, 42 | "customRef": true, 43 | "debouncedRef": true, 44 | "debouncedWatch": true, 45 | "defineAsyncComponent": true, 46 | "defineComponent": true, 47 | "definePage": true, 48 | "defineStore": true, 49 | "eagerComputed": true, 50 | "effectScope": true, 51 | "extendRef": true, 52 | "getActivePinia": true, 53 | "getCurrentInstance": true, 54 | "getCurrentScope": true, 55 | "h": true, 56 | "i18n": true, 57 | "ignorableWatch": true, 58 | "inject": true, 59 | "injectLocal": true, 60 | "isDefined": true, 61 | "isProxy": true, 62 | "isReactive": true, 63 | "isReadonly": true, 64 | "isRef": true, 65 | "makeDestructurable": true, 66 | "mapActions": true, 67 | "mapGetters": true, 68 | "mapState": true, 69 | "mapStores": true, 70 | "mapWritableState": true, 71 | "markRaw": true, 72 | "nextTick": true, 73 | "notivue": true, 74 | "onActivated": true, 75 | "onBeforeMount": true, 76 | "onBeforeRouteLeave": true, 77 | "onBeforeRouteUpdate": true, 78 | "onBeforeUnmount": true, 79 | "onBeforeUpdate": true, 80 | "onClickOutside": true, 81 | "onDeactivated": true, 82 | "onElementRemoval": true, 83 | "onErrorCaptured": true, 84 | "onKeyStroke": true, 85 | "onLongPress": true, 86 | "onMounted": true, 87 | "onRenderTracked": true, 88 | "onRenderTriggered": true, 89 | "onScopeDispose": true, 90 | "onServerPrefetch": true, 91 | "onStartTyping": true, 92 | "onUnmounted": true, 93 | "onUpdated": true, 94 | "onWatcherCleanup": true, 95 | "pausableWatch": true, 96 | "pinia": true, 97 | "provide": true, 98 | "provideLocal": true, 99 | "pushNotification": true, 100 | "reactify": true, 101 | "reactifyObject": true, 102 | "reactive": true, 103 | "reactiveComputed": true, 104 | "reactiveOmit": true, 105 | "reactivePick": true, 106 | "readonly": true, 107 | "ref": true, 108 | "refAutoReset": true, 109 | "refDebounced": true, 110 | "refDefault": true, 111 | "refThrottled": true, 112 | "refWithControl": true, 113 | "resolveComponent": true, 114 | "resolveRef": true, 115 | "resolveUnref": true, 116 | "setActivePinia": true, 117 | "setMapStoreSuffix": true, 118 | "shallowReactive": true, 119 | "shallowReadonly": true, 120 | "shallowRef": true, 121 | "storeToRefs": true, 122 | "syncRef": true, 123 | "syncRefs": true, 124 | "t": true, 125 | "templateRef": true, 126 | "throttledRef": true, 127 | "throttledWatch": true, 128 | "toRaw": true, 129 | "toReactive": true, 130 | "toRef": true, 131 | "toRefs": true, 132 | "toValue": true, 133 | "triggerRef": true, 134 | "tryOnBeforeMount": true, 135 | "tryOnBeforeUnmount": true, 136 | "tryOnMounted": true, 137 | "tryOnScopeDispose": true, 138 | "tryOnUnmounted": true, 139 | "unref": true, 140 | "unrefElement": true, 141 | "until": true, 142 | "useActiveElement": true, 143 | "useAnimate": true, 144 | "useArrayDifference": true, 145 | "useArrayEvery": true, 146 | "useArrayFilter": true, 147 | "useArrayFind": true, 148 | "useArrayFindIndex": true, 149 | "useArrayFindLast": true, 150 | "useArrayIncludes": true, 151 | "useArrayJoin": true, 152 | "useArrayMap": true, 153 | "useArrayReduce": true, 154 | "useArraySome": true, 155 | "useArrayUnique": true, 156 | "useAsyncQueue": true, 157 | "useAsyncState": true, 158 | "useAttrs": true, 159 | "useBase64": true, 160 | "useBattery": true, 161 | "useBluetooth": true, 162 | "useBreakpoints": true, 163 | "useBroadcastChannel": true, 164 | "useBrowserLocalStorage": true, 165 | "useBrowserLocation": true, 166 | "useBrowserSyncStorage": true, 167 | "useCached": true, 168 | "useClipboard": true, 169 | "useClipboardItems": true, 170 | "useCloned": true, 171 | "useColorMode": true, 172 | "useConfirmDialog": true, 173 | "useCounter": true, 174 | "useCssModule": true, 175 | "useCssVar": true, 176 | "useCssVars": true, 177 | "useCurrentElement": true, 178 | "useCycleList": true, 179 | "useDark": true, 180 | "useDateFormat": true, 181 | "useDebounce": true, 182 | "useDebounceFn": true, 183 | "useDebouncedRefHistory": true, 184 | "useDeviceMotion": true, 185 | "useDeviceOrientation": true, 186 | "useDevicePixelRatio": true, 187 | "useDevicesList": true, 188 | "useDisplayMedia": true, 189 | "useDocumentVisibility": true, 190 | "useDraggable": true, 191 | "useDropZone": true, 192 | "useElementBounding": true, 193 | "useElementByPoint": true, 194 | "useElementHover": true, 195 | "useElementSize": true, 196 | "useElementVisibility": true, 197 | "useEventBus": true, 198 | "useEventListener": true, 199 | "useEventSource": true, 200 | "useEyeDropper": true, 201 | "useFavicon": true, 202 | "useFetch": true, 203 | "useFileDialog": true, 204 | "useFileSystemAccess": true, 205 | "useFocus": true, 206 | "useFocusWithin": true, 207 | "useFps": true, 208 | "useFullscreen": true, 209 | "useGamepad": true, 210 | "useGeolocation": true, 211 | "useI18n": true, 212 | "useId": true, 213 | "useIdle": true, 214 | "useImage": true, 215 | "useInfiniteScroll": true, 216 | "useIntersectionObserver": true, 217 | "useInterval": true, 218 | "useIntervalFn": true, 219 | "useKeyModifier": true, 220 | "useLastChanged": true, 221 | "useLink": true, 222 | "useLocalStorage": true, 223 | "useLocale": true, 224 | "useMagicKeys": true, 225 | "useManualRefHistory": true, 226 | "useMediaControls": true, 227 | "useMediaQuery": true, 228 | "useMemoize": true, 229 | "useMemory": true, 230 | "useModel": true, 231 | "useMounted": true, 232 | "useMouse": true, 233 | "useMouseInElement": true, 234 | "useMousePressed": true, 235 | "useMutationObserver": true, 236 | "useNavigatorLanguage": true, 237 | "useNetwork": true, 238 | "useNow": true, 239 | "useObjectUrl": true, 240 | "useOffsetPagination": true, 241 | "useOnline": true, 242 | "useOptionsStore": true, 243 | "usePageLeave": true, 244 | "useParallax": true, 245 | "useParentElement": true, 246 | "usePerformanceObserver": true, 247 | "usePermission": true, 248 | "usePointer": true, 249 | "usePointerLock": true, 250 | "usePointerSwipe": true, 251 | "usePreferredColorScheme": true, 252 | "usePreferredContrast": true, 253 | "usePreferredDark": true, 254 | "usePreferredLanguages": true, 255 | "usePreferredReducedMotion": true, 256 | "usePreferredReducedTransparency": true, 257 | "usePrevious": true, 258 | "useRafFn": true, 259 | "useRefHistory": true, 260 | "useResizeObserver": true, 261 | "useRoute": true, 262 | "useRouter": true, 263 | "useSSRWidth": true, 264 | "useScreenOrientation": true, 265 | "useScreenSafeArea": true, 266 | "useScriptTag": true, 267 | "useScroll": true, 268 | "useScrollLock": true, 269 | "useSessionStorage": true, 270 | "useShare": true, 271 | "useSlots": true, 272 | "useSorted": true, 273 | "useSpeechRecognition": true, 274 | "useSpeechSynthesis": true, 275 | "useStepper": true, 276 | "useStorage": true, 277 | "useStorageAsync": true, 278 | "useStyleTag": true, 279 | "useSupported": true, 280 | "useSwipe": true, 281 | "useTemplateRef": true, 282 | "useTemplateRefsList": true, 283 | "useTestStore": true, 284 | "useTextDirection": true, 285 | "useTextSelection": true, 286 | "useTextareaAutosize": true, 287 | "useTheme": true, 288 | "useThrottle": true, 289 | "useThrottleFn": true, 290 | "useThrottledRefHistory": true, 291 | "useTimeAgo": true, 292 | "useTimeout": true, 293 | "useTimeoutFn": true, 294 | "useTimeoutPoll": true, 295 | "useTimestamp": true, 296 | "useTitle": true, 297 | "useToNumber": true, 298 | "useToString": true, 299 | "useToggle": true, 300 | "useTransition": true, 301 | "useUrlSearchParams": true, 302 | "useUserMedia": true, 303 | "useVModel": true, 304 | "useVModels": true, 305 | "useVibrate": true, 306 | "useVirtualList": true, 307 | "useWakeLock": true, 308 | "useWebNotification": true, 309 | "useWebSocket": true, 310 | "useWebWorker": true, 311 | "useWebWorkerFn": true, 312 | "useWindowFocus": true, 313 | "useWindowScroll": true, 314 | "useWindowSize": true, 315 | "watch": true, 316 | "watchArray": true, 317 | "watchAtMost": true, 318 | "watchDebounced": true, 319 | "watchDeep": true, 320 | "watchEffect": true, 321 | "watchIgnorable": true, 322 | "watchImmediate": true, 323 | "watchOnce": true, 324 | "watchPausable": true, 325 | "watchPostEffect": true, 326 | "watchSyncEffect": true, 327 | "watchThrottled": true, 328 | "watchTriggerable": true, 329 | "watchWithFilter": true, 330 | "whenever": true, 331 | "useHistoryMap": true, 332 | "useSession": true, 333 | "useStore": true, 334 | "useExtractor": true, 335 | "useSchemaEditor": true, 336 | "useSchemaMap": true, 337 | "useSchemaTree": true, 338 | "useSchemaPanel": true, 339 | "TargetPosition": true, 340 | "useDragAndDropTree": true, 341 | "useSchemaSynthesise": true, 342 | "useAnnotation": true 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/content-script/annotation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Highlighting 3 | * 4 | * Highlighting is done using the rangy library. 5 | * @see {@link https://github.com/timdown/rangy} 6 | * 7 | * Specifically, these demos might be useful: 8 | * 9 | * @see {@link https://github.com/timdown/rangy/blob/master/demos/highlighter.html} 10 | * Example usage of highlighter 11 | * 12 | * @see {@link https://github.com/timdown/rangy/blob/master/demos/core.html} 13 | * Example usage of rangy range object 14 | * 15 | * More details on the APIs can be found in these documents: 16 | * 17 | * @see {@link https://github.com/timdown/rangy/wiki/Highlighter-Module} 18 | */ 19 | 20 | import type { Annotation } from '@/types/historymap' 21 | import _ from 'lodash' 22 | import Postmate from 'postmate' 23 | import rangy from 'rangy' 24 | import { sendMessage } from 'webext-bridge/content-script' 25 | import 'rangy/lib/rangy-selectionsaverestore' 26 | import 'rangy/lib/rangy-classapplier' 27 | import 'rangy/lib/rangy-highlighter' 28 | import 'rangy/lib/rangy-serializer' 29 | import 'rangy/lib/rangy-textrange' 30 | 31 | type Rect = Pick 32 | 33 | let annotations: Annotation[] = [] 34 | let selectedAnnotation: Annotation | null = null 35 | const noteboxes: Record = {} 36 | let toolbar: Postmate 37 | 38 | rangy.init() 39 | const highlighter = rangy.createHighlighter() 40 | 41 | /** toolbar iframe */ 42 | function initialiseToolbar() { 43 | const src = chrome.runtime.getURL('src/ui/content-script-iframe/index.html#/toolbar') 44 | 45 | const handshake = new Postmate({ 46 | container: document.body, 47 | url: src, 48 | classListArray: ['historymap-selection-toolbar-iframe'], 49 | }) 50 | 51 | handshake.then((child) => { 52 | // handle text selection 53 | document.addEventListener('mouseup', (event) => { 54 | selectionHandler(event, child) 55 | }) 56 | 57 | child.on('highlight', highlightHandler) 58 | child.on('dehighlight', dehighlightHandler) 59 | 60 | child.on('tagging-start', taggingStartHandler) 61 | }) 62 | 63 | toolbar = handshake 64 | } 65 | 66 | /** note container iframe */ 67 | function createNoteBox(id: number, rect: Rect) { 68 | console.log('creating notebox', id, rect) 69 | 70 | const src = chrome.runtime.getURL('src/ui/content-script-iframe/index.html#/notebox') 71 | const iframe = document.createElement('iframe') 72 | iframe.src = `${src}` 73 | iframe.classList.add('historymap-note-box-iframe') 74 | iframe.removeAttribute('sandbox') 75 | document.body.appendChild(iframe) 76 | 77 | iframe.onload = () => { 78 | // FIXME: more robust way to ensure the message is sent? 79 | setTimeout(() => { 80 | iframe.contentWindow?.postMessage({ type: 'setId', value: id }, '*') 81 | iframe.contentWindow?.postMessage({ 82 | type: 'setTags', 83 | value: annotations.find(d => d.id === id)?.tags || [], 84 | }, '*') 85 | }, 1000) 86 | iframe.style.top = `${rect.top + window.scrollY}px` 87 | } 88 | 89 | noteboxes[id] = iframe 90 | } 91 | 92 | function noteChangeHandler(event) { 93 | const type = event.data.type 94 | if (!type) 95 | return 96 | 97 | if (type === 'add-tag' || type === 'remove-tag') { 98 | const { tag, id } = event.data 99 | sendMessage(type, { id, tag }, 'background') 100 | .then((annotation: Annotation | null) => { 101 | if (annotation) { 102 | updateAnnotation(annotation) 103 | // console.log(annotations) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | /** text selection listener */ 110 | function selectionHandler(event: MouseEvent) { 111 | const selection = rangy.getSelection() 112 | 113 | // if click on marked text, show toolbar 114 | const target = event.target as HTMLElement 115 | if (target.hasAttribute('hm-annotation')) { 116 | const id = Number.parseInt(target.getAttribute('annotation-id') ?? '') 117 | if (Number.isNaN(id)) 118 | return 119 | 120 | const rect = getAnnotationBoundingRect(id) 121 | const annotation = annotations.find(d => d.id === id) 122 | if (annotation) { 123 | setSelectedAnnotation(annotation) 124 | toolbar.then((child) => { 125 | showToolbar(child.frame, rect) 126 | }) 127 | } 128 | return 129 | } 130 | 131 | if (selection.isCollapsed) { 132 | toolbar.then((child) => { 133 | child.frame.style.visibility = 'hidden' 134 | }) 135 | setSelectedAnnotation(null) 136 | return 137 | } 138 | 139 | const rect = getSelectionRect() 140 | toolbar.then((child) => { 141 | showToolbar(child.frame, rect) 142 | }) 143 | } 144 | 145 | /** highlight handler */ 146 | function highlightHandler() { 147 | if (selectedAnnotation) { 148 | const { id, selection, sourceText } = selectedAnnotation 149 | sendMessage('highlight', { id, selection, sourceText }, 'background') 150 | document.querySelectorAll(`[annotation-id="${id}"]`).forEach((element) => { 151 | element.classList.add('highlight') 152 | element.classList.remove('annotate') 153 | }) 154 | } 155 | else { 156 | saveAnnotation('highlight') 157 | } 158 | } 159 | 160 | /** dehighlight handler */ 161 | function dehighlightHandler() { 162 | const id = selectedAnnotation?.id 163 | const annotation = annotations.find(d => d.id === id) 164 | console.log('dehighlight', id, annotation) 165 | if (!annotation) 166 | return 167 | 168 | sendMessage('dehighlight', annotation, 'background') 169 | .then(() => { 170 | const el = document.querySelector(`[annotation-id="${id}"]`) 171 | if (el) { 172 | const highlight = highlighter.getHighlightForElement(el) 173 | highlight.unapply() 174 | annotations = annotations.filter(d => d.id !== annotation.id) 175 | } 176 | }) 177 | } 178 | 179 | /** restore marks */ 180 | function restoreHighlights() { 181 | // wait for document loading complete 182 | if (document.readyState !== 'complete') { 183 | setTimeout(() => { 184 | restoreHighlights() 185 | }, 1500) 186 | return 187 | } 188 | 189 | console.log('restoring highlights') 190 | 191 | annotations.forEach((annotation) => { 192 | const type = annotation.highlighted ? 'highlight' : 'annotate' 193 | const selection = rangy.deserializeSelection(annotation.selection) 194 | highlighter.addClassApplier(rangy.createClassApplier(type, { 195 | ignoreWhiteSpace: true, 196 | tagNames: ['span', 'a'], 197 | elementAttributes: { 198 | 'annotation-id': annotation.id, 199 | 'hm-annotation': true, 200 | }, 201 | })) 202 | highlighter.highlightSelection(type) 203 | selection.removeAllRanges() 204 | }) 205 | 206 | // restore noteboxes 207 | nextTick(() => { 208 | restoreNoteboxes() 209 | }) 210 | } 211 | 212 | function restoreNoteboxes() { 213 | console.log('restoring noteboxes') 214 | annotations.forEach((annotation) => { 215 | if ('tags' in annotation || !annotation.highlighted) { 216 | const rect = getAnnotationBoundingRect(annotation.id) 217 | createNoteBox(annotation.id, rect) 218 | } 219 | }) 220 | } 221 | 222 | /** handle start tagging */ 223 | async function taggingStartHandler() { 224 | if (!selectedAnnotation) { 225 | await saveAnnotation('annotate') 226 | } 227 | if (!selectedAnnotation) 228 | return 229 | 230 | const rect = getAnnotationBoundingRect(selectedAnnotation.id) 231 | 232 | if (selectedAnnotation.id in noteboxes === false) { 233 | const handshake = createNoteBox(selectedAnnotation.id, rect) 234 | } 235 | } 236 | 237 | /** utilities */ 238 | 239 | function showToolbar(frame: HTMLIFrameElement, rect: Rect) { 240 | frame.style.visibility = 'visible' 241 | frame.style.top = `${rect.top + window.scrollY}px` 242 | frame.style.left = `${(rect.left + rect.right) / 2}px` 243 | } 244 | 245 | function getSelectionRect() { 246 | const selection = rangy.getSelection() 247 | const range = selection.getRangeAt(0) 248 | return range.nativeRange.getBoundingClientRect() 249 | } 250 | 251 | function updateAnnotation(annotation: Annotation) { 252 | const index = annotations.findIndex(d => d.id === annotation.id) 253 | if (index >= 0) { 254 | annotations[index] = annotation 255 | } 256 | } 257 | 258 | function mergeAnnotation(annotation: Annotation) { 259 | if (annotations.find(d => d.id === annotation.id)) { 260 | updateAnnotation(annotation) 261 | } 262 | else { 263 | annotations.push(annotation) 264 | } 265 | } 266 | 267 | async function setSelectedAnnotation(annotation: Annotation | null) { 268 | await toolbar.then((child) => { 269 | child.call('setAnnotation', annotation) 270 | }) 271 | selectedAnnotation = annotation 272 | } 273 | 274 | function getAnnotationBoundingRect(id: number): Rect { 275 | const elements = document.querySelectorAll(`[annotation-id="${id}"]`) 276 | const rects = Array.from(elements).map(element => element.getBoundingClientRect()) 277 | return rects.reduce((acc, rect) => { 278 | acc.left = Math.min(acc.left, rect.left) 279 | acc.top = Math.min(acc.top, rect.top) 280 | acc.right = Math.max(acc.right, rect.right) 281 | acc.bottom = Math.max(acc.bottom, rect.bottom) 282 | return acc 283 | }, { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity }) 284 | } 285 | 286 | async function saveAnnotation(type: 'highlight' | 'annotate' = 'highlight') { 287 | const selection = rangy.getSelection() 288 | if (selection.isCollapsed) { 289 | return 290 | } 291 | 292 | const sourceText = selection.toString() 293 | const uuidPattern = /\{([a-f0-9\-]+)\}$/i 294 | const serialized = rangy.serializeSelection(selection) 295 | .replace(uuidPattern, '') 296 | 297 | const id = _.max(annotations.map(d => d.id + 1)) || 0 298 | 299 | // send message to background 300 | const annotation = await sendMessage(type, { 301 | id, 302 | selection: serialized, 303 | sourceText, 304 | }, 'background') as Annotation | null 305 | 306 | if (annotation) { 307 | console.log('annotation', annotation) 308 | mergeAnnotation(annotation) 309 | highlighter.addClassApplier(rangy.createClassApplier(type, { 310 | ignoreWhiteSpace: true, 311 | tagNames: ['span', 'a'], 312 | elementAttributes: { 313 | 'annotation-id': id, 314 | 'hm-annotation': true, 315 | }, 316 | })) 317 | highlighter.highlightSelection(type) 318 | await setSelectedAnnotation(annotation) 319 | } 320 | 321 | return annotation 322 | } 323 | 324 | /** load stored annotations */ 325 | async function loadAnnotations() { 326 | console.info('fetching annotations') 327 | const response = await sendMessage('fetch-annotations', null, 'background') 328 | if (response) { 329 | annotations = response as Annotation[] 330 | } 331 | 332 | restoreHighlights() 333 | } 334 | 335 | /** initialise */ 336 | async function initialise() { 337 | await loadAnnotations() 338 | 339 | initialiseToolbar() 340 | window.addEventListener('message', noteChangeHandler) 341 | } 342 | 343 | initialise() 344 | -------------------------------------------------------------------------------- /src/types/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const Notification: typeof import('notivue')['Notification'] 11 | const Notivue: typeof import('notivue')['Notivue'] 12 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 13 | const appRouter: typeof import('../utils/router/index')['appRouter'] 14 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 15 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 16 | const browser: typeof import('webextension-polyfill') 17 | const computed: typeof import('vue')['computed'] 18 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 19 | const computedEager: typeof import('@vueuse/core')['computedEager'] 20 | const computedInject: typeof import('@vueuse/core')['computedInject'] 21 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 22 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 23 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 24 | const createApp: typeof import('vue')['createApp'] 25 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 26 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 27 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 28 | const createPinia: typeof import('pinia')['createPinia'] 29 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 30 | const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] 31 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 32 | const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] 33 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 34 | const customRef: typeof import('vue')['customRef'] 35 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 36 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 37 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 38 | const defineComponent: typeof import('vue')['defineComponent'] 39 | const definePage: typeof import('vue-router/auto')['definePage'] 40 | const defineStore: typeof import('pinia')['defineStore'] 41 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 42 | const effectScope: typeof import('vue')['effectScope'] 43 | const extendRef: typeof import('@vueuse/core')['extendRef'] 44 | const getActivePinia: typeof import('pinia')['getActivePinia'] 45 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 46 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 47 | const h: typeof import('vue')['h'] 48 | const i18n: typeof import('../utils/i18n')['i18n'] 49 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 50 | const inject: typeof import('vue')['inject'] 51 | const injectLocal: typeof import('@vueuse/core')['injectLocal'] 52 | const isDefined: typeof import('@vueuse/core')['isDefined'] 53 | const isProxy: typeof import('vue')['isProxy'] 54 | const isReactive: typeof import('vue')['isReactive'] 55 | const isReadonly: typeof import('vue')['isReadonly'] 56 | const isRef: typeof import('vue')['isRef'] 57 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 58 | const mapActions: typeof import('pinia')['mapActions'] 59 | const mapGetters: typeof import('pinia')['mapGetters'] 60 | const mapState: typeof import('pinia')['mapState'] 61 | const mapStores: typeof import('pinia')['mapStores'] 62 | const mapWritableState: typeof import('pinia')['mapWritableState'] 63 | const markRaw: typeof import('vue')['markRaw'] 64 | const nextTick: typeof import('vue')['nextTick'] 65 | const notivue: typeof import('../utils/notifications')['notivue'] 66 | const onActivated: typeof import('vue')['onActivated'] 67 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 68 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 69 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 70 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 71 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 72 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 73 | const onDeactivated: typeof import('vue')['onDeactivated'] 74 | const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval'] 75 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 76 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 77 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 78 | const onMounted: typeof import('vue')['onMounted'] 79 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 80 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 81 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 82 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 83 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 84 | const onUnmounted: typeof import('vue')['onUnmounted'] 85 | const onUpdated: typeof import('vue')['onUpdated'] 86 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 87 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 88 | const pinia: typeof import('../utils/pinia')['pinia'] 89 | const provide: typeof import('vue')['provide'] 90 | const provideLocal: typeof import('@vueuse/core')['provideLocal'] 91 | const pushNotification: typeof import('notivue')['push'] 92 | const reactify: typeof import('@vueuse/core')['reactify'] 93 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 94 | const reactive: typeof import('vue')['reactive'] 95 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 96 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 97 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 98 | const readonly: typeof import('vue')['readonly'] 99 | const ref: typeof import('vue')['ref'] 100 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 101 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 102 | const refDefault: typeof import('@vueuse/core')['refDefault'] 103 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 104 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 105 | const resolveComponent: typeof import('vue')['resolveComponent'] 106 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 107 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 108 | const setActivePinia: typeof import('pinia')['setActivePinia'] 109 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 110 | const shallowReactive: typeof import('vue')['shallowReactive'] 111 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 112 | const shallowRef: typeof import('vue')['shallowRef'] 113 | const storeToRefs: typeof import('pinia')['storeToRefs'] 114 | const syncRef: typeof import('@vueuse/core')['syncRef'] 115 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 116 | const t: typeof import('vue-i18n')['t'] 117 | const templateRef: typeof import('@vueuse/core')['templateRef'] 118 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 119 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 120 | const toRaw: typeof import('vue')['toRaw'] 121 | const toReactive: typeof import('@vueuse/core')['toReactive'] 122 | const toRef: typeof import('vue')['toRef'] 123 | const toRefs: typeof import('vue')['toRefs'] 124 | const toValue: typeof import('vue')['toValue'] 125 | const triggerRef: typeof import('vue')['triggerRef'] 126 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 127 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 128 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 129 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 130 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 131 | const unref: typeof import('vue')['unref'] 132 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 133 | const until: typeof import('@vueuse/core')['until'] 134 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 135 | const useAnimate: typeof import('@vueuse/core')['useAnimate'] 136 | const useAnnotation: typeof import('../composables/useAnnotation')['useAnnotation'] 137 | const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] 138 | const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] 139 | const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] 140 | const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] 141 | const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] 142 | const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] 143 | const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] 144 | const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] 145 | const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] 146 | const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] 147 | const useArraySome: typeof import('@vueuse/core')['useArraySome'] 148 | const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] 149 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 150 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 151 | const useAttrs: typeof import('vue')['useAttrs'] 152 | const useBase64: typeof import('@vueuse/core')['useBase64'] 153 | const useBattery: typeof import('@vueuse/core')['useBattery'] 154 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 155 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 156 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 157 | const useBrowserLocalStorage: typeof import('../composables/useBrowserStorage')['useBrowserLocalStorage'] 158 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 159 | const useBrowserSyncStorage: typeof import('../composables/useBrowserStorage')['useBrowserSyncStorage'] 160 | const useCached: typeof import('@vueuse/core')['useCached'] 161 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 162 | const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] 163 | const useCloned: typeof import('@vueuse/core')['useCloned'] 164 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 165 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 166 | const useCounter: typeof import('@vueuse/core')['useCounter'] 167 | const useCssModule: typeof import('vue')['useCssModule'] 168 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 169 | const useCssVars: typeof import('vue')['useCssVars'] 170 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 171 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 172 | const useDark: typeof import('@vueuse/core')['useDark'] 173 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 174 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 175 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 176 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 177 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 178 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 179 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 180 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 181 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 182 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 183 | const useDragAndDropTree: typeof import('../composables/useDnDTree')['useDragAndDropTree'] 184 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 185 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 186 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 187 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 188 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 189 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 190 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 191 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 192 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 193 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 194 | const useExtractor: typeof import('../composables/useExtractor')['useExtractor'] 195 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 196 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 197 | const useFetch: typeof import('@vueuse/core')['useFetch'] 198 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 199 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 200 | const useFocus: typeof import('@vueuse/core')['useFocus'] 201 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 202 | const useFps: typeof import('@vueuse/core')['useFps'] 203 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 204 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 205 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 206 | const useHistoryMap: typeof import('../composables/useHistoryMap')['useHistoryMap'] 207 | const useI18n: typeof import('vue-i18n')['useI18n'] 208 | const useId: typeof import('vue')['useId'] 209 | const useIdle: typeof import('@vueuse/core')['useIdle'] 210 | const useImage: typeof import('@vueuse/core')['useImage'] 211 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 212 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 213 | const useInterval: typeof import('@vueuse/core')['useInterval'] 214 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 215 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 216 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 217 | const useLink: typeof import('vue-router')['useLink'] 218 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 219 | const useLocale: typeof import('../composables/useLocale')['useLocale'] 220 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 221 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 222 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 223 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 224 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 225 | const useMemory: typeof import('@vueuse/core')['useMemory'] 226 | const useModel: typeof import('vue')['useModel'] 227 | const useMounted: typeof import('@vueuse/core')['useMounted'] 228 | const useMouse: typeof import('@vueuse/core')['useMouse'] 229 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 230 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 231 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 232 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 233 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 234 | const useNow: typeof import('@vueuse/core')['useNow'] 235 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 236 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 237 | const useOnline: typeof import('@vueuse/core')['useOnline'] 238 | const useOptionsStore: typeof import('../stores/options.store')['useOptionsStore'] 239 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 240 | const useParallax: typeof import('@vueuse/core')['useParallax'] 241 | const useParentElement: typeof import('@vueuse/core')['useParentElement'] 242 | const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] 243 | const usePermission: typeof import('@vueuse/core')['usePermission'] 244 | const usePointer: typeof import('@vueuse/core')['usePointer'] 245 | const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] 246 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 247 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 248 | const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] 249 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 250 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 251 | const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] 252 | const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency'] 253 | const usePrevious: typeof import('@vueuse/core')['usePrevious'] 254 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 255 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 256 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 257 | const useRoute: typeof import('vue-router')['useRoute'] 258 | const useRouter: typeof import('vue-router')['useRouter'] 259 | const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth'] 260 | const useSchemaEditor: typeof import('../composables/useSchemaEditor')['useSchemaEditor'] 261 | const useSchemaMap: typeof import('../composables/useSchemaMap')['useSchemaMap'] 262 | const useSchemaPanel: typeof import('../composables/useSchemaPanel')['useSchemaPanel'] 263 | const useSchemaSynthesise: typeof import('../composables/useSchemaSynthesise')['useSchemaSynthesise'] 264 | const useSchemaTree: typeof import('../composables/useSchemaTree')['useSchemaTree'] 265 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 266 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 267 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 268 | const useScroll: typeof import('@vueuse/core')['useScroll'] 269 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 270 | const useSession: typeof import('../composables/useSession')['useSession'] 271 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 272 | const useShare: typeof import('@vueuse/core')['useShare'] 273 | const useSlots: typeof import('vue')['useSlots'] 274 | const useSorted: typeof import('@vueuse/core')['useSorted'] 275 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 276 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 277 | const useStepper: typeof import('@vueuse/core')['useStepper'] 278 | const useStorage: typeof import('@vueuse/core')['useStorage'] 279 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 280 | const useStore: typeof import('../stores/historymap.store')['useStore'] 281 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 282 | const useSupported: typeof import('@vueuse/core')['useSupported'] 283 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 284 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 285 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 286 | const useTestStore: typeof import('../stores/test.store')['useTestStore'] 287 | const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] 288 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 289 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 290 | const useTheme: typeof import('../composables/useTheme')['useTheme'] 291 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 292 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 293 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 294 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 295 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 296 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 297 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 298 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 299 | const useTitle: typeof import('@vueuse/core')['useTitle'] 300 | const useToNumber: typeof import('@vueuse/core')['useToNumber'] 301 | const useToString: typeof import('@vueuse/core')['useToString'] 302 | const useToggle: typeof import('@vueuse/core')['useToggle'] 303 | const useTransition: typeof import('@vueuse/core')['useTransition'] 304 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 305 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 306 | const useVModel: typeof import('@vueuse/core')['useVModel'] 307 | const useVModels: typeof import('@vueuse/core')['useVModels'] 308 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 309 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 310 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 311 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 312 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 313 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 314 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 315 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 316 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 317 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 318 | const watch: typeof import('vue')['watch'] 319 | const watchArray: typeof import('@vueuse/core')['watchArray'] 320 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 321 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 322 | const watchDeep: typeof import('@vueuse/core')['watchDeep'] 323 | const watchEffect: typeof import('vue')['watchEffect'] 324 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 325 | const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] 326 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 327 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 328 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 329 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 330 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 331 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 332 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 333 | const whenever: typeof import('@vueuse/core')['whenever'] 334 | } 335 | // for type re-export 336 | declare global { 337 | // @ts-ignore 338 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 339 | import('vue') 340 | // @ts-ignore 341 | export type { TargetPosition } from '../composables/useDnDTree' 342 | import('../composables/useDnDTree') 343 | } 344 | 345 | // for vue template auto import 346 | import { UnwrapRef } from 'vue' 347 | declare module 'vue' { 348 | interface GlobalComponents {} 349 | interface ComponentCustomProperties { 350 | readonly EffectScope: UnwrapRef 351 | readonly Notification: UnwrapRef 352 | readonly Notivue: UnwrapRef 353 | readonly acceptHMRUpdate: UnwrapRef 354 | readonly appRouter: UnwrapRef 355 | readonly asyncComputed: UnwrapRef 356 | readonly autoResetRef: UnwrapRef 357 | readonly browser: UnwrapRef 358 | readonly computed: UnwrapRef 359 | readonly computedAsync: UnwrapRef 360 | readonly computedEager: UnwrapRef 361 | readonly computedInject: UnwrapRef 362 | readonly computedWithControl: UnwrapRef 363 | readonly controlledComputed: UnwrapRef 364 | readonly controlledRef: UnwrapRef 365 | readonly createApp: UnwrapRef 366 | readonly createEventHook: UnwrapRef 367 | readonly createGlobalState: UnwrapRef 368 | readonly createInjectionState: UnwrapRef 369 | readonly createPinia: UnwrapRef 370 | readonly createReactiveFn: UnwrapRef 371 | readonly createReusableTemplate: UnwrapRef 372 | readonly createSharedComposable: UnwrapRef 373 | readonly createTemplatePromise: UnwrapRef 374 | readonly createUnrefFn: UnwrapRef 375 | readonly customRef: UnwrapRef 376 | readonly debouncedRef: UnwrapRef 377 | readonly debouncedWatch: UnwrapRef 378 | readonly defineAsyncComponent: UnwrapRef 379 | readonly defineComponent: UnwrapRef 380 | readonly definePage: UnwrapRef 381 | readonly defineStore: UnwrapRef 382 | readonly eagerComputed: UnwrapRef 383 | readonly effectScope: UnwrapRef 384 | readonly extendRef: UnwrapRef 385 | readonly getActivePinia: UnwrapRef 386 | readonly getCurrentInstance: UnwrapRef 387 | readonly getCurrentScope: UnwrapRef 388 | readonly h: UnwrapRef 389 | readonly i18n: UnwrapRef 390 | readonly ignorableWatch: UnwrapRef 391 | readonly inject: UnwrapRef 392 | readonly injectLocal: UnwrapRef 393 | readonly isDefined: UnwrapRef 394 | readonly isProxy: UnwrapRef 395 | readonly isReactive: UnwrapRef 396 | readonly isReadonly: UnwrapRef 397 | readonly isRef: UnwrapRef 398 | readonly makeDestructurable: UnwrapRef 399 | readonly mapActions: UnwrapRef 400 | readonly mapGetters: UnwrapRef 401 | readonly mapState: UnwrapRef 402 | readonly mapStores: UnwrapRef 403 | readonly mapWritableState: UnwrapRef 404 | readonly markRaw: UnwrapRef 405 | readonly nextTick: UnwrapRef 406 | readonly notivue: UnwrapRef 407 | readonly onActivated: UnwrapRef 408 | readonly onBeforeMount: UnwrapRef 409 | readonly onBeforeRouteLeave: UnwrapRef 410 | readonly onBeforeRouteUpdate: UnwrapRef 411 | readonly onBeforeUnmount: UnwrapRef 412 | readonly onBeforeUpdate: UnwrapRef 413 | readonly onClickOutside: UnwrapRef 414 | readonly onDeactivated: UnwrapRef 415 | readonly onElementRemoval: UnwrapRef 416 | readonly onErrorCaptured: UnwrapRef 417 | readonly onKeyStroke: UnwrapRef 418 | readonly onLongPress: UnwrapRef 419 | readonly onMounted: UnwrapRef 420 | readonly onRenderTracked: UnwrapRef 421 | readonly onRenderTriggered: UnwrapRef 422 | readonly onScopeDispose: UnwrapRef 423 | readonly onServerPrefetch: UnwrapRef 424 | readonly onStartTyping: UnwrapRef 425 | readonly onUnmounted: UnwrapRef 426 | readonly onUpdated: UnwrapRef 427 | readonly onWatcherCleanup: UnwrapRef 428 | readonly pausableWatch: UnwrapRef 429 | readonly pinia: UnwrapRef 430 | readonly provide: UnwrapRef 431 | readonly provideLocal: UnwrapRef 432 | readonly pushNotification: UnwrapRef 433 | readonly reactify: UnwrapRef 434 | readonly reactifyObject: UnwrapRef 435 | readonly reactive: UnwrapRef 436 | readonly reactiveComputed: UnwrapRef 437 | readonly reactiveOmit: UnwrapRef 438 | readonly reactivePick: UnwrapRef 439 | readonly readonly: UnwrapRef 440 | readonly ref: UnwrapRef 441 | readonly refAutoReset: UnwrapRef 442 | readonly refDebounced: UnwrapRef 443 | readonly refDefault: UnwrapRef 444 | readonly refThrottled: UnwrapRef 445 | readonly refWithControl: UnwrapRef 446 | readonly resolveComponent: UnwrapRef 447 | readonly resolveRef: UnwrapRef 448 | readonly resolveUnref: UnwrapRef 449 | readonly setActivePinia: UnwrapRef 450 | readonly setMapStoreSuffix: UnwrapRef 451 | readonly shallowReactive: UnwrapRef 452 | readonly shallowReadonly: UnwrapRef 453 | readonly shallowRef: UnwrapRef 454 | readonly storeToRefs: UnwrapRef 455 | readonly syncRef: UnwrapRef 456 | readonly syncRefs: UnwrapRef 457 | readonly t: UnwrapRef 458 | readonly templateRef: UnwrapRef 459 | readonly throttledRef: UnwrapRef 460 | readonly throttledWatch: UnwrapRef 461 | readonly toRaw: UnwrapRef 462 | readonly toReactive: UnwrapRef 463 | readonly toRef: UnwrapRef 464 | readonly toRefs: UnwrapRef 465 | readonly toValue: UnwrapRef 466 | readonly triggerRef: UnwrapRef 467 | readonly tryOnBeforeMount: UnwrapRef 468 | readonly tryOnBeforeUnmount: UnwrapRef 469 | readonly tryOnMounted: UnwrapRef 470 | readonly tryOnScopeDispose: UnwrapRef 471 | readonly tryOnUnmounted: UnwrapRef 472 | readonly unref: UnwrapRef 473 | readonly unrefElement: UnwrapRef 474 | readonly until: UnwrapRef 475 | readonly useActiveElement: UnwrapRef 476 | readonly useAnimate: UnwrapRef 477 | readonly useArrayDifference: UnwrapRef 478 | readonly useArrayEvery: UnwrapRef 479 | readonly useArrayFilter: UnwrapRef 480 | readonly useArrayFind: UnwrapRef 481 | readonly useArrayFindIndex: UnwrapRef 482 | readonly useArrayFindLast: UnwrapRef 483 | readonly useArrayIncludes: UnwrapRef 484 | readonly useArrayJoin: UnwrapRef 485 | readonly useArrayMap: UnwrapRef 486 | readonly useArrayReduce: UnwrapRef 487 | readonly useArraySome: UnwrapRef 488 | readonly useArrayUnique: UnwrapRef 489 | readonly useAsyncQueue: UnwrapRef 490 | readonly useAsyncState: UnwrapRef 491 | readonly useAttrs: UnwrapRef 492 | readonly useBase64: UnwrapRef 493 | readonly useBattery: UnwrapRef 494 | readonly useBluetooth: UnwrapRef 495 | readonly useBreakpoints: UnwrapRef 496 | readonly useBroadcastChannel: UnwrapRef 497 | readonly useBrowserLocalStorage: UnwrapRef 498 | readonly useBrowserLocation: UnwrapRef 499 | readonly useBrowserSyncStorage: UnwrapRef 500 | readonly useCached: UnwrapRef 501 | readonly useClipboard: UnwrapRef 502 | readonly useClipboardItems: UnwrapRef 503 | readonly useCloned: UnwrapRef 504 | readonly useColorMode: UnwrapRef 505 | readonly useConfirmDialog: UnwrapRef 506 | readonly useCounter: UnwrapRef 507 | readonly useCssModule: UnwrapRef 508 | readonly useCssVar: UnwrapRef 509 | readonly useCssVars: UnwrapRef 510 | readonly useCurrentElement: UnwrapRef 511 | readonly useCycleList: UnwrapRef 512 | readonly useDark: UnwrapRef 513 | readonly useDateFormat: UnwrapRef 514 | readonly useDebounce: UnwrapRef 515 | readonly useDebounceFn: UnwrapRef 516 | readonly useDebouncedRefHistory: UnwrapRef 517 | readonly useDeviceMotion: UnwrapRef 518 | readonly useDeviceOrientation: UnwrapRef 519 | readonly useDevicePixelRatio: UnwrapRef 520 | readonly useDevicesList: UnwrapRef 521 | readonly useDisplayMedia: UnwrapRef 522 | readonly useDocumentVisibility: UnwrapRef 523 | readonly useDragAndDropTree: UnwrapRef 524 | readonly useDraggable: UnwrapRef 525 | readonly useDropZone: UnwrapRef 526 | readonly useElementBounding: UnwrapRef 527 | readonly useElementByPoint: UnwrapRef 528 | readonly useElementHover: UnwrapRef 529 | readonly useElementSize: UnwrapRef 530 | readonly useElementVisibility: UnwrapRef 531 | readonly useEventBus: UnwrapRef 532 | readonly useEventListener: UnwrapRef 533 | readonly useEventSource: UnwrapRef 534 | readonly useExtractor: UnwrapRef 535 | readonly useEyeDropper: UnwrapRef 536 | readonly useFavicon: UnwrapRef 537 | readonly useFetch: UnwrapRef 538 | readonly useFileDialog: UnwrapRef 539 | readonly useFileSystemAccess: UnwrapRef 540 | readonly useFocus: UnwrapRef 541 | readonly useFocusWithin: UnwrapRef 542 | readonly useFps: UnwrapRef 543 | readonly useFullscreen: UnwrapRef 544 | readonly useGamepad: UnwrapRef 545 | readonly useGeolocation: UnwrapRef 546 | readonly useHistoryMap: UnwrapRef 547 | readonly useI18n: UnwrapRef 548 | readonly useId: UnwrapRef 549 | readonly useIdle: UnwrapRef 550 | readonly useImage: UnwrapRef 551 | readonly useInfiniteScroll: UnwrapRef 552 | readonly useIntersectionObserver: UnwrapRef 553 | readonly useInterval: UnwrapRef 554 | readonly useIntervalFn: UnwrapRef 555 | readonly useKeyModifier: UnwrapRef 556 | readonly useLastChanged: UnwrapRef 557 | readonly useLink: UnwrapRef 558 | readonly useLocalStorage: UnwrapRef 559 | readonly useLocale: UnwrapRef 560 | readonly useMagicKeys: UnwrapRef 561 | readonly useManualRefHistory: UnwrapRef 562 | readonly useMediaControls: UnwrapRef 563 | readonly useMediaQuery: UnwrapRef 564 | readonly useMemoize: UnwrapRef 565 | readonly useMemory: UnwrapRef 566 | readonly useModel: UnwrapRef 567 | readonly useMounted: UnwrapRef 568 | readonly useMouse: UnwrapRef 569 | readonly useMouseInElement: UnwrapRef 570 | readonly useMousePressed: UnwrapRef 571 | readonly useMutationObserver: UnwrapRef 572 | readonly useNavigatorLanguage: UnwrapRef 573 | readonly useNetwork: UnwrapRef 574 | readonly useNow: UnwrapRef 575 | readonly useObjectUrl: UnwrapRef 576 | readonly useOffsetPagination: UnwrapRef 577 | readonly useOnline: UnwrapRef 578 | readonly useOptionsStore: UnwrapRef 579 | readonly usePageLeave: UnwrapRef 580 | readonly useParallax: UnwrapRef 581 | readonly useParentElement: UnwrapRef 582 | readonly usePerformanceObserver: UnwrapRef 583 | readonly usePermission: UnwrapRef 584 | readonly usePointer: UnwrapRef 585 | readonly usePointerLock: UnwrapRef 586 | readonly usePointerSwipe: UnwrapRef 587 | readonly usePreferredColorScheme: UnwrapRef 588 | readonly usePreferredContrast: UnwrapRef 589 | readonly usePreferredDark: UnwrapRef 590 | readonly usePreferredLanguages: UnwrapRef 591 | readonly usePreferredReducedMotion: UnwrapRef 592 | readonly usePreferredReducedTransparency: UnwrapRef 593 | readonly usePrevious: UnwrapRef 594 | readonly useRafFn: UnwrapRef 595 | readonly useRefHistory: UnwrapRef 596 | readonly useResizeObserver: UnwrapRef 597 | readonly useRoute: UnwrapRef 598 | readonly useRouter: UnwrapRef 599 | readonly useSSRWidth: UnwrapRef 600 | readonly useSchemaEditor: UnwrapRef 601 | readonly useSchemaMap: UnwrapRef 602 | readonly useSchemaPanel: UnwrapRef 603 | readonly useSchemaSynthesise: UnwrapRef 604 | readonly useScreenOrientation: UnwrapRef 605 | readonly useScreenSafeArea: UnwrapRef 606 | readonly useScriptTag: UnwrapRef 607 | readonly useScroll: UnwrapRef 608 | readonly useScrollLock: UnwrapRef 609 | readonly useSession: UnwrapRef 610 | readonly useSessionStorage: UnwrapRef 611 | readonly useShare: UnwrapRef 612 | readonly useSlots: UnwrapRef 613 | readonly useSorted: UnwrapRef 614 | readonly useSpeechRecognition: UnwrapRef 615 | readonly useSpeechSynthesis: UnwrapRef 616 | readonly useStepper: UnwrapRef 617 | readonly useStorage: UnwrapRef 618 | readonly useStorageAsync: UnwrapRef 619 | readonly useStyleTag: UnwrapRef 620 | readonly useSupported: UnwrapRef 621 | readonly useSwipe: UnwrapRef 622 | readonly useTemplateRef: UnwrapRef 623 | readonly useTemplateRefsList: UnwrapRef 624 | readonly useTestStore: UnwrapRef 625 | readonly useTextDirection: UnwrapRef 626 | readonly useTextSelection: UnwrapRef 627 | readonly useTextareaAutosize: UnwrapRef 628 | readonly useTheme: UnwrapRef 629 | readonly useThrottle: UnwrapRef 630 | readonly useThrottleFn: UnwrapRef 631 | readonly useThrottledRefHistory: UnwrapRef 632 | readonly useTimeAgo: UnwrapRef 633 | readonly useTimeout: UnwrapRef 634 | readonly useTimeoutFn: UnwrapRef 635 | readonly useTimeoutPoll: UnwrapRef 636 | readonly useTimestamp: UnwrapRef 637 | readonly useTitle: UnwrapRef 638 | readonly useToNumber: UnwrapRef 639 | readonly useToString: UnwrapRef 640 | readonly useToggle: UnwrapRef 641 | readonly useTransition: UnwrapRef 642 | readonly useUrlSearchParams: UnwrapRef 643 | readonly useUserMedia: UnwrapRef 644 | readonly useVModel: UnwrapRef 645 | readonly useVModels: UnwrapRef 646 | readonly useVibrate: UnwrapRef 647 | readonly useVirtualList: UnwrapRef 648 | readonly useWakeLock: UnwrapRef 649 | readonly useWebNotification: UnwrapRef 650 | readonly useWebSocket: UnwrapRef 651 | readonly useWebWorker: UnwrapRef 652 | readonly useWebWorkerFn: UnwrapRef 653 | readonly useWindowFocus: UnwrapRef 654 | readonly useWindowScroll: UnwrapRef 655 | readonly useWindowSize: UnwrapRef 656 | readonly watch: UnwrapRef 657 | readonly watchArray: UnwrapRef 658 | readonly watchAtMost: UnwrapRef 659 | readonly watchDebounced: UnwrapRef 660 | readonly watchDeep: UnwrapRef 661 | readonly watchEffect: UnwrapRef 662 | readonly watchIgnorable: UnwrapRef 663 | readonly watchImmediate: UnwrapRef 664 | readonly watchOnce: UnwrapRef 665 | readonly watchPausable: UnwrapRef 666 | readonly watchPostEffect: UnwrapRef 667 | readonly watchSyncEffect: UnwrapRef 668 | readonly watchThrottled: UnwrapRef 669 | readonly watchTriggerable: UnwrapRef 670 | readonly watchWithFilter: UnwrapRef 671 | readonly whenever: UnwrapRef 672 | } 673 | } --------------------------------------------------------------------------------