experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

more extensionworld changes

+403 -191
+117
app/migrations/extension-settings.js
··· 1 + /** 2 + * Migration: Extension Settings localStorage -> Datastore 3 + * 4 + * One-time migration to move extension settings from localStorage 5 + * to the datastore extension_settings table for cross-origin access. 6 + * 7 + * Run this from core background context (peek://app/background.html) 8 + */ 9 + 10 + const api = window.app; 11 + 12 + // Extension ID mapping: old UUID -> new shortname 13 + const EXTENSION_ID_MAP = { 14 + 'ef3bd271-d408-421f-9338-47b615571e43': 'peeks', 15 + '434108f3-18a6-437a-b507-2f998f693bb2': 'slides', 16 + '82de735f-a4b7-4fe6-a458-ec29939ae00d': 'groups' 17 + }; 18 + 19 + const MIGRATION_KEY = 'migration:extension-settings:v1'; 20 + 21 + /** 22 + * Check if migration has already been completed 23 + */ 24 + export const isMigrationComplete = () => { 25 + return localStorage.getItem(MIGRATION_KEY) === 'complete'; 26 + }; 27 + 28 + /** 29 + * Mark migration as complete 30 + */ 31 + const markMigrationComplete = () => { 32 + localStorage.setItem(MIGRATION_KEY, 'complete'); 33 + }; 34 + 35 + /** 36 + * Migrate settings for a single extension 37 + * @param {string} oldId - Old UUID-based extension ID 38 + * @param {string} newId - New shortname-based extension ID 39 + */ 40 + const migrateExtension = async (oldId, newId) => { 41 + console.log(`[migration] Migrating ${oldId} -> ${newId}`); 42 + 43 + const storageKey = oldId; 44 + const storedData = localStorage.getItem(storageKey); 45 + 46 + if (!storedData) { 47 + console.log(`[migration] No localStorage data for ${oldId}`); 48 + return { migrated: false, reason: 'no data' }; 49 + } 50 + 51 + try { 52 + const settings = JSON.parse(storedData); 53 + console.log(`[migration] Found settings for ${oldId}:`, Object.keys(settings)); 54 + 55 + // Write each key to extension_settings table via IPC 56 + // Note: We can't use api.settings here because we're in core context, not extension context 57 + // So we use a direct datastore call 58 + 59 + for (const [key, value] of Object.entries(settings)) { 60 + const rowId = `${newId}:${key}`; 61 + const result = await api.datastore.setRow('extension_settings', rowId, { 62 + extensionId: newId, 63 + key, 64 + value: JSON.stringify(value), 65 + updatedAt: Date.now() 66 + }); 67 + 68 + if (!result.success) { 69 + console.error(`[migration] Failed to migrate ${oldId}.${key}:`, result.error); 70 + } else { 71 + console.log(`[migration] Migrated ${oldId}.${key} to ${newId}`); 72 + } 73 + } 74 + 75 + return { migrated: true, keys: Object.keys(settings) }; 76 + } catch (e) { 77 + console.error(`[migration] Failed to parse settings for ${oldId}:`, e); 78 + return { migrated: false, reason: 'parse error', error: e.message }; 79 + } 80 + }; 81 + 82 + /** 83 + * Run the migration for all extensions 84 + */ 85 + export const runMigration = async () => { 86 + if (isMigrationComplete()) { 87 + console.log('[migration] Extension settings migration already complete'); 88 + return { skipped: true }; 89 + } 90 + 91 + console.log('[migration] Starting extension settings migration...'); 92 + 93 + const results = {}; 94 + 95 + for (const [oldId, newId] of Object.entries(EXTENSION_ID_MAP)) { 96 + results[newId] = await migrateExtension(oldId, newId); 97 + } 98 + 99 + markMigrationComplete(); 100 + console.log('[migration] Extension settings migration complete:', results); 101 + 102 + return { completed: true, results }; 103 + }; 104 + 105 + /** 106 + * Reset migration (for testing) 107 + */ 108 + export const resetMigration = () => { 109 + localStorage.removeItem(MIGRATION_KEY); 110 + console.log('[migration] Migration reset'); 111 + }; 112 + 113 + export default { 114 + isMigrationComplete, 115 + runMigration, 116 + resetMigration 117 + };
+42
app/migrations/index.js
··· 1 + /** 2 + * Migrations Index 3 + * 4 + * Run all pending migrations on app startup. 5 + * Migrations are run in order and only once. 6 + */ 7 + 8 + import extensionSettingsMigration from './extension-settings.js'; 9 + 10 + const migrations = [ 11 + { 12 + name: 'extension-settings-v1', 13 + run: extensionSettingsMigration.runMigration, 14 + check: extensionSettingsMigration.isMigrationComplete 15 + } 16 + ]; 17 + 18 + /** 19 + * Run all pending migrations 20 + */ 21 + export const runMigrations = async () => { 22 + console.log('[migrations] Checking for pending migrations...'); 23 + 24 + for (const migration of migrations) { 25 + if (migration.check && migration.check()) { 26 + console.log(`[migrations] ${migration.name}: already complete`); 27 + continue; 28 + } 29 + 30 + console.log(`[migrations] ${migration.name}: running...`); 31 + try { 32 + const result = await migration.run(); 33 + console.log(`[migrations] ${migration.name}: complete`, result); 34 + } catch (e) { 35 + console.error(`[migrations] ${migration.name}: failed`, e); 36 + } 37 + } 38 + 39 + console.log('[migrations] All migrations checked'); 40 + }; 41 + 42 + export default { runMigrations };
+26
app/settings/settings.css
··· 251 251 margin-bottom: 16px; 252 252 padding-bottom: 12px; 253 253 border-bottom: 1px solid var(--border-primary); 254 + cursor: pointer; 255 + user-select: none; 256 + } 257 + 258 + .item-card-header:hover { 259 + opacity: 0.8; 260 + } 261 + 262 + .item-card.collapsed .item-card-header { 263 + margin-bottom: 0; 264 + padding-bottom: 0; 265 + border-bottom: none; 266 + } 267 + 268 + .item-card.collapsed .item-card-body { 269 + display: none; 254 270 } 255 271 256 272 .item-card-title { 257 273 font-size: 15px; 258 274 font-weight: 600; 259 275 color: var(--text-primary); 276 + } 277 + 278 + .item-card-title::before { 279 + content: '\25BC '; 280 + font-size: 10px; 281 + margin-right: 6px; 282 + } 283 + 284 + .item-card.collapsed .item-card-title::before { 285 + content: '\25B6 '; 260 286 } 261 287 262 288 .item-card-body .form-group {
+8 -1
app/settings/settings.js
··· 801 801 802 802 items.forEach((item, i) => { 803 803 const card = document.createElement('div'); 804 - card.className = 'item-card'; 804 + card.className = 'item-card collapsed'; 805 805 806 806 const header = document.createElement('div'); 807 807 header.className = 'item-card-header'; ··· 818 818 checkbox.type = 'checkbox'; 819 819 checkbox.checked = item.enabled || false; 820 820 checkbox.addEventListener('change', (e) => { 821 + e.stopPropagation(); 821 822 items[i].enabled = e.target.checked; 822 823 save(); 823 824 }); ··· 825 826 wrapper.appendChild(checkbox); 826 827 header.appendChild(wrapper); 827 828 card.appendChild(header); 829 + 830 + // Toggle collapse on header click 831 + header.addEventListener('click', (e) => { 832 + if (e.target === checkbox) return; 833 + card.classList.toggle('collapsed'); 834 + }); 828 835 829 836 const body = document.createElement('div'); 830 837 body.className = 'item-card-body';
+8 -5
extensions/groups/home.js
··· 168 168 if (result.success) { 169 169 state.tags = result.data; 170 170 debug && console.log('Loaded tags:', state.tags.length); 171 + 172 + // Fetch address count for each tag 173 + for (const tag of state.tags) { 174 + const addressResult = await api.datastore.getAddressesByTag(tag.id); 175 + tag.addressCount = addressResult.success ? addressResult.data.length : 0; 176 + } 171 177 } else { 172 178 console.error('Failed to load tags:', result.error); 173 179 state.tags = []; ··· 321 327 322 328 const meta = document.createElement('div'); 323 329 meta.className = 'card-meta'; 324 - if (tag.isSpecial) { 325 - meta.textContent = `${tag.frequency || 0} addresses`; 326 - } else { 327 - meta.textContent = `Used ${tag.frequency || 0} times`; 328 - } 330 + const count = tag.isSpecial ? (tag.frequency || 0) : (tag.addressCount || 0); 331 + meta.textContent = `${count} ${count === 1 ? 'page' : 'pages'}`; 329 332 330 333 content.appendChild(title); 331 334 content.appendChild(meta);
+1 -20
extensions/groups/manifest.json
··· 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 8 "builtin": true, 9 - "schemas": { 10 - "prefs": { 11 - "type": "object", 12 - "properties": { 13 - "shortcutKey": { 14 - "type": "string", 15 - "description": "Global OS hotkey to open groups manager", 16 - "default": "Option+g" 17 - } 18 - } 19 - } 20 - }, 21 - "storageKeys": { 22 - "PREFS": "prefs" 23 - }, 24 - "defaults": { 25 - "prefs": { 26 - "shortcutKey": "Option+g" 27 - } 28 - } 9 + "settingsSchema": "./settings-schema.json" 29 10 }
+20
extensions/groups/settings-schema.json
··· 1 + { 2 + "prefs": { 3 + "type": "object", 4 + "properties": { 5 + "shortcutKey": { 6 + "type": "string", 7 + "description": "Global OS hotkey to open groups manager", 8 + "default": "Option+g" 9 + } 10 + } 11 + }, 12 + "storageKeys": { 13 + "PREFS": "prefs" 14 + }, 15 + "defaults": { 16 + "prefs": { 17 + "shortcutKey": "Option+g" 18 + } 19 + } 20 + }
+1 -86
extensions/peeks/manifest.json
··· 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 8 "builtin": true, 9 - "schemas": { 10 - "prefs": { 11 - "type": "object", 12 - "properties": { 13 - "shortcutKeyPrefix": { 14 - "type": "string", 15 - "description": "Global OS hotkey prefix to trigger peeks - followed by 0-9", 16 - "default": "Option+" 17 - } 18 - } 19 - }, 20 - "item": { 21 - "title": "Peeks", 22 - "type": "object", 23 - "properties": { 24 - "keyNum": { 25 - "type": "integer", 26 - "description": "Number key (0-9) to trigger this peek", 27 - "minimum": 0, 28 - "maximum": 9, 29 - "default": 0 30 - }, 31 - "title": { 32 - "type": "string", 33 - "description": "Name of the peek", 34 - "default": "New Peek" 35 - }, 36 - "address": { 37 - "type": "string", 38 - "description": "URL to load", 39 - "default": "https://example.com" 40 - }, 41 - "height": { 42 - "type": "integer", 43 - "description": "Window height", 44 - "default": 600 45 - }, 46 - "width": { 47 - "type": "integer", 48 - "description": "Window width", 49 - "default": 800 50 - }, 51 - "persistState": { 52 - "type": "boolean", 53 - "description": "Persist local state between sessions", 54 - "default": false 55 - }, 56 - "keepLive": { 57 - "type": "boolean", 58 - "description": "Keep page alive in background", 59 - "default": false 60 - }, 61 - "allowSound": { 62 - "type": "boolean", 63 - "description": "Allow the page to emit sound", 64 - "default": false 65 - }, 66 - "enabled": { 67 - "type": "boolean", 68 - "description": "Whether this peek is enabled", 69 - "default": false 70 - } 71 - } 72 - } 73 - }, 74 - "storageKeys": { 75 - "PREFS": "prefs", 76 - "ITEMS": "items" 77 - }, 78 - "defaults": { 79 - "prefs": { 80 - "shortcutKeyPrefix": "Option+" 81 - }, 82 - "items": [ 83 - { "keyNum": 0, "title": "Peek key 0", "address": "https://example.com/", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": true }, 84 - { "keyNum": 1, "title": "Peek key 1", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 85 - { "keyNum": 2, "title": "Peek key 2", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 86 - { "keyNum": 3, "title": "Peek key 3", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 87 - { "keyNum": 4, "title": "Peek key 4", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 88 - { "keyNum": 5, "title": "Peek key 5", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 89 - { "keyNum": 6, "title": "Peek key 6", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 90 - { "keyNum": 7, "title": "Peek key 7", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 91 - { "keyNum": 8, "title": "Peek key 8", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 92 - { "keyNum": 9, "title": "Peek key 9", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false } 93 - ] 94 - } 9 + "settingsSchema": "./settings-schema.json" 95 10 }
+87
extensions/peeks/settings-schema.json
··· 1 + { 2 + "prefs": { 3 + "type": "object", 4 + "properties": { 5 + "shortcutKeyPrefix": { 6 + "type": "string", 7 + "description": "Global OS hotkey prefix to trigger peeks - followed by 0-9", 8 + "default": "Option+" 9 + } 10 + } 11 + }, 12 + "item": { 13 + "title": "Peeks", 14 + "type": "object", 15 + "properties": { 16 + "keyNum": { 17 + "type": "integer", 18 + "description": "Number key (0-9) to trigger this peek", 19 + "minimum": 0, 20 + "maximum": 9, 21 + "default": 0, 22 + "readOnly": true 23 + }, 24 + "title": { 25 + "type": "string", 26 + "description": "Name of the peek", 27 + "default": "New Peek" 28 + }, 29 + "address": { 30 + "type": "string", 31 + "description": "URL to load", 32 + "default": "https://example.com" 33 + }, 34 + "height": { 35 + "type": "integer", 36 + "description": "Window height", 37 + "default": 600 38 + }, 39 + "width": { 40 + "type": "integer", 41 + "description": "Window width", 42 + "default": 800 43 + }, 44 + "persistState": { 45 + "type": "boolean", 46 + "description": "Persist local state between sessions", 47 + "default": false 48 + }, 49 + "keepLive": { 50 + "type": "boolean", 51 + "description": "Keep page alive in background", 52 + "default": false 53 + }, 54 + "allowSound": { 55 + "type": "boolean", 56 + "description": "Allow the page to emit sound", 57 + "default": false 58 + }, 59 + "enabled": { 60 + "type": "boolean", 61 + "description": "Whether this peek is enabled", 62 + "default": false 63 + } 64 + } 65 + }, 66 + "storageKeys": { 67 + "PREFS": "prefs", 68 + "ITEMS": "items" 69 + }, 70 + "defaults": { 71 + "prefs": { 72 + "shortcutKeyPrefix": "Option+" 73 + }, 74 + "items": [ 75 + { "keyNum": 0, "title": "Peek key 0", "address": "https://example.com/", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": true }, 76 + { "keyNum": 1, "title": "Peek key 1", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 77 + { "keyNum": 2, "title": "Peek key 2", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 78 + { "keyNum": 3, "title": "Peek key 3", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 79 + { "keyNum": 4, "title": "Peek key 4", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 80 + { "keyNum": 5, "title": "Peek key 5", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 81 + { "keyNum": 6, "title": "Peek key 6", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 82 + { "keyNum": 7, "title": "Peek key 7", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 83 + { "keyNum": 8, "title": "Peek key 8", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 84 + { "keyNum": 9, "title": "Peek key 9", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false } 85 + ] 86 + } 87 + }
+1 -78
extensions/slides/manifest.json
··· 6 6 "version": "1.0.0", 7 7 "background": "background.html", 8 8 "builtin": true, 9 - "schemas": { 10 - "prefs": { 11 - "type": "object", 12 - "properties": { 13 - "shortcutKeyPrefix": { 14 - "type": "string", 15 - "description": "Global OS hotkey prefix to trigger slides - followed by arrow keys", 16 - "default": "Option+" 17 - } 18 - } 19 - }, 20 - "item": { 21 - "title": "Slides", 22 - "type": "object", 23 - "properties": { 24 - "screenEdge": { 25 - "type": "string", 26 - "description": "Edge of screen to slide from (Up/Down/Left/Right)", 27 - "default": "Right" 28 - }, 29 - "title": { 30 - "type": "string", 31 - "description": "Name of the slide", 32 - "default": "New Slide" 33 - }, 34 - "address": { 35 - "type": "string", 36 - "description": "URL to load", 37 - "default": "https://example.com" 38 - }, 39 - "height": { 40 - "type": "integer", 41 - "description": "Window height", 42 - "default": 600 43 - }, 44 - "width": { 45 - "type": "integer", 46 - "description": "Window width", 47 - "default": 800 48 - }, 49 - "persistState": { 50 - "type": "boolean", 51 - "description": "Persist local state between sessions", 52 - "default": false 53 - }, 54 - "keepLive": { 55 - "type": "boolean", 56 - "description": "Keep page alive in background", 57 - "default": false 58 - }, 59 - "allowSound": { 60 - "type": "boolean", 61 - "description": "Allow the page to emit sound", 62 - "default": false 63 - }, 64 - "enabled": { 65 - "type": "boolean", 66 - "description": "Whether this slide is enabled", 67 - "default": false 68 - } 69 - } 70 - } 71 - }, 72 - "storageKeys": { 73 - "PREFS": "prefs", 74 - "ITEMS": "items" 75 - }, 76 - "defaults": { 77 - "prefs": { 78 - "shortcutKeyPrefix": "Option+" 79 - }, 80 - "items": [ 81 - { "screenEdge": "Up", "title": "Slide from top", "address": "http://localhost/", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": true }, 82 - { "screenEdge": "Down", "title": "Slide from bottom", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 83 - { "screenEdge": "Left", "title": "Slide from left", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 84 - { "screenEdge": "Right", "title": "Slide from right", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false } 85 - ] 86 - } 9 + "settingsSchema": "./settings-schema.json" 87 10 }
+79
extensions/slides/settings-schema.json
··· 1 + { 2 + "prefs": { 3 + "type": "object", 4 + "properties": { 5 + "shortcutKeyPrefix": { 6 + "type": "string", 7 + "description": "Global OS hotkey prefix to trigger slides - followed by arrow keys", 8 + "default": "Option+" 9 + } 10 + } 11 + }, 12 + "item": { 13 + "title": "Slides", 14 + "type": "object", 15 + "properties": { 16 + "screenEdge": { 17 + "type": "string", 18 + "description": "Edge of screen to slide from (Up/Down/Left/Right)", 19 + "default": "Right", 20 + "readOnly": true 21 + }, 22 + "title": { 23 + "type": "string", 24 + "description": "Name of the slide", 25 + "default": "New Slide" 26 + }, 27 + "address": { 28 + "type": "string", 29 + "description": "URL to load", 30 + "default": "https://example.com" 31 + }, 32 + "height": { 33 + "type": "integer", 34 + "description": "Window height", 35 + "default": 600 36 + }, 37 + "width": { 38 + "type": "integer", 39 + "description": "Window width", 40 + "default": 800 41 + }, 42 + "persistState": { 43 + "type": "boolean", 44 + "description": "Persist local state between sessions", 45 + "default": false 46 + }, 47 + "keepLive": { 48 + "type": "boolean", 49 + "description": "Keep page alive in background", 50 + "default": false 51 + }, 52 + "allowSound": { 53 + "type": "boolean", 54 + "description": "Allow the page to emit sound", 55 + "default": false 56 + }, 57 + "enabled": { 58 + "type": "boolean", 59 + "description": "Whether this slide is enabled", 60 + "default": false 61 + } 62 + } 63 + }, 64 + "storageKeys": { 65 + "PREFS": "prefs", 66 + "ITEMS": "items" 67 + }, 68 + "defaults": { 69 + "prefs": { 70 + "shortcutKeyPrefix": "Option+" 71 + }, 72 + "items": [ 73 + { "screenEdge": "Up", "title": "Slide from top", "address": "http://localhost/", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": true }, 74 + { "screenEdge": "Down", "title": "Slide from bottom", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 75 + { "screenEdge": "Left", "title": "Slide from left", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false }, 76 + { "screenEdge": "Right", "title": "Slide from right", "address": "", "persistState": false, "keepLive": false, "allowSound": false, "height": 600, "width": 800, "enabled": false } 77 + ] 78 + } 79 + }
+13 -1
index.js
··· 551 551 return null; 552 552 } 553 553 554 - // Load manifest 554 + // Load manifest and settings schema 555 555 let manifest = null; 556 556 try { 557 557 const manifestPath = path.join(extPath, 'manifest.json'); 558 558 if (fs.existsSync(manifestPath)) { 559 559 manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); 560 + 561 + // Load settings schema if specified 562 + if (manifest.settingsSchema) { 563 + const schemaPath = path.join(extPath, manifest.settingsSchema); 564 + if (fs.existsSync(schemaPath)) { 565 + const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8')); 566 + // Merge schema fields into manifest for Settings UI 567 + manifest.schemas = { prefs: schema.prefs, item: schema.item }; 568 + manifest.storageKeys = schema.storageKeys; 569 + manifest.defaults = schema.defaults; 570 + } 571 + } 560 572 } 561 573 } catch (err) { 562 574 console.error(`[ext:win] Failed to load manifest for ${extId}:`, err);