experiments in a post-browser web
1/**
2 * Example Extension - Image Gallery
3 *
4 * Demonstrates:
5 * - Feature detection for the Peek API (works as extension or regular website)
6 * - Command registration with mime type acceptance
7 * - Receiving and storing image data
8 * - Displaying stored images
9 *
10 * See docs/PEEK-API.md for the complete API reference.
11 */
12
13// Feature detection - check if Peek API is available
14const hasPeekAPI = typeof window.app !== 'undefined';
15const api = hasPeekAPI ? window.app : null;
16
17// In-memory image storage (when running without Peek API)
18// When Peek API is available, we use the datastore
19const localImageStore = new Map();
20
21/**
22 * Store an image
23 * @param {string} id - Unique image ID
24 * @param {object} imageData - { data: base64, mimeType, name, timestamp }
25 */
26async function storeImage(id, imageData) {
27 if (hasPeekAPI) {
28 // Use Peek datastore for persistent storage
29 await api.datastore.setRow('example_images', id, imageData);
30 console.log('[example] Image stored in datastore:', id);
31 } else {
32 // Fall back to in-memory storage
33 localImageStore.set(id, imageData);
34 console.log('[example] Image stored locally:', id);
35 }
36}
37
38/**
39 * Get all stored images
40 * @returns {Promise<object>} Map of id -> imageData
41 */
42async function getStoredImages() {
43 if (hasPeekAPI) {
44 const result = await api.datastore.getTable('example_images');
45 return result.success ? result.data : {};
46 } else {
47 return Object.fromEntries(localImageStore);
48 }
49}
50
51/**
52 * Generate a unique image ID
53 */
54function generateImageId() {
55 return `img_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
56}
57
58/**
59 * Handle received image data from command execution
60 * @param {object} ctx - Command execution context with input data
61 */
62async function handleImageReceived(ctx) {
63 console.log('[example] Received image data:', ctx);
64
65 // ctx.input contains the data passed to the command
66 // For images, this would be { data: base64, mimeType, name }
67 if (!ctx.input || !ctx.input.data) {
68 console.error('[example] No image data in context');
69 return;
70 }
71
72 const imageId = generateImageId();
73 const imageData = {
74 data: ctx.input.data, // Base64 encoded image
75 mimeType: ctx.input.mimeType || 'image/png',
76 name: ctx.input.name || 'Untitled',
77 timestamp: Date.now()
78 };
79
80 await storeImage(imageId, imageData);
81
82 // Notify that a new image was added
83 if (hasPeekAPI) {
84 api.publish('example:image-added', { id: imageId, ...imageData }, api.scopes.GLOBAL);
85 }
86
87 // Open the gallery to show the new image
88 openGallery();
89}
90
91/**
92 * Open the image gallery window
93 */
94function openGallery() {
95 if (hasPeekAPI) {
96 api.window.open('peek://ext/example/gallery.html', {
97 key: 'example-gallery',
98 width: 800,
99 height: 600,
100 title: 'Image Gallery'
101 });
102 } else {
103 // When running as a regular website, navigate or open in new tab
104 window.open('./gallery.html', '_blank');
105 }
106}
107
108const extension = {
109 id: 'example',
110 labels: {
111 name: 'Example Gallery'
112 },
113
114 /**
115 * Register commands - called when cmd extension is ready
116 */
117 registerCommands() {
118 // Command that accepts images
119 // The 'accepts' array specifies which mime types this command can receive
120 api.commands.register({
121 name: 'example:save-image',
122 description: 'Save an image to the gallery',
123 accepts: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/*'],
124 execute: handleImageReceived
125 });
126
127 // Command to open the gallery
128 api.commands.register({
129 name: 'example:gallery',
130 description: 'Open the image gallery',
131 execute: openGallery
132 });
133
134 console.log('[example] Commands registered');
135 },
136
137 init() {
138 console.log('[example] init - Peek API available:', hasPeekAPI);
139
140 if (!hasPeekAPI) {
141 console.log('[example] Running without Peek API - limited functionality');
142 return;
143 }
144
145 // Wait for cmd:ready before registering commands
146 api.subscribe('cmd:ready', () => {
147 this.registerCommands();
148 }, api.scopes.GLOBAL);
149
150 // Query in case cmd is already ready
151 api.publish('cmd:query', {}, api.scopes.GLOBAL);
152
153 // Note: No global shortcut - use the cmd palette to access this extension
154 // (avoids conflicts with other extensions like groups which uses Option+g)
155
156 console.log('[example] Extension loaded');
157 },
158
159 uninit() {
160 console.log('[example] Cleaning up...');
161
162 if (hasPeekAPI) {
163 api.commands.unregister('example:save-image');
164 api.commands.unregister('example:gallery');
165 }
166 }
167};
168
169// Export for ES module usage (Peek extension)
170export default extension;
171
172// Also expose utilities for the gallery page
173export { hasPeekAPI, api, getStoredImages, storeImage, generateImageId };