mirror of
https://github.com/oilandrust/obsidian-gdocs.git
synced 2026-07-22 07:32:32 +00:00
Add GDocs Obsidian plugin for Google Drive shortcuts.
Opens .gdoc, .gsheet, and other Workspace shortcut files in Obsidian's Web Viewer by parsing JSON metadata (url, resource_id, or doc_id). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
db51a01e62
17 changed files with 1240 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.vscode
|
||||
node_modules
|
||||
main.js
|
||||
*.map
|
||||
data.json
|
||||
.DS_Store
|
||||
86
README.md
Normal file
86
README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# GDocs
|
||||
|
||||
Obsidian plugin that opens Google Drive shortcut files in Obsidian's **Web Viewer**.
|
||||
|
||||
When Google Drive for Desktop (or similar sync) stores Docs, Sheets, and other Workspace files in your vault, they appear as small JSON files (`.gdoc`, `.gsheet`, etc.). GDocs registers those extensions, shows them in the file explorer with their extension, and opens the linked Google URL in a tab when you click them.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Obsidian 1.8+** (desktop for Web Viewer)
|
||||
- **Web Viewer** core plugin enabled: Settings → Core plugins → **Web viewer**
|
||||
- Google Drive shortcut files in your vault (from Drive for Desktop, rclone, Synology Drive, etc.)
|
||||
|
||||
## Supported extensions
|
||||
|
||||
| Extension | Google app |
|
||||
|-----------|------------|
|
||||
| `.gdoc` | Docs |
|
||||
| `.gsheet` | Sheets |
|
||||
| `.gslides` | Slides |
|
||||
| `.gdraw` | Drawings |
|
||||
| `.gform` | Forms |
|
||||
| `.gtable` | Tables |
|
||||
| `.gscript` | Apps Script |
|
||||
| `.gjam` | Jamboard |
|
||||
|
||||
## Installation (development)
|
||||
|
||||
1. Clone this repo into your vault's `.obsidian/plugins/gdocs` folder, or symlink it there.
|
||||
2. Install dependencies and build:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. Enable **GDocs** under Settings → Community plugins.
|
||||
4. Reload Obsidian.
|
||||
|
||||
For development, run `npm run dev` to watch and rebuild.
|
||||
|
||||
## Usage
|
||||
|
||||
Click any `.gdoc`, `.gsheet`, or other supported shortcut in the file explorer. The plugin reads the `url` field from the JSON file and opens it in Web Viewer.
|
||||
|
||||
If Web Viewer is disabled, GDocs can open the link in your system browser instead (see plugin settings).
|
||||
|
||||
### Mobile
|
||||
|
||||
Web Viewer is not available on mobile. GDocs shows the URL with **Copy link** and **Open in browser** buttons.
|
||||
|
||||
## Shortcut file format
|
||||
|
||||
Shortcut files are JSON. Google Drive for Desktop typically uses:
|
||||
|
||||
```json
|
||||
{
|
||||
"doc_id": "1VxFROKm0zoJV2iH678wO94hZ0PY9fN_gAGOxhZacc8w",
|
||||
"email": "you@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
GDocs builds the Docs/Sheets URL from `doc_id` and the file extension (`.gdoc`, `.gsheet`, etc.).
|
||||
|
||||
Older or third-party sync tools may use:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://docs.google.com/spreadsheets/d/…/edit",
|
||||
"resource_id": "spreadsheet:…"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Files not visible in the explorer**
|
||||
Check Settings → Files & links → **Detect all file extensions**, and ensure `.gdoc` / `.gsheet` are not listed under **Excluded files**.
|
||||
|
||||
- **Opens in browser instead of Obsidian**
|
||||
Enable the Web Viewer core plugin, or turn on “Open in system browser when Web Viewer is off” in GDocs settings.
|
||||
|
||||
- **Warning about moving shortcut files**
|
||||
Do not move `.gdoc` / `.gsheet` files out of a syncing Google Drive folder; Google may delete the online document.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtinModules,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2021",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "gdocs",
|
||||
"name": "GDocs",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Open Google Drive shortcut files (.gdoc, .gsheet, etc.) in Obsidian's Web Viewer.",
|
||||
"author": "Olivier",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
625
package-lock.json
generated
Normal file
625
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,625 @@
|
|||
{
|
||||
"name": "obsidian-gdocs",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-gdocs",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.17",
|
||||
"esbuild": "0.25.5",
|
||||
"obsidian": "latest",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.5",
|
||||
"@esbuild/android-arm": "0.25.5",
|
||||
"@esbuild/android-arm64": "0.25.5",
|
||||
"@esbuild/android-x64": "0.25.5",
|
||||
"@esbuild/darwin-arm64": "0.25.5",
|
||||
"@esbuild/darwin-x64": "0.25.5",
|
||||
"@esbuild/freebsd-arm64": "0.25.5",
|
||||
"@esbuild/freebsd-x64": "0.25.5",
|
||||
"@esbuild/linux-arm": "0.25.5",
|
||||
"@esbuild/linux-arm64": "0.25.5",
|
||||
"@esbuild/linux-ia32": "0.25.5",
|
||||
"@esbuild/linux-loong64": "0.25.5",
|
||||
"@esbuild/linux-mips64el": "0.25.5",
|
||||
"@esbuild/linux-ppc64": "0.25.5",
|
||||
"@esbuild/linux-riscv64": "0.25.5",
|
||||
"@esbuild/linux-s390x": "0.25.5",
|
||||
"@esbuild/linux-x64": "0.25.5",
|
||||
"@esbuild/netbsd-arm64": "0.25.5",
|
||||
"@esbuild/netbsd-x64": "0.25.5",
|
||||
"@esbuild/openbsd-arm64": "0.25.5",
|
||||
"@esbuild/openbsd-x64": "0.25.5",
|
||||
"@esbuild/sunos-x64": "0.25.5",
|
||||
"@esbuild/win32-arm64": "0.25.5",
|
||||
"@esbuild/win32-ia32": "0.25.5",
|
||||
"@esbuild/win32-x64": "0.25.5"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.0.tgz",
|
||||
"integrity": "sha512-PHw5+SAPlJ0S3leFvJ0wgFg63Z3DavxL6+d1ll+8toXR2ZlYKc1rMWqdUv9LgUbTwPQUyY6yfhOMMivampRRiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "obsidian-gdocs",
|
||||
"version": "1.0.0",
|
||||
"description": "Open Google Drive shortcut files in Obsidian's Web Viewer",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.17",
|
||||
"esbuild": "0.25.5",
|
||||
"obsidian": "latest",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
14
src/constants.ts
Normal file
14
src/constants.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export const VIEW_TYPE_GDOCS = "gdocs-view";
|
||||
|
||||
export const DEFAULT_GDRIVE_EXTENSIONS = [
|
||||
"gdoc",
|
||||
"gsheet",
|
||||
"gslides",
|
||||
"gdraw",
|
||||
"gform",
|
||||
"gtable",
|
||||
"gscript",
|
||||
"gjam",
|
||||
] as const;
|
||||
|
||||
export type GdriveExtension = (typeof DEFAULT_GDRIVE_EXTENSIONS)[number];
|
||||
141
src/gdocs-view.ts
Normal file
141
src/gdocs-view.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import {
|
||||
FileView,
|
||||
Notice,
|
||||
Platform,
|
||||
WorkspaceLeaf,
|
||||
type TFile,
|
||||
} from "obsidian";
|
||||
import type GDocsPlugin from "./main";
|
||||
import { VIEW_TYPE_GDOCS } from "./constants";
|
||||
import { parseGdriveShortcut } from "./parse-gdrive-shortcut";
|
||||
|
||||
export class GDocsView extends FileView {
|
||||
plugin: GDocsPlugin;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: GDocsPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_GDOCS;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return this.file?.basename ?? "Google Drive";
|
||||
}
|
||||
|
||||
canAcceptExtension(extension: string): boolean {
|
||||
return this.plugin.settings.extensions.includes(extension.toLowerCase());
|
||||
}
|
||||
|
||||
async onLoadFile(file: TFile): Promise<void> {
|
||||
this.clearError();
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await this.app.vault.read(file);
|
||||
} catch {
|
||||
this.showError("Could not read file.", null);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseGdriveShortcut(raw, file.extension);
|
||||
if (!parsed.ok) {
|
||||
this.showError(parsed.error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.openUrl(parsed.url);
|
||||
return;
|
||||
}
|
||||
|
||||
async onUnloadFile(_file: TFile): Promise<void> {
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
private clearError(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private showError(message: string, url: string | null): void {
|
||||
this.clearError();
|
||||
const wrap = this.contentEl.createDiv({ cls: "gdocs-error" });
|
||||
wrap.createDiv({ cls: "gdocs-error-title", text: "Could not open Google shortcut" });
|
||||
wrap.createDiv({ cls: "gdocs-error-detail", text: message });
|
||||
if (url) {
|
||||
wrap.createDiv({ cls: "gdocs-error-url", text: url });
|
||||
}
|
||||
wrap.createEl("p", {
|
||||
text: "You can open the shortcut file as plain text from the file menu to inspect its contents.",
|
||||
});
|
||||
}
|
||||
|
||||
private async openUrl(url: string): Promise<void> {
|
||||
if (Platform.isMobile) {
|
||||
this.showMobileFallback(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isWebViewerEnabled()) {
|
||||
if (this.plugin.settings.openInBrowserIfNoWebViewer) {
|
||||
window.open(url, "_blank");
|
||||
return;
|
||||
}
|
||||
this.showError(
|
||||
"Web Viewer is disabled. Enable it under Settings → Core plugins → Web viewer, or turn on “Open in system browser when Web Viewer is off” in GDocs settings.",
|
||||
url,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.leaf.setViewState({
|
||||
type: "webviewer",
|
||||
state: { url, navigate: true },
|
||||
active: true,
|
||||
});
|
||||
} catch {
|
||||
if (this.plugin.settings.openInBrowserIfNoWebViewer) {
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
this.showError("Failed to open Web Viewer.", url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private showMobileFallback(url: string): void {
|
||||
this.clearError();
|
||||
const wrap = this.contentEl.createDiv({ cls: "gdocs-error" });
|
||||
wrap.createDiv({
|
||||
cls: "gdocs-error-title",
|
||||
text: "Web Viewer is not available on mobile",
|
||||
});
|
||||
wrap.createDiv({
|
||||
cls: "gdocs-error-detail",
|
||||
text: "Copy the link below and open it in your browser.",
|
||||
});
|
||||
wrap.createDiv({ cls: "gdocs-error-url", text: url });
|
||||
|
||||
const actions = wrap.createDiv({ cls: "gdocs-mobile-actions" });
|
||||
actions.createEl("button", { text: "Copy link" }).addEventListener("click", () => {
|
||||
void navigator.clipboard.writeText(url);
|
||||
new Notice("Link copied to clipboard");
|
||||
});
|
||||
actions.createEl("button", { text: "Open in browser" }).addEventListener("click", () => {
|
||||
window.open(url, "_blank");
|
||||
});
|
||||
}
|
||||
|
||||
private isWebViewerEnabled(): boolean {
|
||||
const internal = (
|
||||
this.app as AppWithInternalPlugins
|
||||
).internalPlugins?.getPluginById("web-viewer");
|
||||
return internal?.enabled === true;
|
||||
}
|
||||
}
|
||||
|
||||
interface AppWithInternalPlugins {
|
||||
internalPlugins?: {
|
||||
getPluginById(id: string): { enabled: boolean } | undefined;
|
||||
};
|
||||
}
|
||||
48
src/main.ts
Normal file
48
src/main.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { Plugin } from "obsidian";
|
||||
import { DEFAULT_GDRIVE_EXTENSIONS, VIEW_TYPE_GDOCS } from "./constants";
|
||||
import { GDocsView } from "./gdocs-view";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
GDocsSettingTab,
|
||||
type GDocsSettings,
|
||||
} from "./settings";
|
||||
|
||||
export default class GDocsPlugin extends Plugin {
|
||||
settings: GDocsSettings = DEFAULT_SETTINGS;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_GDOCS,
|
||||
(leaf) => new GDocsView(leaf, this),
|
||||
);
|
||||
|
||||
const extensions = this.getActiveExtensions();
|
||||
if (extensions.length > 0) {
|
||||
this.registerExtensions(extensions, VIEW_TYPE_GDOCS);
|
||||
}
|
||||
|
||||
this.addSettingTab(new GDocsSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE_GDOCS);
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
const loaded = await this.loadData();
|
||||
this.settings = { ...DEFAULT_SETTINGS, ...loaded };
|
||||
if (!Array.isArray(this.settings.extensions)) {
|
||||
this.settings.extensions = [...DEFAULT_GDRIVE_EXTENSIONS];
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
getActiveExtensions(): string[] {
|
||||
return this.settings.extensions.filter((ext) => ext.length > 0);
|
||||
}
|
||||
}
|
||||
112
src/parse-gdrive-shortcut.ts
Normal file
112
src/parse-gdrive-shortcut.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
export type ParseResult =
|
||||
| { ok: true; url: string }
|
||||
| { ok: false; error: string };
|
||||
|
||||
interface GdriveShortcutJson {
|
||||
url?: string;
|
||||
resource_id?: string;
|
||||
doc_id?: string;
|
||||
}
|
||||
|
||||
/** Google Drive for Desktop format (doc_id + file extension). */
|
||||
const EXTENSION_URL_TEMPLATES: Record<string, string> = {
|
||||
gdoc: "https://docs.google.com/document/d/{id}/edit",
|
||||
gsheet: "https://docs.google.com/spreadsheets/d/{id}/edit",
|
||||
gslides: "https://docs.google.com/presentation/d/{id}/edit",
|
||||
gdraw: "https://docs.google.com/drawings/d/{id}/edit",
|
||||
gform: "https://docs.google.com/forms/d/{id}/viewform",
|
||||
gtable: "https://docs.google.com/spreadsheets/d/{id}/edit",
|
||||
gscript: "https://script.google.com/home/projects/{id}/edit",
|
||||
gjam: "https://jamboard.google.com/d/{id}",
|
||||
};
|
||||
|
||||
const RESOURCE_URL_TEMPLATES: Record<string, string> = {
|
||||
document: "https://docs.google.com/document/d/{id}/edit",
|
||||
spreadsheet: "https://docs.google.com/spreadsheets/d/{id}/edit",
|
||||
presentation: "https://docs.google.com/presentation/d/{id}/edit",
|
||||
drawing: "https://docs.google.com/drawings/d/{id}/edit",
|
||||
form: "https://docs.google.com/forms/d/{id}/viewform",
|
||||
freebird: "https://jamboard.google.com/d/{id}",
|
||||
jam: "https://jamboard.google.com/d/{id}",
|
||||
script: "https://script.google.com/d/{id}/edit",
|
||||
table: "https://docs.google.com/spreadsheets/d/{id}/edit",
|
||||
};
|
||||
|
||||
function normalizeUrl(url: string): string | null {
|
||||
const trimmed = url.trim();
|
||||
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
||||
return trimmed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function urlFromResourceId(resourceId: string): string | null {
|
||||
const colon = resourceId.indexOf(":");
|
||||
if (colon <= 0) {
|
||||
return null;
|
||||
}
|
||||
const kind = resourceId.slice(0, colon).toLowerCase();
|
||||
const id = resourceId.slice(colon + 1).trim();
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const template = RESOURCE_URL_TEMPLATES[kind];
|
||||
if (template) {
|
||||
return template.replace("{id}", id);
|
||||
}
|
||||
return `https://drive.google.com/open?id=${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
function urlFromDocId(docId: string, extension?: string): string | null {
|
||||
const id = docId.trim();
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
if (extension) {
|
||||
const template = EXTENSION_URL_TEMPLATES[extension.toLowerCase()];
|
||||
if (template) {
|
||||
return template.replace("{id}", id);
|
||||
}
|
||||
}
|
||||
return `https://drive.google.com/open?id=${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
export function parseGdriveShortcut(raw: string, extension?: string): ParseResult {
|
||||
const trimmed = raw.replace(/^\uFEFF/, "").trim();
|
||||
if (!trimmed) {
|
||||
return { ok: false, error: "File is empty." };
|
||||
}
|
||||
|
||||
let data: GdriveShortcutJson;
|
||||
try {
|
||||
data = JSON.parse(trimmed) as GdriveShortcutJson;
|
||||
} catch {
|
||||
return { ok: false, error: "Invalid JSON. Expected a Google Drive shortcut file." };
|
||||
}
|
||||
|
||||
if (typeof data.url === "string") {
|
||||
const url = normalizeUrl(data.url);
|
||||
if (url) {
|
||||
return { ok: true, url };
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.resource_id === "string") {
|
||||
const url = urlFromResourceId(data.resource_id);
|
||||
if (url) {
|
||||
return { ok: true, url };
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.doc_id === "string") {
|
||||
const url = urlFromDocId(data.doc_id, extension);
|
||||
if (url) {
|
||||
return { ok: true, url };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: "No valid url, doc_id, or resource_id found in shortcut file.",
|
||||
};
|
||||
}
|
||||
48
src/settings.ts
Normal file
48
src/settings.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import type GDocsPlugin from "./main";
|
||||
import { DEFAULT_GDRIVE_EXTENSIONS } from "./constants";
|
||||
|
||||
export interface GDocsSettings {
|
||||
openInBrowserIfNoWebViewer: boolean;
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: GDocsSettings = {
|
||||
openInBrowserIfNoWebViewer: true,
|
||||
extensions: [...DEFAULT_GDRIVE_EXTENSIONS],
|
||||
};
|
||||
|
||||
export class GDocsSettingTab extends PluginSettingTab {
|
||||
plugin: GDocsPlugin;
|
||||
|
||||
constructor(app: App, plugin: GDocsPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "GDocs settings" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Open in system browser when Web Viewer is off")
|
||||
.setDesc(
|
||||
"If the Web Viewer core plugin is disabled, open the Google document in your default browser instead of showing an error.",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.openInBrowserIfNoWebViewer)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.openInBrowserIfNoWebViewer = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("p", {
|
||||
text: `Supported extensions: ${DEFAULT_GDRIVE_EXTENSIONS.map((e) => "." + e).join(", ")}`,
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
}
|
||||
}
|
||||
27
styles.css
Normal file
27
styles.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.gdocs-error {
|
||||
padding: 1.5rem;
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.gdocs-error-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.gdocs-error-detail {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gdocs-error-url {
|
||||
font-size: 0.85em;
|
||||
word-break: break-all;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gdocs-mobile-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
1
test/fixtures/sample-drive-desktop.gdoc
vendored
Normal file
1
test/fixtures/sample-drive-desktop.gdoc
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"":"WARNING! DO NOT EDIT THIS FILE! ANY CHANGES MADE WILL BE LOST!","doc_id":"1VxFROKm0zoJV2iH678wO94hZ0PY9fN_gAGOxhZacc8w","resource_key":"","email":"o.rouiller@gmail.com"}
|
||||
1
test/fixtures/sample.gsheet
vendored
Normal file
1
test/fixtures/sample.gsheet
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"url":"https://docs.google.com/spreadsheets/d/1abc123/edit","resource_id":"spreadsheet:1abc123"}
|
||||
22
test/parse-gdrive-shortcut.test.mjs
Normal file
22
test/parse-gdrive-shortcut.test.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Inline parser checks (mirrors src/parse-gdrive-shortcut.ts logic for CI-less smoke test)
|
||||
function parse(raw) {
|
||||
const trimmed = raw.replace(/^\uFEFF/, "").trim();
|
||||
const data = JSON.parse(trimmed);
|
||||
if (typeof data.url === "string" && /^https?:\/\//.test(data.url.trim())) {
|
||||
return { ok: true, url: data.url.trim() };
|
||||
}
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url));
|
||||
const fixture = readFileSync(join(dir, "fixtures", "sample.gsheet"), "utf8");
|
||||
const r = parse(fixture);
|
||||
if (!r.ok) {
|
||||
console.error("parse failed");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("parse-gdrive-shortcut smoke test passed");
|
||||
13
test/parse-test.mjs
Normal file
13
test/parse-test.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Minimal inline test (parser is bundled for Obsidian; duplicate logic check via build + manual fixtures)
|
||||
const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
|
||||
const sample = readFileSync(join(fixturesDir, "sample.gsheet"), "utf8");
|
||||
const data = JSON.parse(sample);
|
||||
if (!data.url?.startsWith("https://")) {
|
||||
console.error("Fixture invalid");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Fixture OK:", data.url);
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2021",
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["ES2021", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue