mirror of
https://github.com/Luzifer/past3.git
synced 2024-12-22 20:31:16 +00:00
Port application to pure Javascript
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
6f0a3d068d
commit
2ee228f721
6 changed files with 3162 additions and 1009 deletions
5
.babelrc
5
.babelrc
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
[
|
[
|
||||||
"env",
|
"@babel/preset-env",
|
||||||
{
|
{
|
||||||
"targets": {
|
"targets": {
|
||||||
"browsers": [
|
"browsers": [
|
||||||
|
@ -16,5 +16,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"minify"
|
"minify"
|
||||||
]
|
],
|
||||||
|
"comments": false
|
||||||
}
|
}
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -4,7 +4,7 @@ generate: script index-page
|
||||||
|
|
||||||
script: public
|
script: public
|
||||||
docker run --rm -ti -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:alpine \
|
docker run --rm -ti -v $(CURDIR):$(CURDIR) -w $(CURDIR) node:alpine \
|
||||||
sh -exc "npm ci && npx coffee -t -c -o public/app.js app.coffee && chown -R $(shell id -u) public && rm -rf node_modules"
|
sh -exc "npm ci && npx babel -d public app.js && chown -R $(shell id -u) public && rm -rf node_modules"
|
||||||
|
|
||||||
index-page: public
|
index-page: public
|
||||||
./generate.py > public/index.html
|
./generate.py > public/index.html
|
||||||
|
|
202
app.coffee
202
app.coffee
|
@ -1,202 +0,0 @@
|
||||||
$ ->
|
|
||||||
$('#signin').modal
|
|
||||||
backdrop: 'static'
|
|
||||||
keyboard: false
|
|
||||||
|
|
||||||
AWS.config.region = window.past3_config.region
|
|
||||||
|
|
||||||
CodeMirror.modeURL = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.22.2/mode/%N/%N.js'
|
|
||||||
|
|
||||||
window.editor = CodeMirror.fromTextArea document.getElementById('editor'),
|
|
||||||
extraKeys:
|
|
||||||
Tab: (cm) -> cm.replaceSelection(Array(cm.getOption("indentUnit") + 1).join(" "))
|
|
||||||
lineNumbers: true
|
|
||||||
viewportMargin: 25
|
|
||||||
|
|
||||||
window.editor.setSize null, '100%'
|
|
||||||
|
|
||||||
$('#filename').bind 'input', filenameInput
|
|
||||||
|
|
||||||
$('#newFile').bind 'click', () ->
|
|
||||||
$('#filename').val('')
|
|
||||||
$('#file-url').val('n/a')
|
|
||||||
window.editor.setValue('')
|
|
||||||
$('.file-list-item').removeClass 'active'
|
|
||||||
return false
|
|
||||||
|
|
||||||
$('#saveFile').bind 'click', () ->
|
|
||||||
filename = $('#filename').val()
|
|
||||||
window.editor.save()
|
|
||||||
content = $('#editor').val()
|
|
||||||
saveFile(filename, content)
|
|
||||||
|
|
||||||
$('#deleteFile').bind 'click', () ->
|
|
||||||
filename = $('#filename').val()
|
|
||||||
deleteFile(filename)
|
|
||||||
|
|
||||||
$('#file-url').bind 'click', () ->
|
|
||||||
$(this).select()
|
|
||||||
|
|
||||||
$(window).bind 'hashchange', () ->
|
|
||||||
if window.location.hash.length > 1
|
|
||||||
filename = window.location.hash.substring(1)
|
|
||||||
getFile filename
|
|
||||||
|
|
||||||
$('.file-list-item').removeClass 'active'
|
|
||||||
$(".file-list-item").each () ->
|
|
||||||
if $(this).data('file') == filename
|
|
||||||
$(this).addClass 'active'
|
|
||||||
|
|
||||||
$(window).bind 'keydown', (e) ->
|
|
||||||
if (e.metaKey or e.ctrlKey) and e.keyCode == 83 # cmd+s / ctrl+s
|
|
||||||
$('#saveFile').trigger 'click'
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (e.metaKey or e.ctrlKey) and e.keyCode == 78 # cmd+n / ctrl+n
|
|
||||||
$('#newFile').trigger 'click'
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (e.metaKey or e.ctrlKey) and e.keyCode == 82 # cmd+r / ctrl+r
|
|
||||||
listFiles()
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
|
|
||||||
signinCallback = (authResult) ->
|
|
||||||
if authResult.Zi.id_token
|
|
||||||
getAWSCredentials authResult.Zi.id_token
|
|
||||||
|
|
||||||
window.past3_credential_refresh = window.setInterval () ->
|
|
||||||
if new Date(AWS.config.credentials.expireTime - 300000) < new Date()
|
|
||||||
refreshGoogleLogin()
|
|
||||||
, 10000
|
|
||||||
|
|
||||||
getAWSCredentials = (googleIDToken) ->
|
|
||||||
AWS.config.credentials = new AWS.CognitoIdentityCredentials
|
|
||||||
IdentityPoolId: window.past3_config.identity_pool_id
|
|
||||||
Logins:
|
|
||||||
'accounts.google.com': googleIDToken
|
|
||||||
|
|
||||||
AWS.config.credentials.get () ->
|
|
||||||
$('#signin').modal 'hide'
|
|
||||||
listFiles()
|
|
||||||
$(window).trigger 'hashchange'
|
|
||||||
|
|
||||||
renderButton = () ->
|
|
||||||
gapi.signin2.render 'signInButton',
|
|
||||||
'scope': 'profile email',
|
|
||||||
'width': 240,
|
|
||||||
'height': 30,
|
|
||||||
'longtitle': true,
|
|
||||||
'theme': 'dark',
|
|
||||||
'onsuccess': signinCallback,
|
|
||||||
|
|
||||||
getFilePrefix = () ->
|
|
||||||
s3 = new AWS.S3()
|
|
||||||
"#{s3.config.credentials.identityId}/"
|
|
||||||
|
|
||||||
getFile = (filename) ->
|
|
||||||
s3 = new AWS.S3()
|
|
||||||
s3.getObject({
|
|
||||||
Bucket: window.past3_config.bucket
|
|
||||||
Key: getFilePrefix() + filename
|
|
||||||
}, loadFileIntoEditor)
|
|
||||||
$('#filename').val filename
|
|
||||||
$('#filename').trigger 'input'
|
|
||||||
|
|
||||||
listFiles = () ->
|
|
||||||
s3 = new AWS.S3()
|
|
||||||
s3.listObjects({
|
|
||||||
Bucket: window.past3_config.bucket
|
|
||||||
Prefix: getFilePrefix()
|
|
||||||
}, loadFileList)
|
|
||||||
|
|
||||||
saveFile = (filename, content) ->
|
|
||||||
mime = getMimeType filename
|
|
||||||
s3 = new AWS.S3()
|
|
||||||
s3.putObject({
|
|
||||||
ACL: window.past3_config.acl
|
|
||||||
Body: content
|
|
||||||
Bucket: window.past3_config.bucket
|
|
||||||
ContentType: mime.mime
|
|
||||||
Key: getFilePrefix() + filename
|
|
||||||
}, fileActionCallback)
|
|
||||||
|
|
||||||
deleteFile = (filename) ->
|
|
||||||
s3 = new AWS.S3()
|
|
||||||
s3.deleteObject({
|
|
||||||
Bucket: window.past3_config.bucket
|
|
||||||
Key: getFilePrefix() + filename
|
|
||||||
}, fileActionCallback)
|
|
||||||
|
|
||||||
loadFileIntoEditor = (err, data) ->
|
|
||||||
if err
|
|
||||||
error err
|
|
||||||
return
|
|
||||||
|
|
||||||
window.editor.setValue(String(data.Body))
|
|
||||||
|
|
||||||
loadFileList = (err, data) ->
|
|
||||||
if err
|
|
||||||
error err
|
|
||||||
return
|
|
||||||
|
|
||||||
$('.file-list-item').remove()
|
|
||||||
for obj in data.Contents
|
|
||||||
key = obj.Key.replace getFilePrefix(), ''
|
|
||||||
|
|
||||||
li = $("<a href='##{key}' class='list-group-item file-list-item'><span class='badge'></span> #{key}</a>")
|
|
||||||
li.find('.badge').text formatDate(obj.LastModified)
|
|
||||||
li.data 'file', key
|
|
||||||
|
|
||||||
if key == $('#filename').val()
|
|
||||||
li.addClass 'active'
|
|
||||||
|
|
||||||
li.appendTo $('#fileList')
|
|
||||||
|
|
||||||
fileActionCallback = (err, data) ->
|
|
||||||
if err
|
|
||||||
error err
|
|
||||||
return
|
|
||||||
|
|
||||||
listFiles()
|
|
||||||
|
|
||||||
formatDate = (src) ->
|
|
||||||
$.format.date src, 'yyyy-MM-dd HH:mm:ss'
|
|
||||||
|
|
||||||
setEditorMime = (filename) ->
|
|
||||||
if window.mime_detect
|
|
||||||
window.clearTimeout window.mime_detect
|
|
||||||
|
|
||||||
window.mime_detect = window.setTimeout () ->
|
|
||||||
autoMime = getMimeType filename
|
|
||||||
window.editor.setOption 'mode', autoMime.mime
|
|
||||||
CodeMirror.autoLoadMode window.editor, autoMime.mode
|
|
||||||
, 500
|
|
||||||
|
|
||||||
getMimeType = (filename) ->
|
|
||||||
name_parts = filename.split('.')
|
|
||||||
ext = name_parts[name_parts.length - 1]
|
|
||||||
|
|
||||||
mime = CodeMirror.findModeByExtension ext
|
|
||||||
|
|
||||||
if mime == undefined
|
|
||||||
mime = CodeMirror.findModeByExtension 'txt'
|
|
||||||
|
|
||||||
return mime
|
|
||||||
|
|
||||||
filenameInput = () ->
|
|
||||||
setEditorMime $(this).val()
|
|
||||||
|
|
||||||
$('#file-url').val window.past3_config.base_url + getFilePrefix() + $(this).val()
|
|
||||||
|
|
||||||
error = (err) ->
|
|
||||||
ed = $('#errorDisplay')
|
|
||||||
ed.find('.alert').text err
|
|
||||||
ed.show()
|
|
||||||
|
|
||||||
refreshGoogleLogin = () ->
|
|
||||||
console.log "Refreshing Google login / AWS credentials to keep editor working"
|
|
||||||
gapi.auth2.getAuthInstance().currentUser.get().reloadAuthResponse().then (data) ->
|
|
||||||
getAWSCredentials data.id_token
|
|
264
app.js
Normal file
264
app.js
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
// deleteFile removes the specified file from the AWS bucket
|
||||||
|
function deleteFile(filename) {
|
||||||
|
let s3 = new AWS.S3()
|
||||||
|
s3.deleteObject({
|
||||||
|
Bucket: window.past3_config.bucket,
|
||||||
|
Key: getFilePrefix() + filename,
|
||||||
|
}, fileActionCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// error displays the error in the frontend
|
||||||
|
function error(err) {
|
||||||
|
let ed = $('#errorDisplay')
|
||||||
|
ed.find('.alert').text(err)
|
||||||
|
ed.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileActionCallback is used to trigger a reload of the file list
|
||||||
|
function fileActionCallback(err, data) {
|
||||||
|
if (err) {
|
||||||
|
return error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// filenameInput is the callback for changes in the filename
|
||||||
|
function filenameInput() {
|
||||||
|
setEditorMime($(this).val())
|
||||||
|
$('#file-url').val(window.past3_config.base_url + getFilePrefix() + $(this).val())
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDate formats a Date() object into iso-like format
|
||||||
|
function formatDate(src) {
|
||||||
|
return $.format.date(src, 'yyyy-MM-dd HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAWSCredentials retrieves AWS credentials via Cognito using the Google ID Token
|
||||||
|
function getAWSCredentials(googleIDToken) {
|
||||||
|
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
|
||||||
|
IdentityPoolId: window.past3_config.identity_pool_id,
|
||||||
|
Logins: {
|
||||||
|
'accounts.google.com': googleIDToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
AWS.config.credentials.get(() => {
|
||||||
|
$('#signin').modal('hide')
|
||||||
|
listFiles()
|
||||||
|
$(window).trigger('hashchange')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFile retrieves an object from the bucket and loads it into the editor
|
||||||
|
function getFile(filename) {
|
||||||
|
let s3 = new AWS.S3()
|
||||||
|
s3.getObject({
|
||||||
|
Bucket: window.past3_config.bucket,
|
||||||
|
Key: getFilePrefix() + filename,
|
||||||
|
}, loadFileIntoEditor)
|
||||||
|
|
||||||
|
$('#filename').val(filename)
|
||||||
|
$('#filename').trigger('input')
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFilePrefix retrieves the file prefix using the Cognito identityId
|
||||||
|
function getFilePrefix() {
|
||||||
|
let s3 = new AWS.S3()
|
||||||
|
return `${s3.config.credentials.identityId}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMimeType uses CodeMirror mime guessing to get the mime type of
|
||||||
|
// the file and falls back to txt if none is found
|
||||||
|
function getMimeType(filename) {
|
||||||
|
let name_parts = filename.split('.')
|
||||||
|
let ext = name_parts[name_parts.length - 1]
|
||||||
|
|
||||||
|
let mime = CodeMirror.findModeByExtension(ext)
|
||||||
|
|
||||||
|
if (mime === undefined) {
|
||||||
|
mime = CodeMirror.findModeByExtension('txt')
|
||||||
|
}
|
||||||
|
|
||||||
|
return mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// init initializes the interface with its listeners
|
||||||
|
function init() {
|
||||||
|
// Show sign-in modal
|
||||||
|
$('#signin').modal({
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configure AWS and CodeMirror
|
||||||
|
AWS.config.region = window.past3_config.region
|
||||||
|
CodeMirror.modeURL = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.22.2/mode/%N/%N.js'
|
||||||
|
|
||||||
|
// Initialize the editor
|
||||||
|
window.editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
|
||||||
|
extraKeys: {
|
||||||
|
Tab: (cm) => cm.replaceSelection(Array(cm.getOption("indentUnit") + 1).join(" ")),
|
||||||
|
},
|
||||||
|
lineNumbers: true,
|
||||||
|
viewportMargin: 25,
|
||||||
|
})
|
||||||
|
window.editor.setSize(null, '100%')
|
||||||
|
|
||||||
|
// Set up bindings
|
||||||
|
$('#filename').bind('input', filenameInput)
|
||||||
|
|
||||||
|
$('#newFile').bind('click', () => {
|
||||||
|
$('#filename').val('')
|
||||||
|
$('#file-url').val('n/a')
|
||||||
|
window.editor.setValue('')
|
||||||
|
$('.file-list-item').removeClass('active')
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#saveFile').bind('click', () => {
|
||||||
|
window.editor.save()
|
||||||
|
let filename = $('#filename').val()
|
||||||
|
let content = $('#editor').val()
|
||||||
|
saveFile(filename, content)
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#deleteFile').bind('click', () => deleteFile($('#filename').val()))
|
||||||
|
|
||||||
|
$('#file-url').bind('click', () => $(this).select())
|
||||||
|
|
||||||
|
$(window).bind('hashchange', () => {
|
||||||
|
if (window.location.hash.length > 1) {
|
||||||
|
let filename = window.location.hash.substring(1)
|
||||||
|
getFile(filename)
|
||||||
|
|
||||||
|
$('.file-list-item').removeClass('active')
|
||||||
|
$(".file-list-item").each(() => {
|
||||||
|
if ($(this).data('file') == filename) {
|
||||||
|
$(this).addClass('active')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$(window).bind('keydown', (e) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.keyCode == 83) { // cmd + s / ctrl + s
|
||||||
|
$('#saveFile').trigger('click')
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.keyCode == 78) { // cmd + n / ctrl + n
|
||||||
|
$('#newFile').trigger('click')
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.keyCode == 82) { // cmd + r / ctrl + r
|
||||||
|
listFiles()
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// listFiles triggers a reload of the file list
|
||||||
|
function listFiles() {
|
||||||
|
let s3 = new AWS.S3()
|
||||||
|
s3.listObjects({
|
||||||
|
Bucket: window.past3_config.bucket,
|
||||||
|
Prefix: getFilePrefix(),
|
||||||
|
}, loadFileList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFileIntoEditor sets the editor content
|
||||||
|
function loadFileIntoEditor(err, data) {
|
||||||
|
if (err) {
|
||||||
|
return error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.editor.setValue(String(data.Body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFileList re-renders the file list from the AWS bucket list response
|
||||||
|
function loadFileList(err, data) {
|
||||||
|
if (err) {
|
||||||
|
return error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.file-list-item').remove()
|
||||||
|
for (let obj of data.Contents) {
|
||||||
|
let key = obj.Key.replace(getFilePrefix(), '')
|
||||||
|
|
||||||
|
let li = $(`<a href='#${key}' class='list-group-item file-list-item'><span class='badge'></span> ${key}</a>`)
|
||||||
|
li.find('.badge').text(formatDate(obj.LastModified))
|
||||||
|
li.data('file', key)
|
||||||
|
|
||||||
|
if (key === $('#filename').val()) {
|
||||||
|
li.addClass('active')
|
||||||
|
}
|
||||||
|
|
||||||
|
li.appendTo($('#fileList'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshGoogleLogin triggers a refresh of the Google token
|
||||||
|
function refreshGoogleLogin() {
|
||||||
|
console.log("Refreshing Google login / AWS credentials to keep editor working")
|
||||||
|
gapi.auth2.getAuthInstance().currentUser.get().reloadAuthResponse()
|
||||||
|
.then((data) => getAWSCredentials(data.id_token))
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderButton displays the sign-in with Google button
|
||||||
|
function renderButton() {
|
||||||
|
gapi.signin2.render('signInButton', {
|
||||||
|
'scope': 'profile email',
|
||||||
|
'width': 240,
|
||||||
|
'height': 30,
|
||||||
|
'longtitle': true,
|
||||||
|
'theme': 'dark',
|
||||||
|
'onsuccess': signinCallback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveFile saves the editor content into the S3 bucket
|
||||||
|
function saveFile(filename, content) {
|
||||||
|
let mime = getMimeType(filename)
|
||||||
|
let s3 = new AWS.S3()
|
||||||
|
s3.putObject({
|
||||||
|
ACL: window.past3_config.acl,
|
||||||
|
Body: content,
|
||||||
|
Bucket: window.past3_config.bucket,
|
||||||
|
ContentType: mime.mime,
|
||||||
|
Key: getFilePrefix() + filename,
|
||||||
|
}, fileActionCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setEditorMime updates the mime type of the file content loaded into the editor
|
||||||
|
function setEditorMime(filename) {
|
||||||
|
if (window.mime_detect) {
|
||||||
|
window.clearTimeout(window.mime_detect)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mime_detect = window.setTimeout(() => {
|
||||||
|
let autoMime = getMimeType(filename)
|
||||||
|
window.editor.setOption('mode', autoMime.mime)
|
||||||
|
CodeMirror.autoLoadMode(window.editor, autoMime.mode)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// signinCallback is triggered by the Google sign-in button
|
||||||
|
function signinCallback(authResult) {
|
||||||
|
if (authResult.Zi.id_token) {
|
||||||
|
getAWSCredentials(authResult.Zi.id_token)
|
||||||
|
|
||||||
|
window.past3_credential_refresh = window.setInterval(() => {
|
||||||
|
if (new Date(AWS.config.credentials.expireTime - 300000) < new Date()) {
|
||||||
|
refreshGoogleLogin()
|
||||||
|
}
|
||||||
|
}, 10000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize app on document ready
|
||||||
|
$(() => init())
|
3690
package-lock.json
generated
3690
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,9 +3,9 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"internal": true,
|
"internal": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.3",
|
"@babel/cli": "^7.1.0",
|
||||||
"babel-preset-env": "^1.7.0",
|
"@babel/core": "^7.1.0",
|
||||||
"babel-preset-minify": "^0.4.3",
|
"@babel/preset-env": "^7.1.0",
|
||||||
"coffeescript": "^2.3.1"
|
"babel-preset-minify": "^0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue