diff --git a/filadelfia_web/api/article/routes.mjs b/filadelfia_web/api/article/routes.mjs index 0fc3d2c..44b58e7 100644 --- a/filadelfia_web/api/article/routes.mjs +++ b/filadelfia_web/api/article/routes.mjs @@ -39,11 +39,12 @@ export default class ArticleRoutes extends OriginalArticleRoutes { ctx.body = { token: Client.createJwt({ iss: media.iss }, media.secret), path: config.get('media:directFilePath'), + resize: config.get('media:path'), } } /** PUT: /api/auth/articles/:id */ async updateCreateArticle(ctx) { - return this.private_getUpdateArticle(ctx, ctx.req.body, null, ctx.req.body.media) + return this.private_getUpdateArticle(ctx, ctx.req.body, ctx.req.body.banner, ctx.req.body.media) } } diff --git a/filadelfia_web/app/input.js b/filadelfia_web/app/input.js index 233c951..dd9b7cf 100644 --- a/filadelfia_web/app/input.js +++ b/filadelfia_web/app/input.js @@ -21,13 +21,37 @@ const Input = { this.tempus = null this.subscription = null this.input = null + this.preview = null }, onremove: function(vnode) { - if (!this.tempus) return if (this.subscription) this.subscription.unsubscribe() - this.tempus.dispose() - this.tempus = null + if (this.tempus) { + this.tempus.dispose() + this.tempus = null + } + if (this.preview) { + this.preview.clear() + } + }, + + imageChanged: function(vnode, e) { + let file = vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.files?.[0] || null + if (this.preview) { + this.preview.clear() + this.preview = null + } + if (!file) return + + if (file.type.startsWith('image')) { + this.preview = { + file: file, + preview: URL.createObjectURL(file), + clear: function() { + URL.revokeObjectURL(this.preview) + }, + } + } }, getInput: function(vnode) { @@ -67,6 +91,21 @@ const Input = { onclick: () => { this.tempus.toggle(); return false }, }) ]) + case 'image': + let imageLink = this.preview && this.preview.preview || vnode.attrs.form[vnode.attrs.formKey] + + return m('div.form-row.image-banner', { + style: { + 'background-image': typeof imageLink === 'string' ? 'url("' + (imageLink) + '")' : null, + }, + }, [ + m('input.cover', { + type: 'file', + accept: vnode.attrs.accept, + disabled: api.loading, + onchange: this.imageChanged.bind(this, vnode), + }), + ]) default: return m('input', { disabled: api.loading, diff --git a/filadelfia_web/app/lang.js b/filadelfia_web/app/lang.js index 934b1f0..9bebf0d 100644 --- a/filadelfia_web/app/lang.js +++ b/filadelfia_web/app/lang.js @@ -39,6 +39,8 @@ const i18n = { 'Dagsetning vantar'], upload_missing_file: ['Video file missing', 'Myndaskrá vantar'], + upload_missing_banner: ['Poster image missing', + 'Mynd vantar'], upload_error: ['Error while uploading: {0}', 'Villa við að hlaða upp myndefni: {0}'], unsplash: ['Photo by {0} on {1}', diff --git a/filadelfia_web/app/page_article.js b/filadelfia_web/app/page_article.js index a074a5f..db6aaea 100644 --- a/filadelfia_web/app/page_article.js +++ b/filadelfia_web/app/page_article.js @@ -1,6 +1,7 @@ const m = require('mithril') const api = require('./api') const Authentication = require('./authentication') +const Input = require('./input') const lang = require('./lang') const Article = { @@ -9,6 +10,15 @@ const Article = { this.error = '' this.path = '' this.data = null + this.editing = false + this.form = { + title: 'Sunnudagssamkoma', + date: new Date(), + banner: null, + metadata: { + speaker: '', + }, + } this.onbeforeupdate(vnode) }, @@ -30,16 +40,25 @@ const Article = { }) .then((result) => { this.data = result.article - this.afterData() + this.gotArticle(vnode) }, (err) => { this.error = err.message }) }, - afterData: function() { + gotArticle: function(vnode) { if (!this.data) { - this.error = 'Article not found' + return this.error = 'Article not found' } + this.form.title = this.data.name + this.form.date = new Date(this.data.publish_at) + this.form.banner = this.data.banner_path + this.form.metadata.speaker = this.data.content.speaker + }, + + updatevideo: function(vnode, e) { + this.error = '' + if (this.error) return false }, view: function(vnode) { @@ -58,7 +77,7 @@ const Article = { crossorigin: '', controls: true, preload: 'none', - poster: '/assets/placeholder.avif', + poster: this.data.banner_path || '/assets/placeholder.avif', }, [ m('source', { src: this.data.media_path @@ -67,6 +86,46 @@ const Article = { ]), ] : null, + this.editing + ? m('form.article', { + onsubmit: this.updatevideo.bind(this, vnode), + }, [ + m('div.form-row', [ + m('div.form-columns', [ + m(Input, { + label: 'Mynd', + type: 'file', + accept: 'image/*', + utility: 'image', + form: this.form, + formKey: 'banner', + }), + ]), + m('div.form-columns', [ + m(Input, { + label: 'Title', + form: this.form, + formKey: 'title', + }), + m(Input, { + label: 'Date (dd.mm.yyyy)', + type: 'text', + utility: 'datetime', + form: this.form, + formKey: 'date', + }), + ]), + ]), + m('p.separator', 'Optional'), + m(Input, { + label: 'Speaker', + form: this.form.metadata, + formKey: 'speaker', + }), + ]) + : m('div.article', [ + m('h1', this.data.name) + ]), ] }, } diff --git a/filadelfia_web/app/page_browse.js b/filadelfia_web/app/page_browse.js index 833f7e9..fc98133 100644 --- a/filadelfia_web/app/page_browse.js +++ b/filadelfia_web/app/page_browse.js @@ -11,7 +11,10 @@ const Browse = { mArticles: function(vnode, articles) { return articles.map(article => { - return m(m.route.Link, { href: ['', article.publish_at.getFullYear(), article.publish_at.getMonth() + 1, article.id].join('/') }, [ + return m(m.route.Link, { + href: ['', article.publish_at.getFullYear(), article.publish_at.getMonth() + 1, article.id].join('/'), + style: article.avif_preview ? `background-image: url('${article.avif_preview}')` : null, + }, [ m('span', article.publish_at.toUTCString()), m('span', article.name), ]) diff --git a/filadelfia_web/app/page_upload.js b/filadelfia_web/app/page_upload.js index f186cae..28a896a 100644 --- a/filadelfia_web/app/page_upload.js +++ b/filadelfia_web/app/page_upload.js @@ -16,12 +16,14 @@ const Upload = { d.setSeconds(0) d.setMilliseconds(0) - this.cache = null + this.cacheVideo = null + this.cacheImage = null this.uploading = null this.form = { title: 'Sunnudagssamkoma', date: d, file: null, + banner: null, metadata: { speaker: '', }, @@ -33,7 +35,8 @@ const Upload = { if (!this.form.title) this.error = lang.upload_missing_title // Title is missing if (!this.form.date) this.error = lang.upload_missing_date // Date is missing - if (!this.form.file) this.error = lang.upload_missing_file // Video file missing + // if (!this.form.file) this.error = lang.upload_missing_file // Video file missing + if (!this.form.banner) this.error = lang.upload_missing_banner // Video file missing if (this.error) return false @@ -43,8 +46,69 @@ const Upload = { body: this.form, }) .then(res => { - if (this.cache?.file === this.form.file) { - return this.cache + if (this.cacheImage?.file === this.form.banner) { + return this.cacheImage + } + + var data = new FormData() + data.append('file', this.form.banner) + data.append('preview', JSON.stringify({ + "out": "base64", + "format": "avif", + "resize": { + "width": 360, + "height": 203, + "fit": "cover", + "withoutEnlargement": true, + "kernel": "mitchell" + }, + "avif": { + "quality": 50, + "effort": 9 + } + })) + data.append('medium', JSON.stringify({ + "format": "avif", + "resize": { + "width": 1280, + "height": 720, + "fit": "cover", + "withoutEnlargement": true, + "kernel": "mitchell" + }, + "avif": { + "quality": 75, + "effort": 3 + } + })) + + return api.sendRequest({ + method: 'POST', + url: res.resize + '?token=' + res.token, + body: data, + }) + .then(banner => { + this.cacheImage = { + file: this.form.banner, + medium: { + filename: banner.medium.filename, + path: banner.medium.path, + }, + preview: { + base64: banner.preview.base64, + }, + } + api.sendRequest({ + method: 'DELETE', + url: res.resize.replace('resize', banner.filename) + '?token=' + res.token, + }).catch(err => console.log(err)) + + return res + }) + }) + .then(res => { + if (this.cacheVideo?.file === this.form.file) { + return this.cacheVideo } return api.uploadFileProgress({ @@ -59,7 +123,7 @@ const Upload = { }) }) .then(res => { - this.cache = { + this.cacheVideo = { file: this.form.file, filename: res.filename, path: res.path, @@ -79,10 +143,19 @@ const Upload = { is_featured: false, media: { filename: res.filename, - type: this.form.file.type, path: res.path, + type: this.form.file.type, size: this.form.file.size, - } + }, + banner: { + filename: this.cacheImage.medium.filename, + path: this.cacheImage.medium.path, + type: 'image/avif', + size: this.form.file.size, + preview: { + base64: this.cacheImage.preview.base64, + }, + }, }, }) }) @@ -138,6 +211,14 @@ const Upload = { form: this.form, formKey: 'file', }), + m(Input, { + label: 'Mynd', + type: 'file', + accept: 'image/*', + utility: 'image', + form: this.form, + formKey: 'banner', + }), m('p.separator', 'Optional'), m(Input, { label: 'Speaker', diff --git a/filadelfia_web/public/index.html b/filadelfia_web/public/index.html index 22b5186..c7e8fdb 100644 --- a/filadelfia_web/public/index.html +++ b/filadelfia_web/public/index.html @@ -79,104 +79,6 @@ a, a:visited, button { background: transparent; } - -input[type=text], -input[type=password], -input[type=datetime] { - border: 1px solid var(--main); - background: #fff; - color: var(--color); - border-radius: 0; - padding: 0.25rem; - line-height: 1rem; - outline: none; - width: 100%; -} - -input[type=text]:disabled, -input[type=password]:disabled, -input[type=datetime]:disabled { - background: var(--bg-component); - border-color: var(--color-alt); - color: var(--color-alt); -} - -.form-row input:disabled + button { - border-color: var(--color-alt); -} - -.form-row { - display: flex; - position: relative; -} - -.form-row input { - flex: 2 1 auto; -} - -.form-row button { - min-width: 30px; - text-align: center; - border: 1px solid var(--main); - border-left: none; - background: var(--bg-component); - text-decoration: none; -} - -.form-row .cover { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; - cursor: pointer; -} - -.button, input[type=submit] { - background: var(--main); - color:var(--main-fg); - border-radius: 10px; - padding: 0.25rem 1rem; - border: none; - margin: 1rem 0 2rem; - align-self: center; - cursor: pointer; - text-decoration: none; -} - -.button.spinner, input[type=submit].spinner { - height: 2rem; - margin-top: 2rem; - margin-bottom: 1.5rem; -} - -.button-alert { - background: var(--error-bg); - color: var(--color); -} - -.loading-bar { - border: 1px solid var(--main); - background: var(--bg-component); -} - -.loading-bar::after { - height: 1rem; - background: var(--main); - min-width: 1px; - content: ''; - display: block; - width: var(--progress); - transition: width 3s; -} - -input[type=text]:focus, -input[type=password]:focus, -input[type=datetime]:focus { - outline: 1px solid var(--main); -} - h1 { margin-bottom: 1rem; } @@ -206,20 +108,6 @@ h1 { margin-bottom: 1rem; } -form p, label { - font-size: 0.75rem; - font-weight: 500; - margin: 0.75rem 0 0.5rem 0; - display: block; -} - -form p.separator { - color: var(--color-alt); - margin-top: 1.5rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--color-alt); -} - .error { color: var(--error); } @@ -266,6 +154,7 @@ form p.separator { background-repeat: no-repeat; background-position: center; background-size: cover; + background-image: url('./assets/bg_admin.avif'); } .page-upload footer { @@ -277,11 +166,133 @@ form p.separator { margin-bottom: 1rem; } -.avifsupport .page-upload { - background-image: url('./assets/bg_admin.avif'); +/* Common components */ + +input[type=text], +input[type=password], +input[type=datetime] { + border: 1px solid var(--main); + background: #fff; + color: var(--color); + border-radius: 0; + padding: 0.25rem; + line-height: 1rem; + outline: none; + width: 100%; } -.jpegonly .page-upload { - background-image: url('./assets/bg_admin.jpg'); + +input[type=text]:disabled, +input[type=password]:disabled, +input[type=datetime]:disabled { + background: var(--bg-component); + border-color: var(--color-alt); + color: var(--color-alt); +} + +.form-row input:disabled + button { + border-color: var(--color-alt); +} + +.form-row { + display: flex; + position: relative; +} + +.form-row input { + flex: 2 1 auto; +} + +.form-row .form-column { + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.form-row button { + min-width: 30px; + text-align: center; + border: 1px solid var(--main); + border-left: none; + background: var(--bg-component); + text-decoration: none; +} + +.form-row.image-banner { + background-size: cover; + background-color: white; + border: 2px dashed var(--main); + aspect-ratio: 16 / 9; + align-self: center; + width: 100%; + max-width: 360px; +} + +.form-row .cover { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} + +input[type=text]:focus, +input[type=password]:focus, +input[type=datetime]:focus { + outline: 1px solid var(--main); +} + +.button, input[type=submit] { + background: var(--main); + color:var(--main-fg); + border-radius: 10px; + padding: 0.25rem 1rem; + border: none; + margin: 1rem 0 2rem; + align-self: center; + cursor: pointer; + text-decoration: none; +} + +.button.spinner, input[type=submit].spinner { + height: 2rem; + margin-top: 2rem; + margin-bottom: 1.5rem; +} + +.button-alert { + background: var(--error-bg); + color: var(--color); +} + +.loading-bar { + border: 1px solid var(--main); + background: var(--bg-component); +} + +.loading-bar::after { + height: 1rem; + background: var(--main); + min-width: 1px; + content: ''; + display: block; + width: var(--progress); + transition: width 3s; +} + +form p, label { + font-size: 0.75rem; + font-weight: 500; + margin: 0.75rem 0 0.5rem 0; + display: block; +} + +form p.separator { + color: var(--color-alt); + margin-top: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-alt); } /* Nav */ @@ -463,13 +474,15 @@ footer a { justify-content: flex-end; background: url('./assets/placeholder.avif') center no-repeat; background-size: cover; - padding: 1rem; margin: 0 1rem 1rem; text-align: center; border: 1px solid var(--main); - text-shadow: 2px 0 10px #fffc, -2px 0 10px #fffc, 0 2px 10px #fffc, 0 -2px 10px #fffc, - 1px 1px 10px #fffc, -1px -1px 10px #fffc, 1px -1px 10px #fffc, -1px 1px 10px #fffc; - +} + +.gallery .group a span { + align-self: stretch; + text-align: center; + background: #fffb; } /* Player */