From 70f7fac23e8d2c457e3b35d6b1e1a8d449fa8b94 Mon Sep 17 00:00:00 2001 From: Ignacio Ballesteros Date: Tue, 17 Feb 2026 21:25:46 +0100 Subject: [PATCH] chore: initial project commit --- .env.example | 12 + .eslintrc.js | 15 + .gitea/workflows/release.yml | 85 + .gitignore | 39 + .prettierrc | 7 + AGENTS.md | 286 ++ README.md | 211 ++ addon/bootstrap.js | 70 + addon/content/icons/icon.svg | 1 + addon/content/icons/icon@2x.svg | 1 + addon/content/icons/menu-icon.svg | 1 + addon/content/preferences.xhtml | 83 + addon/content/scripts/preferences.js | 76 + addon/locale/en-US/addon.ftl | 27 + addon/locale/en-US/preferences.ftl | 40 + addon/manifest.json | 20 + addon/prefs.js | 7 + flake.lock | 61 + flake.nix | 72 + package-lock.json | 4488 ++++++++++++++++++++++++++ package.json | 53 + src/addon.ts | 53 + src/hooks.ts | 157 + src/index.ts | 31 + src/modules/converter.ts | 58 + src/modules/exporter.ts | 280 ++ src/modules/menu.ts | 159 + src/modules/notifier.ts | 73 + src/modules/prefs.ts | 103 + tsconfig.json | 25 + typings/global.d.ts | 109 + typings/i10n.d.ts | 36 + typings/prefs.d.ts | 19 + zotero-plugin.config.ts | 28 + 34 files changed, 6786 insertions(+) create mode 100644 .env.example create mode 100644 .eslintrc.js create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 addon/bootstrap.js create mode 100644 addon/content/icons/icon.svg create mode 100644 addon/content/icons/icon@2x.svg create mode 100644 addon/content/icons/menu-icon.svg create mode 100644 addon/content/preferences.xhtml create mode 100644 addon/content/scripts/preferences.js create mode 100644 addon/locale/en-US/addon.ftl create mode 100644 addon/locale/en-US/preferences.ftl create mode 100644 addon/manifest.json create mode 100644 addon/prefs.js create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/addon.ts create mode 100644 src/hooks.ts create mode 100644 src/index.ts create mode 100644 src/modules/converter.ts create mode 100644 src/modules/exporter.ts create mode 100644 src/modules/menu.ts create mode 100644 src/modules/notifier.ts create mode 100644 src/modules/prefs.ts create mode 100644 tsconfig.json create mode 100644 typings/global.d.ts create mode 100644 typings/i10n.d.ts create mode 100644 typings/prefs.d.ts create mode 100644 zotero-plugin.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a10cd46 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Path to Zotero executable +# Linux: /usr/bin/zotero +# macOS: /Applications/Zotero.app/Contents/MacOS/zotero +# Windows: C:\Program Files\Zotero\zotero.exe +ZOTERO_PLUGIN_ZOTERO_BIN_PATH= + +# Path to Zotero profile directory for development +# Find via: Help -> Troubleshooting Information -> Profile Directory +ZOTERO_PLUGIN_PROFILE_PATH= + +# Data directory (optional, defaults to profile's default) +# ZOTERO_PLUGIN_DATA_DIR= diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b6cb072 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + project: "./tsconfig.json", + }, + plugins: ["@typescript-eslint"], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + ignorePatterns: [".scaffold/", "node_modules/", "addon/bootstrap.js", "*.config.js"], + rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "warn", + }, +}; diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..d1d7e63 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,85 @@ +name: Release + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Get version + id: version + run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create release and upload assets + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="${GITHUB_REF#refs/tags/}" + REPO="${GITHUB_REPOSITORY}" + SERVER_URL="${GITHUB_SERVER_URL}" + API_URL="${SERVER_URL}/api/v1" + + # Create the release + RELEASE=$(curl -s -X POST "${API_URL}/repos/${REPO}/releases" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"${TAG}\", + \"name\": \"${TAG}\", + \"body\": \"Release ${TAG}\", + \"draft\": false, + \"prerelease\": false + }") + + RELEASE_ID=$(echo "${RELEASE}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + + if [ -z "${RELEASE_ID}" ]; then + echo "Failed to create release. Response: ${RELEASE}" + exit 1 + fi + + echo "Created release ID: ${RELEASE_ID}" + + # Upload all XPI files + for XPI in .scaffold/build/*.xpi; do + [ -f "${XPI}" ] || continue + FILENAME=$(basename "${XPI}") + echo "Uploading ${FILENAME}..." + curl -s -X POST "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${XPI}" + done + + # Upload update manifests if present + for JSON in .scaffold/build/update.json .scaffold/build/update-beta.json; do + [ -f "${JSON}" ] || continue + FILENAME=$(basename "${JSON}") + echo "Uploading ${FILENAME}..." + curl -s -X POST "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + --data-binary "@${JSON}" + done + + echo "Release ${TAG} published successfully." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d727332 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Build output +.scaffold/ +*.xpi + +# Environment +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Test +coverage/ + +# Nix +result +.direnv/ + +# AI agent artifacts +.agent-shell/ +opencode.json + +# Internal development artifacts +PLAN.md +zotero-notes-export.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1a88ab1 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..83cb779 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,286 @@ +# AGENTS.md + +## Development Environment Commands + +### Primary Commands (run inside `nix develop`) + +```bash +# Development +nix develop # Enter dev shell with Node.js and tooling +npm run start # Start dev server with hot reload +npm run build # Build production XPI to .scaffold/build/ + +# Code Quality +npm run lint # Run ESLint on TypeScript source +npm run lint:fix # Auto-fix ESLint issues +npm run format # Format with Prettier +npm run format:check # Check formatting without changes + +# Release +npm run release # Bump version, commit, tag, and build +``` + +### Testing + +This project does not currently have automated tests. To test manually: + +1. Run `npm run build` to create the XPI +2. Install the XPI in Zotero 7 +3. Test export functionality manually via Tools menu + +--- + +## Code Style Guidelines + +### Imports and Exports + +```typescript +// Use relative imports for internal modules +import { OrgExporter } from "./modules/exporter"; +import type { Prefs } from "./modules/prefs"; + +// Use type-only imports where possible +import type { AddonInfo } from "./addon"; + +// Use default exports for single main entity +export class OrgExportAnnotationsAddon {} +export function createHooks() {} + +// Use named exports for utilities +export async function convertToOrg() {} +export async function testPandoc() {} +``` + +**Rules:** + +- Prefer named exports for functions +- Use `import type` for type-only imports +- Import from `./modules/` with relative paths +- No namespace imports (`import * as X`) +- No `export default` for modules (use named exports) + +### Formatting (Prettier) + +```json +{ + "semi": true, // Always use semicolons + "singleQuote": false, // Use double quotes + "tabWidth": 2, // 2 space indentation + "trailingComma": "es5", // Trailing commas in valid ES5 + "printWidth": 100 // Max line length +} +``` + +### TypeScript Configuration + +- **Strict mode enabled**: All type checking rules active +- **Target**: ES2022 with DOM types +- **Module**: ESNext with bundler resolution +- **Source maps**: Enabled for debugging +- **Output**: Bundled to `.scaffold/build/addon/content/scripts/` + +### Naming Conventions + +| Entity | Convention | Examples | +| --------------- | ---------------- | ---------------------------------------------------------- | +| Classes | PascalCase | `OrgExporter`, `Prefs`, `AddonInfo` | +| Functions | camelCase | `convertToOrg`, `isAnnotationNote`, `ensureNotesDirectory` | +| Methods | camelCase | `init()`, `exportAttachment()`, `cleanupTempFile()` | +| Private Methods | camelCase | Prefix with `private` keyword | +| Constants | UPPER_SNAKE_CASE | `PREF_PREFIX`, `MENU_IDS` | +| Interfaces | PascalCase | `AddonInfo`, `Hooks` | +| Types | PascalCase | `export type Preference = ...` | +| File Names | kebab-case | `converter.ts`, `exporter.ts`, `preferences.js` | + +```typescript +// Constants +const PREF_PREFIX = "extensions.zotero.orgexportannotations"; +const MENU_IDS = { toolsExportAll: "...", itemExportSelected: "..." }; + +// Private methods +private async getLastAnnotationsNote(): Promise {} +private isAnnotationNote(noteHTML: string): boolean {} +``` + +### Type Annotations + +```typescript +// Explicit return types on functions +export async function convertToOrg(...): Promise {} +export function testPandoc(pandocPath: string): Promise {} + +// Type assertions for Zotero API returns +const title = attachment.parentItem.getField("title") as string; +const citationKey = item.getField("citationKey") as string; + +// Optional chaining with nullish coalescing +const citationKey = note.parentItem?.getField("citationKey") || "temp"; + +// Generic type parameters +private getPref(key: string, defaultValue: T): T { + return (value as T) ?? defaultValue; +} +``` + +### Error Handling + +```typescript +// Standard pattern with logging +try { + await operation(); + Zotero.OrgExportAnnotations.log("Operation succeeded"); +} catch (error) { + Zotero.OrgExportAnnotations.error("Operation failed", error as Error); + // Optionally re-throw + throw error; +} + +// Empty catch blocks must use underscore prefix +try { + await cleanup(); +} catch (_error) { + // Cleanup errors are non-critical +} + +// Return error values instead of throwing for utility functions +export async function testPandoc(pandocPath: string): Promise { + try { + await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]); + return true; + } catch { + return false; + } +} +``` + +### Logging + +```typescript +// Debug logging (respects debug mode) +Zotero.OrgExportAnnotations.log("Message", ...args); + +// Error logging (always logged) +Zotero.OrgExportAnnotations.error("Message", error as Error); + +// Warning logging (always logged) +Zotero.OrgExportAnnotations.warn("Message"); + +// Verbose debugging with objects +Zotero.OrgExportAnnotations.log("Processing item", { id, title }); +``` + +### Async/Await Patterns + +```typescript +// Use for...of for sequential async operations +for (const attachment of attachments) { + if (await this.exportAttachment(attachment)) { + count++; + } +} + +// Early returns with await +if (!item.isFileAttachment()) return false; +if (!path) throw new Error("Notes path not configured"); +``` + +### Zotero API Usage + +```typescript +// Non-null assertions for known APIs +Zotero.BetterNotes!.api.convert.note2md(note, notesPath); + +// Zotero utilities for file operations +await Zotero.File.putContentsAsync(path, content); +const content = await Zotero.File.getContentsAsync(path); +await Zotero.Utilities.Internal.exec(command, args); + +// Zotero preferences +Zotero.Prefs.get(key, global); +Zotero.Prefs.set(key, value, global); + +// Zotero items +const item = Zotero.Items.get(id); +await Zotero.Items.trashTx(id); +``` + +### Class Structure + +```typescript +export class MyClass { + // Public properties + public property!: string; + + // Private properties + private internal = { enabled: false }; + + // Constructor with dependency injection + constructor(private deps: Dependencies) {} + + // Public methods + public async doSomething(): Promise { + try { + // Implementation + } catch (error) { + this.handleError(error as Error); + } + } + + // Private helper methods + private handleError(error: Error): void { + Zotero.OrgExportAnnotations.error("Error occurred", error); + } +} +``` + +### ESLint Rules + +- **Unused variables**: Error, but ignored if prefixed with `_` +- **Explicit any**: Warning (prefer specific types) +- **No-empty**: Error for empty catch blocks (use `catch (_error)`) + +### File Organization + +``` +src/ +├── index.ts # Main entry point +├── addon.ts # Core addon class +├── hooks.ts # Lifecycle hooks +└── modules/ + ├── prefs.ts # Preferences management + ├── exporter.ts # Core export logic + ├── converter.ts # Format conversion + ├── notifier.ts # Zotero event handlers + └── menu.ts # UI menu registration +``` + +### Common Patterns + +**Preference getters/setters**: + +```typescript +get notesPath(): string { + const path = this.getPref("notesPath", ""); + if (path && path.startsWith("~")) { + return path.replace("~", PathUtils.profileDir); + } + return path; +} +set notesPath(value: string) { + this.setPref("notesPath", value); +} +``` + +**Filter operations**: + +```typescript +return items + .filter((item) => this.checkCondition(item)) + .filter((item) => item.parentItem !== undefined); +``` + +**Reduce operations**: + +```typescript +return notes.reduce((max, current) => (current.dateModified > max.dateModified ? current : max)); +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c7d59a --- /dev/null +++ b/README.md @@ -0,0 +1,211 @@ +# Zotero Org Export Annotations + +Export PDF and EPUB annotations from Zotero to Org-mode files for use with Emacs, org-roam, and citar. + +## Features + +- Export annotations to individual Org files named by citation key +- Automatic export after Zotero sync completes +- Export when closing a PDF/EPUB reader tab +- Configurable output directory +- Optional attachment of Org files to Zotero items + +## Requirements + +- **[Zotero](https://www.zotero.org/) 7.0+** +- **[Better Notes](https://github.com/windingwind/zotero-better-notes)** plugin — must be installed before this plugin +- **[Pandoc](https://pandoc.org/)** — for Markdown to Org conversion + +## Installation + +1. Install the **Better Notes** plugin first (see its own instructions) +2. Install **Pandoc** and ensure it is on your `PATH` (or note its full path) +3. Download the latest `.xpi` file from [Releases](../../releases) +4. In Zotero, go to **Tools → Add-ons** +5. Click the gear icon (top-right) and select **Install Add-on From File...** +6. Select the downloaded `.xpi` file and restart Zotero if prompted + +## Configuration + +After installation, open **Tools → Org Export Preferences** to configure: + +| Preference | Default | Description | +| ----------------------- | --------- | --------------------------------------------------------------- | +| Notes Directory | _(empty)_ | Directory where Org files will be saved. Supports `~` for home. | +| Pandoc Path | `pandoc` | Full path to the pandoc binary if not on `PATH` | +| Attach Org file to item | `false` | Link exported Org files as Zotero attachments | +| Auto-export on sync | `true` | Export automatically after Zotero sync completes | +| Export on tab close | `true` | Export when closing a PDF/EPUB reader tab | +| Show notifications | `true` | Display a progress notification during export | +| Debug mode | `false` | Enable verbose logging to the Zotero error console | + +## Usage + +### Manual Export + +- **Export all items**: **Tools → Export All Annotations to Org** +- **Export selected items**: Right-click one or more items → **Export Annotations to Org** + +### Automatic Export + +When enabled in preferences, exports happen automatically: + +- After a Zotero sync completes (only items with new/changed annotations are exported) +- When you close a PDF or EPUB reader tab + +### Output Format + +Files are written to the configured Notes Directory, named by citation key: + +``` +~/org/notes/smith2020.org +~/org/notes/doe2019.org +``` + +Each file contains: + +```org +#+title: The Full Title of the Referenced Work + +* Annotation heading + Converted annotation text... +``` + +Only items that have a citation key (via Better BibTeX or the `citationKey` field) will be exported. + +## For Maintainers + +This section covers building, developing, and releasing the plugin. + +### Architecture + +``` +src/ +├── index.ts # Entry point: mounts addon to Zotero global +├── addon.ts # Core addon class (logging, init, module wiring) +├── hooks.ts # Lifecycle hooks (startup/shutdown, menu actions) +└── modules/ + ├── prefs.ts # Typed read/write for all preferences + ├── exporter.ts # Attachment detection, sync checking, batch export + ├── converter.ts # Markdown → Org via pandoc, file I/O + ├── notifier.ts # Zotero event handlers (sync finish, tab close) + └── menu.ts # Menu registration (Zotero 7 + 8 compatible) + +addon/ # Static plugin files copied verbatim into the XPI +├── manifest.json +├── bootstrap.js # Zotero plugin lifecycle entry point +├── prefs.js # Default preference values +└── content/ + ├── preferences.xhtml # Preferences pane UI (XUL) + ├── scripts/preferences.js + ├── icons/ + └── locale/en-US/ # Fluent (.ftl) string bundles +``` + +The build system uses [zotero-plugin-scaffold](https://github.com/windingwind/zotero-plugin-scaffold) with esbuild, which bundles `src/` into a single `index.js` and packages everything as an XPI. + +### Prerequisites + +**With Nix (recommended):** + +```bash +# Requires Nix with flakes enabled +nix develop +``` + +This provides Node.js 20, npm, TypeScript, ESLint, Prettier, and Pandoc. + +**Without Nix:** + +- Node.js 20+ +- npm +- Pandoc (for manual testing) + +```bash +npm install +``` + +### Environment Setup + +Copy the environment template and fill in your local paths: + +```bash +cp .env.example .env +``` + +Edit `.env`: + +```bash +# Path to the Zotero binary (required for the dev server) +ZOTERO_PLUGIN_ZOTERO_BIN_PATH=/path/to/zotero + +# Path to a Zotero development profile directory +ZOTERO_PLUGIN_PROFILE_PATH=/path/to/zotero-dev-profile +``` + +Create a dedicated Zotero profile for development to avoid affecting your main library. + +### Development Workflow + +```bash +# Start the dev server — builds and hot-reloads the plugin into a running Zotero instance +npm start + +# One-off build — outputs XPI to .scaffold/build/ +npm run build + +# Lint TypeScript source +npm run lint +npm run lint:fix # auto-fix + +# Format source files +npm run format +npm run format:check # check without writing +``` + +The dev server (`npm start`) watches for changes in `src/` and `addon/`, rebuilds, and reloads the plugin inside Zotero automatically. Zotero must already be running with the configured profile. + +### Testing + +There are no automated tests. To test manually: + +1. `npm run build` to produce the XPI +2. Install the XPI in a Zotero 7 instance via **Tools → Add-ons → Install Add-on From File...** +3. Exercise the export functionality manually via the Tools menu and right-click context menu + +### Releasing + +Releases are created by pushing a version tag. The CI/CD pipeline (Gitea Actions) will build the XPI and publish a release automatically. + +To cut a release locally: + +```bash +npm run release +``` + +This runs [bumpp](https://github.com/antfu/bumpp) interactively to select the new version, then: + +1. Updates `package.json` version +2. Commits and tags the version +3. Pushes the commit and tag to the remote +4. Builds the XPI + +Once the tag is pushed, Gitea Actions takes over and creates the release with the XPI attached. + +### Code Style + +- **Formatter**: Prettier (config in `.prettierrc`) — double quotes, 2-space indent, semicolons +- **Linter**: ESLint with TypeScript-ESLint (config in `.eslintrc.js`) — strict mode +- **Naming**: PascalCase for classes/types, camelCase for functions/methods, UPPER_SNAKE_CASE for constants +- **Imports**: relative paths, `import type` for type-only imports, no namespace imports +- **Error handling**: always catch with a typed `error as Error`; use `_error` for non-critical ignores + +See `AGENTS.md` for the full style guide used during AI-assisted development. + +## License + +GPL-3.0-or-later + +## Credits + +Based on the [zotero-plugin-template](https://github.com/windingwind/zotero-plugin-template) by windingwind. diff --git a/addon/bootstrap.js b/addon/bootstrap.js new file mode 100644 index 0000000..430134a --- /dev/null +++ b/addon/bootstrap.js @@ -0,0 +1,70 @@ +/* eslint-disable no-undef */ + +var chromeHandle; + +async function startup({ id, version, rootURI }, reason) { + await Zotero.initializationPromise; + + try { + const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService( + Ci.amIAddonManagerStartup + ); + const manifestURI = Services.io.newURI(rootURI + "manifest.json"); + chromeHandle = aomStartup.registerChrome(manifestURI, [ + ["content", "orgexportannotations", rootURI + "content/"], + ]); + + Services.scriptloader.loadSubScript(rootURI + "content/scripts/index.js"); + + if (typeof Zotero.OrgExportAnnotations === "undefined") { + throw new Error("Zotero.OrgExportAnnotations not loaded from index.js"); + } + + await Zotero.OrgExportAnnotations.init({ id, version, rootURI }); + await Zotero.OrgExportAnnotations.hooks.onStartup(); + } catch (error) { + Zotero.logError("[OrgExportAnnotations] Startup failed: " + error); + throw error; + } +} + +async function onMainWindowLoad({ window }) { + try { + await Zotero.OrgExportAnnotations?.hooks?.onMainWindowLoad(window); + } catch (error) { + Zotero.logError("[OrgExportAnnotations] onMainWindowLoad failed: " + error); + } +} + +async function onMainWindowUnload({ window }) { + try { + await Zotero.OrgExportAnnotations?.hooks?.onMainWindowUnload(window); + } catch (error) { + Zotero.logError("[OrgExportAnnotations] onMainWindowUnload failed: " + error); + } +} + +function shutdown({ id, version, rootURI }, reason) { + if (reason === APP_SHUTDOWN) { + return; + } + + try { + Zotero.OrgExportAnnotations?.hooks?.onShutdown(); + } catch (error) { + Zotero.logError("[OrgExportAnnotations] onShutdown failed: " + error); + } + + try { + chromeHandle?.destruct(); + } catch (error) { + Zotero.logError("[OrgExportAnnotations] Failed to destruct chrome handle: " + error); + } + chromeHandle = null; + + delete Zotero.OrgExportAnnotations; +} + +function install() {} + +function uninstall() {} diff --git a/addon/content/icons/icon.svg b/addon/content/icons/icon.svg new file mode 100644 index 0000000..16cd525 --- /dev/null +++ b/addon/content/icons/icon.svg @@ -0,0 +1 @@ + diff --git a/addon/content/icons/icon@2x.svg b/addon/content/icons/icon@2x.svg new file mode 100644 index 0000000..113a97c --- /dev/null +++ b/addon/content/icons/icon@2x.svg @@ -0,0 +1 @@ + diff --git a/addon/content/icons/menu-icon.svg b/addon/content/icons/menu-icon.svg new file mode 100644 index 0000000..653dbf7 --- /dev/null +++ b/addon/content/icons/menu-icon.svg @@ -0,0 +1 @@ + diff --git a/addon/content/preferences.xhtml b/addon/content/preferences.xhtml new file mode 100644 index 0000000..2df1dd4 --- /dev/null +++ b/addon/content/preferences.xhtml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + +