Rewild Your Web
18
fork

Configure Feed

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

at main 336 lines 9.1 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3// Task chooser UI component. 4// Listens for taskrequest events on navigator.embedder, displays a list of 5// matching task providers, and responds with the user's selection. 6// Handles both window mode (new webview) and inline mode (overlay dialog). 7 8import { 9 LitElement, 10 html, 11 css, 12} from "beaver://shared/third_party/lit/lit-all.min.js"; 13 14export class TaskChooser extends LitElement { 15 static properties = { 16 open: { type: Boolean, reflect: true }, 17 _taskName: { state: true }, 18 _providers: { state: true }, 19 _requestId: { state: true }, 20 }; 21 22 static styles = css` 23 :host { 24 display: none; 25 position: fixed; 26 inset: 0; 27 z-index: var(--z-modal); 28 } 29 30 :host([open]) { 31 display: flex; 32 } 33 34 .overlay { 35 position: absolute; 36 inset: 0; 37 background: var(--color-backdrop); 38 display: flex; 39 align-items: flex-end; 40 justify-content: center; 41 } 42 43 .panel { 44 background: var(--bg-surface); 45 border-radius: var(--radius-md) var(--radius-md) 0 0; 46 padding: var(--spacing-md); 47 width: 100%; 48 max-width: 400px; 49 max-height: 60vh; 50 overflow-y: auto; 51 box-shadow: 0 -4px 20px var(--color-shadow); 52 font-family: var(--font-family-base); 53 } 54 55 .title { 56 font-size: var(--font-size-menu); 57 font-weight: var(--font-weight-bold); 58 color: var(--color-text); 59 margin-bottom: var(--spacing-md); 60 } 61 62 .provider { 63 display: flex; 64 align-items: center; 65 gap: var(--spacing-md); 66 padding: var(--spacing-sm); 67 border-radius: var(--radius-sm); 68 cursor: pointer; 69 transition: background var(--transition-fast); 70 } 71 72 .provider:hover { 73 background: var(--bg-hover); 74 } 75 76 .provider-icon { 77 width: 24px; 78 height: 24px; 79 border-radius: var(--radius-sm); 80 object-fit: contain; 81 flex-shrink: 0; 82 } 83 84 .provider-title { 85 font-size: var(--font-size-menu); 86 font-weight: var(--font-weight-bold); 87 color: var(--color-text); 88 } 89 90 .provider-description { 91 font-size: var(--font-size-sm); 92 color: var(--color-text-secondary); 93 } 94 95 .provider-origin { 96 font-size: var(--font-size-xs); 97 color: var(--color-text-tertiary); 98 } 99 100 .cancel { 101 display: block; 102 width: 100%; 103 margin-top: var(--spacing-md); 104 padding: var(--spacing-sm); 105 border: 1px solid var(--color-border); 106 border-radius: var(--radius-sm); 107 background: var(--bg-surface); 108 color: var(--color-text); 109 font-size: var(--font-size-menu); 110 font-family: var(--font-family-base); 111 cursor: pointer; 112 text-align: center; 113 transition: background var(--transition-fast); 114 } 115 116 .cancel:hover { 117 background: var(--bg-hover); 118 } 119 120 .always-use { 121 display: flex; 122 align-items: center; 123 gap: var(--spacing-sm); 124 padding: var(--spacing-sm); 125 font-size: var(--font-size-sm); 126 color: var(--color-text-secondary); 127 cursor: pointer; 128 } 129 `; 130 131 constructor() { 132 super(); 133 this.open = false; 134 this._taskName = ""; 135 this._providers = []; 136 this._requestId = ""; 137 this._selectedDisplay = null; 138 } 139 140 connectedCallback() { 141 super.connectedCallback(); 142 if (navigator.embedder) { 143 this._onTaskRequest = (e) => this._handleTaskRequest(e); 144 navigator.embedder.addEventListener("taskrequest", this._onTaskRequest); 145 146 this._onOpenProvider = (e) => this._handleOpenProvider(e); 147 navigator.embedder.addEventListener( 148 "opentaskprovider", 149 this._onOpenProvider, 150 ); 151 152 this._onProvidersUpdate = (e) => this._handleProvidersUpdate(e); 153 navigator.embedder.addEventListener( 154 "taskprovidersupdate", 155 this._onProvidersUpdate, 156 ); 157 } 158 } 159 160 disconnectedCallback() { 161 super.disconnectedCallback(); 162 if (navigator.embedder) { 163 if (this._onTaskRequest) { 164 navigator.embedder.removeEventListener( 165 "taskrequest", 166 this._onTaskRequest, 167 ); 168 } 169 if (this._onOpenProvider) { 170 navigator.embedder.removeEventListener( 171 "opentaskprovider", 172 this._onOpenProvider, 173 ); 174 } 175 if (this._onProvidersUpdate) { 176 navigator.embedder.removeEventListener( 177 "taskprovidersupdate", 178 this._onProvidersUpdate, 179 ); 180 } 181 } 182 } 183 184 _handleProvidersUpdate(e) { 185 const { requestId, providers } = e.detail; 186 if (requestId !== this._requestId) return; 187 188 try { 189 const newProviders = 190 typeof providers === "string" ? JSON.parse(providers) : providers; 191 if (newProviders.length > 0) { 192 console.log( 193 `[TaskChooser] Remote providers update: +${newProviders.length}`, 194 ); 195 this._providers = [...this._providers, ...newProviders]; 196 } 197 } catch { 198 // Ignore parse errors. 199 } 200 } 201 202 _handleOpenProvider(e) { 203 const { requestId, url, title } = e.detail; 204 console.log(`[TaskChooser] Opening provider: ${title} (${url})`); 205 206 if (this._selectedDisplay === "inline") { 207 // Dispatch event to show inline provider on the caller's webview. 208 this.dispatchEvent( 209 new CustomEvent("show-inline-provider", { 210 bubbles: true, 211 composed: true, 212 detail: { requestId, url, title }, 213 }), 214 ); 215 } else { 216 // Dispatch event to index.js to open the provider in a new webview. 217 this.dispatchEvent( 218 new CustomEvent("open-task-provider", { 219 bubbles: true, 220 composed: true, 221 detail: { requestId, url, title }, 222 }), 223 ); 224 } 225 } 226 227 _handleTaskRequest(e) { 228 const { requestId, taskName, providers, defaultProvider } = e.detail; 229 console.log("[TaskChooser] Task request received:", taskName, requestId); 230 231 this._requestId = requestId; 232 this._taskName = taskName; 233 this._selectedDisplay = null; 234 235 try { 236 this._providers = 237 typeof providers === "string" ? JSON.parse(providers) : providers; 238 } catch { 239 this._providers = []; 240 } 241 242 if (this._providers.length === 0) { 243 console.log("[TaskChooser] No providers, cancelling"); 244 this._respond(null); 245 return; 246 } 247 248 // If there's a default and it's in the provider list, auto-select it. 249 if (defaultProvider) { 250 const defaultProv = this._providers.find((p) => p.id === defaultProvider); 251 if (defaultProv) { 252 console.log("[TaskChooser] Using default provider:", defaultProv.title); 253 this._selectProvider(defaultProv); 254 return; 255 } 256 } 257 258 this.open = true; 259 } 260 261 _selectProvider(provider) { 262 console.log("[TaskChooser] Provider selected:", provider.id, provider.title); 263 264 // If "always use this" is checked, save the default. 265 const checkbox = this.shadowRoot?.querySelector("#always-use"); 266 if (checkbox?.checked) { 267 navigator.embedder.setTaskDefault( 268 this._taskName, 269 provider.href, 270 provider.remote_peer_id || null, 271 ); 272 } 273 274 // Store display mode for when opentaskprovider arrives. 275 this._selectedDisplay = provider.display === "Inline" ? "inline" : "window"; 276 this._respond(provider.id); 277 this.open = false; 278 } 279 280 _cancel() { 281 console.log("[TaskChooser] Cancelled"); 282 this._respond(null); 283 this.open = false; 284 } 285 286 _respond(providerId) { 287 if (this._requestId && navigator.embedder) { 288 navigator.embedder.respondToTaskRequest(this._requestId, providerId); 289 this._requestId = ""; 290 } 291 } 292 293 render() { 294 if (!this.open) return html``; 295 296 // Provider chooser. 297 return html` 298 <div class="overlay" @click=${this._cancel}> 299 <div class="panel" @click=${(e) => e.stopPropagation()}> 300 <div class="title">${this._taskName} with...</div> 301 ${this._providers.map( 302 (p) => html` 303 <div class="provider" @click=${() => this._selectProvider(p)}> 304 ${p.icon 305 ? html`<img class="provider-icon" src=${p.icon} alt="" />` 306 : ""} 307 <div> 308 <div class="provider-title">${p.title}</div> 309 ${p.description 310 ? html`<div class="provider-description"> 311 ${p.description} 312 </div>` 313 : ""} 314 <div class="provider-origin"> 315 ${p.origin}${p.device_name 316 ? html` <span class="device-name">(${p.device_name})</span>` 317 : ""} 318 </div> 319 </div> 320 </div> 321 `, 322 )} 323 ${this._providers.length > 1 324 ? html`<label class="always-use"> 325 <input type="checkbox" id="always-use" /> 326 Always use the selected provider 327 </label>` 328 : ""} 329 <button class="cancel" @click=${this._cancel}>Cancel</button> 330 </div> 331 </div> 332 `; 333 } 334} 335 336customElements.define("task-chooser", TaskChooser);