orca-gallery-panel 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -59
- package/gallery.config.schema.json +26 -0
- package/lib/build.js +8 -3
- package/lib/manifest.js +14 -3
- package/lib/options.js +36 -2
- package/lib/panel/config.js +43 -2
- package/lib/panel/i18n.js +16 -0
- package/lib/panel/main.js +261 -52
- package/lib/panel/panel.css +167 -0
- package/lib/panel/template.html +44 -9
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -17,11 +17,14 @@ can change without leaving Orca.
|
|
|
17
17
|
|
|
18
18
|
- **Slideshow**: shuffles your images and changes them on an interval you choose. Every image
|
|
19
19
|
shows once before any repeats, and the same one never appears twice in a row.
|
|
20
|
-
- **
|
|
21
|
-
|
|
20
|
+
- **Several at once**: `--layout stack` stacks 2 to 4 images top to bottom, with no image on
|
|
21
|
+
screen twice. They change one at a time, taking turns, or all together with `--rotate all`.
|
|
22
|
+
- **Controls** on hover: previous, pause/resume, next, a position counter (slideshow only),
|
|
23
|
+
interval, display settings and fit/fill.
|
|
22
24
|
- **Settings in the panel**: click the clock to type the seconds between images (`0` stops the
|
|
23
|
-
automatic change), or the sliders to turn on a blurred background, a slow Ken Burns zoom
|
|
24
|
-
the image's name
|
|
25
|
+
automatic change), or the sliders to turn on a blurred background, a slow Ken Burns zoom,
|
|
26
|
+
the image's name or the stacked layout, and to pick how many images it stacks and whether they
|
|
27
|
+
change one at a time. Changes last until Orca reloads the panel, because Orca gives panels no
|
|
25
28
|
storage; the defaults come from your build settings.
|
|
26
29
|
- **Blurred background**: an image that doesn't fill the panel sits on a blurred copy of itself
|
|
27
30
|
instead of empty bars.
|
|
@@ -38,88 +41,120 @@ can change without leaving Orca.
|
|
|
38
41
|
|
|
39
42
|
## Quick start
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
### 1. Try it from Orca
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
[Open in Orca](https://onorca.dev/plugins/jerrazuw.gallery-panel-demo?catalog=community) takes you
|
|
47
|
+
to **Gallery Panel (demo)** in Orca's plugin directory; the first time, Orca asks to add its
|
|
48
|
+
community catalog. Click **Install**, then open the panel from the right sidebar. It rotates
|
|
49
|
+
through a few sample images and explains, inside the panel, how to show your own.
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
(here `~/orca-gallery/dist`). Then open the panel from the right sidebar.
|
|
51
|
+
Orca installs plugins from the directory as they are, so the listed one can only carry sample
|
|
52
|
+
images. Your own photos take one command, below.
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
repository. With `npx`, replace `npm run build --` with `npx orca-gallery-panel` and run it from
|
|
54
|
-
the same folder each time, so it finds your `gallery.config.json` and rebuilds the same `dist/`.
|
|
54
|
+
### 2. Show your own photos
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
You need [Node.js](https://nodejs.org) 22.18 or later and Orca 1.4.160 or later. In a terminal,
|
|
57
|
+
run this with the path to your folder instead of `~/Pictures/wallpapers`:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
|
|
61
|
-
cd orca-gallery-panel
|
|
62
|
-
npm install
|
|
63
|
-
npm run build -- ~/Pictures/wallpapers --title "Wallpapers"
|
|
60
|
+
npx orca-gallery-panel ~/Pictures/wallpapers --out ~/orca-gallery --watch
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
It packs every image in the folder into an Orca plugin in `~/orca-gallery`, then keeps watching
|
|
64
|
+
the folder. In Orca, go to **Settings → Plugins → Development**, click **Add path** and enter the
|
|
65
|
+
full path of that folder (`/Users/<you>/orca-gallery` on a Mac). Open the panel from the right
|
|
66
|
+
sidebar.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
If you installed the demo, uninstall it from **Settings → Plugins** so you don't have two
|
|
69
|
+
galleries.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
### 3. Add photos whenever you like
|
|
72
|
+
|
|
73
|
+
While the command runs, copy photos into your folder or delete them, and the panel updates by
|
|
74
|
+
itself within a few seconds: the plugin is rebuilt and Orca reloads it. Copying 20 photos at once
|
|
75
|
+
triggers a single rebuild. A good place to leave it running is a terminal tab inside Orca.
|
|
76
|
+
|
|
77
|
+
`Ctrl+C` stops it. Next time, run the same command again: it picks up whatever changed in the
|
|
78
|
+
meantime and goes back to watching. Without `--watch`, it builds once and exits, so run it again
|
|
79
|
+
after changing your photos.
|
|
80
|
+
|
|
81
|
+
### Saving your settings
|
|
82
|
+
|
|
83
|
+
Instead of typing the options every time, put them in a `gallery.config.json` in the folder you
|
|
84
|
+
run the command from (your home folder, for example):
|
|
73
85
|
|
|
74
86
|
```json
|
|
75
87
|
{
|
|
76
|
-
"$schema": "
|
|
88
|
+
"$schema": "https://unpkg.com/orca-gallery-panel/gallery.config.schema.json",
|
|
77
89
|
"folder": "~/Pictures/wallpapers",
|
|
90
|
+
"out": "~/orca-gallery",
|
|
78
91
|
"title": "Wallpapers",
|
|
79
92
|
"interval": 60
|
|
80
93
|
}
|
|
81
94
|
```
|
|
82
95
|
|
|
83
|
-
From then on, `
|
|
84
|
-
|
|
85
|
-
support JSON Schema. Options passed on the command line still override the file for that
|
|
96
|
+
From then on, `npx orca-gallery-panel --watch` is enough, and editing the file while it runs
|
|
97
|
+
rebuilds the panel too. The `$schema` line gives you autocomplete and validation in editors that
|
|
98
|
+
support JSON Schema. Options passed on the command line still override the file for that run,
|
|
86
99
|
and `--config <file>` reads a different one.
|
|
87
100
|
|
|
88
|
-
|
|
101
|
+
If you installed the plugin with **Install plugin → Local folder** instead of **Development**,
|
|
102
|
+
Orca keeps a copy, so you need to install it again after each build.
|
|
103
|
+
|
|
104
|
+
### Working from a clone
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
**Development** reloads on its own.
|
|
106
|
+
To change the code, or to try the bundled samples with `npm run build -- examples/images`:
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/jerrazuw/orca-gallery-panel.git
|
|
110
|
+
cd orca-gallery-panel
|
|
111
|
+
npm install
|
|
112
|
+
npm run build -- ~/Pictures/wallpapers --watch
|
|
113
|
+
```
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
Here `npm run build --` takes the place of `npx orca-gallery-panel`, and the plugin goes to
|
|
116
|
+
`dist/` unless you pass `--out`. `gallery.config.json` at the root of the clone is ignored by
|
|
117
|
+
git, so your paths stay on your machine; `gallery.config.example.json` is a starting point.
|
|
99
118
|
|
|
100
119
|
## Options
|
|
101
120
|
|
|
102
|
-
| Option | Config key | Default
|
|
103
|
-
| ----------------------- | ----------- |
|
|
104
|
-
| `[folder]` | `folder` | `images`
|
|
105
|
-
| `-o, --out <dir>` | `out` | `dist`
|
|
106
|
-
| `-t, --title <text>` | `title` | `Gallery`
|
|
107
|
-
| `-i, --interval <sec>` | `interval` | `180`
|
|
108
|
-
| `-s, --max-size <px>` | `maxSize` | `800`
|
|
109
|
-
| `-q, --quality <1-100>` | `quality` | `80`
|
|
110
|
-
| `--fit <mode>` | `fit` | `contain`
|
|
111
|
-
| `--effect <name>` | `effect` | `none`
|
|
112
|
-
| `--no-blur` | `blur` | `true`
|
|
113
|
-
| `--caption` | `caption` | `false`
|
|
114
|
-
| `-r, --recursive` | `recursive` | `false`
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
121
|
+
| Option | Config key | Default | Description |
|
|
122
|
+
| ----------------------- | ----------- | ----------- | -------------------------------------------------- |
|
|
123
|
+
| `[folder]` | `folder` | `images` | Folder with your images. |
|
|
124
|
+
| `-o, --out <dir>` | `out` | `dist` | Output folder for the plugin. |
|
|
125
|
+
| `-t, --title <text>` | `title` | `Gallery` | Panel title shown in Orca. |
|
|
126
|
+
| `-i, --interval <sec>` | `interval` | `180` | Seconds between images. `0` disables rotation. |
|
|
127
|
+
| `-s, --max-size <px>` | `maxSize` | `800` | Longest side of each image, in pixels. |
|
|
128
|
+
| `-q, --quality <1-100>` | `quality` | `80` | WebP quality. |
|
|
129
|
+
| `--fit <mode>` | `fit` | `contain` | Initial mode: `contain` (whole image) or `cover`. |
|
|
130
|
+
| `--effect <name>` | `effect` | `none` | `none`, or `kenburns` for a slow zoom. |
|
|
131
|
+
| `--no-blur` | `blur` | `true` | Blurred background behind images. |
|
|
132
|
+
| `--caption` | `caption` | `false` | Show the image's file name. |
|
|
133
|
+
| `-r, --recursive` | `recursive` | `false` | Include images in subfolders. |
|
|
134
|
+
| `--layout <mode>` | `layout` | `slideshow` | `slideshow`, or `stack` for several at once. |
|
|
135
|
+
| `--count <2-4>` | `count` | `3` | Images stacked at once with `stack`. |
|
|
136
|
+
| `--rotate <mode>` | `rotate` | `one` | With `stack`: change `one` per interval, or `all`. |
|
|
137
|
+
| | `galleries` | | Several galleries; see above. |
|
|
138
|
+
| `-w, --watch` | | | Rebuild when images or the config file change. |
|
|
139
|
+
| `-c, --config <file>` | | | Config file to read instead of the default. |
|
|
118
140
|
|
|
119
141
|
Precedence: command line, then the gallery's own settings, then the top level of
|
|
120
142
|
`gallery.config.json`, then the defaults. Paths in the config file are relative to the file, and
|
|
121
|
-
`~` means your home folder. The `fit`, `interval`, `blur`, `effect
|
|
122
|
-
only where the panel starts: you can change them from the panel
|
|
143
|
+
`~` means your home folder. The `fit`, `interval`, `blur`, `effect`, `caption`, `layout`,
|
|
144
|
+
`count` and `rotate` settings are only where the panel starts: you can change them from the panel
|
|
145
|
+
itself.
|
|
146
|
+
|
|
147
|
+
### Several images at once
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx orca-gallery-panel ~/Pictures/wallpapers --out ~/orca-gallery --layout stack --watch
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The panel splits into three images stacked top to bottom (`--count 2` to `4` changes that), never
|
|
154
|
+
the same image twice. Every interval one of them changes, top to bottom in turn, so each stays up
|
|
155
|
+
for three intervals; with `--rotate all` the whole set changes at once. Next and previous swap the
|
|
156
|
+
whole set, and clicking an image replaces just that one. With fewer images than places, it stacks
|
|
157
|
+
as many as there are.
|
|
123
158
|
|
|
124
159
|
### Several galleries
|
|
125
160
|
|
|
@@ -182,6 +217,7 @@ npm run preview # writes dist/preview.html: the built panel inside an Orca-
|
|
|
182
217
|
| `src/preview.ts` | Reproduces Orca's panel shell so the panel can be checked in a browser. |
|
|
183
218
|
| `test/` | Unit tests (`node:test`) and browser tests (`test/e2e/`, Playwright). |
|
|
184
219
|
| `examples/images/` | Sample images, made by `scripts/generate-examples.ts`. |
|
|
220
|
+
| `plugin/` | The demo build listed in Orca's plugin directory, by `npm run build:demo`. |
|
|
185
221
|
|
|
186
222
|
TypeScript runs directly on Node through
|
|
187
223
|
[type stripping](https://nodejs.org/api/typescript.html#type-stripping), so there is no compile
|
|
@@ -190,10 +226,13 @@ compiled to `lib/` by `npm run build:lib`, which `npm pack` runs on its own. CI
|
|
|
190
226
|
macOS and Windows with Node 22.18, 22 and 24, and installs the packed package in an empty
|
|
191
227
|
project to check that the command works.
|
|
192
228
|
|
|
193
|
-
##
|
|
229
|
+
## In Orca's plugin directory
|
|
194
230
|
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
Orca lists a plugin from a tagged commit of its repository and installs it as it is, with no build
|
|
232
|
+
step, so it cannot pack your photos. The listed plugin is the build in `plugin/`: the sample images
|
|
233
|
+
with a note on how to load your own, under its own id (`gallery-panel-demo`) so it can sit next to
|
|
234
|
+
the plugin you build. `npm run build:demo` writes it, and `npm version` reruns that for every
|
|
235
|
+
release.
|
|
197
236
|
|
|
198
237
|
## Contributing
|
|
199
238
|
|
|
@@ -67,6 +67,23 @@
|
|
|
67
67
|
"description": "Include images in subfolders.",
|
|
68
68
|
"default": false
|
|
69
69
|
},
|
|
70
|
+
"layout": {
|
|
71
|
+
"enum": ["slideshow", "stack"],
|
|
72
|
+
"description": "slideshow shows one image at a time; stack shows several stacked top to bottom.",
|
|
73
|
+
"default": "slideshow"
|
|
74
|
+
},
|
|
75
|
+
"count": {
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"minimum": 2,
|
|
78
|
+
"maximum": 4,
|
|
79
|
+
"description": "Images shown at once with the stack layout.",
|
|
80
|
+
"default": 3
|
|
81
|
+
},
|
|
82
|
+
"rotate": {
|
|
83
|
+
"enum": ["one", "all"],
|
|
84
|
+
"description": "With the stack layout: change one image per interval, taking turns, or all of them at once.",
|
|
85
|
+
"default": "one"
|
|
86
|
+
},
|
|
70
87
|
"out": {
|
|
71
88
|
"type": "string",
|
|
72
89
|
"minLength": 1,
|
|
@@ -120,6 +137,15 @@
|
|
|
120
137
|
},
|
|
121
138
|
"recursive": {
|
|
122
139
|
"$ref": "#/properties/recursive"
|
|
140
|
+
},
|
|
141
|
+
"layout": {
|
|
142
|
+
"$ref": "#/properties/layout"
|
|
143
|
+
},
|
|
144
|
+
"count": {
|
|
145
|
+
"$ref": "#/properties/count"
|
|
146
|
+
},
|
|
147
|
+
"rotate": {
|
|
148
|
+
"$ref": "#/properties/rotate"
|
|
123
149
|
}
|
|
124
150
|
}
|
|
125
151
|
}
|
package/lib/build.js
CHANGED
|
@@ -48,7 +48,7 @@ async function assertFolder(folder) {
|
|
|
48
48
|
if (!info?.isDirectory())
|
|
49
49
|
throw new BuildError(`Image folder not found: ${folder}`);
|
|
50
50
|
}
|
|
51
|
-
async function buildGallery(gallery, logger) {
|
|
51
|
+
async function buildGallery(gallery, logger, demo) {
|
|
52
52
|
await assertFolder(gallery.input);
|
|
53
53
|
const files = await findImages(gallery.input, gallery.recursive);
|
|
54
54
|
const skipped = [];
|
|
@@ -87,6 +87,10 @@ async function buildGallery(gallery, logger) {
|
|
|
87
87
|
blur: gallery.blur,
|
|
88
88
|
effect: gallery.effect,
|
|
89
89
|
caption: gallery.caption,
|
|
90
|
+
layout: gallery.layout,
|
|
91
|
+
count: gallery.count,
|
|
92
|
+
rotate: gallery.rotate,
|
|
93
|
+
demo,
|
|
90
94
|
});
|
|
91
95
|
const panelBytes = Buffer.byteLength(html);
|
|
92
96
|
if (panelBytes > PANEL_ENTRY_MAX_BYTES) {
|
|
@@ -104,8 +108,9 @@ async function removeStalePanels(outDir, keep) {
|
|
|
104
108
|
export async function buildPlugin(options, logger = console) {
|
|
105
109
|
const entries = panelEntries(options.galleries.map((gallery) => gallery.title));
|
|
106
110
|
const built = [];
|
|
111
|
+
const demo = options.demo ?? false;
|
|
107
112
|
for (const gallery of options.galleries)
|
|
108
|
-
built.push(await buildGallery(gallery, logger));
|
|
113
|
+
built.push(await buildGallery(gallery, logger, demo));
|
|
109
114
|
const totalBytes = built.reduce((sum, { html }) => sum + Buffer.byteLength(html), 0);
|
|
110
115
|
if (totalBytes > PLUGIN_MAX_BYTES) {
|
|
111
116
|
throw new BuildError(`The galleries add up to ${formatBytes(totalBytes)}, over Orca's ${formatBytes(PLUGIN_MAX_BYTES)} limit for a plugin.`);
|
|
@@ -113,7 +118,7 @@ export async function buildPlugin(options, logger = console) {
|
|
|
113
118
|
await mkdir(options.outDir, { recursive: true });
|
|
114
119
|
await Promise.all([
|
|
115
120
|
...built.map(({ html }, i) => writeFile(join(options.outDir, entries[i]?.entry ?? ''), html)),
|
|
116
|
-
writeFile(join(options.outDir, MANIFEST_FILE), `${JSON.stringify(createManifest(options.name, entries), null, 2)}\n`),
|
|
121
|
+
writeFile(join(options.outDir, MANIFEST_FILE), `${JSON.stringify(createManifest(options.name, entries, { demo }), null, 2)}\n`),
|
|
117
122
|
]);
|
|
118
123
|
await removeStalePanels(options.outDir, new Set(entries.map(({ entry }) => entry)));
|
|
119
124
|
return {
|
package/lib/manifest.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { pkg } from "./package-info.js";
|
|
2
2
|
export const PLUGIN_ID = 'gallery-panel';
|
|
3
|
+
/**
|
|
4
|
+
* The sample build in plugin/, listed in Orca's plugin directory. A different id
|
|
5
|
+
* lets it sit next to the plugin people build from their own photos.
|
|
6
|
+
*/
|
|
7
|
+
export const DEMO_PLUGIN_ID = 'gallery-panel-demo';
|
|
8
|
+
export const DEMO_NAME = 'Gallery Panel (demo)';
|
|
9
|
+
export const DEMO_TITLE = 'Gallery (demo)';
|
|
10
|
+
export const DEMO_DESCRIPTION = 'Background images for Orca: a side panel that rotates through your pictures. This install shows sample images; the panel explains how to show a folder of your own photos that updates as you add more.';
|
|
11
|
+
/** Square plugin icon, written next to the demo manifest. */
|
|
12
|
+
export const DEMO_ICON = 'icon.png';
|
|
3
13
|
export const PUBLISHER = 'jerrazuw';
|
|
4
14
|
export const PANEL_ID = 'gallery';
|
|
5
15
|
export const PANEL_ENTRY = 'panel.html';
|
|
@@ -41,17 +51,18 @@ export function panelEntries(titles) {
|
|
|
41
51
|
export function isGeneratedPanelFile(file) {
|
|
42
52
|
return file === PANEL_ENTRY || /^panel-[a-z0-9]+(?:-[a-z0-9]+)*\.html$/.test(file);
|
|
43
53
|
}
|
|
44
|
-
export function createManifest(name, panels) {
|
|
54
|
+
export function createManifest(name, panels, { demo = false } = {}) {
|
|
45
55
|
return {
|
|
46
56
|
manifestVersion: 1,
|
|
47
|
-
id: PLUGIN_ID,
|
|
57
|
+
id: demo ? DEMO_PLUGIN_ID : PLUGIN_ID,
|
|
48
58
|
publisher: PUBLISHER,
|
|
49
59
|
name,
|
|
50
60
|
version: pkg.version,
|
|
51
|
-
description: pkg.description,
|
|
61
|
+
description: demo ? DEMO_DESCRIPTION : pkg.description,
|
|
52
62
|
author: pkg.author,
|
|
53
63
|
repository: pkg.repository.url.replace(/^git\+/, ''),
|
|
54
64
|
engines: { orca: `>=${MIN_ORCA_VERSION}` },
|
|
65
|
+
...(demo ? { icon: DEMO_ICON } : {}),
|
|
55
66
|
pluginApi: 1,
|
|
56
67
|
contributes: {
|
|
57
68
|
panels: panels.map(({ id, title, entry }) => ({ id, title, icon: PANEL_ICON, entry })),
|
package/lib/options.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { basename, resolve } from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { EFFECTS, FIT_MODES, isEffect, isFitMode, MAX_INTERVAL_SECONDS, } from "./panel/config.js";
|
|
4
|
+
import { EFFECTS, FIT_MODES, isEffect, isFitMode, isLayout, isRotation, LAYOUTS, MAX_INTERVAL_SECONDS, MAX_STACK_COUNT, MIN_STACK_COUNT, ROTATIONS, } from "./panel/config.js";
|
|
5
5
|
export class UsageError extends Error {
|
|
6
6
|
name = 'UsageError';
|
|
7
7
|
}
|
|
@@ -18,6 +18,9 @@ export const DEFAULTS = {
|
|
|
18
18
|
effect: 'none',
|
|
19
19
|
caption: false,
|
|
20
20
|
recursive: false,
|
|
21
|
+
layout: 'slideshow',
|
|
22
|
+
count: 3,
|
|
23
|
+
rotate: 'one',
|
|
21
24
|
};
|
|
22
25
|
export const GALLERY_KEYS = [
|
|
23
26
|
'folder',
|
|
@@ -30,6 +33,9 @@ export const GALLERY_KEYS = [
|
|
|
30
33
|
'effect',
|
|
31
34
|
'caption',
|
|
32
35
|
'recursive',
|
|
36
|
+
'layout',
|
|
37
|
+
'count',
|
|
38
|
+
'rotate',
|
|
33
39
|
];
|
|
34
40
|
export const SETTING_KEYS = [
|
|
35
41
|
...GALLERY_KEYS,
|
|
@@ -40,6 +46,7 @@ const LIMITS = {
|
|
|
40
46
|
interval: [0, MAX_INTERVAL_SECONDS],
|
|
41
47
|
maxSize: [16, 8192],
|
|
42
48
|
quality: [1, 100],
|
|
49
|
+
count: [MIN_STACK_COUNT, MAX_STACK_COUNT],
|
|
43
50
|
};
|
|
44
51
|
export const HELP = `Build an Orca side panel plugin from a folder of images.
|
|
45
52
|
|
|
@@ -60,6 +67,9 @@ Options:
|
|
|
60
67
|
--effect <name> ${EFFECTS.join(' | ')} (default: ${DEFAULTS.effect})
|
|
61
68
|
--no-blur No blurred background behind images that don't fill the panel
|
|
62
69
|
--caption Show the image name
|
|
70
|
+
--layout <mode> ${LAYOUTS.join(' | ')}: one image, or several stacked (default: ${DEFAULTS.layout})
|
|
71
|
+
--count <2-4> Images stacked at once with --layout stack (default: ${String(DEFAULTS.count)})
|
|
72
|
+
--rotate <mode> With --layout stack, change ${ROTATIONS.join(' | ')} image(s) per interval (default: ${DEFAULTS.rotate})
|
|
63
73
|
-r, --recursive Include images in subfolders
|
|
64
74
|
-w, --watch Rebuild whenever the images or the config file change
|
|
65
75
|
-c, --config <file> Read settings from this file (default: ./${CONFIG_FILE} if it exists)
|
|
@@ -95,7 +105,7 @@ export function validateGallerySettings(raw, name) {
|
|
|
95
105
|
throw new UsageError(`${name('title')} must be at most 256 characters`);
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
|
-
for (const key of ['interval', 'maxSize', 'quality']) {
|
|
108
|
+
for (const key of ['interval', 'maxSize', 'quality', 'count']) {
|
|
99
109
|
const value = raw[key];
|
|
100
110
|
if (value === undefined)
|
|
101
111
|
continue;
|
|
@@ -117,6 +127,18 @@ export function validateGallerySettings(raw, name) {
|
|
|
117
127
|
}
|
|
118
128
|
settings.effect = raw['effect'];
|
|
119
129
|
}
|
|
130
|
+
if (raw['layout'] !== undefined) {
|
|
131
|
+
if (!isLayout(raw['layout'])) {
|
|
132
|
+
throw new UsageError(`${name('layout')} must be one of ${LAYOUTS.join(', ')}, got ${JSON.stringify(raw['layout'])}`);
|
|
133
|
+
}
|
|
134
|
+
settings.layout = raw['layout'];
|
|
135
|
+
}
|
|
136
|
+
if (raw['rotate'] !== undefined) {
|
|
137
|
+
if (!isRotation(raw['rotate'])) {
|
|
138
|
+
throw new UsageError(`${name('rotate')} must be one of ${ROTATIONS.join(', ')}, got ${JSON.stringify(raw['rotate'])}`);
|
|
139
|
+
}
|
|
140
|
+
settings.rotate = raw['rotate'];
|
|
141
|
+
}
|
|
120
142
|
for (const key of ['blur', 'caption', 'recursive']) {
|
|
121
143
|
if (raw[key] !== undefined)
|
|
122
144
|
settings[key] = validateBoolean(raw[key], name(key));
|
|
@@ -163,6 +185,9 @@ const FLAG_NAMES = {
|
|
|
163
185
|
blur: '--no-blur',
|
|
164
186
|
caption: '--caption',
|
|
165
187
|
recursive: '--recursive',
|
|
188
|
+
layout: '--layout',
|
|
189
|
+
count: '--count',
|
|
190
|
+
rotate: '--rotate',
|
|
166
191
|
};
|
|
167
192
|
/** Numeric flags arrive as text; anything that is not a plain number is kept so validation reports it. */
|
|
168
193
|
function numberFlag(value) {
|
|
@@ -188,6 +213,9 @@ export function parseCommand(argv) {
|
|
|
188
213
|
'no-blur': { type: 'boolean' },
|
|
189
214
|
caption: { type: 'boolean' },
|
|
190
215
|
recursive: { type: 'boolean', short: 'r' },
|
|
216
|
+
layout: { type: 'string' },
|
|
217
|
+
count: { type: 'string' },
|
|
218
|
+
rotate: { type: 'string' },
|
|
191
219
|
watch: { type: 'boolean', short: 'w', default: false },
|
|
192
220
|
config: { type: 'string', short: 'c' },
|
|
193
221
|
help: { type: 'boolean', short: 'h', default: false },
|
|
@@ -218,6 +246,9 @@ export function parseCommand(argv) {
|
|
|
218
246
|
blur: values['no-blur'] === true ? false : undefined,
|
|
219
247
|
caption: values.caption,
|
|
220
248
|
recursive: values.recursive,
|
|
249
|
+
layout: values.layout,
|
|
250
|
+
count: numberFlag(values.count),
|
|
251
|
+
rotate: values.rotate,
|
|
221
252
|
}, (key) => FLAG_NAMES[key] ?? key);
|
|
222
253
|
return { kind: 'build', settings, configPath: values.config, watch: values.watch };
|
|
223
254
|
}
|
|
@@ -253,6 +284,9 @@ function galleryOptions(layers, cwd, title) {
|
|
|
253
284
|
effect: pick('effect').value,
|
|
254
285
|
caption: pick('caption').value,
|
|
255
286
|
recursive: pick('recursive').value,
|
|
287
|
+
layout: pick('layout').value,
|
|
288
|
+
count: pick('count').value,
|
|
289
|
+
rotate: pick('rotate').value,
|
|
256
290
|
};
|
|
257
291
|
}
|
|
258
292
|
/**
|
package/lib/panel/config.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export const FIT_MODES = ['contain', 'cover'];
|
|
2
2
|
export const EFFECTS = ['none', 'kenburns'];
|
|
3
|
+
/** slideshow: one image at a time. stack: several images stacked top to bottom. */
|
|
4
|
+
export const LAYOUTS = ['slideshow', 'stack'];
|
|
5
|
+
/** How a stack changes: one image per interval, taking turns, or all of them at once. */
|
|
6
|
+
export const ROTATIONS = ['one', 'all'];
|
|
7
|
+
export const MIN_STACK_COUNT = 2;
|
|
8
|
+
export const MAX_STACK_COUNT = 4;
|
|
3
9
|
export const CONFIG_ELEMENT_ID = 'gallery-config';
|
|
4
10
|
export const MAX_INTERVAL_SECONDS = 86_400;
|
|
5
11
|
export function isFitMode(value) {
|
|
@@ -8,10 +14,22 @@ export function isFitMode(value) {
|
|
|
8
14
|
export function isEffect(value) {
|
|
9
15
|
return EFFECTS.includes(value);
|
|
10
16
|
}
|
|
17
|
+
export function isLayout(value) {
|
|
18
|
+
return LAYOUTS.includes(value);
|
|
19
|
+
}
|
|
20
|
+
export function isRotation(value) {
|
|
21
|
+
return ROTATIONS.includes(value);
|
|
22
|
+
}
|
|
23
|
+
export function isStackCount(value) {
|
|
24
|
+
return (typeof value === 'number' &&
|
|
25
|
+
Number.isInteger(value) &&
|
|
26
|
+
value >= MIN_STACK_COUNT &&
|
|
27
|
+
value <= MAX_STACK_COUNT);
|
|
28
|
+
}
|
|
11
29
|
export function parseConfig(raw) {
|
|
12
30
|
if (typeof raw !== 'object' || raw === null)
|
|
13
31
|
throw new TypeError('Config must be an object');
|
|
14
|
-
const { images, intervalMs, fit, blur, effect, caption } = raw;
|
|
32
|
+
const { images, intervalMs, fit, blur, effect, caption, layout, count, rotate, demo } = raw;
|
|
15
33
|
if (!Array.isArray(images))
|
|
16
34
|
throw new TypeError('Config.images must be an array');
|
|
17
35
|
const parsed = images.map((image, i) => {
|
|
@@ -33,7 +51,30 @@ export function parseConfig(raw) {
|
|
|
33
51
|
throw new TypeError('Config.blur must be a boolean');
|
|
34
52
|
if (typeof caption !== 'boolean')
|
|
35
53
|
throw new TypeError('Config.caption must be a boolean');
|
|
36
|
-
|
|
54
|
+
if (demo !== undefined && typeof demo !== 'boolean') {
|
|
55
|
+
throw new TypeError('Config.demo must be a boolean');
|
|
56
|
+
}
|
|
57
|
+
if (layout !== undefined && !isLayout(layout)) {
|
|
58
|
+
throw new TypeError(`Config.layout must be one of ${LAYOUTS.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
if (count !== undefined && !isStackCount(count)) {
|
|
61
|
+
throw new TypeError(`Config.count must be an integer from ${String(MIN_STACK_COUNT)} to ${String(MAX_STACK_COUNT)}`);
|
|
62
|
+
}
|
|
63
|
+
if (rotate !== undefined && !isRotation(rotate)) {
|
|
64
|
+
throw new TypeError(`Config.rotate must be one of ${ROTATIONS.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
images: parsed,
|
|
68
|
+
intervalMs,
|
|
69
|
+
fit,
|
|
70
|
+
blur,
|
|
71
|
+
effect,
|
|
72
|
+
caption,
|
|
73
|
+
layout: layout ?? 'slideshow',
|
|
74
|
+
count: count ?? 3,
|
|
75
|
+
rotate: rotate ?? 'one',
|
|
76
|
+
demo: demo ?? false,
|
|
77
|
+
};
|
|
37
78
|
}
|
|
38
79
|
/** Parses what a user typed as the interval; undefined unless it is a whole number of seconds in range. */
|
|
39
80
|
export function parseIntervalSeconds(text) {
|
package/lib/panel/i18n.js
CHANGED
|
@@ -16,10 +16,18 @@ const en = {
|
|
|
16
16
|
blur: 'Blurred background',
|
|
17
17
|
kenburns: 'Slow zoom (Ken Burns)',
|
|
18
18
|
caption: 'Show image name',
|
|
19
|
+
stack: 'Several images, stacked',
|
|
20
|
+
count: 'Images at once',
|
|
21
|
+
rotateOne: 'Change one at a time',
|
|
19
22
|
settingsNote: 'Changes last until the panel reloads. Set defaults in gallery.config.json.',
|
|
20
23
|
counter: (position, total) => `Image ${String(position)} of ${String(total)}`,
|
|
21
24
|
empty: 'No images yet. Rebuild the panel with a folder that contains images.',
|
|
22
25
|
allFailed: 'None of the images could be displayed.',
|
|
26
|
+
demoTitle: 'These are sample images',
|
|
27
|
+
demoRun: 'To show your own photos, run this in a terminal, with the path to your folder:',
|
|
28
|
+
demoAdd: 'Then, in Orca, go to Settings → Plugins → Development, click Add path and enter the full path of orca-gallery in your home folder.',
|
|
29
|
+
demoWatch: 'Leave the command running: photos you add to or remove from your folder show up in the panel on their own.',
|
|
30
|
+
demoDismiss: 'Hide this note',
|
|
23
31
|
};
|
|
24
32
|
const es = {
|
|
25
33
|
previous: 'Imagen anterior',
|
|
@@ -39,10 +47,18 @@ const es = {
|
|
|
39
47
|
blur: 'Fondo difuminado',
|
|
40
48
|
kenburns: 'Zoom lento (Ken Burns)',
|
|
41
49
|
caption: 'Mostrar nombre',
|
|
50
|
+
stack: 'Varias imágenes apiladas',
|
|
51
|
+
count: 'Imágenes a la vez',
|
|
52
|
+
rotateOne: 'Cambiar de a una',
|
|
42
53
|
settingsNote: 'Los cambios duran hasta que el panel se recargue. Los valores por defecto van en gallery.config.json.',
|
|
43
54
|
counter: (position, total) => `Imagen ${String(position)} de ${String(total)}`,
|
|
44
55
|
empty: 'Todavía no hay imágenes. Vuelve a generar el panel con una carpeta que tenga imágenes.',
|
|
45
56
|
allFailed: 'No se pudo mostrar ninguna de las imágenes.',
|
|
57
|
+
demoTitle: 'Estas son imágenes de ejemplo',
|
|
58
|
+
demoRun: 'Para ver tus propias fotos, corre esto en una terminal, con la ruta de tu carpeta:',
|
|
59
|
+
demoAdd: 'Después, en Orca, ve a Settings → Plugins → Development, haz clic en Add path y escribe la ruta completa de orca-gallery en tu carpeta personal.',
|
|
60
|
+
demoWatch: 'Deja el comando corriendo: las fotos que agregues o quites de tu carpeta aparecen solas en el panel.',
|
|
61
|
+
demoDismiss: 'Ocultar este aviso',
|
|
46
62
|
};
|
|
47
63
|
export const catalogs = { en, es };
|
|
48
64
|
export function pickLocale(languages) {
|
package/lib/panel/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONFIG_ELEMENT_ID, parseConfig, parseIntervalSeconds, } from "./config.js";
|
|
1
|
+
import { CONFIG_ELEMENT_ID, MAX_STACK_COUNT, parseConfig, parseIntervalSeconds, } from "./config.js";
|
|
2
2
|
import { catalogs, pickLocale } from "./i18n.js";
|
|
3
3
|
import { Playlist } from "./playlist.js";
|
|
4
4
|
const KEN_BURNS_IDLE_MS = 30_000;
|
|
@@ -16,6 +16,20 @@ function showMessage(text) {
|
|
|
16
16
|
document.body.classList.add('message');
|
|
17
17
|
query('#message', HTMLElement).textContent = text;
|
|
18
18
|
}
|
|
19
|
+
/** In the sample build, explains how to load a folder of your own photos. */
|
|
20
|
+
function showDemoNote(t) {
|
|
21
|
+
const note = query('#demo-note', HTMLElement);
|
|
22
|
+
query('#demo-title', HTMLElement).textContent = t.demoTitle;
|
|
23
|
+
query('#demo-run', HTMLElement).textContent = t.demoRun;
|
|
24
|
+
query('#demo-add', HTMLElement).textContent = t.demoAdd;
|
|
25
|
+
query('#demo-watch', HTMLElement).textContent = t.demoWatch;
|
|
26
|
+
const dismiss = query('#demo-dismiss', HTMLButtonElement);
|
|
27
|
+
setLabel(dismiss, t.demoDismiss);
|
|
28
|
+
dismiss.addEventListener('click', () => {
|
|
29
|
+
note.hidden = true;
|
|
30
|
+
});
|
|
31
|
+
note.hidden = false;
|
|
32
|
+
}
|
|
19
33
|
function readConfig() {
|
|
20
34
|
const raw = document.getElementById(CONFIG_ELEMENT_ID)?.textContent ?? '';
|
|
21
35
|
return parseConfig(JSON.parse(raw));
|
|
@@ -66,24 +80,87 @@ class Popover {
|
|
|
66
80
|
this.button.focus();
|
|
67
81
|
}
|
|
68
82
|
}
|
|
83
|
+
const SWITCHES = ['blur', 'kenburns', 'caption', 'stack', 'rotateOne'];
|
|
84
|
+
/** Steps "previous" can walk back through in the stack layout. */
|
|
85
|
+
const MAX_SNAPSHOTS = 100;
|
|
86
|
+
/** One place for an image: two layers that cross-fade, a blurred backdrop each, and a caption. */
|
|
87
|
+
class Slot {
|
|
88
|
+
root;
|
|
89
|
+
#layers;
|
|
90
|
+
#caption;
|
|
91
|
+
#front = 0;
|
|
92
|
+
#shown = 0;
|
|
93
|
+
#request = 0;
|
|
94
|
+
/** The image shown or loading here; undefined while the slot is not in use. */
|
|
95
|
+
index;
|
|
96
|
+
constructor(root) {
|
|
97
|
+
this.root = root;
|
|
98
|
+
const layers = root.querySelectorAll('.layer');
|
|
99
|
+
const [a, b] = [layers[0], layers[1]];
|
|
100
|
+
if (!a || !b)
|
|
101
|
+
throw new Error('A slot needs two layers');
|
|
102
|
+
this.#layers = [a, b];
|
|
103
|
+
this.#caption = query('.caption', HTMLElement, root);
|
|
104
|
+
}
|
|
105
|
+
/** Loads into the hidden layer and cross-fades once it is decoded; stale loads are ignored. */
|
|
106
|
+
show(image, index, loaded, failed) {
|
|
107
|
+
this.index = index;
|
|
108
|
+
const request = ++this.#request;
|
|
109
|
+
const back = this.#layers[this.#front === 0 ? 1 : 0];
|
|
110
|
+
const front = this.#layers[this.#front];
|
|
111
|
+
const photo = query('.photo', HTMLImageElement, back);
|
|
112
|
+
const backdrop = query('.backdrop', HTMLImageElement, back);
|
|
113
|
+
photo.onload = () => {
|
|
114
|
+
if (request !== this.#request)
|
|
115
|
+
return;
|
|
116
|
+
backdrop.src = image.src;
|
|
117
|
+
back.classList.toggle('alternate', this.#shown++ % 2 === 1);
|
|
118
|
+
// Flushes the removal of "animate" so the zoom restarts even if no frame ran in between.
|
|
119
|
+
back.getBoundingClientRect();
|
|
120
|
+
back.classList.add('visible', 'animate');
|
|
121
|
+
front.classList.remove('visible');
|
|
122
|
+
this.#front = this.#front === 0 ? 1 : 0;
|
|
123
|
+
this.#caption.textContent = image.label;
|
|
124
|
+
loaded();
|
|
125
|
+
};
|
|
126
|
+
photo.onerror = () => {
|
|
127
|
+
if (request !== this.#request)
|
|
128
|
+
return;
|
|
129
|
+
failed();
|
|
130
|
+
};
|
|
131
|
+
// The outgoing layer keeps its zoom while it fades; this one restarts it once loaded.
|
|
132
|
+
back.classList.remove('animate');
|
|
133
|
+
photo.alt = image.label;
|
|
134
|
+
photo.src = image.src;
|
|
135
|
+
}
|
|
136
|
+
/** Takes the slot out of use, drops any load still in flight, and blanks it. */
|
|
137
|
+
clear() {
|
|
138
|
+
this.index = undefined;
|
|
139
|
+
this.#request++;
|
|
140
|
+
for (const layer of this.#layers)
|
|
141
|
+
layer.classList.remove('visible', 'animate');
|
|
142
|
+
this.#caption.textContent = '';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
69
145
|
class Gallery {
|
|
70
146
|
#config;
|
|
71
147
|
#t;
|
|
72
148
|
#playlist;
|
|
73
|
-
#
|
|
149
|
+
#slots;
|
|
74
150
|
#buttons;
|
|
75
151
|
#counter;
|
|
76
|
-
#caption;
|
|
77
152
|
#interval;
|
|
78
153
|
#settings;
|
|
79
154
|
#switches;
|
|
155
|
+
#counts;
|
|
80
156
|
#failed = new Set();
|
|
81
157
|
#state;
|
|
82
|
-
|
|
83
|
-
#
|
|
158
|
+
/** In the stack layout, which slot changes next when they take turns. */
|
|
159
|
+
#turn = 0;
|
|
160
|
+
/** In the stack layout, what was on screen before each change, for "previous". */
|
|
161
|
+
#snapshots = [];
|
|
84
162
|
#paused = false;
|
|
85
163
|
#timer;
|
|
86
|
-
#request = 0;
|
|
87
164
|
constructor(config, t) {
|
|
88
165
|
this.#config = config;
|
|
89
166
|
this.#t = t;
|
|
@@ -93,11 +170,19 @@ class Gallery {
|
|
|
93
170
|
blur: config.blur,
|
|
94
171
|
effect: config.effect,
|
|
95
172
|
caption: config.caption,
|
|
173
|
+
layout: config.layout,
|
|
174
|
+
count: config.count,
|
|
175
|
+
rotate: config.rotate,
|
|
96
176
|
};
|
|
97
177
|
this.#playlist = new Playlist(config.images.length);
|
|
98
|
-
this.#layers = [query('#layer-a', HTMLElement), query('#layer-b', HTMLElement)];
|
|
99
178
|
this.#counter = query('#counter', HTMLElement);
|
|
100
|
-
|
|
179
|
+
const container = query('#slots', HTMLElement);
|
|
180
|
+
const template = query('#slot-template', HTMLTemplateElement);
|
|
181
|
+
this.#slots = Array.from({ length: MAX_STACK_COUNT }, () => {
|
|
182
|
+
const root = query('.slot', HTMLElement, template.content.cloneNode(true));
|
|
183
|
+
container.append(root);
|
|
184
|
+
return new Slot(root);
|
|
185
|
+
});
|
|
101
186
|
const button = (action) => query(`[data-action="${action}"]`, HTMLButtonElement);
|
|
102
187
|
this.#buttons = {
|
|
103
188
|
previous: button('previous'),
|
|
@@ -124,23 +209,40 @@ class Gallery {
|
|
|
124
209
|
blur: toggle('blur'),
|
|
125
210
|
kenburns: toggle('kenburns'),
|
|
126
211
|
caption: toggle('caption'),
|
|
212
|
+
stack: toggle('stack'),
|
|
213
|
+
rotateOne: toggle('rotateOne'),
|
|
127
214
|
};
|
|
215
|
+
this.#counts = Array.from(document.querySelectorAll('input[name="count"]'));
|
|
128
216
|
}
|
|
129
217
|
get #size() {
|
|
130
218
|
return this.#config.images.length;
|
|
131
219
|
}
|
|
220
|
+
/** How many slots are in use: one for the slideshow, up to `count` for the stack. */
|
|
221
|
+
get #active() {
|
|
222
|
+
return this.#state.layout === 'stack' ? Math.min(this.#state.count, this.#size) : 1;
|
|
223
|
+
}
|
|
224
|
+
get #activeSlots() {
|
|
225
|
+
return this.#slots.slice(0, this.#active);
|
|
226
|
+
}
|
|
132
227
|
start() {
|
|
133
228
|
this.#renderStaticText();
|
|
134
229
|
this.#renderAll();
|
|
135
230
|
this.#bindToolbar();
|
|
136
231
|
this.#bindInterval();
|
|
137
232
|
this.#bindSettings();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (this.#size
|
|
233
|
+
this.#slots.forEach((slot) => {
|
|
234
|
+
slot.root.addEventListener('click', () => {
|
|
235
|
+
if (this.#size <= 1)
|
|
236
|
+
return;
|
|
237
|
+
if (this.#state.layout === 'stack') {
|
|
238
|
+
this.#remember();
|
|
239
|
+
this.#replace(slot);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
141
242
|
this.#next();
|
|
243
|
+
}
|
|
142
244
|
});
|
|
143
|
-
}
|
|
245
|
+
});
|
|
144
246
|
document.addEventListener('pointerdown', (event) => {
|
|
145
247
|
const open = Popover.current;
|
|
146
248
|
if (open && !(event.target instanceof Node && open.contains(event.target)))
|
|
@@ -152,7 +254,7 @@ class Gallery {
|
|
|
152
254
|
document.addEventListener('visibilitychange', () => {
|
|
153
255
|
this.#schedule();
|
|
154
256
|
});
|
|
155
|
-
this.#
|
|
257
|
+
this.#applyLayout();
|
|
156
258
|
}
|
|
157
259
|
#renderStaticText() {
|
|
158
260
|
const text = (selector, value) => {
|
|
@@ -166,7 +268,8 @@ class Gallery {
|
|
|
166
268
|
this.#interval.apply.textContent = this.#t.apply;
|
|
167
269
|
text('#settings-title', this.#t.settings);
|
|
168
270
|
text('#settings-note', this.#t.settingsNote);
|
|
169
|
-
|
|
271
|
+
text('#count-label', this.#t.count);
|
|
272
|
+
for (const name of SWITCHES) {
|
|
170
273
|
query('span', HTMLElement, this.#switches[name].closest('label') ?? document).textContent =
|
|
171
274
|
this.#t[name];
|
|
172
275
|
}
|
|
@@ -210,22 +313,39 @@ class Gallery {
|
|
|
210
313
|
this.#switches.caption.addEventListener('change', () => {
|
|
211
314
|
this.#update({ caption: this.#switches.caption.checked });
|
|
212
315
|
});
|
|
316
|
+
this.#switches.stack.addEventListener('change', () => {
|
|
317
|
+
this.#update({ layout: this.#switches.stack.checked ? 'stack' : 'slideshow' });
|
|
318
|
+
});
|
|
319
|
+
this.#switches.rotateOne.addEventListener('change', () => {
|
|
320
|
+
this.#update({ rotate: this.#switches.rotateOne.checked ? 'one' : 'all' });
|
|
321
|
+
});
|
|
322
|
+
for (const radio of this.#counts) {
|
|
323
|
+
radio.addEventListener('change', () => {
|
|
324
|
+
if (radio.checked)
|
|
325
|
+
this.#update({ count: Number(radio.value) });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
213
328
|
}
|
|
214
329
|
#update(change) {
|
|
215
330
|
Object.assign(this.#state, change);
|
|
216
331
|
this.#renderAll();
|
|
217
|
-
if (change.
|
|
332
|
+
if (change.layout !== undefined || change.count !== undefined)
|
|
333
|
+
this.#applyLayout();
|
|
334
|
+
else if (change.intervalMs !== undefined || change.rotate !== undefined)
|
|
218
335
|
this.#schedule();
|
|
219
336
|
}
|
|
220
337
|
#renderAll() {
|
|
221
|
-
const { fit, blur, effect, caption, intervalMs } = this.#state;
|
|
338
|
+
const { fit, blur, effect, caption, intervalMs, layout, count, rotate } = this.#state;
|
|
222
339
|
const body = document.body;
|
|
223
340
|
body.dataset['fit'] = fit;
|
|
341
|
+
body.dataset['layout'] = layout;
|
|
224
342
|
body.classList.toggle('blur', blur);
|
|
225
343
|
body.classList.toggle('kenburns', effect === 'kenburns');
|
|
226
344
|
body.classList.toggle('show-caption', caption);
|
|
227
345
|
body.classList.toggle('paused', this.#paused);
|
|
228
|
-
|
|
346
|
+
// Taking turns, each image stays up for one interval per slot.
|
|
347
|
+
const visibleMs = layout === 'stack' && rotate === 'one' ? intervalMs * this.#active : intervalMs;
|
|
348
|
+
body.style.setProperty('--gallery-kenburns', `${String(visibleMs > 0 ? visibleMs + 600 : KEN_BURNS_IDLE_MS)}ms`);
|
|
229
349
|
setLabel(this.#buttons.fit, fit === 'contain' ? this.#t.fill : this.#t.fit);
|
|
230
350
|
setLabel(this.#interval.popover.button, this.#t.interval(intervalMs / 1000));
|
|
231
351
|
this.#buttons.toggle.setAttribute('aria-pressed', String(this.#paused));
|
|
@@ -233,60 +353,147 @@ class Gallery {
|
|
|
233
353
|
this.#switches.blur.checked = blur;
|
|
234
354
|
this.#switches.kenburns.checked = effect === 'kenburns';
|
|
235
355
|
this.#switches.caption.checked = caption;
|
|
356
|
+
this.#switches.stack.checked = layout === 'stack';
|
|
357
|
+
this.#switches.rotateOne.checked = rotate === 'one';
|
|
358
|
+
this.#switches.rotateOne.disabled = layout !== 'stack';
|
|
359
|
+
for (const radio of this.#counts) {
|
|
360
|
+
radio.checked = Number(radio.value) === count;
|
|
361
|
+
radio.disabled = layout !== 'stack';
|
|
362
|
+
}
|
|
236
363
|
}
|
|
237
|
-
|
|
238
|
-
|
|
364
|
+
/** Shows the slots the layout uses, hides the rest, and fills any that are empty. */
|
|
365
|
+
#applyLayout() {
|
|
366
|
+
const active = this.#active;
|
|
367
|
+
this.#slots.forEach((slot, i) => {
|
|
368
|
+
slot.root.hidden = i >= active;
|
|
369
|
+
if (i >= active)
|
|
370
|
+
slot.clear();
|
|
371
|
+
});
|
|
372
|
+
this.#snapshots = [];
|
|
373
|
+
this.#turn = 0;
|
|
374
|
+
for (const slot of this.#activeSlots) {
|
|
375
|
+
if (slot.index === undefined)
|
|
376
|
+
this.#replace(slot);
|
|
377
|
+
}
|
|
378
|
+
const [first] = this.#slots;
|
|
379
|
+
if (this.#state.layout === 'slideshow' && first?.index !== undefined) {
|
|
380
|
+
this.#renderCounter(first.index);
|
|
381
|
+
}
|
|
382
|
+
this.#schedule();
|
|
239
383
|
}
|
|
240
|
-
|
|
241
|
-
|
|
384
|
+
/** The next image from the playlist that hasn't failed and isn't in `exclude`. */
|
|
385
|
+
#draw(exclude) {
|
|
386
|
+
// Every cycle holds each image once, so two cycles always reach any free image.
|
|
387
|
+
for (let tries = 0; tries < this.#size * 2; tries++) {
|
|
388
|
+
const index = this.#playlist.next();
|
|
389
|
+
if (!this.#failed.has(index) && !exclude.has(index))
|
|
390
|
+
return index;
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
242
393
|
}
|
|
243
|
-
/**
|
|
244
|
-
#
|
|
394
|
+
/** Images on screen in the slots in use, except in `slot`. */
|
|
395
|
+
#onScreen(except) {
|
|
396
|
+
const shown = new Set();
|
|
397
|
+
for (const slot of this.#activeSlots) {
|
|
398
|
+
if (slot !== except && slot.index !== undefined)
|
|
399
|
+
shown.add(slot.index);
|
|
400
|
+
}
|
|
401
|
+
return shown;
|
|
402
|
+
}
|
|
403
|
+
/** Puts a different image, not on screen elsewhere, in one slot. */
|
|
404
|
+
#replace(slot) {
|
|
405
|
+
const exclude = this.#onScreen(slot);
|
|
406
|
+
if (slot.index !== undefined)
|
|
407
|
+
exclude.add(slot.index);
|
|
408
|
+
const index = this.#draw(exclude);
|
|
409
|
+
if (index !== undefined)
|
|
410
|
+
this.#load(slot, index);
|
|
411
|
+
}
|
|
412
|
+
#load(slot, index) {
|
|
245
413
|
const image = this.#config.images[index];
|
|
246
414
|
if (!image)
|
|
247
415
|
return;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const photo = query('.photo', HTMLImageElement, back);
|
|
252
|
-
const backdrop = query('.backdrop', HTMLImageElement, back);
|
|
253
|
-
photo.onload = () => {
|
|
254
|
-
if (request !== this.#request)
|
|
255
|
-
return;
|
|
256
|
-
backdrop.src = image.src;
|
|
257
|
-
back.classList.toggle('alternate', this.#shown++ % 2 === 1);
|
|
258
|
-
// Flushes the removal of "animate" so the zoom restarts even if no frame ran in between.
|
|
259
|
-
back.getBoundingClientRect();
|
|
260
|
-
back.classList.add('visible', 'animate');
|
|
261
|
-
front.classList.remove('visible');
|
|
262
|
-
this.#front = this.#front === 0 ? 1 : 0;
|
|
263
|
-
this.#caption.textContent = image.label;
|
|
264
|
-
this.#renderCounter(index);
|
|
416
|
+
slot.show(image, index, () => {
|
|
417
|
+
if (this.#state.layout === 'slideshow')
|
|
418
|
+
this.#renderCounter(index);
|
|
265
419
|
this.#schedule();
|
|
266
|
-
}
|
|
267
|
-
photo.onerror = () => {
|
|
268
|
-
if (request !== this.#request)
|
|
269
|
-
return;
|
|
420
|
+
}, () => {
|
|
270
421
|
this.#failed.add(index);
|
|
422
|
+
// Keeps showing what was there while another image loads.
|
|
423
|
+
slot.index = undefined;
|
|
271
424
|
if (this.#failed.size >= this.#size) {
|
|
272
425
|
clearTimeout(this.#timer);
|
|
273
426
|
showMessage(this.#t.allFailed);
|
|
274
427
|
return;
|
|
275
428
|
}
|
|
276
|
-
this.#
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
429
|
+
this.#replace(slot);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** Saves what is on screen so "previous" in the stack layout can bring it back. */
|
|
433
|
+
#remember() {
|
|
434
|
+
const current = this.#activeSlots.map((slot) => slot.index);
|
|
435
|
+
if (current.some((index) => index === undefined))
|
|
436
|
+
return;
|
|
437
|
+
this.#snapshots.push(current);
|
|
438
|
+
if (this.#snapshots.length > MAX_SNAPSHOTS)
|
|
439
|
+
this.#snapshots.shift();
|
|
440
|
+
}
|
|
441
|
+
#next() {
|
|
442
|
+
if (this.#state.layout === 'slideshow') {
|
|
443
|
+
const index = this.#draw(new Set());
|
|
444
|
+
const [slot] = this.#slots;
|
|
445
|
+
if (index !== undefined && slot)
|
|
446
|
+
this.#load(slot, index);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// A whole new set, with no image twice.
|
|
450
|
+
this.#remember();
|
|
451
|
+
const chosen = new Set();
|
|
452
|
+
for (const slot of this.#activeSlots) {
|
|
453
|
+
const index = this.#draw(chosen);
|
|
454
|
+
if (index === undefined)
|
|
455
|
+
continue;
|
|
456
|
+
chosen.add(index);
|
|
457
|
+
this.#load(slot, index);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
#previous() {
|
|
461
|
+
if (this.#state.layout === 'slideshow') {
|
|
462
|
+
const [slot] = this.#slots;
|
|
463
|
+
if (slot)
|
|
464
|
+
this.#load(slot, this.#playlist.previous());
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const snapshot = this.#snapshots.pop();
|
|
468
|
+
if (!snapshot)
|
|
469
|
+
return;
|
|
470
|
+
this.#activeSlots.forEach((slot, i) => {
|
|
471
|
+
const index = snapshot[i];
|
|
472
|
+
if (index !== undefined && index !== slot.index)
|
|
473
|
+
this.#load(slot, index);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/** What the timer does: the next image, the next set, or the next slot's turn. */
|
|
477
|
+
#advance() {
|
|
478
|
+
if (this.#state.layout === 'stack' && this.#state.rotate === 'one') {
|
|
479
|
+
const slots = this.#activeSlots;
|
|
480
|
+
const slot = slots[this.#turn % slots.length];
|
|
481
|
+
this.#turn = (this.#turn + 1) % slots.length;
|
|
482
|
+
if (!slot)
|
|
483
|
+
return;
|
|
484
|
+
this.#remember();
|
|
485
|
+
this.#replace(slot);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.#next();
|
|
282
489
|
}
|
|
283
490
|
#schedule() {
|
|
284
491
|
clearTimeout(this.#timer);
|
|
285
492
|
this.#timer = undefined;
|
|
286
|
-
const rotates = this.#state.intervalMs > 0 && this.#size >
|
|
493
|
+
const rotates = this.#state.intervalMs > 0 && this.#size > this.#active;
|
|
287
494
|
if (rotates && !this.#paused && !document.hidden) {
|
|
288
495
|
this.#timer = setTimeout(() => {
|
|
289
|
-
this.#
|
|
496
|
+
this.#advance();
|
|
290
497
|
}, this.#state.intervalMs);
|
|
291
498
|
}
|
|
292
499
|
}
|
|
@@ -358,5 +565,7 @@ function main() {
|
|
|
358
565
|
return;
|
|
359
566
|
}
|
|
360
567
|
new Gallery(config, t).start();
|
|
568
|
+
if (config.demo)
|
|
569
|
+
showDemoNote(t);
|
|
361
570
|
}
|
|
362
571
|
main();
|
package/lib/panel/panel.css
CHANGED
|
@@ -40,6 +40,26 @@ body {
|
|
|
40
40
|
overflow: hidden;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* One slot for the slideshow; up to four stacked top to bottom for the stack layout. */
|
|
44
|
+
.slots {
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset: 0;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.slot {
|
|
53
|
+
position: relative;
|
|
54
|
+
flex: 1 1 0;
|
|
55
|
+
min-height: 0;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.slot[hidden] {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
.layer {
|
|
44
64
|
position: absolute;
|
|
45
65
|
inset: 0;
|
|
@@ -144,6 +164,7 @@ body.message .message-text {
|
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
body.message .controls,
|
|
167
|
+
body.message .slots,
|
|
147
168
|
body.message .layer,
|
|
148
169
|
body.message .caption {
|
|
149
170
|
display: none;
|
|
@@ -344,6 +365,72 @@ body.single .counter {
|
|
|
344
365
|
transform: translateX(12px);
|
|
345
366
|
}
|
|
346
367
|
|
|
368
|
+
/* The stack shows several images, so a single position means nothing. */
|
|
369
|
+
body[data-layout='stack'] .counter {
|
|
370
|
+
display: none;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.count-row {
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
justify-content: space-between;
|
|
377
|
+
gap: 8px;
|
|
378
|
+
padding: 5px 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.segmented {
|
|
382
|
+
display: inline-flex;
|
|
383
|
+
padding: 2px;
|
|
384
|
+
border-radius: calc(var(--gallery-radius) - 4px);
|
|
385
|
+
background: var(--gallery-input);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.segmented label {
|
|
389
|
+
position: relative;
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.segmented input {
|
|
394
|
+
position: absolute;
|
|
395
|
+
inset: 0;
|
|
396
|
+
margin: 0;
|
|
397
|
+
opacity: 0;
|
|
398
|
+
cursor: pointer;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.segmented span {
|
|
402
|
+
display: block;
|
|
403
|
+
min-width: 22px;
|
|
404
|
+
padding: 1px 6px;
|
|
405
|
+
border-radius: calc(var(--gallery-radius) - 6px);
|
|
406
|
+
text-align: center;
|
|
407
|
+
font-variant-numeric: tabular-nums;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.segmented input:checked + span {
|
|
411
|
+
background: var(--gallery-primary);
|
|
412
|
+
color: var(--gallery-primary-fg);
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.segmented input:focus-visible + span {
|
|
417
|
+
outline: 2px solid var(--gallery-ring);
|
|
418
|
+
outline-offset: 1px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Count and turn-taking only apply to the stack. */
|
|
422
|
+
.switch:has(input:disabled),
|
|
423
|
+
.count-row:has(input:disabled) {
|
|
424
|
+
opacity: 0.45;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.switch:has(input:disabled),
|
|
428
|
+
.switch input:disabled,
|
|
429
|
+
.segmented input:disabled,
|
|
430
|
+
.segmented label:has(input:disabled) {
|
|
431
|
+
cursor: default;
|
|
432
|
+
}
|
|
433
|
+
|
|
347
434
|
.hint {
|
|
348
435
|
margin: 6px 0 0;
|
|
349
436
|
color: var(--gallery-muted);
|
|
@@ -353,9 +440,89 @@ body.single .counter {
|
|
|
353
440
|
color: var(--gallery-danger);
|
|
354
441
|
}
|
|
355
442
|
|
|
443
|
+
/* Note in the sample build listed in Orca's plugin directory */
|
|
444
|
+
|
|
445
|
+
.demo-note {
|
|
446
|
+
position: absolute;
|
|
447
|
+
inset: 12px 12px auto;
|
|
448
|
+
max-height: calc(100% - 80px);
|
|
449
|
+
overflow-y: auto;
|
|
450
|
+
padding: 10px 32px 10px 12px;
|
|
451
|
+
border: 1px solid var(--gallery-border);
|
|
452
|
+
border-radius: var(--gallery-radius);
|
|
453
|
+
background: color-mix(in srgb, var(--gallery-surface) 92%, transparent);
|
|
454
|
+
backdrop-filter: blur(6px);
|
|
455
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.25);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.demo-note[hidden] {
|
|
459
|
+
display: none;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.demo-note p {
|
|
463
|
+
margin: 0 0 6px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.demo-note p:last-of-type {
|
|
467
|
+
margin-bottom: 0;
|
|
468
|
+
color: var(--gallery-muted);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.demo-title {
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.demo-command {
|
|
476
|
+
display: block;
|
|
477
|
+
margin: 0 0 6px;
|
|
478
|
+
padding: 6px 8px;
|
|
479
|
+
border-radius: calc(var(--gallery-radius) - 4px);
|
|
480
|
+
background: var(--gallery-bg);
|
|
481
|
+
font:
|
|
482
|
+
11px/1.4 ui-monospace,
|
|
483
|
+
monospace;
|
|
484
|
+
overflow-wrap: break-word;
|
|
485
|
+
user-select: all;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.demo-dismiss {
|
|
489
|
+
position: absolute;
|
|
490
|
+
top: 6px;
|
|
491
|
+
right: 6px;
|
|
492
|
+
display: grid;
|
|
493
|
+
place-items: center;
|
|
494
|
+
width: 22px;
|
|
495
|
+
height: 22px;
|
|
496
|
+
padding: 0;
|
|
497
|
+
border: 0;
|
|
498
|
+
border-radius: calc(var(--gallery-radius) - 4px);
|
|
499
|
+
background: transparent;
|
|
500
|
+
color: var(--gallery-muted);
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.demo-dismiss:hover {
|
|
505
|
+
background: color-mix(in srgb, var(--gallery-fg) 10%, transparent);
|
|
506
|
+
color: var(--gallery-fg);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.demo-dismiss svg {
|
|
510
|
+
width: 14px;
|
|
511
|
+
height: 14px;
|
|
512
|
+
fill: none;
|
|
513
|
+
stroke: currentColor;
|
|
514
|
+
stroke-width: 2;
|
|
515
|
+
stroke-linecap: round;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
body.message .demo-note {
|
|
519
|
+
display: none;
|
|
520
|
+
}
|
|
521
|
+
|
|
356
522
|
/* Focus and motion */
|
|
357
523
|
|
|
358
524
|
.control:focus-visible,
|
|
525
|
+
.demo-dismiss:focus-visible,
|
|
359
526
|
.apply:focus-visible,
|
|
360
527
|
.interval-row input:focus-visible,
|
|
361
528
|
.switch input:focus-visible {
|
package/lib/panel/template.html
CHANGED
|
@@ -10,18 +10,37 @@
|
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<main class="stage">
|
|
13
|
-
<div id="
|
|
14
|
-
<img class="backdrop" alt="" aria-hidden="true" />
|
|
15
|
-
<img class="photo" alt="" />
|
|
16
|
-
</div>
|
|
17
|
-
<div id="layer-b" class="layer">
|
|
18
|
-
<img class="backdrop" alt="" aria-hidden="true" />
|
|
19
|
-
<img class="photo" alt="" />
|
|
20
|
-
</div>
|
|
21
|
-
<p id="caption" class="caption"></p>
|
|
13
|
+
<div id="slots" class="slots"></div>
|
|
22
14
|
<p id="message" class="message-text" role="status"></p>
|
|
23
15
|
</main>
|
|
24
16
|
|
|
17
|
+
<template id="slot-template">
|
|
18
|
+
<div class="slot">
|
|
19
|
+
<div class="layer">
|
|
20
|
+
<img class="backdrop" alt="" aria-hidden="true" />
|
|
21
|
+
<img class="photo" alt="" />
|
|
22
|
+
</div>
|
|
23
|
+
<div class="layer">
|
|
24
|
+
<img class="backdrop" alt="" aria-hidden="true" />
|
|
25
|
+
<img class="photo" alt="" />
|
|
26
|
+
</div>
|
|
27
|
+
<p class="caption"></p>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<aside id="demo-note" class="demo-note" aria-labelledby="demo-title" hidden>
|
|
32
|
+
<p id="demo-title" class="demo-title"></p>
|
|
33
|
+
<p id="demo-run"></p>
|
|
34
|
+
<code class="demo-command"
|
|
35
|
+
>npx orca-gallery-panel ~/Pictures --out ~/orca-gallery --watch</code
|
|
36
|
+
>
|
|
37
|
+
<p id="demo-add"></p>
|
|
38
|
+
<p id="demo-watch"></p>
|
|
39
|
+
<button type="button" id="demo-dismiss" class="demo-dismiss">
|
|
40
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" /></svg>
|
|
41
|
+
</button>
|
|
42
|
+
</aside>
|
|
43
|
+
|
|
25
44
|
<div class="controls" role="toolbar">
|
|
26
45
|
<button type="button" class="control" data-action="previous">
|
|
27
46
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg>
|
|
@@ -102,6 +121,22 @@
|
|
|
102
121
|
<input type="checkbox" role="switch" data-setting="caption" />
|
|
103
122
|
<span></span>
|
|
104
123
|
</label>
|
|
124
|
+
<label class="switch">
|
|
125
|
+
<input type="checkbox" role="switch" data-setting="stack" />
|
|
126
|
+
<span></span>
|
|
127
|
+
</label>
|
|
128
|
+
<div class="count-row" role="radiogroup" aria-labelledby="count-label">
|
|
129
|
+
<span id="count-label"></span>
|
|
130
|
+
<span class="segmented">
|
|
131
|
+
<label><input type="radio" name="count" value="2" /><span>2</span></label>
|
|
132
|
+
<label><input type="radio" name="count" value="3" /><span>3</span></label>
|
|
133
|
+
<label><input type="radio" name="count" value="4" /><span>4</span></label>
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
<label class="switch">
|
|
137
|
+
<input type="checkbox" role="switch" data-setting="rotateOne" />
|
|
138
|
+
<span></span>
|
|
139
|
+
</label>
|
|
105
140
|
<p id="settings-note" class="hint"></p>
|
|
106
141
|
</div>
|
|
107
142
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orca-gallery-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Background images for the Orca agent IDE: a side panel with a rotating gallery of your own pictures.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"orca",
|
|
@@ -50,7 +50,9 @@
|
|
|
50
50
|
"check": "npm run typecheck && npm run lint && npm run format:check && npm test",
|
|
51
51
|
"test:e2e": "node --test --test-concurrency=1 \"test/e2e/**/*.test.ts\"",
|
|
52
52
|
"build:lib": "node scripts/build-lib.ts",
|
|
53
|
-
"
|
|
53
|
+
"build:demo": "node scripts/build-demo-plugin.ts",
|
|
54
|
+
"prepack": "npm run build:lib",
|
|
55
|
+
"version": "npm run build:demo && git add plugin"
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
58
|
"esbuild": "^0.28.2",
|