2023-04-29 15:27:00 -04:00
import { LitElement , html , css , svg } from '/static/lit/lit-all.min.js' ;
2023-04-29 12:52:35 -04:00
2024-01-13 12:40:47 -05:00
let cm6 ;
2022-06-18 17:12:38 -04:00
let gSocket ;
let gCurrentFile ;
let gFiles = { } ;
2023-03-21 19:08:04 -04:00
let gApp = { files : { } , emoji : '📦' } ;
2022-06-18 17:12:38 -04:00
let gEditor ;
let gOriginalInput ;
let kErrorColor = "#dc322f" ;
let kStatusColor = "#fff" ;
2022-08-14 12:58:26 -04:00
/* Functions that server-side app code can call through the app object. */
2022-06-18 17:12:38 -04: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-26 20:27:10 -04:00
requestPermission : { args : [ 'permission' , 'id' ] , func : api _requestPermission } ,
2022-08-14 12:58:26 -04:00
print : { args : [ '...' ] , func : api _print } ,
2022-09-14 20:16:37 -04:00
setHash : { args : [ 'hash' ] , func : api _setHash } ,
2022-06-18 17:12:38 -04:00
} ;
2017-05-22 15:38:49 -04:00
2023-04-29 14:23:08 -04:00
const k _global _style = css `
a : link {
color : # 268 bd2 ;
}
a : visited {
color : # 6 c71c4 ;
}
a : hover {
color : # 859900 ;
}
a : active {
color : # 2 aa198 ;
}
` ;
class TfNavigationElement extends LitElement {
static get properties ( ) {
return {
credentials : { type : Object } ,
permissions : { type : Object } ,
show _permissions : { type : Boolean } ,
status : { type : Object } ,
2023-04-29 15:27:00 -04:00
spark _lines : { type : Object } ,
2023-06-28 19:00:34 -04:00
version : { type : Object } ,
show _version : { type : Boolean } ,
2023-04-29 14:23:08 -04:00
} ;
}
constructor ( ) {
super ( ) ;
this . permissions = { } ;
this . show _permissions = false ;
this . status = { } ;
2023-04-29 15:27:00 -04:00
this . spark _lines = { } ;
2023-04-29 14:23:08 -04:00
}
toggle _edit ( event ) {
event . preventDefault ( ) ;
if ( editing ( ) ) {
closeEditor ( ) ;
} else {
edit ( ) ;
}
}
reset _permission ( key ) {
send ( { action : "resetPermission" , permission : key } ) ;
}
2023-04-29 15:27:00 -04:00
get _spark _line ( key , options ) {
if ( ! this . spark _lines [ key ] ) {
let spark _line = document . createElement ( 'tf-sparkline' ) ;
2023-06-14 18:23:22 -04:00
spark _line . style . display = 'flex' ;
spark _line . style . flexDirection = 'row' ;
2023-09-04 16:13:17 -04:00
spark _line . style . flex = '0 50 5em' ;
2023-04-29 15:27:00 -04:00
spark _line . title = key ;
if ( options ) {
if ( options . max ) {
spark _line . max = options . max ;
}
}
this . spark _lines [ key ] = spark _line ;
this . requestUpdate ( ) ;
}
return this . spark _lines [ key ] ;
}
2023-04-29 14:23:08 -04:00
render _login ( ) {
if ( this ? . credentials ? . session ? . name ) {
2023-08-02 20:30:48 -04:00
return html ` <a id="login" href="/login/logout?return= ${ url ( ) + hash ( ) } ">logout ${ this . credentials . session . name } </a> ` ;
2023-04-29 14:23:08 -04:00
} else {
2023-08-02 20:30:48 -04:00
return html ` <a id="login" href="/login?return= ${ url ( ) + hash ( ) } ">login</a> ` ;
2023-04-29 14:23:08 -04:00
}
}
render _permissions ( ) {
if ( this . show _permissions ) {
return html `
2024-01-18 21:32:55 -05:00
< link type = "text/css" rel = "stylesheet" href = "/static/w3.css" >
2023-04-29 14:23:08 -04:00
< 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" >
< div > This app has the following permissions : < / d i v >
$ { Object . keys ( this . permissions ) . map ( key => html `
< div >
< span > $ { key } < / s p a n > : $ { t h i s . p e r m i s s i o n s [ k e y ] ? ' ✅ A l l o w e d ' : ' ❌ D e n i e d ' }
2024-01-18 21:32:55 -05:00
< button @ click = $ { ( ) => this . reset _permission ( key ) } class = ' w3 - button w3 - red " > Reset < / b u t t o n >
2023-04-29 14:23:08 -04:00
< / d i v >
` )}
2024-01-18 21:32:55 -05:00
< button @ click = $ { ( ) => this . show _permissions = false } class = "w3-button w3-blue" > Close < / b u t t o n >
2023-04-29 14:23:08 -04:00
< / d i v >
< / d i v >
` ;
}
}
render ( ) {
let self = this ;
return html `
< style >
$ { k _global _style }
2023-12-29 13:12:14 -05:00
. tooltip {
position : absolute ;
z - index : 1 ;
display : none ;
border : 1 px solid black ;
padding : 4 px ;
color : black ;
background : white ;
}
. tooltip _parent : hover . tooltip {
display : inline - block ;
}
2023-04-29 14:23:08 -04:00
< / s t y l e >
2023-09-04 16:13:17 -04:00
< div style = "margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center" >
2023-06-28 19:00:34 -04:00
< span style = "cursor: pointer" @ click = $ { ( ) => this . show _version = ! this . show _version } > 😎 < / s p a n >
2023-07-20 01:15:44 -04:00
< span ? hidden = $ { ! this . show _version } style = "flex: 0 0; white-space: nowrap" 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 } < / s p a n >
2024-01-03 18:52:05 -05:00
< a accesskey = "h" @ mouseover = $ { set _access _key _title } data - tip = "Open home app." href = "/" style = "color: #fff; white-space: nowrap" > TF < / a >
< a accesskey = "a" @ mouseover = $ { set _access _key _title } data - tip = "Open apps list." href = "/~core/apps/" > apps < / a >
< a accesskey = "e" @ mouseover = $ { set _access _key _title } data - tip = "Toggle the app editor." href = "#" @ click = $ { this . toggle _edit } > edit < / a >
< a 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 15:27:00 -04:00
< span style = "display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}" > $ { this . status . message } < / s p a n >
< span id = "requests" > < / s p a n >
$ { this . render _permissions ( ) }
2023-09-04 16:13:17 -04:00
< span style = "flex: 1 1; display: flex; flex-direction: row; white-space: nowrap; margin: 0; padding: 0" > $ { Object . keys ( this . spark _lines ) . sort ( ) . map ( x => this . spark _lines [ x ] ) . map ( x => [ html ` <span style="font-size: xx-small"> ${ x . dataset . emoji } </span> ` , x ] ) } < / s p a n >
2023-04-29 16:49:06 -04:00
< span style = "flex: 0 0; white-space: nowrap" > $ { this . render _login ( ) } < / s p a n >
2023-04-29 15:27:00 -04:00
< / d i v >
2023-04-29 14:23:08 -04:00
` ;
}
}
customElements . define ( 'tf-navigation' , TfNavigationElement ) ;
2023-04-29 12:52:35 -04:00
class TfFilesElement extends LitElement {
static get properties ( ) {
return {
current : { type : String } ,
files : { type : Object } ,
2023-10-17 17:37:42 -04:00
dropping : { type : Number } ,
2023-04-29 12:52:35 -04:00
} ;
}
constructor ( ) {
super ( ) ;
this . files = { } ;
2023-10-17 17:37:42 -04:00
this . dropping = 0 ;
2023-04-29 12:52:35 -04:00
}
file _click ( file ) {
this . dispatchEvent ( new CustomEvent ( 'file_click' , {
detail : {
file : file ,
} ,
bubbles : true ,
composed : true ,
} ) ) ;
}
render _file ( file ) {
let classes = [ 'file' ] ;
if ( file == this . current ) {
classes . push ( 'current' ) ;
}
if ( ! this . files [ file ] . clean ) {
classes . push ( 'dirty' ) ;
}
return html ` <div class=" ${ classes . join ( ' ' ) } " @click= ${ x => this . file _click ( file ) } > ${ file } </div> ` ;
}
2023-10-17 17:37:42 -04:00
async drop ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
this . dropping = 0 ;
for ( let file of event . dataTransfer . files ) {
2023-10-18 13:51:26 -04:00
let buffer = await file . arrayBuffer ( ) ;
let text = new TextDecoder ( 'latin1' ) . decode ( buffer ) ;
2023-10-17 17:37:42 -04:00
gFiles [ file . name ] = {
2024-01-13 12:40:47 -05:00
doc : new cm6 . EditorState . create ( { doc : text , extensions : cm6 . extensions } ) ,
2023-10-18 13:51:26 -04:00
buffer : buffer ,
2023-10-17 17:37:42 -04:00
generation : - 1 ,
2023-10-18 13:51:26 -04:00
isNew : true ,
2023-10-17 17:37:42 -04:00
} ;
gCurrentFile = file . name ;
}
openFile ( gCurrentFile ) ;
updateFiles ( ) ;
}
drag _enter ( event ) {
this . dropping ++ ;
event . preventDefault ( ) ;
}
drag _leave ( event ) {
this . dropping -- ;
}
2023-04-29 12:52:35 -04:00
render ( ) {
let self = this ;
return html `
< style >
div . file {
padding : 0.5 em ;
2023-04-29 14:23:08 -04:00
cursor : pointer ;
}
div . file : hover {
background - color : # 1 a9188 ;
2023-04-29 12:52:35 -04:00
}
div . file : : before {
content : '📄 ' ;
}
div . file . current {
font - weight : bold ;
background - color : # 2 aa198 ;
}
div . file . dirty : : after {
content : '*' ;
}
< / s t y l e >
2023-10-17 17:37:42 -04:00
< div @ drop = $ { this . drop } @ dragenter = $ { this . drag _enter } @ dragleave = $ { this . drag _leave } >
2023-04-29 12:52:35 -04:00
$ { Object . keys ( this . files ) . sort ( ) . map ( x => self . render _file ( x ) ) }
< / d i v >
2023-10-17 17:37:42 -04:00
< div
? hidden = $ { this . dropping == 0 }
@ drop = $ { this . drop } @ dragenter = $ { this . drag _enter } @ dragleave = $ { this . drag _leave }
style = "text-align: center; vertical-align: middle; outline: 16px solid red; margin: -8px; background-color: rgba(255, 0, 0, 0.5); position: absolute; left: 16px; top: 16px; width: calc(100% - 16px); height: calc(100% - 16px); z-index: 1000" >
Drop File ( s )
< / d i v >
2023-04-29 12:52:35 -04:00
` ;
}
}
customElements . define ( 'tf-files' , TfFilesElement ) ;
2023-05-03 19:12:34 -04:00
class TfFilesPaneElement extends LitElement {
static get properties ( ) {
return {
expanded : { type : Boolean } ,
current : { type : String } ,
files : { type : Object } ,
} ;
}
constructor ( ) {
super ( ) ;
2023-05-14 14:05:28 -04:00
this . expanded = window . localStorage . getItem ( 'files' ) != '0' ;
2023-05-03 19:12:34 -04:00
this . files = { } ;
}
set _expanded ( expanded ) {
this . expanded = expanded ;
window . localStorage . setItem ( 'files' , expanded ? '1' : '0' ) ;
}
render ( ) {
let self = this ;
let expander = this . expanded ?
2024-01-20 11:05:00 -05:00
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 ) } >
< span style = "flex: 1 1" font - weight : bold ; text - align : center ; flex : 1 " > Files < / s p a n >
< span style = "flex: 0 0" > « < / s p a n >
< / d i v > ` :
html ` <div class="w3-button w3-bar-item w3-blue" @click= ${ ( ) => self . set _expanded ( true ) } >»</div> ` ;
2023-05-03 19:12:34 -04:00
let content = html `
2024-01-23 22:11:49 -05:00
< tf - files style = "flex: 1 1; overflow: auto" . files = $ { self . files } current = $ { self . current } @ file _click = $ { event => openFile ( event . detail . file ) } > < / t f - f i l e s >
2024-01-20 11:05:00 -05:00
< 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 < / b u t t o n > < / d i v >
< 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 < / b u t t o n > < / d i v >
2023-05-03 19:12:34 -04:00
` ;
return html `
2024-01-20 11:05:00 -05:00
< link type = "text/css" rel = "stylesheet" href = "/static/w3.css" >
< div style = "display: flex; flex-direction: column; height: 100%" >
$ { expander }
2023-05-03 19:12:34 -04:00
$ { this . expanded ? content : undefined }
< / d i v >
` ;
}
}
customElements . define ( 'tf-files-pane' , TfFilesPaneElement ) ;
2023-04-29 15:27:00 -04: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 18:47:00 -04:00
this . k _values _max = 100 ;
2023-04-29 15:27:00 -04: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 18:47:00 -04:00
values : Array ( this . k _values _max ) . fill ( 0 ) ,
2023-04-29 15:27:00 -04:00
} ;
this . lines . push ( line ) ;
}
2023-05-03 18:47:00 -04:00
if ( line . values . length >= this . k _values _max ) {
2023-04-29 15:27:00 -04:00
line . values . shift ( ) ;
}
2023-05-03 18:47:00 -04:00
line . values . push ( value ) ;
2023-04-29 15:27:00 -04:00
this . requestUpdate ( ) ;
}
render _line ( line ) {
if ( line ? . values ? . length >= 2 ) {
2023-05-03 18:47:00 -04:00
let max = Math . max ( this . max , ... line . values ) ;
2023-09-04 16:13:17 -04: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 18:47:00 -04:00
return svg ` <polyline points= ${ points . join ( ' ' ) } stroke= ${ line . style } fill="none"/> ` ;
2023-04-29 15:27:00 -04:00
}
}
render ( ) {
2023-05-03 19:37:02 -04:00
let max = Math . round ( 10.0 * Math . max ( ... this . lines . map ( line => line . values [ line . values . length - 1 ] ) ) ) / 10.0 ;
2023-04-29 15:27:00 -04:00
return html `
2023-09-04 16:13:17 -04:00
< svg style = "max-width: 7.5em; max-height: 1.5em; margin: 0; padding: 0; background: #000" viewBox = "0 0 50 10" xmlns = "http://www.w3.org/2000/svg" >
2023-04-29 15:27:00 -04:00
$ { this . lines . map ( x => this . render _line ( x ) ) }
2023-05-03 19:37:02 -04:00
< text x = "0" y = "1em" style = "font: 8px sans-serif; fill: #fff" > $ { max } < / t e x t >
2023-04-29 15:27:00 -04:00
< / s v g >
` ;
}
}
customElements . define ( 'tf-sparkline' , TfSparkLineElement ) ;
2017-01-16 10:24:44 -05:00
window . addEventListener ( "keydown" , function ( event ) {
2022-02-03 18:57:47 -05:00
if ( event . keyCode == 83 && ( event . altKey || event . ctrlKey ) ) {
2017-01-16 10:24:44 -05:00
if ( editing ( ) ) {
save ( ) ;
event . preventDefault ( ) ;
}
} else if ( event . keyCode == 66 && event . altKey ) {
if ( editing ( ) ) {
closeEditor ( ) ;
event . preventDefault ( ) ;
}
}
} ) ;
function ensureLoaded ( nodes , callback ) {
if ( ! nodes . length ) {
callback ( ) ;
return ;
}
2022-06-18 17:12:38 -04:00
let search = nodes . shift ( ) ;
let head = document . head ;
let found = false ;
for ( let i = 0 ; i < head . childNodes . length ; i ++ ) {
2017-01-16 10:24:44 -05:00
if ( head . childNodes [ i ] . tagName == search . tagName ) {
2022-06-18 17:12:38 -04:00
let match = true ;
for ( let attribute in search . attributes ) {
2017-01-16 10:24:44 -05:00
if ( head . childNodes [ i ] . attributes [ attribute ] . value != search . attributes [ attribute ] ) {
match = false ;
}
}
if ( match ) {
found = true ;
break ;
}
}
}
if ( found ) {
ensureLoaded ( nodes , callback ) ;
} else {
2022-06-18 17:12:38 -04:00
let node = document . createElement ( search . tagName ) ;
2017-01-16 10:24:44 -05:00
node . onreadystatechange = node . onload = function ( ) {
ensureLoaded ( nodes , callback ) ;
} ;
2022-06-18 17:12:38 -04:00
for ( let attribute in search . attributes ) {
2017-01-16 10:24:44 -05:00
node . setAttribute ( attribute , search . attributes [ attribute ] ) ;
}
head . insertBefore ( node , head . firstChild ) ;
}
}
function editing ( ) {
return document . getElementById ( "editPane" ) . style . display != 'none' ;
}
2023-08-16 20:49:02 -04:00
function is _edit _only ( ) {
return window . location . search == '?editonly=1' || window . innerWidth < 1024 ;
}
2024-01-13 12:40:47 -05:00
async function edit ( ) {
2017-01-16 10:24:44 -05:00
if ( editing ( ) ) {
return ;
}
2022-03-07 16:06:20 -05:00
window . localStorage . setItem ( 'editing' , '1' ) ;
2023-05-23 18:03:17 -04:00
document . getElementById ( "editPane" ) . style . display = 'flex' ;
2023-08-16 20:49:02 -04:00
document . getElementById ( 'viewPane' ) . style . display = is _edit _only ( ) ? 'none' : 'flex' ;
2022-01-12 21:18:40 -05:00
2024-01-13 12:40:47 -05:00
try {
cm6 = await import ( '/codemirror/cm6.js' ) ;
gEditor = cm6 . TildeFriendsEditorView ( document . getElementById ( "editor" ) ) ;
gEditor . onDocChange = updateFiles ;
await load ( ) ;
} catch ( error ) {
alert ( ` ${ error . message } \n \n ${ error . stack } ` ) ;
closeEditor ( ) ;
}
2021-01-02 13:10:00 -05:00
}
2022-01-02 14:10:45 -05:00
function trace ( ) {
2023-02-17 19:51:22 -05:00
window . open ( ` /speedscope/#profileURL= ${ encodeURIComponent ( '/trace' ) } ` ) ;
2022-01-02 14:10:45 -05:00
}
2021-01-02 13:10:00 -05:00
function guessMode ( name ) {
return name . endsWith ( ".js" ) ? "javascript" :
name . endsWith ( ".html" ) ? "htmlmixed" :
null ;
}
2017-01-16 10:24:44 -05:00
2021-01-02 13:10:00 -05:00
function loadFile ( name , id ) {
2022-02-16 21:29:04 -05:00
return fetch ( '/' + id + '/view' ) . then ( function ( response ) {
if ( ! response . ok ) {
2023-10-22 14:52:20 -04:00
alert ( ` Request failed for ${ name } : ${ response . status } ${ response . statusText } ` ) ;
2023-10-18 14:54:37 -04:00
return 'missing file!' ;
2022-02-16 21:29:04 -05:00
}
return response . text ( ) ;
} ) . then ( function ( text ) {
2024-01-13 12:40:47 -05:00
gFiles [ name ] . doc = cm6 . EditorState . create ( { doc : text , extensions : cm6 . extensions } ) ;
gFiles [ name ] . original = gFiles [ name ] . doc . doc . toString ( ) ;
2022-02-16 21:29:04 -05:00
if ( ! Object . values ( gFiles ) . some ( x => ! x . doc ) ) {
openFile ( Object . keys ( gFiles ) . sort ( ) [ 0 ] ) ;
}
2021-01-02 13:10:00 -05:00
} ) ;
2017-01-16 10:24:44 -05:00
}
2024-01-13 12:40:47 -05:00
async function load ( path ) {
let response = await fetch ( ( path || url ( ) ) + 'view' ) ;
2024-01-28 08:09:31 -05: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 12:40:47 -05:00
}
gFiles = { } ;
let isApp = false ;
let promises = [ ] ;
if ( json && json [ 'type' ] == 'tildefriends-app' ) {
isApp = true ;
Object . keys ( json [ 'files' ] ) . forEach ( function ( name ) {
gFiles [ name ] = { } ;
promises . push ( loadFile ( name , json [ 'files' ] [ name ] ) ) ;
} ) ;
if ( Object . keys ( json [ 'files' ] ) . length == 0 ) {
2022-02-16 21:29:04 -05:00
document . getElementById ( "editPane" ) . style . display = 'flex' ;
}
2024-01-13 12:40:47 -05:00
gApp = json ;
gApp . emoji = gApp . emoji || '📦' ;
document . getElementById ( 'icon' ) . innerHTML = gApp . emoji ;
}
if ( ! isApp ) {
document . getElementById ( "editPane" ) . style . display = 'flex' ;
let text = '// New script.\n' ;
gCurrentFile = 'app.js' ;
gFiles [ gCurrentFile ] = {
doc : cm6 . EditorState . create ( { doc : text , extensions : cm6 . extensions } ) ,
} ;
openFile ( gCurrentFile ) ;
}
return Promise . all ( promises ) ;
2017-01-16 10:24:44 -05:00
}
function closeEditor ( ) {
2022-03-07 16:06:20 -05:00
window . localStorage . setItem ( 'editing' , '0' ) ;
2017-01-16 10:24:44 -05:00
document . getElementById ( "editPane" ) . style . display = 'none' ;
2023-08-16 20:49:02 -04:00
document . getElementById ( 'viewPane' ) . style . display = 'flex' ;
2017-01-16 10:24:44 -05:00
}
function explodePath ( ) {
return /^\/~([^\/]+)\/([^\/]+)(.*)/ . exec ( window . location . pathname ) ;
}
2022-01-30 09:51:09 -05:00
function save ( save _to ) {
2017-01-16 10:24:44 -05:00
document . getElementById ( "save" ) . disabled = true ;
2021-01-02 13:10:00 -05:00
if ( gCurrentFile ) {
2024-01-13 12:40:47 -05:00
gFiles [ gCurrentFile ] . doc = gEditor . state ;
if ( ! gFiles [ gCurrentFile ] . isNew && ! gFiles [ gCurrentFile ] . doc . doc . toString ( ) == gFiles [ gCurrentFile ] . original ) {
2023-10-18 13:51:26 -04:00
delete gFiles [ gCurrentFile ] . buffer ;
}
2021-01-02 13:10:00 -05:00
}
2017-01-16 10:24:44 -05:00
2022-06-18 17:12:38 -04:00
let save _path = save _to ;
2022-02-16 21:29:04 -05:00
if ( ! save _path ) {
2022-06-18 17:12:38 -04:00
let name = document . getElementById ( "name" ) ;
2022-01-30 09:51:09 -05:00
if ( name && name . value ) {
save _path = name . value ;
} else {
save _path = url ( ) ;
}
}
2022-06-18 17:12:38 -04:00
let promises = [ ] ;
2022-02-16 21:29:04 -05:00
for ( let name of Object . keys ( gFiles ) ) {
let file = gFiles [ name ] ;
2024-01-13 12:40:47 -05:00
if ( ! file . isNew && file . doc . doc . toString ( ) == file . original ) {
2022-02-16 21:29:04 -05:00
continue ;
2021-01-12 21:40:46 -05:00
}
2021-01-02 13:10:00 -05:00
delete file . id ;
2023-10-18 13:51:26 -04:00
delete file . isNew ;
2022-02-16 21:29:04 -05:00
promises . push ( fetch ( '/save' , {
method : 'POST' ,
headers : {
2023-10-18 13:51:26 -04:00
'Content-Type' : 'application/binary' ,
2022-02-16 21:29:04 -05:00
} ,
2024-01-13 12:40:47 -05:00
body : file . buffer ? ? file . doc . doc . toString ( ) ,
2022-02-16 21:29:04 -05:00
} ) . 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 ) ;
}
} ) ) ;
}
return Promise . all ( promises ) . then ( function ( ) {
2022-06-18 17:12:38 -04:00
let app = {
2022-02-16 21:29:04 -05:00
type : "tildefriends-app" ,
files : Object . fromEntries ( Object . keys ( gFiles ) . map ( x => [ x , gFiles [ x ] . id || gApp . files [ x ] ] ) ) ,
2023-03-21 19:08:04 -04:00
emoji : gApp . emoji || '📦' ,
2022-02-16 21:29:04 -05:00
} ;
Object . values ( gFiles ) . forEach ( function ( file ) { delete file . id ; } ) ;
gApp = JSON . parse ( JSON . stringify ( app ) ) ;
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 ) ;
}
if ( save _path != window . location . pathname ) {
alert ( 'Saved to ' + save _path + '.' ) ;
2021-01-02 13:10:00 -05:00
} else {
2022-02-16 21:29:04 -05:00
reconnect ( save _path ) ;
2021-01-02 13:10:00 -05:00
}
} ) ;
2022-02-16 21:29:04 -05:00
} ) . catch ( function ( error ) {
alert ( error ) ;
} ) . finally ( function ( ) {
document . getElementById ( "save" ) . disabled = false ;
Object . values ( gFiles ) . forEach ( function ( file ) {
2024-01-13 12:40:47 -05:00
file . original = file . doc . doc . toString ( ) ;
2021-01-02 13:10:00 -05:00
} ) ;
2022-02-16 21:29:04 -05:00
updateFiles ( ) ;
2021-01-02 13:10:00 -05:00
} ) ;
2016-03-12 13:50:43 -05:00
}
2023-03-21 19:08:04 -04:00
function changeIcon ( ) {
let value = prompt ( 'Enter a new app icon emoji:' ) ;
if ( value !== undefined ) {
gApp . emoji = value || '📦' ;
2024-01-06 10:47:14 -05:00
document . getElementById ( 'icon' ) . innerHTML = gApp . emoji ;
2023-03-21 19:08:04 -04:00
}
}
2022-06-20 14:13:19 -04:00
function deleteApp ( ) {
let name = document . getElementById ( "name" ) ;
let path = name && name . value ? name . value : url ( ) ;
if ( confirm ( ` Are you sure you want to delete the app ' ${ path } '? ` ) ) {
fetch ( path + 'delete' ) . then ( function ( response ) {
if ( ! response . ok ) {
throw new Error ( response . status + ' ' + response . statusText ) ;
}
alert ( 'Deleted.' ) ;
} ) . catch ( function ( error ) {
alert ( error ) ;
} ) ;
}
}
2016-03-12 13:50:43 -05:00
function url ( ) {
2022-06-18 17:12:38 -04:00
let hash = window . location . href . indexOf ( '#' ) ;
let question = window . location . href . indexOf ( '?' ) ;
let end = - 1 ;
2016-04-06 21:30:07 -04:00
if ( hash != - 1 && ( hash < end || end == - 1 ) )
{
end = hash ;
}
if ( question != - 1 && ( question < end || end == - 1 ) )
{
end = question ;
}
2016-03-12 13:50:43 -05:00
return end != - 1 ? window . location . href . substring ( 0 , end ) : window . location . href ;
}
2016-04-06 21:30:07 -04:00
function hash ( ) {
return window . location . hash != "#" ? window . location . hash : "" ;
}
2022-06-18 17:12:38 -04:00
function api _setDocument ( content ) {
let iframe = document . getElementById ( "document" ) ;
iframe . srcdoc = content ;
}
function api _postMessage ( message ) {
let iframe = document . getElementById ( "document" ) ;
iframe . contentWindow . postMessage ( message , "*" ) ;
}
function api _error ( error ) {
if ( error ) {
if ( typeof ( error ) == 'string' ) {
setStatusMessage ( '⚠️ ' + error , '#f00' ) ;
} else {
2022-06-19 14:01:21 -04:00
setStatusMessage ( '⚠️ ' + error . message + '\n' + error . stack , '#f00' ) ;
2022-06-18 17:12:38 -04:00
}
}
console . log ( 'error' , error ) ;
}
function api _localStorageSet ( key , value ) {
window . localStorage . setItem ( 'app:' + key , value ) ;
}
2023-01-20 19:16:18 -05:00
function api _localStorageGet ( key ) {
2022-08-13 14:58:06 -04:00
return window . localStorage . getItem ( 'app:' + key ) ;
2022-06-18 17:12:38 -04:00
}
2022-07-26 20:27:10 -04:00
function api _requestPermission ( permission , id ) {
2023-04-29 20:56:59 -04:00
let outer = document . createElement ( 'div' ) ;
outer . classList . add ( 'permissions' ) ;
2022-08-07 18:39:58 -04:00
let container = document . createElement ( 'div' ) ;
container . classList . add ( 'permissions_contents' ) ;
2022-07-26 20:27:10 -04:00
let div = document . createElement ( 'div' ) ;
2022-08-07 18:39:58 -04:00
div . appendChild ( document . createTextNode ( 'This app is requesting the following permission:' ) ) ;
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-18 21:32:55 -05:00
check . classList . add ( 'w3-check' ) ;
check . classList . add ( 'w3-blue' ) ;
2022-08-07 18:39:58 -04:00
div . appendChild ( check ) ;
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-02 20:30:48 -04:00
id : 'allow' ,
2022-08-07 18:39:58 -04:00
text : '✅ Allow' ,
grant : [ 'allow once' , 'allow' ] ,
} ,
{
2023-08-02 20:30:48 -04:00
id : 'deny' ,
2022-08-07 18:39:58 -04:00
text : '❌ Deny' ,
grant : [ 'deny once' , 'deny' ] ,
} ,
] ;
2022-08-13 21:46:11 -04:00
return new Promise ( function ( resolve , reject ) {
div = document . createElement ( 'div' ) ;
for ( let option of k _options ) {
let button = document . createElement ( 'button' ) ;
2024-01-18 21:32:55 -05:00
button . classList . add ( 'w3-button' ) ;
button . classList . add ( 'w3-blue' ) ;
2022-08-13 21:46:11 -04:00
button . innerText = option . text ;
2023-08-02 20:30:48 -04:00
button . id = option . id ;
2022-08-13 21:46:11 -04:00
button . onclick = function ( ) {
resolve ( option . grant [ check . checked ? 1 : 0 ] ) ;
2023-04-29 20:56:59 -04:00
document . body . removeChild ( outer ) ;
2022-08-07 18:39:58 -04:00
}
2022-08-13 21:46:11 -04:00
div . appendChild ( button ) ;
2022-07-26 20:27:10 -04:00
}
2022-08-13 21:46:11 -04:00
container . appendChild ( div ) ;
2023-04-29 20:56:59 -04:00
outer . appendChild ( container ) ;
2022-08-07 18:39:58 -04:00
2023-04-29 20:56:59 -04:00
document . body . appendChild ( outer ) ;
2022-08-13 21:46:11 -04:00
} ) ;
2022-07-26 20:27:10 -04:00
}
2022-08-14 12:58:26 -04:00
function api _print ( ) {
console . log ( 'app>' , ... arguments ) ;
}
2022-09-14 20:16:37 -04:00
function api _setHash ( hash ) {
window . location . hash = hash ;
}
2022-08-14 12:58:26 -04:00
function _receive _websocket _message ( message ) {
2021-01-02 13:10:00 -05:00
if ( message && message . action == "session" ) {
2022-06-18 16:51:22 -04:00
setStatusMessage ( "🟢 Executing..." , kStatusColor ) ;
2023-04-29 14:23:08 -04:00
document . getElementsByTagName ( 'tf-navigation' ) [ 0 ] . credentials = message . credentials ;
2022-08-14 14:24:41 -04:00
} else if ( message && message . action == 'permissions' ) {
2023-04-29 14:23:08 -04:00
document . getElementsByTagName ( 'tf-navigation' ) [ 0 ] . permissions = message . permissions ? ? { } ;
2021-01-02 13:10:00 -05:00
} else if ( message && message . action == "ready" ) {
setStatusMessage ( null ) ;
if ( window . location . hash ) {
send ( { event : "hashChange" , hash : window . location . hash } ) ;
}
2023-06-28 19:00:34 -04:00
document . getElementsByTagName ( 'tf-navigation' ) [ 0 ] . version = message . version ;
2023-08-16 20:49:02 -04:00
document . getElementById ( 'viewPane' ) . style . display = message . edit _only ? 'none' : 'flex' ;
2023-04-29 15:27:00 -04:00
send ( { action : 'enableStats' , enabled : true } ) ;
2021-01-02 13:10:00 -05:00
} else if ( message && message . action == "ping" ) {
2022-01-29 15:43:19 -05:00
send ( { action : "pong" } ) ;
2022-01-20 21:53:15 -05:00
} else if ( message && message . action == "stats" ) {
2022-06-18 17:12:38 -04:00
let now = new Date ( ) . getTime ( ) ;
for ( let key of Object . keys ( message . stats ) ) {
2022-06-04 12:38:45 -04:00
const k _groups = {
rpc _in : { group : 'rpc' , name : 'in' } ,
rpc _out : { group : 'rpc' , name : 'out' } ,
2023-02-28 20:36:26 -05:00
cpu _percent : { group : 'cpu' , name : 'main' } ,
thread _percent : { group : 'cpu' , name : 'work' } ,
2022-06-17 17:18:10 -04:00
arena _percent : { group : 'memory' , name : 'm' } ,
2022-06-04 12:38:45 -04:00
js _malloc _percent : { group : 'memory' , name : 'js' } ,
2022-06-17 17:18:10 -04:00
memory _percent : { group : 'memory' , name : 'tot' } ,
2022-06-04 12:38:45 -04:00
sqlite3 _memory _percent : { group : 'memory' , name : 'sql' } ,
2022-06-04 13:04:51 -04:00
tf _malloc _percent : { group : 'memory' , name : 'tf' } ,
2022-06-04 12:38:45 -04:00
tls _malloc _percent : { group : 'memory' , name : 'tls' } ,
uv _malloc _percent : { group : 'memory' , name : 'uv' } ,
2023-04-29 15:46:33 -04:00
messages _stored : { group : 'store' , name : 'messages' } ,
blobs _stored : { group : 'store' , name : 'blobs' } ,
2023-01-18 17:52:54 -05:00
2022-06-04 12:38:45 -04: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 17:18:10 -04:00
const k _colors = [ '#0f0' , '#88f' , '#ff0' , '#f0f' , '#0ff' , '#f00' , '#888' ] ;
2022-06-04 12:38:45 -04:00
let graph _key = k _groups [ key ] ? . group || key ;
2023-09-04 16:13:17 -04:00
if ( [ 'cpu' , 'rpc' , 'store' , 'memory' ] . indexOf ( graph _key ) != - 1 ) {
2023-04-29 15:46:33 -04:00
let line = document . getElementsByTagName ( 'tf-navigation' ) [ 0 ] . get _spark _line ( graph _key , { max : 100 } ) ;
line . dataset . emoji = {
'cpu' : '💻' ,
'rpc' : '🔁' ,
'store' : '💾' ,
2023-09-04 16:13:17 -04:00
'memory' : '🐏' ,
2023-04-29 15:46:33 -04:00
} [ graph _key ] ;
line . append ( key , message . stats [ key ] ) ;
2023-04-29 15:27:00 -04:00
}
2022-01-17 16:46:32 -05:00
}
2022-08-13 14:58:06 -04:00
} else if ( message &&
message . message === 'tfrpc' &&
message . method ) {
let api = k _api [ message . method ] ;
2023-01-20 19:16:18 -05:00
let id = message . id ;
let params = message . params ;
2022-06-18 17:12:38 -04:00
if ( api ) {
2023-01-20 19:16:18 -05:00
Promise . resolve ( api . func ( ... params ) ) . then ( function ( result ) {
2022-08-13 14:58:06 -04:00
send ( {
message : 'tfrpc' ,
2023-01-20 19:16:18 -05:00
id : id ,
2022-08-13 14:58:06 -04:00
result : result ,
} ) ;
} ) . catch ( function ( error ) {
send ( {
message : 'tfrpc' ,
2023-01-20 19:16:18 -05:00
id : id ,
2022-08-13 14:58:06 -04:00
error : error ,
} ) ;
} ) ;
2022-06-18 17:12:38 -04:00
}
2016-03-12 13:50:43 -05:00
}
}
2022-06-18 16:51:22 -04:00
function setStatusMessage ( message , color ) {
2023-04-29 14:23:08 -04:00
document . getElementsByTagName ( 'tf-navigation' ) [ 0 ] . status = { message : message , color : color } ;
2016-04-10 20:28:42 -04:00
}
2021-01-02 13:10:00 -05:00
function send ( value ) {
2016-04-10 20:09:21 -04:00
try {
2022-06-18 16:51:22 -04:00
if ( gSocket && gSocket . readyState == gSocket . OPEN ) {
gSocket . send ( JSON . stringify ( value ) ) ;
}
2016-04-10 20:09:21 -04:00
} catch ( error ) {
2022-06-18 16:51:22 -04:00
setStatusMessage ( '🤷 Send failed: ' + error . toString ( ) , kErrorColor ) ;
2016-04-10 20:09:21 -04:00
}
2016-03-12 13:50:43 -05:00
}
function fixImage ( sourceData , maxWidth , maxHeight , callback ) {
2022-06-18 17:12:38 -04:00
let result = sourceData ;
let image = new Image ( ) ;
2016-03-12 13:50:43 -05:00
image . crossOrigin = "anonymous" ;
image . referrerPolicy = "no-referrer" ;
image . onload = function ( ) {
if ( image . width > maxWidth || image . height > maxHeight ) {
2022-06-18 17:12:38 -04:00
let downScale = Math . min ( maxWidth / image . width , maxHeight / image . height ) ;
let canvas = document . createElement ( "canvas" ) ;
2016-03-12 13:50:43 -05:00
canvas . width = image . width * downScale ;
canvas . height = image . height * downScale ;
2022-06-18 17:12:38 -04:00
let context = canvas . getContext ( "2d" ) ;
2016-03-12 13:50:43 -05:00
context . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
image . width = canvas . width ;
image . height = canvas . height ;
context . drawImage ( image , 0 , 0 , image . width , image . height ) ;
result = canvas . toDataURL ( ) ;
}
callback ( result ) ;
} ;
image . src = sourceData ;
}
function sendImage ( image ) {
fixImage ( image , 320 , 240 , function ( result ) {
send ( { image : result } ) ;
} ) ;
}
function hashChange ( ) {
send ( { event : 'hashChange' , hash : window . location . hash } ) ;
}
function focus ( ) {
2016-05-07 07:07:54 -04:00
if ( gSocket && gSocket . readyState == gSocket . CLOSED ) {
connectSocket ( ) ;
} else {
send ( { event : "focus" } ) ;
}
2016-03-12 13:50:43 -05:00
}
function blur ( ) {
2016-05-07 07:07:54 -04:00
if ( gSocket && gSocket . readyState == gSocket . OPEN ) {
send ( { event : "blur" } ) ;
}
2016-03-12 13:50:43 -05:00
}
2021-01-02 13:10:00 -05:00
function message ( event ) {
2016-09-17 16:53:03 -04:00
if ( event . data && event . data . event == "resizeMe" && event . data . width && event . data . height ) {
2022-06-18 17:12:38 -04:00
let iframe = document . getElementById ( "iframe_" + event . data . name ) ;
2016-09-17 16:53:03 -04:00
iframe . setAttribute ( "width" , event . data . width ) ;
iframe . setAttribute ( "height" , event . data . height ) ;
2022-01-06 20:52:47 -05:00
} else if ( event . data && event . data . action == "setHash" ) {
window . location . hash = event . data . hash ;
2022-01-27 22:11:09 -05:00
} else if ( event . data && event . data . action == 'storeBlob' ) {
2022-02-16 21:29:04 -05:00
fetch ( '/save' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/binary' ,
} ,
body : event . data . blob . buffer ,
} ) . then ( function ( response ) {
if ( ! response . ok ) {
throw new Error ( response . status + ' ' + response . statusText ) ;
2022-01-27 22:11:09 -05:00
}
2022-02-16 21:29:04 -05:00
return response . text ( ) ;
} ) . then ( function ( text ) {
2022-06-18 17:12:38 -04:00
let iframe = document . getElementById ( "document" ) ;
2022-04-14 19:47:06 -04:00
iframe . contentWindow . postMessage ( { 'storeBlobComplete' : { name : event . data . blob . name , path : text , type : event . data . blob . type , context : event . data . context } } , '*' ) ;
2022-01-27 22:11:09 -05:00
} ) ;
2016-09-17 16:53:03 -04:00
} else {
2021-01-02 13:10:00 -05:00
send ( { event : "message" , message : event . data } ) ;
2016-05-01 09:24:37 -04:00
}
}
2016-04-10 20:09:21 -04:00
2021-01-02 13:10:00 -05:00
function reconnect ( path ) {
2017-01-16 10:24:44 -05:00
let oldSocket = gSocket ;
gSocket = null
2023-03-12 18:16:18 -04:00
if ( oldSocket ) {
oldSocket . onopen = null ;
oldSocket . onclose = null ;
oldSocket . onmessage = null ;
oldSocket . close ( ) ;
}
2021-01-02 13:10:00 -05:00
connectSocket ( path ) ;
2017-01-16 10:24:44 -05:00
}
2021-01-02 13:10:00 -05:00
function connectSocket ( path ) {
2021-01-19 21:01:14 -05:00
if ( ! gSocket || gSocket . readyState != gSocket . OPEN ) {
if ( gSocket ) {
gSocket . onopen = null ;
gSocket . onclose = null ;
gSocket . onmessage = null ;
gSocket . close ( ) ;
}
2022-06-18 16:51:22 -04:00
setStatusMessage ( "⚪ Connecting..." , kStatusColor ) ;
2016-05-07 07:07:54 -04:00
gSocket = new WebSocket (
( window . location . protocol == "https:" ? "wss://" : "ws://" )
+ window . location . hostname
+ ( window . location . port . length ? ":" + window . location . port : "" )
2021-01-02 13:10:00 -05:00
+ "/app/socket" ) ;
2016-05-07 07:07:54 -04:00
gSocket . onopen = function ( ) {
2022-06-18 16:51:22 -04:00
setStatusMessage ( "🟡 Authenticating..." , kStatusColor ) ;
2022-08-07 21:48:23 -04:00
let connect _path = path ? ? window . location . pathname ;
2016-05-07 07:07:54 -04:00
gSocket . send ( JSON . stringify ( {
action : "hello" ,
2022-08-07 21:48:23 -04:00
path : connect _path ,
2023-07-30 20:26:09 -04:00
url : window . location . href ,
2023-08-16 20:49:02 -04:00
edit _only : editing ( ) && is _edit _only ( ) ,
2022-06-18 17:12:38 -04:00
api : Object . entries ( k _api ) . map ( ( [ key , value ] ) => [ ] . concat ( [ key ] , value . args ) ) ,
2016-05-07 07:07:54 -04:00
} ) ) ;
}
gSocket . onmessage = function ( event ) {
2022-08-14 12:58:26 -04:00
_receive _websocket _message ( JSON . parse ( event . data ) ) ;
2016-05-07 07:07:54 -04:00
}
gSocket . onclose = function ( event ) {
2022-01-29 15:43:19 -05:00
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' ,
} ;
2022-06-18 16:51:22 -04:00
setStatusMessage ( "🔴 Closed: " + ( k _codes [ event . code ] || event . code ) , kErrorColor ) ;
2016-05-07 07:07:54 -04:00
}
}
}
2021-01-02 13:10:00 -05:00
function openFile ( name ) {
2024-01-13 12:40:47 -05:00
let newDoc = ( name && gFiles [ name ] ) ? gFiles [ name ] . doc : cm6 . EditorState . create ( { doc : "" , extensions : cm6 . extensions } ) ;
let oldDoc = gEditor . state ;
gEditor . setState ( newDoc ) ;
2021-01-02 13:10:00 -05:00
if ( gFiles [ gCurrentFile ] ) {
gFiles [ gCurrentFile ] . doc = oldDoc ;
2024-01-13 12:40:47 -05:00
if ( ! gFiles [ gCurrentFile ] . isNew && gFiles [ gCurrentFile ] . doc . doc . toString ( ) == oldDoc . doc . toString ( ) ) {
2023-10-18 13:51:26 -04:00
delete gFiles [ gCurrentFile ] . buffer ;
}
2021-01-02 13:10:00 -05:00
}
gCurrentFile = name ;
updateFiles ( ) ;
gEditor . focus ( ) ;
}
function updateFiles ( ) {
2023-05-03 19:12:34 -04:00
let files = document . getElementsByTagName ( "tf-files-pane" ) [ 0 ] ;
if ( files ) {
files . files = Object . fromEntries ( Object . keys ( gFiles ) . map ( file => [ file , {
2024-01-13 12:40:47 -05:00
clean : ( file == gCurrentFile ? gEditor . state . doc . toString ( ) : gFiles [ file ] . doc . doc . toString ( ) ) == gFiles [ file ] . original ,
2023-05-03 19:12:34 -04:00
} ] ) ) ;
files . current = gCurrentFile ;
}
2021-01-02 13:10:00 -05:00
gEditor . focus ( ) ;
}
function makeNewFile ( name ) {
gFiles [ name ] = {
2024-01-13 12:40:47 -05:00
doc : cm6 . EditorState . create ( { extensions : cm6 . extensions } ) ,
2021-01-12 21:40:46 -05:00
generation : - 1 ,
2021-01-02 13:10:00 -05:00
} ;
openFile ( name ) ;
}
function newFile ( ) {
2022-06-18 17:12:38 -04:00
let name = prompt ( "Name of new file:" , "file.js" ) ;
2021-01-02 13:10:00 -05:00
if ( name && ! gFiles [ name ] ) {
makeNewFile ( name ) ;
}
}
function removeFile ( ) {
if ( confirm ( "Remove " + gCurrentFile + "?" ) ) {
delete gFiles [ gCurrentFile ] ;
openFile ( Object . keys ( gFiles ) [ 0 ] ) ;
}
}
2024-01-21 18:56:36 -05: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 ( ) ;
zip . file ( ` ${ name } .json ` , JSON . stringify ( {
type : "tildefriends-app" ,
emoji : gApp . emoji || '📦' ,
} ) ) ;
for ( let file of Object . keys ( gFiles ) ) {
zip . file ( ` ${ name } / ${ file } ` , gFiles [ file ] . buffer ? ? gFiles [ file ] . doc . doc . toString ( ) ) ;
}
let content = await zip . generateAsync ( {
type : 'blob' ,
compression : 'DEFLATE' ,
} ) ;
let a = document . createElement ( 'a' ) ;
a . href = URL . createObjectURL ( content ) ;
a . download = ` ${ owner } _ ${ name } .zip ` ;
a . click ( ) ;
}
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 ) {
throw new Error ( 'Saving "' + name + '": ' + response . status + ' ' + response . statusText ) ;
}
let blob _id = await response . text ( ) ;
if ( blob _id . charAt ( 0 ) == '/' ) {
blob _id = blob _id . substr ( 1 ) ;
}
return blob _id ;
}
async function appImport ( ) {
let JsZip = ( await import ( '/static/jszip.min.js' ) ) . default ;
let input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . click ( ) ;
input . onchange = async function ( ) {
try {
for ( let file of input . files ) {
if ( file . type != 'application/zip' ) {
console . log ( 'This does not look like a .zip.' ) ;
continue ;
}
let buffer = await file . arrayBuffer ( ) ;
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 ;
}
app _object . files [ name . substring ( app _name . length + '/' . length ) ] = await save _file _to _blob _id ( name , await object . async ( 'arrayBuffer' ) ) ;
}
let path = '/' + await save _file _to _blob _id ( ` ${ app _name } .json ` , JSON . stringify ( app _object ) ) + '/' ;
console . log ( 'Redirecting to:' , path ) ;
window . location . pathname = path ;
}
}
} catch ( e ) {
alert ( e . toString ( ) ) ;
}
}
}
2016-04-11 11:54:26 -04:00
window . addEventListener ( "load" , function ( ) {
2016-03-12 13:50:43 -05:00
window . addEventListener ( "hashchange" , hashChange ) ;
window . addEventListener ( "focus" , focus ) ;
window . addEventListener ( "blur" , blur ) ;
2021-01-02 13:10:00 -05:00
window . addEventListener ( "message" , message , false ) ;
2016-05-07 07:07:54 -04:00
window . addEventListener ( "online" , connectSocket ) ;
2021-01-02 13:10:00 -05:00
document . getElementById ( "name" ) . value = window . location . pathname ;
2023-01-28 17:44:45 -05:00
document . getElementById ( 'closeEditor' ) . addEventListener ( 'click' , ( ) => closeEditor ( ) ) ;
document . getElementById ( 'save' ) . addEventListener ( 'click' , ( ) => save ( ) ) ;
2023-03-21 19:08:04 -04:00
document . getElementById ( 'icon' ) . addEventListener ( 'click' , ( ) => changeIcon ( ) ) ;
2023-01-28 17:44:45 -05:00
document . getElementById ( 'delete' ) . addEventListener ( 'click' , ( ) => deleteApp ( ) ) ;
2024-01-21 18:56:36 -05:00
document . getElementById ( 'export' ) . addEventListener ( 'click' , ( ) => appExport ( ) ) ;
document . getElementById ( 'import' ) . addEventListener ( 'click' , ( ) => appImport ( ) ) ;
2023-01-28 17:44:45 -05:00
document . getElementById ( 'trace_button' ) . addEventListener ( 'click' , function ( event ) {
2023-04-29 14:23:08 -04:00
event . preventDefault ( ) ;
2023-01-28 17:44:45 -05:00
trace ( ) ;
} ) ;
2021-01-02 13:10:00 -05:00
connectSocket ( window . location . pathname ) ;
2022-03-07 16:06:20 -05:00
if ( window . localStorage . getItem ( 'editing' ) == '1' ) {
edit ( ) ;
} else {
closeEditor ( ) ;
}
2016-03-12 13:50:43 -05:00
} ) ;