| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * \file | 
					
						
							|  |  |  |  |  * \defgroup tfclient Tilde Friends Client JS | 
					
						
							|  |  |  |  |  * Tilde Friends client-side browser JavaScript. | 
					
						
							|  |  |  |  |  * @{ | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** \cond */ | 
					
						
							| 
									
										
										
										
											2024-02-09 01:21:57 +00:00
										 |  |  |  | import {LitElement, html, css, svg} from '/lit/lit-all.min.js'; | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | let cm6; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | let g_socket; | 
					
						
							|  |  |  |  | let g_current_file; | 
					
						
							|  |  |  |  | let g_files = {}; | 
					
						
							|  |  |  |  | let g_app = {files: {}, emoji: '📦'}; | 
					
						
							|  |  |  |  | let g_editor; | 
					
						
							|  |  |  |  | let g_unloading; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | let k_color_error = '#dc322f'; | 
					
						
							|  |  |  |  | let k_color_disconnect = '#f00'; | 
					
						
							|  |  |  |  | let k_color_status = '#fff'; | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | /** \endcond */ | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | /** Functions that server-side app code can call through the app object. */ | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | const k_api = { | 
					
						
							|  |  |  |  | 	setDocument: {args: ['content'], func: api_setDocument}, | 
					
						
							|  |  |  |  | 	postMessage: {args: ['message'], func: api_postMessage}, | 
					
						
							|  |  |  |  | 	error: {args: ['error'], func: api_error}, | 
					
						
							|  |  |  |  | 	localStorageSet: {args: ['key', 'value'], func: api_localStorageSet}, | 
					
						
							|  |  |  |  | 	localStorageGet: {args: ['key'], func: api_localStorageGet}, | 
					
						
							| 
									
										
										
										
											2022-07-27 00:27:10 +00:00
										 |  |  |  | 	requestPermission: {args: ['permission', 'id'], func: api_requestPermission}, | 
					
						
							| 
									
										
										
										
											2022-08-14 16:58:26 +00:00
										 |  |  |  | 	print: {args: ['...'], func: api_print}, | 
					
						
							| 
									
										
										
										
											2022-09-15 00:16:37 +00:00
										 |  |  |  | 	setHash: {args: ['hash'], func: api_setHash}, | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | }; | 
					
						
							| 
									
										
										
										
											2017-05-22 19:38:49 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Class that represents the top bar | 
					
						
							|  |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | class TfNavigationElement extends LitElement { | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Get Lit Html properties. | 
					
						
							|  |  |  |  | 	 * @return The properties. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	static get properties() { | 
					
						
							|  |  |  |  | 		return { | 
					
						
							|  |  |  |  | 			credentials: {type: Object}, | 
					
						
							|  |  |  |  | 			permissions: {type: Object}, | 
					
						
							|  |  |  |  | 			show_permissions: {type: Boolean}, | 
					
						
							|  |  |  |  | 			status: {type: Object}, | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			spark_lines: {type: Object}, | 
					
						
							| 
									
										
										
										
											2023-06-28 23:00:34 +00:00
										 |  |  |  | 			version: {type: Object}, | 
					
						
							| 
									
										
										
										
											2024-11-20 20:06:33 -05:00
										 |  |  |  | 			show_expanded: {type: Boolean}, | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 			identity: {type: String}, | 
					
						
							|  |  |  |  | 			identities: {type: Array}, | 
					
						
							| 
									
										
										
										
											2024-04-13 21:51:18 -04:00
										 |  |  |  | 			names: {type: Object}, | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 		}; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Create a TfNavigationElement instance. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	constructor() { | 
					
						
							|  |  |  |  | 		super(); | 
					
						
							|  |  |  |  | 		this.permissions = {}; | 
					
						
							|  |  |  |  | 		this.show_permissions = false; | 
					
						
							|  |  |  |  | 		this.status = {}; | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 		this.spark_lines = {}; | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 		this.identities = []; | 
					
						
							| 
									
										
										
										
											2024-04-13 21:51:18 -04:00
										 |  |  |  | 		this.names = {}; | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Toggle editor visibility. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param event The HTML event. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	toggle_edit(event) { | 
					
						
							|  |  |  |  | 		event.preventDefault(); | 
					
						
							|  |  |  |  | 		if (editing()) { | 
					
						
							|  |  |  |  | 			closeEditor(); | 
					
						
							|  |  |  |  | 		} else { | 
					
						
							|  |  |  |  | 			edit(); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Remove a stored permission. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param key The permission to reset. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	reset_permission(key) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		send({action: 'resetPermission', permission: key}); | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Get or create a spark line. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param key The spark line identifier. | 
					
						
							|  |  |  |  | 	 * @param options Spark line options. | 
					
						
							|  |  |  |  | 	 * @return A spark line HTML element. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 	get_spark_line(key, options) { | 
					
						
							|  |  |  |  | 		if (!this.spark_lines[key]) { | 
					
						
							|  |  |  |  | 			let spark_line = document.createElement('tf-sparkline'); | 
					
						
							|  |  |  |  | 			spark_line.title = key; | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 			spark_line.classList.add('w3-bar-item'); | 
					
						
							|  |  |  |  | 			spark_line.style.paddingRight = '0'; | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			if (options) { | 
					
						
							|  |  |  |  | 				if (options.max) { | 
					
						
							|  |  |  |  | 					spark_line.max = options.max; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 			this.spark_lines[key] = spark_line; | 
					
						
							|  |  |  |  | 			this.requestUpdate(); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 		return this.spark_lines[key]; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Set the active SSB identity for the current application. | 
					
						
							|  |  |  |  | 	 * @param id The identity. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-04-14 17:47:47 -04:00
										 |  |  |  | 	set_active_identity(id) { | 
					
						
							|  |  |  |  | 		send({action: 'setActiveIdentity', identity: id}); | 
					
						
							|  |  |  |  | 		this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show'); | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Create a new SSB identity. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							|  |  |  |  | 	create_identity() { | 
					
						
							| 
									
										
										
										
											2024-04-13 20:32:17 -04:00
										 |  |  |  | 		if (confirm('Are you sure you want to create a new identity?')) { | 
					
						
							|  |  |  |  | 			send({action: 'createIdentity'}); | 
					
						
							|  |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Toggle visibility of the ID dropdown. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-04-14 17:47:47 -04:00
										 |  |  |  | 	toggle_id_dropdown() { | 
					
						
							|  |  |  |  | 		this.renderRoot.getElementById('id_dropdown').classList.toggle('w3-show'); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Edit the current identity's SSB profile. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-04-17 17:21:44 +01:00
										 |  |  |  | 	edit_profile() { | 
					
						
							|  |  |  |  | 		window.location.href = '/~core/ssb/#' + this.identity; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Sign out of the current Tilde Friends user. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 	logout() { | 
					
						
							|  |  |  |  | 		window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Render the identity dropdown. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 	render_identity() { | 
					
						
							| 
									
										
										
										
											2024-04-13 21:51:18 -04:00
										 |  |  |  | 		let self = this; | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | 		if (this?.credentials?.session?.name) { | 
					
						
							|  |  |  |  | 			if (this.identities?.length) { | 
					
						
							|  |  |  |  | 				return html`
 | 
					
						
							|  |  |  |  | 					<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							|  |  |  |  | 					<div class="w3-dropdown-click w3-right" style="max-width: 100%"> | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  |  | 						<button | 
					
						
							| 
									
										
										
										
											2025-01-18 21:56:43 -05:00
										 |  |  |  | 							class="w3-button w3-rest" | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 							style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%" | 
					
						
							|  |  |  |  | 							id="identity" | 
					
						
							|  |  |  |  | 							@click=${self.toggle_id_dropdown} | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  |  | 						> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 							${self.names[this.identity]}▾ | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  |  | 						</button> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 						<div | 
					
						
							|  |  |  |  | 							id="id_dropdown" | 
					
						
							|  |  |  |  | 							class="w3-dropdown-content w3-bar-block w3-card-4" | 
					
						
							|  |  |  |  | 							style="max-width: 100%; right: 0" | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  |  | 						> | 
					
						
							| 
									
										
										
										
											2024-11-13 18:58:09 -05:00
										 |  |  |  | 							<div | 
					
						
							|  |  |  |  | 								style="position: fixed; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.25); z-index: -100" | 
					
						
							|  |  |  |  | 								@click=${self.toggle_id_dropdown} | 
					
						
							|  |  |  |  | 							></div> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 							<button | 
					
						
							|  |  |  |  | 								class="w3-bar-item w3-button w3-border" | 
					
						
							|  |  |  |  | 								@click=${() => (window.location.href = '/~core/identity')} | 
					
						
							|  |  |  |  | 							> | 
					
						
							|  |  |  |  | 								Manage Identities... | 
					
						
							|  |  |  |  | 							</button> | 
					
						
							|  |  |  |  | 							<button | 
					
						
							| 
									
										
										
										
											2024-11-25 09:35:55 -05:00
										 |  |  |  | 								id="edit_profile" | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 								class="w3-bar-item w3-button w3-border" | 
					
						
							|  |  |  |  | 								@click=${self.edit_profile} | 
					
						
							|  |  |  |  | 							> | 
					
						
							|  |  |  |  | 								Edit Profile... | 
					
						
							|  |  |  |  | 							</button> | 
					
						
							|  |  |  |  | 							${this.identities.map( | 
					
						
							|  |  |  |  | 								(x) => html`
 | 
					
						
							|  |  |  |  | 									<button | 
					
						
							|  |  |  |  | 										class="w3-bar-item w3-button ${x === self.identity | 
					
						
							|  |  |  |  | 											? 'w3-cyan' | 
					
						
							|  |  |  |  | 											: ''}" | 
					
						
							|  |  |  |  | 										@click=${() => self.set_active_identity(x)} | 
					
						
							|  |  |  |  | 										style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap" | 
					
						
							|  |  |  |  | 									> | 
					
						
							|  |  |  |  | 										${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`} | 
					
						
							|  |  |  |  | 									</button> | 
					
						
							|  |  |  |  | 								`
 | 
					
						
							|  |  |  |  | 							)} | 
					
						
							|  |  |  |  | 							<button | 
					
						
							|  |  |  |  | 								class="w3-bar-item w3-button w3-border" | 
					
						
							|  |  |  |  | 								id="logout" | 
					
						
							|  |  |  |  | 								@click=${self.logout} | 
					
						
							|  |  |  |  | 							> | 
					
						
							| 
									
										
										
										
											2024-06-23 11:47:12 -04:00
										 |  |  |  | 								Logout ${this.credentials.session.name} | 
					
						
							|  |  |  |  | 							</button> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 						</div> | 
					
						
							| 
									
										
										
										
											2024-04-14 17:47:47 -04:00
										 |  |  |  | 					</div> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 				`;
 | 
					
						
							|  |  |  |  | 			} else if ( | 
					
						
							|  |  |  |  | 				this.credentials?.session?.name && | 
					
						
							|  |  |  |  | 				this.credentials.session.name !== 'guest' | 
					
						
							|  |  |  |  | 			) { | 
					
						
							|  |  |  |  | 				return html`
 | 
					
						
							|  |  |  |  | 					<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							|  |  |  |  | 					<button | 
					
						
							| 
									
										
										
										
											2024-06-23 11:47:12 -04:00
										 |  |  |  | 						class="w3-bar-item w3-button w3-right w3-cyan" | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 						id="logout" | 
					
						
							|  |  |  |  | 						@click=${self.logout} | 
					
						
							|  |  |  |  | 					> | 
					
						
							| 
									
										
										
										
											2024-06-23 11:47:12 -04:00
										 |  |  |  | 						Logout ${this.credentials.session.name} | 
					
						
							|  |  |  |  | 					</button> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 					<button | 
					
						
							|  |  |  |  | 						id="create_identity" | 
					
						
							|  |  |  |  | 						@click=${this.create_identity} | 
					
						
							| 
									
										
										
										
											2024-06-23 11:47:12 -04:00
										 |  |  |  | 						class="w3-button w3-mobile w3-red w3-right" | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 					> | 
					
						
							|  |  |  |  | 						Create an Identity | 
					
						
							|  |  |  |  | 					</button> | 
					
						
							|  |  |  |  | 				`;
 | 
					
						
							|  |  |  |  | 			} else { | 
					
						
							|  |  |  |  | 				return html`
 | 
					
						
							|  |  |  |  | 					<button | 
					
						
							|  |  |  |  | 						class="w3-bar-item w3-button w3-right w3-cyan" | 
					
						
							|  |  |  |  | 						id="logout" | 
					
						
							|  |  |  |  | 						@click=${self.logout} | 
					
						
							|  |  |  |  | 					> | 
					
						
							| 
									
										
										
										
											2024-06-23 11:47:12 -04:00
										 |  |  |  | 						Logout ${this.credentials.session.name} | 
					
						
							|  |  |  |  | 					</button> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 				`;
 | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 		} else { | 
					
						
							|  |  |  |  | 			return html`<a
 | 
					
						
							|  |  |  |  | 				class="w3-bar-item w3-cyan w3-right" | 
					
						
							|  |  |  |  | 				id="login" | 
					
						
							|  |  |  |  | 				href="/login?return=${url() + hash()}" | 
					
						
							|  |  |  |  | 				>login</a | 
					
						
							|  |  |  |  | 			>`;
 | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render the permissions popup. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	render_permissions() { | 
					
						
							|  |  |  |  | 		if (this.show_permissions) { | 
					
						
							|  |  |  |  | 			return html`
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							|  |  |  |  | 				<div | 
					
						
							|  |  |  |  | 					style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%" | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 					<div | 
					
						
							|  |  |  |  | 						style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff" | 
					
						
							|  |  |  |  | 					> | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 						<div>This app has the following permissions:</div> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 						${Object.keys(this.permissions).map( | 
					
						
							|  |  |  |  | 							(key) => html`
 | 
					
						
							| 
									
										
										
										
											2024-04-13 10:03:06 -04:00
										 |  |  |  | 								<div> | 
					
						
							|  |  |  |  | 									<span>${key}</span>: | 
					
						
							|  |  |  |  | 									${this.permissions[key] ? '✅ Allowed' : '❌ Denied'} | 
					
						
							|  |  |  |  | 									<button | 
					
						
							|  |  |  |  | 										@click=${() => this.reset_permission(key)} | 
					
						
							|  |  |  |  | 										class="w3-button w3-red" | 
					
						
							| 
									
										
										
										
											2025-04-16 12:32:34 -04:00
										 |  |  |  | 										id=${'permission_reset:' + key} | 
					
						
							| 
									
										
										
										
											2024-04-13 10:03:06 -04:00
										 |  |  |  | 									> | 
					
						
							|  |  |  |  | 										Reset | 
					
						
							|  |  |  |  | 									</button> | 
					
						
							|  |  |  |  | 								</div> | 
					
						
							|  |  |  |  | 							`
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 						)} | 
					
						
							|  |  |  |  | 						<button | 
					
						
							|  |  |  |  | 							@click=${() => (this.show_permissions = false)} | 
					
						
							|  |  |  |  | 							class="w3-button w3-blue" | 
					
						
							| 
									
										
										
										
											2025-04-16 12:32:34 -04:00
										 |  |  |  | 							id="permissions_close" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 						> | 
					
						
							|  |  |  |  | 							Close | 
					
						
							|  |  |  |  | 						</button> | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 					</div> | 
					
						
							|  |  |  |  | 				</div> | 
					
						
							|  |  |  |  | 			`;
 | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Clear the current error. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  |  | 	clear_error() { | 
					
						
							|  |  |  |  | 		this.status = {}; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render the navigation bar. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 	render() { | 
					
						
							|  |  |  |  | 		let self = this; | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 		const k_global_style = css`
 | 
					
						
							|  |  |  |  | 			a:link { | 
					
						
							|  |  |  |  | 				color: #268bd2; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 			a:visited { | 
					
						
							|  |  |  |  | 				color: #6c71c4; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 			a:hover { | 
					
						
							|  |  |  |  | 				color: #859900; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 			a:active { | 
					
						
							|  |  |  |  | 				color: #2aa198; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 		`;
 | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 		return html`
 | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 			<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 			<style> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				${k_global_style} .tooltip { | 
					
						
							| 
									
										
										
										
											2023-12-29 18:12:14 +00:00
										 |  |  |  | 					position: absolute; | 
					
						
							|  |  |  |  | 					z-index: 1; | 
					
						
							|  |  |  |  | 					display: none; | 
					
						
							|  |  |  |  | 					border: 1px solid black; | 
					
						
							|  |  |  |  | 					padding: 4px; | 
					
						
							|  |  |  |  | 					color: black; | 
					
						
							|  |  |  |  | 					background: white; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 				.tooltip_parent:hover .tooltip { | 
					
						
							|  |  |  |  | 					display: inline-block; | 
					
						
							|  |  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 			</style> | 
					
						
							| 
									
										
										
										
											2024-04-13 20:07:39 -04:00
										 |  |  |  | 			<div class="w3-black w3-bar"> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				<span | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					style="cursor: pointer" | 
					
						
							| 
									
										
										
										
											2024-11-20 20:06:33 -05:00
										 |  |  |  | 					@click=${() => (this.show_expanded = !this.show_expanded)} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					>😎</span | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 				<span | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2024-04-13 20:07:39 -04:00
										 |  |  |  | 					style=${'white-space: nowrap' + | 
					
						
							| 
									
										
										
										
											2024-11-20 20:06:33 -05:00
										 |  |  |  | 					(this.show_expanded ? '' : '; display: none')} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					title=${this.version?.name + | 
					
						
							|  |  |  |  | 					' ' + | 
					
						
							|  |  |  |  | 					Object.entries(this.version || {}) | 
					
						
							|  |  |  |  | 						.filter((x) => ['name', 'number'].indexOf(x[0]) == -1) | 
					
						
							|  |  |  |  | 						.map((x) => `\n* ${x[0]}: ${x[1]}`)} | 
					
						
							|  |  |  |  | 					>${this.version?.number}</span | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 				<a | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					accesskey="h" | 
					
						
							|  |  |  |  | 					@mouseover=${set_access_key_title} | 
					
						
							|  |  |  |  | 					data-tip="Open home app." | 
					
						
							|  |  |  |  | 					href="/" | 
					
						
							|  |  |  |  | 					style="color: #fff; white-space: nowrap" | 
					
						
							|  |  |  |  | 					>TF</a | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 				<a | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					accesskey="a" | 
					
						
							|  |  |  |  | 					@mouseover=${set_access_key_title} | 
					
						
							|  |  |  |  | 					data-tip="Open apps list." | 
					
						
							|  |  |  |  | 					href="/~core/apps/" | 
					
						
							|  |  |  |  | 					>apps</a | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 				<a | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					accesskey="e" | 
					
						
							|  |  |  |  | 					@mouseover=${set_access_key_title} | 
					
						
							|  |  |  |  | 					data-tip="Toggle the app editor." | 
					
						
							|  |  |  |  | 					href="#" | 
					
						
							|  |  |  |  | 					@click=${this.toggle_edit} | 
					
						
							|  |  |  |  | 					>edit</a | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 				<a | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					accesskey="p" | 
					
						
							|  |  |  |  | 					@mouseover=${set_access_key_title} | 
					
						
							|  |  |  |  | 					data-tip="View and change permissions." | 
					
						
							|  |  |  |  | 					href="#" | 
					
						
							|  |  |  |  | 					@click=${() => (self.show_permissions = !self.show_permissions)} | 
					
						
							|  |  |  |  | 					>🎛️</a | 
					
						
							|  |  |  |  | 				> | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 				${this.render_permissions()} | 
					
						
							| 
									
										
										
										
											2024-04-18 12:46:06 -04:00
										 |  |  |  | 				${this.status?.message && !this.status.is_error | 
					
						
							|  |  |  |  | 					? html`
 | 
					
						
							| 
									
										
										
										
											2024-04-24 19:23:13 -04:00
										 |  |  |  | 							<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							|  |  |  |  | 							<div | 
					
						
							|  |  |  |  | 								class="w3-bar-item" | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 								style="color: ${this.status.color ?? k_color_status}" | 
					
						
							| 
									
										
										
										
											2024-04-24 19:23:13 -04:00
										 |  |  |  | 							> | 
					
						
							|  |  |  |  | 								${this.status.message} | 
					
						
							|  |  |  |  | 							</div> | 
					
						
							| 
									
										
										
										
											2024-04-18 12:46:06 -04:00
										 |  |  |  | 						`
 | 
					
						
							|  |  |  |  | 					: undefined} | 
					
						
							| 
									
										
										
										
											2024-11-20 20:06:33 -05:00
										 |  |  |  | 				<span class=${this.show_expanded ? '' : 'w3-hide-small'}> | 
					
						
							|  |  |  |  | 					${Object.keys(this.spark_lines) | 
					
						
							|  |  |  |  | 						.sort() | 
					
						
							|  |  |  |  | 						.map((x) => this.spark_lines[x])} | 
					
						
							|  |  |  |  | 				</span> | 
					
						
							| 
									
										
										
										
											2024-06-20 20:41:27 -04:00
										 |  |  |  | 				${this.render_identity()} | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			</div> | 
					
						
							| 
									
										
										
										
											2024-04-18 12:46:06 -04:00
										 |  |  |  | 			${this.status?.is_error | 
					
						
							|  |  |  |  | 				? html`
 | 
					
						
							|  |  |  |  | 					<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							|  |  |  |  | 					<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1"> | 
					
						
							|  |  |  |  | 						<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em"> | 
					
						
							| 
									
										
										
										
											2024-10-25 12:34:22 -04:00
										 |  |  |  | 							<span id="close_error" @click=${self.clear_error} class="w3-button w3-display-topright">×</span> | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 							<div style="color: ${this.status.color ?? k_color_error}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div> | 
					
						
							| 
									
										
										
										
											2024-04-17 20:56:33 -04:00
										 |  |  |  | 						</div> | 
					
						
							| 
									
										
										
										
											2024-04-18 12:46:06 -04:00
										 |  |  |  | 					</div> | 
					
						
							|  |  |  |  | 					`
 | 
					
						
							|  |  |  |  | 				: undefined} | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 		`;
 | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-03 18:47:34 -04:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Create a tf-navigation element. | 
					
						
							|  |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | customElements.define('tf-navigation', TfNavigationElement); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * A file in the files sidebar. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | class TfFilesElement extends LitElement { | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * LitElement properties. | 
					
						
							|  |  |  |  | 	 * @return The properties. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	static get properties() { | 
					
						
							|  |  |  |  | 		return { | 
					
						
							|  |  |  |  | 			current: {type: String}, | 
					
						
							|  |  |  |  | 			files: {type: Object}, | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 			dropping: {type: Number}, | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 			drop_target: {type: String}, | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 		}; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Create a TfFilesElement instance. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	constructor() { | 
					
						
							|  |  |  |  | 		super(); | 
					
						
							|  |  |  |  | 		this.files = {}; | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 		this.dropping = 0; | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Select a clicked file. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param file The file. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	file_click(file) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		this.dispatchEvent( | 
					
						
							|  |  |  |  | 			new CustomEvent('file_click', { | 
					
						
							|  |  |  |  | 				detail: { | 
					
						
							|  |  |  |  | 					file: file, | 
					
						
							|  |  |  |  | 				}, | 
					
						
							|  |  |  |  | 				bubbles: true, | 
					
						
							|  |  |  |  | 				composed: true, | 
					
						
							|  |  |  |  | 			}) | 
					
						
							|  |  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render a single file in the file list. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param file The file. | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	render_file(file) { | 
					
						
							|  |  |  |  | 		let classes = ['file']; | 
					
						
							|  |  |  |  | 		if (file == this.current) { | 
					
						
							|  |  |  |  | 			classes.push('current'); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 		if (!this.files[file].clean) { | 
					
						
							|  |  |  |  | 			classes.push('dirty'); | 
					
						
							|  |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 		if (this.drop_target == file) { | 
					
						
							|  |  |  |  | 			classes.push('drop'); | 
					
						
							|  |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		return html`<div
 | 
					
						
							|  |  |  |  | 			class="${classes.join(' ')}" | 
					
						
							|  |  |  |  | 			@click=${(x) => this.file_click(file)} | 
					
						
							|  |  |  |  | 		> | 
					
						
							|  |  |  |  | 			${file} | 
					
						
							|  |  |  |  | 		</div>`; | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Create a file entry for a dropped file. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param event The event. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 	async drop(event) { | 
					
						
							|  |  |  |  | 		event.preventDefault(); | 
					
						
							|  |  |  |  | 		event.stopPropagation(); | 
					
						
							|  |  |  |  | 		this.dropping = 0; | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 		this.drop_target = undefined; | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 		for (let file of event.dataTransfer.files) { | 
					
						
							| 
									
										
										
										
											2023-10-18 17:51:26 +00:00
										 |  |  |  | 			let buffer = await file.arrayBuffer(); | 
					
						
							|  |  |  |  | 			let text = new TextDecoder('latin1').decode(buffer); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_files[file.name] = { | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 				doc: cm6.EditorState.create({ | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					doc: text, | 
					
						
							|  |  |  |  | 					extensions: cm6.extensions, | 
					
						
							|  |  |  |  | 				}), | 
					
						
							| 
									
										
										
										
											2023-10-18 17:51:26 +00:00
										 |  |  |  | 				buffer: buffer, | 
					
						
							|  |  |  |  | 				isNew: true, | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 			}; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_current_file = file.name; | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		openFile(g_current_file); | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 		updateFiles(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Called when a file starts being dragged over the file. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param event The event. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 	drag_enter(event) { | 
					
						
							|  |  |  |  | 		this.dropping++; | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 		this.drop_target = event.srcElement.innerText.trim(); | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 		event.preventDefault(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Called when a file stops being dragged over the file. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param event The event. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 	drag_leave(event) { | 
					
						
							|  |  |  |  | 		this.dropping--; | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 		if (this.dropping == 0) { | 
					
						
							|  |  |  |  | 			this.drop_target = undefined; | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Called when a file is being dragged over the file. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param event The event. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 	drag_over(event) { | 
					
						
							|  |  |  |  | 		event.preventDefault(); | 
					
						
							| 
									
										
										
										
											2023-10-17 21:37:42 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render the file. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 	render() { | 
					
						
							|  |  |  |  | 		let self = this; | 
					
						
							|  |  |  |  | 		return html`
 | 
					
						
							|  |  |  |  | 			<style> | 
					
						
							|  |  |  |  | 				div.file { | 
					
						
							|  |  |  |  | 					padding: 0.5em; | 
					
						
							| 
									
										
										
										
											2023-04-29 18:23:08 +00:00
										 |  |  |  | 					cursor: pointer; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 				div.file:hover { | 
					
						
							|  |  |  |  | 					background-color: #1a9188; | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 				} | 
					
						
							|  |  |  |  | 				div.file::before { | 
					
						
							|  |  |  |  | 					content: '📄 '; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 				div.file.current { | 
					
						
							|  |  |  |  | 					font-weight: bold; | 
					
						
							|  |  |  |  | 					background-color: #2aa198; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 				div.file.drop { | 
					
						
							|  |  |  |  | 					border: 4px solid red; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 				div.file.dirty::after { | 
					
						
							|  |  |  |  | 					content: '*'; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 			</style> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			<div | 
					
						
							|  |  |  |  | 				@drop=${this.drop} | 
					
						
							|  |  |  |  | 				@dragenter=${this.drag_enter} | 
					
						
							|  |  |  |  | 				@dragleave=${this.drag_leave} | 
					
						
							| 
									
										
										
										
											2025-04-27 10:18:20 -04:00
										 |  |  |  | 				@dragover=${this.drag_over} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			> | 
					
						
							|  |  |  |  | 				${Object.keys(this.files) | 
					
						
							|  |  |  |  | 					.sort() | 
					
						
							|  |  |  |  | 					.map((x) => self.render_file(x))} | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | 			</div> | 
					
						
							|  |  |  |  | 		`;
 | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-29 16:52:35 +00:00
										 |  |  |  | customElements.define('tf-files', TfFilesElement); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * The files pane element. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | class TfFilesPaneElement extends LitElement { | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Get Lit Html properties. | 
					
						
							|  |  |  |  | 	 * @return The properties. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 	static get properties() { | 
					
						
							|  |  |  |  | 		return { | 
					
						
							|  |  |  |  | 			expanded: {type: Boolean}, | 
					
						
							|  |  |  |  | 			current: {type: String}, | 
					
						
							|  |  |  |  | 			files: {type: Object}, | 
					
						
							|  |  |  |  | 		}; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	/** | 
					
						
							|  |  |  |  | 	 * Create a TfFilesPaneElement instance. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 	constructor() { | 
					
						
							|  |  |  |  | 		super(); | 
					
						
							| 
									
										
										
										
											2023-05-14 18:05:28 +00:00
										 |  |  |  | 		this.expanded = window.localStorage.getItem('files') != '0'; | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 		this.files = {}; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Set whether the files pane is expanded. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 	 * @param expanded Whether the files pane is expanded. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 	set_expanded(expanded) { | 
					
						
							|  |  |  |  | 		this.expanded = expanded; | 
					
						
							|  |  |  |  | 		window.localStorage.setItem('files', expanded ? '1' : '0'); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render the files pane element. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 	render() { | 
					
						
							|  |  |  |  | 		let self = this; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		let expander = this.expanded | 
					
						
							|  |  |  |  | 			? html`<div class="w3-button w3-bar-item w3-blue" style="flex: 0 0 auto; display: flex; flex-direction: row" @click=${() => self.set_expanded(false)}>
 | 
					
						
							| 
									
										
										
										
											2024-01-20 16:05:00 +00:00
										 |  |  |  | 				<span style="flex: 1 1" font-weight: bold; text-align: center; flex: 1">Files</span> | 
					
						
							|  |  |  |  | 				<span style="flex: 0 0">«</span> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			</div>` | 
					
						
							|  |  |  |  | 			: html`<div
 | 
					
						
							|  |  |  |  | 					class="w3-button w3-bar-item w3-blue" | 
					
						
							|  |  |  |  | 					@click=${() => self.set_expanded(true)} | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 					» | 
					
						
							|  |  |  |  | 				</div>`; | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 		let content = html`
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			<tf-files | 
					
						
							|  |  |  |  | 				style="flex: 1 1; overflow: auto" | 
					
						
							|  |  |  |  | 				.files=${self.files} | 
					
						
							|  |  |  |  | 				current=${self.current} | 
					
						
							|  |  |  |  | 				@file_click=${(event) => openFile(event.detail.file)} | 
					
						
							|  |  |  |  | 			></tf-files> | 
					
						
							|  |  |  |  | 			<div> | 
					
						
							|  |  |  |  | 				<button | 
					
						
							|  |  |  |  | 					class="w3-bar-item w3-button w3-blue" | 
					
						
							|  |  |  |  | 					style="width: 100%; flex: 0 0" | 
					
						
							|  |  |  |  | 					@click=${() => newFile()} | 
					
						
							|  |  |  |  | 					accesskey="n" | 
					
						
							|  |  |  |  | 					@mouseover=${set_access_key_title} | 
					
						
							|  |  |  |  | 					data-tip="Add a new, empty file to the app" | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 					📄 New File | 
					
						
							|  |  |  |  | 				</button> | 
					
						
							|  |  |  |  | 			</div> | 
					
						
							|  |  |  |  | 			<div> | 
					
						
							|  |  |  |  | 				<button | 
					
						
							|  |  |  |  | 					class="w3-bar-item w3-button w3-blue" | 
					
						
							|  |  |  |  | 					style="width: 100%; flex: 0 0" | 
					
						
							|  |  |  |  | 					@click=${() => removeFile()} | 
					
						
							|  |  |  |  | 					accesskey="r" | 
					
						
							|  |  |  |  | 					@mouseover=${set_access_key_title} | 
					
						
							|  |  |  |  | 					data-tip="Remove the selected file from the app" | 
					
						
							|  |  |  |  | 				> | 
					
						
							|  |  |  |  | 					🚮 Remove File | 
					
						
							|  |  |  |  | 				</button> | 
					
						
							|  |  |  |  | 			</div> | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 		`;
 | 
					
						
							|  |  |  |  | 		return html`
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			<link type="text/css" rel="stylesheet" href="/static/w3.css" /> | 
					
						
							| 
									
										
										
										
											2024-01-20 16:05:00 +00:00
										 |  |  |  | 			<div style="display: flex; flex-direction: column; height: 100%"> | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				${expander} ${this.expanded ? content : undefined} | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 			</div> | 
					
						
							|  |  |  |  | 		`;
 | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | customElements.define('tf-files-pane', TfFilesPaneElement); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * A tiny graph. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | class TfSparkLineElement extends LitElement { | 
					
						
							|  |  |  |  | 	static get properties() { | 
					
						
							|  |  |  |  | 		return { | 
					
						
							|  |  |  |  | 			lines: {type: Array}, | 
					
						
							|  |  |  |  | 			min: {type: Number}, | 
					
						
							|  |  |  |  | 			max: {type: Number}, | 
					
						
							|  |  |  |  | 		}; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 	constructor() { | 
					
						
							|  |  |  |  | 		super(); | 
					
						
							|  |  |  |  | 		this.min = 0; | 
					
						
							|  |  |  |  | 		this.max = 1.0; | 
					
						
							|  |  |  |  | 		this.lines = []; | 
					
						
							| 
									
										
										
										
											2023-05-03 22:47:00 +00:00
										 |  |  |  | 		this.k_values_max = 100; | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Add a data point to the graph. | 
					
						
							|  |  |  |  | 	 * @param key The line to which the point applies. | 
					
						
							|  |  |  |  | 	 * @param value The numeric value of the data point. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 	append(key, value) { | 
					
						
							|  |  |  |  | 		let line = null; | 
					
						
							|  |  |  |  | 		for (let it of this.lines) { | 
					
						
							|  |  |  |  | 			if (it.name == key) { | 
					
						
							|  |  |  |  | 				line = it; | 
					
						
							|  |  |  |  | 				break; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 		if (!line) { | 
					
						
							|  |  |  |  | 			const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888']; | 
					
						
							|  |  |  |  | 			line = { | 
					
						
							|  |  |  |  | 				name: key, | 
					
						
							|  |  |  |  | 				style: k_colors[this.lines.length % k_colors.length], | 
					
						
							| 
									
										
										
										
											2023-05-03 22:47:00 +00:00
										 |  |  |  | 				values: Array(this.k_values_max).fill(0), | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			}; | 
					
						
							|  |  |  |  | 			this.lines.push(line); | 
					
						
							|  |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-03 22:47:00 +00:00
										 |  |  |  | 		if (line.values.length >= this.k_values_max) { | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			line.values.shift(); | 
					
						
							|  |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-03 22:47:00 +00:00
										 |  |  |  | 		line.values.push(value); | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 		this.requestUpdate(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render a single series line. | 
					
						
							|  |  |  |  | 	 * @param line The line data. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 	render_line(line) { | 
					
						
							|  |  |  |  | 		if (line?.values?.length >= 2) { | 
					
						
							| 
									
										
										
										
											2023-05-03 22:47:00 +00:00
										 |  |  |  | 			let max = Math.max(this.max, ...line.values); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			let points = [].concat( | 
					
						
							|  |  |  |  | 				...line.values.map((x, i) => [ | 
					
						
							|  |  |  |  | 					(50.0 * i) / (line.values.length - 1), | 
					
						
							|  |  |  |  | 					10.0 - (10.0 * (x - this.min)) / (max - this.min), | 
					
						
							|  |  |  |  | 				]) | 
					
						
							|  |  |  |  | 			); | 
					
						
							| 
									
										
										
										
											2023-05-03 22:47:00 +00:00
										 |  |  |  | 			return svg`<polyline points=${points.join(' ')} stroke=${line.style} fill="none"/>`; | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | 	 * Render the graph. | 
					
						
							|  |  |  |  | 	 * @return Lit HTML. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 	render() { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		let max = | 
					
						
							|  |  |  |  | 			Math.round( | 
					
						
							|  |  |  |  | 				10.0 * | 
					
						
							|  |  |  |  | 					Math.max( | 
					
						
							|  |  |  |  | 						...this.lines.map((line) => line.values[line.values.length - 1]) | 
					
						
							|  |  |  |  | 					) | 
					
						
							|  |  |  |  | 			) / 10.0; | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 		return html`
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			<svg | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 				style="max-width: 7.5em; margin: 0; padding: 0; background: #000; height: 1em" | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				viewBox="0 0 50 10" | 
					
						
							|  |  |  |  | 				xmlns="http://www.w3.org/2000/svg" | 
					
						
							|  |  |  |  | 			> | 
					
						
							|  |  |  |  | 				${this.lines.map((x) => this.render_line(x))} | 
					
						
							|  |  |  |  | 				<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff"> | 
					
						
							| 
									
										
										
										
											2024-04-13 16:52:30 -04:00
										 |  |  |  | 					${this.dataset.emoji}${max} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				</text> | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			</svg> | 
					
						
							|  |  |  |  | 		`;
 | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | customElements.define('tf-sparkline', TfSparkLineElement); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  *  A keyboard key is pressed down. | 
					
						
							|  |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | window.addEventListener('keydown', function (event) { | 
					
						
							| 
									
										
										
										
											2022-02-03 23:57:47 +00:00
										 |  |  |  | 	if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) { | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 		if (editing()) { | 
					
						
							|  |  |  |  | 			save(); | 
					
						
							|  |  |  |  | 			event.preventDefault(); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} else if (event.keyCode == 66 && event.altKey) { | 
					
						
							|  |  |  |  | 		if (editing()) { | 
					
						
							|  |  |  |  | 			closeEditor(); | 
					
						
							|  |  |  |  | 			event.preventDefault(); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | }); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Make sure a set of dependencies are loaded | 
					
						
							|  |  |  |  |  * @param nodes An array of descriptions of dependencies to load. | 
					
						
							|  |  |  |  |  * @param callback Called when all dependencies are loaded. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | function ensureLoaded(nodes, callback) { | 
					
						
							|  |  |  |  | 	if (!nodes.length) { | 
					
						
							|  |  |  |  | 		callback(); | 
					
						
							|  |  |  |  | 		return; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 	let search = nodes.shift(); | 
					
						
							|  |  |  |  | 	let head = document.head; | 
					
						
							|  |  |  |  | 	let found = false; | 
					
						
							|  |  |  |  | 	for (let i = 0; i < head.childNodes.length; i++) { | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 		if (head.childNodes[i].tagName == search.tagName) { | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 			let match = true; | 
					
						
							|  |  |  |  | 			for (let attribute in search.attributes) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				if ( | 
					
						
							|  |  |  |  | 					head.childNodes[i].attributes[attribute].value != | 
					
						
							|  |  |  |  | 					search.attributes[attribute] | 
					
						
							|  |  |  |  | 				) { | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 					match = false; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 			if (match) { | 
					
						
							|  |  |  |  | 				found = true; | 
					
						
							|  |  |  |  | 				break; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 	if (found) { | 
					
						
							|  |  |  |  | 		ensureLoaded(nodes, callback); | 
					
						
							|  |  |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		let node = document.createElement(search.tagName); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		node.onreadystatechange = node.onload = function () { | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 			ensureLoaded(nodes, callback); | 
					
						
							|  |  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		for (let attribute in search.attributes) { | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 			node.setAttribute(attribute, search.attributes[attribute]); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 		head.insertBefore(node, head.firstChild); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Check whether the editior is currently visible. | 
					
						
							|  |  |  |  |  * @return true if the editor is visible. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | function editing() { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	return document.getElementById('editPane').style.display != 'none'; | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Check whether only the editor is visible and the app is hidden. | 
					
						
							|  |  |  |  |  * @return true if the editor is visible and the app is not. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-08-17 00:49:02 +00:00
										 |  |  |  | function is_edit_only() { | 
					
						
							|  |  |  |  | 	return window.location.search == '?editonly=1' || window.innerWidth < 1024; | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Show the editor. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | async function edit() { | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 	if (editing()) { | 
					
						
							|  |  |  |  | 		return; | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-07 21:06:20 +00:00
										 |  |  |  | 	window.localStorage.setItem('editing', '1'); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	document.getElementById('editPane').style.display = 'flex'; | 
					
						
							|  |  |  |  | 	document.getElementById('viewPane').style.display = is_edit_only() | 
					
						
							|  |  |  |  | 		? 'none' | 
					
						
							|  |  |  |  | 		: 'flex'; | 
					
						
							| 
									
										
										
										
											2022-01-13 02:18:40 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		if (!g_editor) { | 
					
						
							| 
									
										
										
										
											2024-01-28 20:59:46 +00:00
										 |  |  |  | 			cm6 = await import('/codemirror/cm6.js'); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_editor = cm6.TildeFriendsEditorView(document.getElementById('editor')); | 
					
						
							| 
									
										
										
										
											2024-01-28 20:59:46 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_editor.onDocChange = updateFiles; | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 		await load(); | 
					
						
							|  |  |  |  | 	} catch (error) { | 
					
						
							|  |  |  |  | 		alert(`${error.message}\n\n${error.stack}`); | 
					
						
							|  |  |  |  | 		closeEditor(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Open a performance trace. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-01-02 19:10:45 +00:00
										 |  |  |  | function trace() { | 
					
						
							| 
									
										
										
										
											2023-02-18 00:51:22 +00:00
										 |  |  |  | 	window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`); | 
					
						
							| 
									
										
										
										
											2022-01-02 19:10:45 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Load a single file. | 
					
						
							|  |  |  |  |  * @param name The name by which the file is known. | 
					
						
							|  |  |  |  |  * @param id The file's ID. | 
					
						
							|  |  |  |  |  * @return A promise resolved with the file's contents. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function loadFile(name, id) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	return fetch('/' + id + '/view') | 
					
						
							|  |  |  |  | 		.then(function (response) { | 
					
						
							|  |  |  |  | 			if (!response.ok) { | 
					
						
							|  |  |  |  | 				alert( | 
					
						
							|  |  |  |  | 					`Request failed for ${name}: ${response.status} ${response.statusText}` | 
					
						
							|  |  |  |  | 				); | 
					
						
							|  |  |  |  | 				return 'missing file!'; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 			return response.text(); | 
					
						
							|  |  |  |  | 		}) | 
					
						
							|  |  |  |  | 		.then(function (text) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_files[name].doc = cm6.EditorState.create({ | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				doc: text, | 
					
						
							|  |  |  |  | 				extensions: cm6.extensions, | 
					
						
							|  |  |  |  | 			}); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_files[name].original = g_files[name].doc.doc.toString(); | 
					
						
							|  |  |  |  | 			if (!Object.values(g_files).some((x) => !x.doc)) { | 
					
						
							|  |  |  |  | 				openFile(Object.keys(g_files).sort()[0]); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			} | 
					
						
							|  |  |  |  | 		}); | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Load files for the app. | 
					
						
							|  |  |  |  |  * @param path The app path to load. | 
					
						
							|  |  |  |  |  * @return A promise resolved when the app is laoded. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | async function load(path) { | 
					
						
							|  |  |  |  | 	let response = await fetch((path || url()) + 'view'); | 
					
						
							| 
									
										
										
										
											2024-01-28 13:09:31 +00:00
										 |  |  |  | 	let json; | 
					
						
							|  |  |  |  | 	if (response.ok) { | 
					
						
							|  |  |  |  | 		json = await response.json(); | 
					
						
							|  |  |  |  | 	} else if (response.status != 404) { | 
					
						
							|  |  |  |  | 		throw new Error(response.status + ' ' + response.statusText); | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	g_files = {}; | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 	let isApp = false; | 
					
						
							|  |  |  |  | 	let promises = []; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 	if (json && json['type'] == 'tildefriends-app') { | 
					
						
							|  |  |  |  | 		isApp = true; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		Object.keys(json['files']).forEach(function (name) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_files[name] = {}; | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 			promises.push(loadFile(name, json['files'][name])); | 
					
						
							|  |  |  |  | 		}); | 
					
						
							|  |  |  |  | 		if (Object.keys(json['files']).length == 0) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			document.getElementById('editPane').style.display = 'flex'; | 
					
						
							| 
									
										
										
										
											2022-02-17 02:29:04 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_app = json; | 
					
						
							|  |  |  |  | 		g_app.emoji = g_app.emoji || '📦'; | 
					
						
							|  |  |  |  | 		document.getElementById('icon').innerHTML = g_app.emoji; | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 	if (!isApp) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		document.getElementById('editPane').style.display = 'flex'; | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 		let text = '// New script.\n'; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_current_file = 'app.js'; | 
					
						
							|  |  |  |  | 		g_files[g_current_file] = { | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 			doc: cm6.EditorState.create({doc: text, extensions: cm6.extensions}), | 
					
						
							|  |  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		openFile(g_current_file); | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 	return Promise.all(promises); | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Hide the editor. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | function closeEditor() { | 
					
						
							| 
									
										
										
										
											2022-03-07 21:06:20 +00:00
										 |  |  |  | 	window.localStorage.setItem('editing', '0'); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	document.getElementById('editPane').style.display = 'none'; | 
					
						
							| 
									
										
										
										
											2023-08-17 00:49:02 +00:00
										 |  |  |  | 	document.getElementById('viewPane').style.display = 'flex'; | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Save the app. | 
					
						
							|  |  |  |  |  * @param save_to An optional path to which to save the app. | 
					
						
							|  |  |  |  |  * @return A promise resoled when the app is saved. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-01-30 14:51:09 +00:00
										 |  |  |  | function save(save_to) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	document.getElementById('save').disabled = true; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	if (g_current_file) { | 
					
						
							|  |  |  |  | 		g_files[g_current_file].doc = g_editor.state; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		if ( | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			!g_files[g_current_file].isNew && | 
					
						
							|  |  |  |  | 			!g_files[g_current_file].doc.doc.toString() == | 
					
						
							|  |  |  |  | 				g_files[g_current_file].original | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			delete g_files[g_current_file].buffer; | 
					
						
							| 
									
										
										
										
											2023-10-18 17:51:26 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 	let save_path = save_to; | 
					
						
							| 
									
										
										
										
											2022-02-17 02:29:04 +00:00
										 |  |  |  | 	if (!save_path) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		let name = document.getElementById('name'); | 
					
						
							| 
									
										
										
										
											2022-01-30 14:51:09 +00:00
										 |  |  |  | 		if (name && name.value) { | 
					
						
							|  |  |  |  | 			save_path = name.value; | 
					
						
							|  |  |  |  | 		} else { | 
					
						
							|  |  |  |  | 			save_path = url(); | 
					
						
							|  |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 	let promises = []; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	for (let name of Object.keys(g_files)) { | 
					
						
							|  |  |  |  | 		let file = g_files[name]; | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 		if (!file.isNew && file.doc.doc.toString() == file.original) { | 
					
						
							| 
									
										
										
										
											2022-02-17 02:29:04 +00:00
										 |  |  |  | 			continue; | 
					
						
							| 
									
										
										
										
											2021-01-13 02:40:46 +00:00
										 |  |  |  | 		} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 		delete file.id; | 
					
						
							| 
									
										
										
										
											2023-10-18 17:51:26 +00:00
										 |  |  |  | 		delete file.isNew; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		promises.push( | 
					
						
							|  |  |  |  | 			fetch('/save', { | 
					
						
							|  |  |  |  | 				method: 'POST', | 
					
						
							|  |  |  |  | 				headers: { | 
					
						
							|  |  |  |  | 					'Content-Type': 'application/binary', | 
					
						
							|  |  |  |  | 				}, | 
					
						
							|  |  |  |  | 				body: file.buffer ?? file.doc.doc.toString(), | 
					
						
							|  |  |  |  | 			}) | 
					
						
							|  |  |  |  | 				.then(function (response) { | 
					
						
							|  |  |  |  | 					if (!response.ok) { | 
					
						
							|  |  |  |  | 						throw new Error( | 
					
						
							|  |  |  |  | 							'Saving "' + | 
					
						
							|  |  |  |  | 								name + | 
					
						
							|  |  |  |  | 								'": ' + | 
					
						
							|  |  |  |  | 								response.status + | 
					
						
							|  |  |  |  | 								' ' + | 
					
						
							|  |  |  |  | 								response.statusText | 
					
						
							|  |  |  |  | 						); | 
					
						
							|  |  |  |  | 					} | 
					
						
							|  |  |  |  | 					return response.text(); | 
					
						
							|  |  |  |  | 				}) | 
					
						
							|  |  |  |  | 				.then(function (text) { | 
					
						
							|  |  |  |  | 					file.id = text; | 
					
						
							|  |  |  |  | 					if (file.id.charAt(0) == '/') { | 
					
						
							|  |  |  |  | 						file.id = file.id.substr(1); | 
					
						
							|  |  |  |  | 					} | 
					
						
							|  |  |  |  | 				}) | 
					
						
							|  |  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2022-02-17 02:29:04 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	return Promise.all(promises) | 
					
						
							|  |  |  |  | 		.then(function () { | 
					
						
							|  |  |  |  | 			let app = { | 
					
						
							|  |  |  |  | 				type: 'tildefriends-app', | 
					
						
							|  |  |  |  | 				files: Object.fromEntries( | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 					Object.keys(g_files).map((x) => [x, g_files[x].id || g_app.files[x]]) | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				), | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 				emoji: g_app.emoji || '📦', | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			}; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			Object.values(g_files).forEach(function (file) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				delete file.id; | 
					
						
							|  |  |  |  | 			}); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_app = JSON.parse(JSON.stringify(app)); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | 			return fetch(save_path + 'save', { | 
					
						
							|  |  |  |  | 				method: 'POST', | 
					
						
							|  |  |  |  | 				headers: { | 
					
						
							|  |  |  |  | 					'Content-Type': 'application/json', | 
					
						
							|  |  |  |  | 				}, | 
					
						
							|  |  |  |  | 				body: JSON.stringify(app), | 
					
						
							|  |  |  |  | 			}).then(function (response) { | 
					
						
							|  |  |  |  | 				if (!response.ok) { | 
					
						
							|  |  |  |  | 					throw new Error(response.status + ' ' + response.statusText); | 
					
						
							|  |  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-02-23 10:35:39 +01:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				if (save_path != window.location.pathname) { | 
					
						
							|  |  |  |  | 					alert('Saved to ' + save_path + '.'); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 				} else if (!g_files['app.js']) { | 
					
						
							| 
									
										
										
										
											2025-07-16 12:47:16 -04:00
										 |  |  |  | 					window.location.reload(); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				} else { | 
					
						
							|  |  |  |  | 					reconnect(save_path); | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 			}); | 
					
						
							|  |  |  |  | 		}) | 
					
						
							|  |  |  |  | 		.catch(function (error) { | 
					
						
							|  |  |  |  | 			alert(error); | 
					
						
							|  |  |  |  | 		}) | 
					
						
							|  |  |  |  | 		.finally(function () { | 
					
						
							|  |  |  |  | 			document.getElementById('save').disabled = false; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			Object.values(g_files).forEach(function (file) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				file.original = file.doc.doc.toString(); | 
					
						
							|  |  |  |  | 			}); | 
					
						
							|  |  |  |  | 			updateFiles(); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 		}); | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Prompt to set the app icon. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-03-21 23:08:04 +00:00
										 |  |  |  | function changeIcon() { | 
					
						
							|  |  |  |  | 	let value = prompt('Enter a new app icon emoji:'); | 
					
						
							|  |  |  |  | 	if (value !== undefined) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_app.emoji = value || '📦'; | 
					
						
							|  |  |  |  | 		document.getElementById('icon').innerHTML = g_app.emoji; | 
					
						
							| 
									
										
										
										
											2023-03-21 23:08:04 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Prompt to delete the current app. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-06-20 18:13:19 +00:00
										 |  |  |  | function deleteApp() { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	let name = document.getElementById('name'); | 
					
						
							| 
									
										
										
										
											2022-06-20 18:13:19 +00:00
										 |  |  |  | 	let path = name && name.value ? name.value : url(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 	if (confirm(`Are you sure you want to delete the app '${path}'?`)) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		fetch(path + 'delete') | 
					
						
							|  |  |  |  | 			.then(function (response) { | 
					
						
							|  |  |  |  | 				if (!response.ok) { | 
					
						
							|  |  |  |  | 					throw new Error(response.status + ' ' + response.statusText); | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 				alert('Deleted.'); | 
					
						
							|  |  |  |  | 			}) | 
					
						
							|  |  |  |  | 			.catch(function (error) { | 
					
						
							|  |  |  |  | 				alert(error); | 
					
						
							|  |  |  |  | 			}); | 
					
						
							| 
									
										
										
										
											2022-06-20 18:13:19 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Get the current app URL. | 
					
						
							|  |  |  |  |  * @return The app URL. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | function url() { | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 	let hash = window.location.href.indexOf('#'); | 
					
						
							|  |  |  |  | 	let question = window.location.href.indexOf('?'); | 
					
						
							|  |  |  |  | 	let end = -1; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	if (hash != -1 && (hash < end || end == -1)) { | 
					
						
							| 
									
										
										
										
											2016-04-07 01:30:07 +00:00
										 |  |  |  | 		end = hash; | 
					
						
							|  |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	if (question != -1 && (question < end || end == -1)) { | 
					
						
							| 
									
										
										
										
											2016-04-07 01:30:07 +00:00
										 |  |  |  | 		end = question; | 
					
						
							|  |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	return end != -1 | 
					
						
							|  |  |  |  | 		? window.location.href.substring(0, end) | 
					
						
							|  |  |  |  | 		: window.location.href; | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Get the window hash without the lone '#' if it is empty. | 
					
						
							|  |  |  |  |  * @return The hash. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2016-04-07 01:30:07 +00:00
										 |  |  |  | function hash() { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	return window.location.hash != '#' ? window.location.hash : ''; | 
					
						
							| 
									
										
										
										
											2016-04-07 01:30:07 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Set the iframe document contents. | 
					
						
							|  |  |  |  |  * @param content The contents. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | function api_setDocument(content) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	let iframe = document.getElementById('document'); | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 	iframe.srcdoc = content; | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Send a message to the sandboxed iframe. | 
					
						
							|  |  |  |  |  * @param message The message. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | function api_postMessage(message) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	let iframe = document.getElementById('document'); | 
					
						
							|  |  |  |  | 	iframe.contentWindow.postMessage(message, '*'); | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Show an error. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * @param error The error. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | function api_error(error) { | 
					
						
							|  |  |  |  | 	if (error) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		if (typeof error == 'string') { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			setStatusMessage('⚠️ ' + error, k_color_error); | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			setStatusMessage( | 
					
						
							|  |  |  |  | 				'⚠️ ' + error.message + '\n' + error.stack, | 
					
						
							|  |  |  |  | 				k_color_error | 
					
						
							|  |  |  |  | 			); | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		} | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 	console.log('error', error); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  et a value in local storage. | 
					
						
							|  |  |  |  |  * @param key The key. | 
					
						
							|  |  |  |  |  * @param value The value. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | function api_localStorageSet(key, value) { | 
					
						
							|  |  |  |  | 	window.localStorage.setItem('app:' + key, value); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Get a value from local storage. | 
					
						
							|  |  |  |  |  * @param key The key. | 
					
						
							|  |  |  |  |  * @return The value. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2023-01-21 00:16:18 +00:00
										 |  |  |  | function api_localStorageGet(key) { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  |  | 	return window.localStorage.getItem('app:' + key); | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Request a permission | 
					
						
							|  |  |  |  |  * @param permission The permission to request. | 
					
						
							|  |  |  |  |  * @param id The id requeesting the permission. | 
					
						
							|  |  |  |  |  * @return A promise fulfilled if the permission was granted. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-07-27 00:27:10 +00:00
										 |  |  |  | function api_requestPermission(permission, id) { | 
					
						
							| 
									
										
										
										
											2023-04-30 00:56:59 +00:00
										 |  |  |  | 	let outer = document.createElement('div'); | 
					
						
							|  |  |  |  | 	outer.classList.add('permissions'); | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | 	let container = document.createElement('div'); | 
					
						
							|  |  |  |  | 	container.classList.add('permissions_contents'); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-27 00:27:10 +00:00
										 |  |  |  | 	let div = document.createElement('div'); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	div.appendChild( | 
					
						
							| 
									
										
										
										
											2024-11-01 18:18:16 -04:00
										 |  |  |  | 		document.createTextNode('This app is requesting the following permission: ') | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	); | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 	let span = document.createElement('span'); | 
					
						
							|  |  |  |  | 	span.style = 'font-weight: bold'; | 
					
						
							|  |  |  |  | 	span.appendChild(document.createTextNode(permission)); | 
					
						
							|  |  |  |  | 	div.appendChild(span); | 
					
						
							|  |  |  |  | 	container.appendChild(div); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 	div = document.createElement('div'); | 
					
						
							|  |  |  |  | 	div.style = 'padding: 1em'; | 
					
						
							|  |  |  |  | 	let check = document.createElement('input'); | 
					
						
							|  |  |  |  | 	check.id = 'permissions_remember_check'; | 
					
						
							|  |  |  |  | 	check.type = 'checkbox'; | 
					
						
							| 
									
										
										
										
											2024-01-19 02:32:55 +00:00
										 |  |  |  | 	check.classList.add('w3-check'); | 
					
						
							|  |  |  |  | 	check.classList.add('w3-blue'); | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 	div.appendChild(check); | 
					
						
							| 
									
										
										
										
											2024-11-01 18:18:16 -04:00
										 |  |  |  | 	div.appendChild(document.createTextNode(' ')); | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 	let label = document.createElement('label'); | 
					
						
							|  |  |  |  | 	label.htmlFor = check.id; | 
					
						
							|  |  |  |  | 	label.appendChild(document.createTextNode('Remember this decision.')); | 
					
						
							|  |  |  |  | 	div.appendChild(label); | 
					
						
							|  |  |  |  | 	container.appendChild(div); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 	const k_options = [ | 
					
						
							|  |  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2023-08-03 00:30:48 +00:00
										 |  |  |  | 			id: 'allow', | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 			text: '✅ Allow', | 
					
						
							|  |  |  |  | 			grant: ['allow once', 'allow'], | 
					
						
							|  |  |  |  | 		}, | 
					
						
							|  |  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2023-08-03 00:30:48 +00:00
										 |  |  |  | 			id: 'deny', | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 			text: '❌ Deny', | 
					
						
							|  |  |  |  | 			grant: ['deny once', 'deny'], | 
					
						
							|  |  |  |  | 		}, | 
					
						
							|  |  |  |  | 	]; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	return new Promise(function (resolve, reject) { | 
					
						
							| 
									
										
										
										
											2022-08-14 01:46:11 +00:00
										 |  |  |  | 		div = document.createElement('div'); | 
					
						
							|  |  |  |  | 		for (let option of k_options) { | 
					
						
							|  |  |  |  | 			let button = document.createElement('button'); | 
					
						
							| 
									
										
										
										
											2024-01-19 02:32:55 +00:00
										 |  |  |  | 			button.classList.add('w3-button'); | 
					
						
							|  |  |  |  | 			button.classList.add('w3-blue'); | 
					
						
							| 
									
										
										
										
											2022-08-14 01:46:11 +00:00
										 |  |  |  | 			button.innerText = option.text; | 
					
						
							| 
									
										
										
										
											2023-08-03 00:30:48 +00:00
										 |  |  |  | 			button.id = option.id; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			button.onclick = function () { | 
					
						
							| 
									
										
										
										
											2022-08-14 01:46:11 +00:00
										 |  |  |  | 				resolve(option.grant[check.checked ? 1 : 0]); | 
					
						
							| 
									
										
										
										
											2023-04-30 00:56:59 +00:00
										 |  |  |  | 				document.body.removeChild(outer); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			}; | 
					
						
							| 
									
										
										
										
											2022-08-14 01:46:11 +00:00
										 |  |  |  | 			div.appendChild(button); | 
					
						
							| 
									
										
										
										
											2022-07-27 00:27:10 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2022-08-14 01:46:11 +00:00
										 |  |  |  | 		container.appendChild(div); | 
					
						
							| 
									
										
										
										
											2023-04-30 00:56:59 +00:00
										 |  |  |  | 		outer.appendChild(container); | 
					
						
							| 
									
										
										
										
											2022-08-07 22:39:58 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-30 00:56:59 +00:00
										 |  |  |  | 		document.body.appendChild(outer); | 
					
						
							| 
									
										
										
										
											2022-08-14 01:46:11 +00:00
										 |  |  |  | 	}); | 
					
						
							| 
									
										
										
										
											2022-07-27 00:27:10 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Log from the app to the console. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-08-14 16:58:26 +00:00
										 |  |  |  | function api_print() { | 
					
						
							|  |  |  |  | 	console.log('app>', ...arguments); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Set the window's location hash. | 
					
						
							|  |  |  |  |  * @param hash The new hash. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-09-15 00:16:37 +00:00
										 |  |  |  | function api_setHash(hash) { | 
					
						
							|  |  |  |  | 	window.location.hash = hash; | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Process an incoming WebSocket message. | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * @param message The message. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-08-14 16:58:26 +00:00
										 |  |  |  | function _receive_websocket_message(message) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	if (message && message.action == 'session') { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		setStatusMessage('🟢 Executing...', k_color_status); | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 		let navigation = document.getElementsByTagName('tf-navigation')[0]; | 
					
						
							|  |  |  |  | 		navigation.credentials = message.credentials; | 
					
						
							|  |  |  |  | 		navigation.identities = message.identities; | 
					
						
							|  |  |  |  | 		navigation.identity = message.identity; | 
					
						
							| 
									
										
										
										
											2024-04-13 21:51:18 -04:00
										 |  |  |  | 		navigation.names = message.names; | 
					
						
							| 
									
										
										
										
											2022-08-14 18:24:41 +00:00
										 |  |  |  | 	} else if (message && message.action == 'permissions') { | 
					
						
							| 
									
										
										
										
											2024-04-13 13:22:59 -04:00
										 |  |  |  | 		let navigation = document.getElementsByTagName('tf-navigation')[0]; | 
					
						
							|  |  |  |  | 		navigation.permissions = message.permissions ?? {}; | 
					
						
							|  |  |  |  | 	} else if (message && message.action == 'identities') { | 
					
						
							|  |  |  |  | 		let navigation = document.getElementsByTagName('tf-navigation')[0]; | 
					
						
							|  |  |  |  | 		navigation.identities = message.identities; | 
					
						
							|  |  |  |  | 		navigation.identity = message.identity; | 
					
						
							| 
									
										
										
										
											2024-04-13 21:51:18 -04:00
										 |  |  |  | 		navigation.names = message.names; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	} else if (message && message.action == 'ready') { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 		setStatusMessage(null); | 
					
						
							|  |  |  |  | 		if (window.location.hash) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			send({event: 'hashChange', hash: window.location.hash}); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-06-28 23:00:34 +00:00
										 |  |  |  | 		document.getElementsByTagName('tf-navigation')[0].version = message.version; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		document.getElementById('viewPane').style.display = message.edit_only | 
					
						
							|  |  |  |  | 			? 'none' | 
					
						
							|  |  |  |  | 			: 'flex'; | 
					
						
							|  |  |  |  | 	} else if (message && message.action == 'ping') { | 
					
						
							|  |  |  |  | 		send({action: 'pong'}); | 
					
						
							|  |  |  |  | 	} else if (message && message.action == 'stats') { | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		let now = new Date().getTime(); | 
					
						
							|  |  |  |  | 		for (let key of Object.keys(message.stats)) { | 
					
						
							| 
									
										
										
										
											2022-06-04 16:38:45 +00:00
										 |  |  |  | 			const k_groups = { | 
					
						
							|  |  |  |  | 				rpc_in: {group: 'rpc', name: 'in'}, | 
					
						
							|  |  |  |  | 				rpc_out: {group: 'rpc', name: 'out'}, | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-01 01:36:26 +00:00
										 |  |  |  | 				cpu_percent: {group: 'cpu', name: 'main'}, | 
					
						
							|  |  |  |  | 				thread_percent: {group: 'cpu', name: 'work'}, | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-17 21:18:10 +00:00
										 |  |  |  | 				arena_percent: {group: 'memory', name: 'm'}, | 
					
						
							| 
									
										
										
										
											2022-06-04 16:38:45 +00:00
										 |  |  |  | 				js_malloc_percent: {group: 'memory', name: 'js'}, | 
					
						
							| 
									
										
										
										
											2022-06-17 21:18:10 +00:00
										 |  |  |  | 				memory_percent: {group: 'memory', name: 'tot'}, | 
					
						
							| 
									
										
										
										
											2022-06-04 16:38:45 +00:00
										 |  |  |  | 				sqlite3_memory_percent: {group: 'memory', name: 'sql'}, | 
					
						
							| 
									
										
										
										
											2022-06-04 17:04:51 +00:00
										 |  |  |  | 				tf_malloc_percent: {group: 'memory', name: 'tf'}, | 
					
						
							| 
									
										
										
										
											2022-06-04 16:38:45 +00:00
										 |  |  |  | 				tls_malloc_percent: {group: 'memory', name: 'tls'}, | 
					
						
							|  |  |  |  | 				uv_malloc_percent: {group: 'memory', name: 'uv'}, | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-29 19:46:33 +00:00
										 |  |  |  | 				messages_stored: {group: 'store', name: 'messages'}, | 
					
						
							|  |  |  |  | 				blobs_stored: {group: 'store', name: 'blobs'}, | 
					
						
							| 
									
										
										
										
											2023-01-18 22:52:54 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-04 16:38:45 +00:00
										 |  |  |  | 				socket_count: {group: 'socket', name: 'total'}, | 
					
						
							|  |  |  |  | 				socket_open_count: {group: 'socket', name: 'open'}, | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 				import_count: {group: 'functions', name: 'imports'}, | 
					
						
							|  |  |  |  | 				export_count: {group: 'functions', name: 'exports'}, | 
					
						
							|  |  |  |  | 			}; | 
					
						
							| 
									
										
										
										
											2022-06-17 21:18:10 +00:00
										 |  |  |  | 			const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888']; | 
					
						
							| 
									
										
										
										
											2022-06-04 16:38:45 +00:00
										 |  |  |  | 			let graph_key = k_groups[key]?.group || key; | 
					
						
							| 
									
										
										
										
											2023-09-04 20:13:17 +00:00
										 |  |  |  | 			if (['cpu', 'rpc', 'store', 'memory'].indexOf(graph_key) != -1) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				let line = document | 
					
						
							|  |  |  |  | 					.getElementsByTagName('tf-navigation')[0] | 
					
						
							|  |  |  |  | 					.get_spark_line(graph_key, {max: 100}); | 
					
						
							| 
									
										
										
										
											2023-04-29 19:46:33 +00:00
										 |  |  |  | 				line.dataset.emoji = { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					cpu: '💻', | 
					
						
							|  |  |  |  | 					rpc: '🔁', | 
					
						
							|  |  |  |  | 					store: '💾', | 
					
						
							|  |  |  |  | 					memory: '🐏', | 
					
						
							| 
									
										
										
										
											2023-04-29 19:46:33 +00:00
										 |  |  |  | 				}[graph_key]; | 
					
						
							|  |  |  |  | 				line.append(key, message.stats[key]); | 
					
						
							| 
									
										
										
										
											2023-04-29 19:27:00 +00:00
										 |  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2022-01-17 21:46:32 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  |  | 	} else if (message && message.action === 'tfrpc' && message.method) { | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  |  | 		let api = k_api[message.method]; | 
					
						
							| 
									
										
										
										
											2023-01-21 00:16:18 +00:00
										 |  |  |  | 		let id = message.id; | 
					
						
							|  |  |  |  | 		let params = message.params; | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		if (api) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			Promise.resolve(api.func(...params)) | 
					
						
							|  |  |  |  | 				.then(function (result) { | 
					
						
							|  |  |  |  | 					send({ | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  |  | 						action: 'tfrpc', | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 						id: id, | 
					
						
							|  |  |  |  | 						result: result, | 
					
						
							|  |  |  |  | 					}); | 
					
						
							|  |  |  |  | 				}) | 
					
						
							|  |  |  |  | 				.catch(function (error) { | 
					
						
							|  |  |  |  | 					send({ | 
					
						
							| 
									
										
										
										
											2025-02-27 14:28:07 -05:00
										 |  |  |  | 						action: 'tfrpc', | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 						id: id, | 
					
						
							|  |  |  |  | 						error: error, | 
					
						
							|  |  |  |  | 					}); | 
					
						
							| 
									
										
										
										
											2022-08-13 18:58:06 +00:00
										 |  |  |  | 				}); | 
					
						
							| 
									
										
										
										
											2022-06-18 21:12:38 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Set the status message. | 
					
						
							|  |  |  |  |  * @param message The message. | 
					
						
							|  |  |  |  |  * @param color The message's color. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2022-06-18 20:51:22 +00:00
										 |  |  |  | function setStatusMessage(message, color) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	document.getElementsByTagName('tf-navigation')[0].status = { | 
					
						
							|  |  |  |  | 		message: message, | 
					
						
							|  |  |  |  | 		color: color, | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		is_error: color == k_color_error, | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	}; | 
					
						
							| 
									
										
										
										
											2016-04-11 00:28:42 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Send a message to the app. | 
					
						
							|  |  |  |  |  * @param value The message. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function send(value) { | 
					
						
							| 
									
										
										
										
											2016-04-11 00:09:21 +00:00
										 |  |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		if (g_socket && g_socket.readyState == g_socket.OPEN) { | 
					
						
							|  |  |  |  | 			g_socket.send(JSON.stringify(value)); | 
					
						
							| 
									
										
										
										
											2022-06-18 20:51:22 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2016-04-11 00:09:21 +00:00
										 |  |  |  | 	} catch (error) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		setStatusMessage('🤷 Send failed: ' + error.toString(), k_color_error); | 
					
						
							| 
									
										
										
										
											2016-04-11 00:09:21 +00:00
										 |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Notify the app of the window hash changing. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | function hashChange() { | 
					
						
							|  |  |  |  | 	send({event: 'hashChange', hash: window.location.hash}); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Make sure the app is connected on window focus, and notify the app. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | function focus() { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	if (g_socket && g_socket.readyState == g_socket.CLOSED) { | 
					
						
							| 
									
										
										
										
											2016-05-07 11:07:54 +00:00
										 |  |  |  | 		connectSocket(); | 
					
						
							|  |  |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		send({event: 'focus'}); | 
					
						
							| 
									
										
										
										
											2016-05-07 11:07:54 +00:00
										 |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Notify the app of lost focus. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | function blur() { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	send({event: 'blur'}); | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Handle a message. | 
					
						
							|  |  |  |  |  * @param event The message. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function message(event) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	if ( | 
					
						
							|  |  |  |  | 		event.data && | 
					
						
							|  |  |  |  | 		event.data.event == 'resizeMe' && | 
					
						
							|  |  |  |  | 		event.data.width && | 
					
						
							|  |  |  |  | 		event.data.height | 
					
						
							|  |  |  |  | 	) { | 
					
						
							|  |  |  |  | 		let iframe = document.getElementById('iframe_' + event.data.name); | 
					
						
							|  |  |  |  | 		iframe.setAttribute('width', event.data.width); | 
					
						
							|  |  |  |  | 		iframe.setAttribute('height', event.data.height); | 
					
						
							|  |  |  |  | 	} else if (event.data && event.data.action == 'setHash') { | 
					
						
							| 
									
										
										
										
											2022-01-07 01:52:47 +00:00
										 |  |  |  | 		window.location.hash = event.data.hash; | 
					
						
							| 
									
										
										
										
											2022-01-28 03:11:09 +00:00
										 |  |  |  | 	} else if (event.data && event.data.action == 'storeBlob') { | 
					
						
							| 
									
										
										
										
											2022-02-17 02:29:04 +00:00
										 |  |  |  | 		fetch('/save', { | 
					
						
							|  |  |  |  | 			method: 'POST', | 
					
						
							|  |  |  |  | 			headers: { | 
					
						
							|  |  |  |  | 				'Content-Type': 'application/binary', | 
					
						
							|  |  |  |  | 			}, | 
					
						
							|  |  |  |  | 			body: event.data.blob.buffer, | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		}) | 
					
						
							|  |  |  |  | 			.then(function (response) { | 
					
						
							|  |  |  |  | 				if (!response.ok) { | 
					
						
							|  |  |  |  | 					throw new Error(response.status + ' ' + response.statusText); | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 				return response.text(); | 
					
						
							|  |  |  |  | 			}) | 
					
						
							|  |  |  |  | 			.then(function (text) { | 
					
						
							|  |  |  |  | 				let iframe = document.getElementById('document'); | 
					
						
							|  |  |  |  | 				iframe.contentWindow.postMessage( | 
					
						
							|  |  |  |  | 					{ | 
					
						
							|  |  |  |  | 						storeBlobComplete: { | 
					
						
							|  |  |  |  | 							name: event.data.blob.name, | 
					
						
							|  |  |  |  | 							path: text, | 
					
						
							|  |  |  |  | 							type: event.data.blob.type, | 
					
						
							|  |  |  |  | 							context: event.data.context, | 
					
						
							|  |  |  |  | 						}, | 
					
						
							|  |  |  |  | 					}, | 
					
						
							|  |  |  |  | 					'*' | 
					
						
							|  |  |  |  | 				); | 
					
						
							|  |  |  |  | 			}); | 
					
						
							| 
									
										
										
										
											2016-09-17 20:53:03 +00:00
										 |  |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		send({event: 'message', message: event.data}); | 
					
						
							| 
									
										
										
										
											2016-05-01 13:24:37 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							| 
									
										
										
										
											2016-04-11 00:09:21 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Reconnect the WebSocket. | 
					
						
							|  |  |  |  |  * @param path The path to which the WebSocket should be connected. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function reconnect(path) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	let oldSocket = g_socket; | 
					
						
							|  |  |  |  | 	g_socket = null; | 
					
						
							| 
									
										
										
										
											2023-03-12 22:16:18 +00:00
										 |  |  |  | 	if (oldSocket) { | 
					
						
							|  |  |  |  | 		oldSocket.onopen = null; | 
					
						
							|  |  |  |  | 		oldSocket.onclose = null; | 
					
						
							|  |  |  |  | 		oldSocket.onmessage = null; | 
					
						
							|  |  |  |  | 		oldSocket.close(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	connectSocket(path); | 
					
						
							| 
									
										
										
										
											2017-01-16 15:24:44 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Connect the WebSocket. | 
					
						
							|  |  |  |  |  * @param path The path to which to connect. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function connectSocket(path) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	if (!g_socket || g_socket.readyState != g_socket.OPEN) { | 
					
						
							|  |  |  |  | 		if (g_socket) { | 
					
						
							|  |  |  |  | 			g_socket.onopen = null; | 
					
						
							|  |  |  |  | 			g_socket.onclose = null; | 
					
						
							|  |  |  |  | 			g_socket.onmessage = null; | 
					
						
							|  |  |  |  | 			g_socket.close(); | 
					
						
							| 
									
										
										
										
											2021-01-20 02:01:14 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		setStatusMessage('⚪ Connecting...', k_color_status); | 
					
						
							|  |  |  |  | 		g_socket = new WebSocket( | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			(window.location.protocol == 'https:' ? 'wss://' : 'ws://') + | 
					
						
							|  |  |  |  | 				window.location.hostname + | 
					
						
							|  |  |  |  | 				(window.location.port.length ? ':' + window.location.port : '') + | 
					
						
							|  |  |  |  | 				'/app/socket' | 
					
						
							|  |  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_socket.onopen = function () { | 
					
						
							|  |  |  |  | 			setStatusMessage('🟡 Authenticating...', k_color_status); | 
					
						
							| 
									
										
										
										
											2022-08-08 01:48:23 +00:00
										 |  |  |  | 			let connect_path = path ?? window.location.pathname; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_socket.send( | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				JSON.stringify({ | 
					
						
							|  |  |  |  | 					action: 'hello', | 
					
						
							|  |  |  |  | 					path: connect_path, | 
					
						
							|  |  |  |  | 					url: window.location.href, | 
					
						
							|  |  |  |  | 					edit_only: editing() && is_edit_only(), | 
					
						
							|  |  |  |  | 					api: Object.entries(k_api).map(([key, value]) => | 
					
						
							|  |  |  |  | 						[].concat([key], value.args) | 
					
						
							|  |  |  |  | 					), | 
					
						
							|  |  |  |  | 				}) | 
					
						
							|  |  |  |  | 			); | 
					
						
							|  |  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_socket.onmessage = function (event) { | 
					
						
							| 
									
										
										
										
											2022-08-14 16:58:26 +00:00
										 |  |  |  | 			_receive_websocket_message(JSON.parse(event.data)); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_socket.onclose = function (event) { | 
					
						
							|  |  |  |  | 			if (g_unloading) { | 
					
						
							|  |  |  |  | 				setStatusMessage('⚪ Closing...', k_color_status); | 
					
						
							| 
									
										
										
										
											2025-06-11 12:10:28 -04:00
										 |  |  |  | 			} else { | 
					
						
							|  |  |  |  | 				const k_codes = { | 
					
						
							|  |  |  |  | 					1000: 'Normal closure', | 
					
						
							|  |  |  |  | 					1001: 'Going away', | 
					
						
							|  |  |  |  | 					1002: 'Protocol error', | 
					
						
							|  |  |  |  | 					1003: 'Unsupported data', | 
					
						
							|  |  |  |  | 					1005: 'No status received', | 
					
						
							|  |  |  |  | 					1006: 'Abnormal closure', | 
					
						
							|  |  |  |  | 					1007: 'Invalid frame payload data', | 
					
						
							|  |  |  |  | 					1008: 'Policy violation', | 
					
						
							|  |  |  |  | 					1009: 'Message too big', | 
					
						
							|  |  |  |  | 					1010: 'Missing extension', | 
					
						
							|  |  |  |  | 					1011: 'Internal error', | 
					
						
							|  |  |  |  | 					1012: 'Service restart', | 
					
						
							|  |  |  |  | 					1013: 'Try again later', | 
					
						
							|  |  |  |  | 					1014: 'Bad gateway', | 
					
						
							|  |  |  |  | 					1015: 'TLS handshake', | 
					
						
							|  |  |  |  | 				}; | 
					
						
							|  |  |  |  | 				setStatusMessage( | 
					
						
							|  |  |  |  | 					'🔴 Closed: ' + (k_codes[event.code] || event.code), | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 					k_color_disconnect | 
					
						
							| 
									
										
										
										
											2025-06-11 12:10:28 -04:00
										 |  |  |  | 				); | 
					
						
							|  |  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		}; | 
					
						
							| 
									
										
										
										
											2016-05-07 11:07:54 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Open a file by name. | 
					
						
							|  |  |  |  |  * @param name The file to open. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function openFile(name) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	let newDoc = | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		name && g_files[name] | 
					
						
							|  |  |  |  | 			? g_files[name].doc | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			: cm6.EditorState.create({doc: '', extensions: cm6.extensions}); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	let oldDoc = g_editor.state; | 
					
						
							|  |  |  |  | 	g_editor.setState(newDoc); | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	if (g_files[g_current_file]) { | 
					
						
							|  |  |  |  | 		g_files[g_current_file].doc = oldDoc; | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		if ( | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			!g_files[g_current_file].isNew && | 
					
						
							|  |  |  |  | 			g_files[g_current_file].doc.doc.toString() == oldDoc.doc.toString() | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			delete g_files[g_current_file].buffer; | 
					
						
							| 
									
										
										
										
											2023-10-18 17:51:26 +00:00
										 |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	g_current_file = name; | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	updateFiles(); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	g_editor.focus(); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Refresh the files list. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function updateFiles() { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	let files = document.getElementsByTagName('tf-files-pane')[0]; | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 	if (files) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		files.files = Object.fromEntries( | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			Object.keys(g_files).map((file) => [ | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				file, | 
					
						
							|  |  |  |  | 				{ | 
					
						
							|  |  |  |  | 					clean: | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 						(file == g_current_file | 
					
						
							|  |  |  |  | 							? g_editor.state.doc.toString() | 
					
						
							|  |  |  |  | 							: g_files[file].doc.doc.toString()) == g_files[file].original, | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				}, | 
					
						
							|  |  |  |  | 			]) | 
					
						
							|  |  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		files.current = g_current_file; | 
					
						
							| 
									
										
										
										
											2023-05-03 23:12:34 +00:00
										 |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	g_editor.focus(); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Create a new file with the given name. | 
					
						
							|  |  |  |  |  * @param name The file's name. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function makeNewFile(name) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	g_files[name] = { | 
					
						
							| 
									
										
										
										
											2024-01-13 17:40:47 +00:00
										 |  |  |  | 		doc: cm6.EditorState.create({extensions: cm6.extensions}), | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	}; | 
					
						
							|  |  |  |  | 	openFile(name); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Prompt to create a new file. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function newFile() { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	let name = prompt('Name of new file:', 'file.js'); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	if (name && !g_files[name]) { | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 		makeNewFile(name); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Prompt to remove a file. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | function removeFile() { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	if (confirm('Remove ' + g_current_file + '?')) { | 
					
						
							|  |  |  |  | 		delete g_files[g_current_file]; | 
					
						
							|  |  |  |  | 		openFile(Object.keys(g_files)[0]); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Export the app to a zip file, which is downloaded by the browser. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | async function appExport() { | 
					
						
							|  |  |  |  | 	let JsZip = (await import('/static/jszip.min.js')).default; | 
					
						
							|  |  |  |  | 	let owner = window.location.pathname.split('/')[1].replace('~', ''); | 
					
						
							|  |  |  |  | 	let name = window.location.pathname.split('/')[2]; | 
					
						
							|  |  |  |  | 	let zip = new JsZip(); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	zip.file( | 
					
						
							|  |  |  |  | 		`${name}.json`, | 
					
						
							|  |  |  |  | 		JSON.stringify({ | 
					
						
							|  |  |  |  | 			type: 'tildefriends-app', | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			emoji: g_app.emoji || '📦', | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		}) | 
					
						
							|  |  |  |  | 	); | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	for (let file of Object.keys(g_files)) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		zip.file( | 
					
						
							|  |  |  |  | 			`${name}/${file}`, | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 			g_files[file].buffer ?? g_files[file].doc.doc.toString() | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 	let content = await zip.generateAsync({ | 
					
						
							| 
									
										
										
										
											2024-02-07 00:05:07 +00:00
										 |  |  |  | 		type: 'base64', | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 		compression: 'DEFLATE', | 
					
						
							|  |  |  |  | 	}); | 
					
						
							|  |  |  |  | 	let a = document.createElement('a'); | 
					
						
							| 
									
										
										
										
											2024-02-07 00:05:07 +00:00
										 |  |  |  | 	a.href = `data:application/zip;base64,${content}`; | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 	a.download = `${owner}_${name}.zip`; | 
					
						
							|  |  |  |  | 	a.click(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Save a file. | 
					
						
							|  |  |  |  |  * @param name The file to svae. | 
					
						
							|  |  |  |  |  * @param file The file contents. | 
					
						
							|  |  |  |  |  * @return A promise resolved with the blob ID of the saved file. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | async function save_file_to_blob_id(name, file) { | 
					
						
							|  |  |  |  | 	console.log(`Saving ${name}.`); | 
					
						
							|  |  |  |  | 	let response = await fetch('/save', { | 
					
						
							|  |  |  |  | 		method: 'POST', | 
					
						
							|  |  |  |  | 		headers: { | 
					
						
							|  |  |  |  | 			'Content-Type': 'application/binary', | 
					
						
							|  |  |  |  | 		}, | 
					
						
							|  |  |  |  | 		body: file, | 
					
						
							|  |  |  |  | 	}); | 
					
						
							|  |  |  |  | 	if (!response.ok) { | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 		throw new Error( | 
					
						
							|  |  |  |  | 			'Saving "' + name + '": ' + response.status + ' ' + response.statusText | 
					
						
							|  |  |  |  | 		); | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | 	let blob_id = await response.text(); | 
					
						
							|  |  |  |  | 	if (blob_id.charAt(0) == '/') { | 
					
						
							|  |  |  |  | 		blob_id = blob_id.substr(1); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | 	return blob_id; | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  |  * Prompt to import an app from a zip file. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | async function appImport() { | 
					
						
							|  |  |  |  | 	let JsZip = (await import('/static/jszip.min.js')).default; | 
					
						
							|  |  |  |  | 	let input = document.createElement('input'); | 
					
						
							|  |  |  |  | 	input.type = 'file'; | 
					
						
							|  |  |  |  | 	input.click(); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	input.onchange = async function () { | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 		try { | 
					
						
							|  |  |  |  | 			for (let file of input.files) { | 
					
						
							|  |  |  |  | 				if (file.type != 'application/zip') { | 
					
						
							| 
									
										
										
										
											2024-02-07 00:05:07 +00:00
										 |  |  |  | 					console.log(`This does not look like a .zip (${file.type}).`); | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 					continue; | 
					
						
							|  |  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-02-07 00:05:07 +00:00
										 |  |  |  | 				let buffer = new Uint8Array(await file.arrayBuffer()); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 				console.log( | 
					
						
							|  |  |  |  | 					'ZIP', | 
					
						
							|  |  |  |  | 					file.name, | 
					
						
							|  |  |  |  | 					file.type, | 
					
						
							|  |  |  |  | 					buffer, | 
					
						
							|  |  |  |  | 					buffer?.byteLength, | 
					
						
							|  |  |  |  | 					buffer?.length | 
					
						
							|  |  |  |  | 				); | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 				let zip = new JsZip(); | 
					
						
							|  |  |  |  | 				await zip.loadAsync(buffer); | 
					
						
							|  |  |  |  | 				let app_object; | 
					
						
							|  |  |  |  | 				let app_name; | 
					
						
							|  |  |  |  | 				for (let [name, object] of Object.entries(zip.files)) { | 
					
						
							|  |  |  |  | 					if (name.endsWith('.json') && name.indexOf('/') == -1) { | 
					
						
							|  |  |  |  | 						try { | 
					
						
							|  |  |  |  | 							let parsed = JSON.parse(await object.async('text')); | 
					
						
							|  |  |  |  | 							if (parsed.type == 'tildefriends-app') { | 
					
						
							|  |  |  |  | 								app_object = parsed; | 
					
						
							|  |  |  |  | 								app_name = name.substring(0, name.length - '.json'.length); | 
					
						
							|  |  |  |  | 								break; | 
					
						
							|  |  |  |  | 							} | 
					
						
							|  |  |  |  | 						} catch (e) { | 
					
						
							|  |  |  |  | 							console.log(e); | 
					
						
							|  |  |  |  | 						} | 
					
						
							|  |  |  |  | 					} | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 				if (app_object) { | 
					
						
							|  |  |  |  | 					app_object.files = {}; | 
					
						
							|  |  |  |  | 					for (let [name, object] of Object.entries(zip.files)) { | 
					
						
							|  |  |  |  | 						if (!name.startsWith(app_name + '/') || name.endsWith('/')) { | 
					
						
							|  |  |  |  | 							continue; | 
					
						
							|  |  |  |  | 						} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 						app_object.files[name.substring(app_name.length + '/'.length)] = | 
					
						
							|  |  |  |  | 							await save_file_to_blob_id( | 
					
						
							|  |  |  |  | 								name, | 
					
						
							|  |  |  |  | 								await object.async('arrayBuffer') | 
					
						
							|  |  |  |  | 							); | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 					let path = | 
					
						
							|  |  |  |  | 						'/' + | 
					
						
							|  |  |  |  | 						(await save_file_to_blob_id( | 
					
						
							|  |  |  |  | 							`${app_name}.json`, | 
					
						
							|  |  |  |  | 							JSON.stringify(app_object) | 
					
						
							|  |  |  |  | 						)) + | 
					
						
							|  |  |  |  | 						'/'; | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | 					console.log('Redirecting to:', path); | 
					
						
							|  |  |  |  | 					window.location.pathname = path; | 
					
						
							|  |  |  |  | 				} | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 		} catch (e) { | 
					
						
							|  |  |  |  | 			alert(e.toString()); | 
					
						
							|  |  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	}; | 
					
						
							| 
									
										
										
										
											2024-01-21 23:56:36 +00:00
										 |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  |  * Prettify the current source file. | 
					
						
							| 
									
										
										
										
											2024-02-19 19:12:42 +01:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | async function sourcePretty() { | 
					
						
							|  |  |  |  | 	let prettier = (await import('/prettier/standalone.mjs')).default; | 
					
						
							|  |  |  |  | 	let babel = (await import('/prettier/babel.mjs')).default; | 
					
						
							|  |  |  |  | 	let estree = (await import('/prettier/estree.mjs')).default; | 
					
						
							| 
									
										
										
										
											2024-10-08 20:15:04 -04:00
										 |  |  |  | 	let prettier_html = (await import('/prettier/html.mjs')).default; | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 	let source = g_editor.state.doc.toString(); | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | 	let formatted = await prettier.format(source, { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		parser: g_current_file?.toLowerCase()?.endsWith('.html') ? 'html' : 'babel', | 
					
						
							| 
									
										
										
										
											2024-10-08 20:15:04 -04:00
										 |  |  |  | 		plugins: [babel, estree, prettier_html], | 
					
						
							| 
									
										
										
										
											2024-02-22 21:31:15 +01:00
										 |  |  |  | 		trailingComma: 'es5', | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | 		useTabs: true, | 
					
						
							| 
									
										
										
										
											2024-02-22 21:31:15 +01:00
										 |  |  |  | 		semi: true, | 
					
						
							|  |  |  |  | 		singleQuote: true, | 
					
						
							|  |  |  |  | 		bracketSpacing: false, | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | 	}); | 
					
						
							|  |  |  |  | 	if (source !== formatted) { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_editor.dispatch({ | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | 			changes: { | 
					
						
							|  |  |  |  | 				from: 0, | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 				to: g_editor.state.doc.length, | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | 				insert: formatted, | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 			}, | 
					
						
							| 
									
										
										
										
											2024-02-17 18:53:21 +00:00
										 |  |  |  | 		}); | 
					
						
							|  |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Toggle visible whitespace. | 
					
						
							|  |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-02-25 22:31:31 -05:00
										 |  |  |  | function toggleVisibleWhitespace() { | 
					
						
							|  |  |  |  | 	let editor_style = document.getElementById('editor_style'); | 
					
						
							|  |  |  |  | 	/* | 
					
						
							|  |  |  |  | 	 * There is likely a better way to do this, but stomping on the CSS was | 
					
						
							|  |  |  |  | 	 * the easiest to wrangle at the time. | 
					
						
							|  |  |  |  | 	 */ | 
					
						
							|  |  |  |  | 	if (editor_style.innerHTML.length) { | 
					
						
							|  |  |  |  | 		editor_style.innerHTML = ''; | 
					
						
							| 
									
										
										
										
											2024-03-13 20:23:48 -04:00
										 |  |  |  | 		window.localStorage.setItem('visible_whitespace', '0'); | 
					
						
							| 
									
										
										
										
											2024-02-25 22:31:31 -05:00
										 |  |  |  | 	} else { | 
					
						
							|  |  |  |  | 		editor_style.innerHTML = css`
 | 
					
						
							|  |  |  |  | 			.cm-trailingSpace { | 
					
						
							|  |  |  |  | 				background-color: unset !important; | 
					
						
							|  |  |  |  | 			} | 
					
						
							|  |  |  |  | 			.cm-highlightTab { | 
					
						
							|  |  |  |  | 				background-image: unset !important; | 
					
						
							|  |  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-10-08 19:56:58 -04:00
										 |  |  |  | 			.cm-highlightSpace { | 
					
						
							|  |  |  |  | 				background-image: unset !important; | 
					
						
							| 
									
										
										
										
											2024-02-25 22:31:31 -05:00
										 |  |  |  | 			} | 
					
						
							|  |  |  |  | 		`;
 | 
					
						
							| 
									
										
										
										
											2024-03-13 20:23:48 -04:00
										 |  |  |  | 		window.localStorage.setItem('visible_whitespace', '1'); | 
					
						
							| 
									
										
										
										
											2024-02-25 22:31:31 -05:00
										 |  |  |  | 	} | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 09:07:45 -04:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Register event handlers and connect the WebSocket on load. | 
					
						
							|  |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2016-04-11 15:54:26 +00:00
										 |  |  |  | window.addEventListener('load', function () { | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | 	window.addEventListener('hashchange', hashChange); | 
					
						
							|  |  |  |  | 	window.addEventListener('focus', focus); | 
					
						
							|  |  |  |  | 	window.addEventListener('blur', blur); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	window.addEventListener('message', message, false); | 
					
						
							| 
									
										
										
										
											2016-05-07 11:07:54 +00:00
										 |  |  |  | 	window.addEventListener('online', connectSocket); | 
					
						
							| 
									
										
										
										
											2025-06-11 12:10:28 -04:00
										 |  |  |  | 	window.addEventListener('beforeunload', function () { | 
					
						
							| 
									
										
										
										
											2025-10-09 12:45:38 -04:00
										 |  |  |  | 		g_unloading = true; | 
					
						
							| 
									
										
										
										
											2025-06-11 12:10:28 -04:00
										 |  |  |  | 	}); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	document.getElementById('name').value = window.location.pathname; | 
					
						
							| 
									
										
										
										
											2023-01-28 22:44:45 +00:00
										 |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('closeEditor') | 
					
						
							|  |  |  |  | 		.addEventListener('click', () => closeEditor()); | 
					
						
							|  |  |  |  | 	document.getElementById('save').addEventListener('click', () => save()); | 
					
						
							| 
									
										
										
										
											2023-03-21 23:08:04 +00:00
										 |  |  |  | 	document.getElementById('icon').addEventListener('click', () => changeIcon()); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('delete') | 
					
						
							|  |  |  |  | 		.addEventListener('click', () => deleteApp()); | 
					
						
							|  |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('export') | 
					
						
							|  |  |  |  | 		.addEventListener('click', () => appExport()); | 
					
						
							|  |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('import') | 
					
						
							|  |  |  |  | 		.addEventListener('click', () => appImport()); | 
					
						
							|  |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('pretty') | 
					
						
							|  |  |  |  | 		.addEventListener('click', () => sourcePretty()); | 
					
						
							| 
									
										
										
										
											2024-02-25 22:31:31 -05:00
										 |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('whitespace') | 
					
						
							|  |  |  |  | 		.addEventListener('click', () => toggleVisibleWhitespace()); | 
					
						
							| 
									
										
										
										
											2024-02-24 11:09:34 -05:00
										 |  |  |  | 	document | 
					
						
							|  |  |  |  | 		.getElementById('trace_button') | 
					
						
							|  |  |  |  | 		.addEventListener('click', function (event) { | 
					
						
							|  |  |  |  | 			event.preventDefault(); | 
					
						
							|  |  |  |  | 			trace(); | 
					
						
							|  |  |  |  | 		}); | 
					
						
							| 
									
										
										
										
											2021-01-02 18:10:00 +00:00
										 |  |  |  | 	connectSocket(window.location.pathname); | 
					
						
							| 
									
										
										
										
											2022-03-07 21:06:20 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | 	if (window.localStorage.getItem('editing') == '1') { | 
					
						
							|  |  |  |  | 		edit(); | 
					
						
							|  |  |  |  | 	} else { | 
					
						
							|  |  |  |  | 		closeEditor(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-03-13 20:23:48 -04:00
										 |  |  |  | 	if (window.localStorage.getItem('visible_whitespace') == '1') { | 
					
						
							|  |  |  |  | 		toggleVisibleWhitespace(); | 
					
						
							|  |  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2016-03-12 18:50:43 +00:00
										 |  |  |  | }); | 
					
						
							| 
									
										
										
										
											2025-07-27 21:48:18 -04:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | /** @} */ |