Compare commits
No commits in common. "fad7acd5f7b7d73cb10e69f12b876cb2e8b7cccd" and "857a087410064d79c4dd9ceea8c5a264aac3d687" have entirely different histories.
fad7acd5f7
...
857a087410
29 changed files with 157 additions and 872 deletions
|
@ -77,7 +77,6 @@ export default class ArticleRoutes {
|
||||||
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
|
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
|
||||||
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
|
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
|
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
|
||||||
|
|
||||||
ctx.body = this.private_getUpdateArticle_resOutput(res)
|
ctx.body = this.private_getUpdateArticle_resOutput(res)
|
||||||
|
|
|
@ -53,7 +53,6 @@ nconf.defaults({
|
||||||
"iss": "dev",
|
"iss": "dev",
|
||||||
"path": "https://media.nfp.is/media/resize",
|
"path": "https://media.nfp.is/media/resize",
|
||||||
"filePath": "https://media.nfp.is/media",
|
"filePath": "https://media.nfp.is/media",
|
||||||
"directFilePath": "https://media.nfp.is/media/noprefix",
|
|
||||||
"removePath": "https://media.nfp.is/media/",
|
"removePath": "https://media.nfp.is/media/",
|
||||||
"preview": {
|
"preview": {
|
||||||
"out": "base64",
|
"out": "base64",
|
||||||
|
|
|
@ -15,6 +15,8 @@ export function uploadMedia(file) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(media)
|
||||||
|
|
||||||
let body = {}
|
let body = {}
|
||||||
|
|
||||||
if (media.preview) {
|
if (media.preview) {
|
||||||
|
|
|
@ -5,9 +5,9 @@ export function mediaToDatabase(media, removeFlag) {
|
||||||
media.type,
|
media.type,
|
||||||
media.path,
|
media.path,
|
||||||
media.size,
|
media.size,
|
||||||
media.preview?.base64 || null,
|
media.preview.base64,
|
||||||
media.sizes?.small?.avif?.path?.replace(/_small\.avif$/, '') || null,
|
media.sizes.small.avif.path.replace(/_small\.avif$/, ''),
|
||||||
JSON.stringify(media.sizes || {}),
|
JSON.stringify(media.sizes),
|
||||||
removeFlag ? 1 : 0,
|
removeFlag ? 1 : 0,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import config from '../../base/config.mjs'
|
|
||||||
import Client from '../../base/media/client.mjs'
|
|
||||||
import OriginalArticleRoutes from '../../base/article/routes.mjs'
|
|
||||||
import { deleteFile, uploadFile } from '../../base/media/upload.mjs'
|
|
||||||
import { parseVideos, parseVideo } from './util.mjs'
|
|
||||||
|
|
||||||
export default class ArticleRoutes extends OriginalArticleRoutes {
|
|
||||||
constructor(opts = {}) {
|
|
||||||
opts.requireAuth = true
|
|
||||||
super(opts)
|
|
||||||
|
|
||||||
Object.assign(this, {
|
|
||||||
uploadFile: uploadFile,
|
|
||||||
deleteFile: deleteFile,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
register(server) {
|
|
||||||
server.flaska.get('/api/auth/articles', server.authenticate(), this.getVideos.bind(this))
|
|
||||||
server.flaska.put('/api/auth/articles/:id', [server.authenticate(), server.jsonHandler()], this.updateCreateArticle.bind(this))
|
|
||||||
server.flaska.get('/api/auth/articles/:id', server.authenticate(), this.auth_getSingleArticle.bind(this))
|
|
||||||
server.flaska.get('/api/auth/uploadToken', server.authenticate(), this.getUploadToken.bind(this))
|
|
||||||
server.flaska.delete('/api/auth/articles/:id', server.authenticate(), this.auth_removeSingleArticle.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET: /api/auth/articles */
|
|
||||||
async getVideos(ctx) {
|
|
||||||
let res = await ctx.db.safeCallProc('filadelfia_archive.article_auth_get_all', [ctx.state.auth_token])
|
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
videos: parseVideos(res.results[0]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET: /api/auth/uploadToken */
|
|
||||||
async getUploadToken(ctx) {
|
|
||||||
const media = config.get('media')
|
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
token: Client.createJwt({ iss: media.iss }, media.secret),
|
|
||||||
path: config.get('media:directFilePath'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PUT: /api/auth/articles/:id */
|
|
||||||
async updateCreateArticle(ctx) {
|
|
||||||
return this.private_getUpdateArticle(ctx, ctx.req.body, null, ctx.req.body.media)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,19 +3,19 @@ import Parent from '../base/server.mjs'
|
||||||
import StaticRoutes from '../base/static_routes.mjs'
|
import StaticRoutes from '../base/static_routes.mjs'
|
||||||
import ServeHandler from './serve.mjs'
|
import ServeHandler from './serve.mjs'
|
||||||
import AuthenticationRoutes from '../base/authentication/routes.mjs'
|
import AuthenticationRoutes from '../base/authentication/routes.mjs'
|
||||||
import ArticleRoutes from './article/routes.mjs'
|
import VideoRoutes from './video/routes.mjs'
|
||||||
|
|
||||||
export default class Server extends Parent {
|
export default class Server extends Parent {
|
||||||
init() {
|
init() {
|
||||||
super.init()
|
super.init()
|
||||||
let localUtil = new this.core.sc.Util(import.meta.url)
|
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||||
|
|
||||||
this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; connect-src 'self' https://media.nfp.is/; media-src 'self' https://cdn.nfp.is/`,
|
delete this.flaskaOptions.appendHeaders['Content-Security-Policy']
|
||||||
this.flaskaOptions.nonce = []
|
this.flaskaOptions.nonce = []
|
||||||
this.routes = {
|
this.routes = {
|
||||||
static: new StaticRoutes(),
|
static: new StaticRoutes(),
|
||||||
auth: new AuthenticationRoutes(),
|
auth: new AuthenticationRoutes(),
|
||||||
article: new ArticleRoutes(),
|
videos: new VideoRoutes(),
|
||||||
}
|
}
|
||||||
this.routes.serve = new ServeHandler({
|
this.routes.serve = new ServeHandler({
|
||||||
root: localUtil.getPathFromRoot('../public'),
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
|
36
filadelfia_web/api/video/routes.mjs
Normal file
36
filadelfia_web/api/video/routes.mjs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import config from '../../base/config.mjs'
|
||||||
|
import Client from '../../base/media/client.mjs'
|
||||||
|
import { deleteFile, uploadFile } from '../../base/media/upload.mjs'
|
||||||
|
import { parseVideos, parseVideo } from './util.mjs'
|
||||||
|
|
||||||
|
export default class VideoRoutes {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, {
|
||||||
|
uploadFile: uploadFile,
|
||||||
|
deleteFile: deleteFile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/api/videos', server.authenticate(), this.getVideos.bind(this))
|
||||||
|
server.flaska.get('/api/videos/uploadToken', server.authenticate(), this.getUploadToken.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET: /api/videos */
|
||||||
|
async getVideos(ctx) {
|
||||||
|
console.log(ctx.state.auth_token)
|
||||||
|
let res = await ctx.db.safeCallProc('filadelfia_archive.videos_auth_get_all', [ctx.state.auth_token])
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
videos: parseVideos(res.results[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadToken(ctx) {
|
||||||
|
const media = config.get('media')
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: Client.createJwt({ iss: media.iss }, media.secret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,5 @@ export function parseVideo(video) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
parseMediaAndBanner(video)
|
parseMediaAndBanner(video)
|
||||||
video.metadata = JSON.parse(video.metadata || '{}')
|
|
||||||
delete video.banner_alt_prefix
|
|
||||||
delete video.media_alt_prefix
|
|
||||||
return video
|
return video
|
||||||
}
|
}
|
|
@ -1,25 +1,4 @@
|
||||||
const Authentication = require('./authentication')
|
const Authentication = require('./authentication')
|
||||||
const lang = require('./lang')
|
|
||||||
|
|
||||||
function safeParseReponse(str, status, url) {
|
|
||||||
if (status === 0) {
|
|
||||||
return new Error(lang.api_down)
|
|
||||||
}
|
|
||||||
if (str.slice(0, 9) === '<!doctype') {
|
|
||||||
if (status === 500) {
|
|
||||||
return new Error('Server is temporarily down, try again later.')
|
|
||||||
}
|
|
||||||
return new Error('Expected JSON but got HTML (' + status + ': ' + url.split('?')[0] + ')')
|
|
||||||
}
|
|
||||||
if (!str) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(str)
|
|
||||||
} catch (err) {
|
|
||||||
return new Error('Unexpected non-JSON response: ' + err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -34,10 +13,31 @@ const api = {
|
||||||
}
|
}
|
||||||
|
|
||||||
options.extract = function(xhr) {
|
options.extract = function(xhr) {
|
||||||
let out = safeParseReponse(xhr.responseText, xhr.status, this.url)
|
if (xhr.responseText && xhr.responseText.slice(0, 9) === '<!doctype') {
|
||||||
|
if (xhr.status === 500) {
|
||||||
|
throw new Error('Server is temporarily down, try again later.')
|
||||||
|
}
|
||||||
|
throw new Error('Expected JSON but got HTML (' + xhr.status + ': ' + this.url.split('?')[0] + ')')
|
||||||
|
}
|
||||||
|
let out = null
|
||||||
|
if (pagination && xhr.status < 300) {
|
||||||
|
let headers = {}
|
||||||
|
|
||||||
if (out instanceof Error) {
|
xhr.getAllResponseHeaders().split('\r\n').forEach(function(item) {
|
||||||
throw out
|
var splitted = item.split(': ')
|
||||||
|
headers[splitted[0]] = splitted[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
out = {
|
||||||
|
headers: headers || {},
|
||||||
|
data: JSON.parse(xhr.responseText),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (xhr.responseText) {
|
||||||
|
out = JSON.parse(xhr.responseText)
|
||||||
|
} else {
|
||||||
|
out = {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (xhr.status >= 300) {
|
if (xhr.status >= 300) {
|
||||||
if (out.message) {
|
if (out.message) {
|
||||||
|
@ -46,19 +46,6 @@ const api = {
|
||||||
console.error('Got error ' + xhr.status + ' but no error message:', out)
|
console.error('Got error ' + xhr.status + ' but no error message:', out)
|
||||||
throw new Error('Unknown or empty response from server.')
|
throw new Error('Unknown or empty response from server.')
|
||||||
}
|
}
|
||||||
if (pagination) {
|
|
||||||
let headers = {}
|
|
||||||
|
|
||||||
xhr.getAllResponseHeaders().split('\r\n').forEach(function(item) {
|
|
||||||
let splitted = item.split(': ')
|
|
||||||
headers[splitted[0]] = splitted[1]
|
|
||||||
})
|
|
||||||
|
|
||||||
out = {
|
|
||||||
headers: headers || {},
|
|
||||||
data: out,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,84 +67,7 @@ const api = {
|
||||||
}
|
}
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
|
|
||||||
prettyFormatBytes: function(bytes) {
|
|
||||||
if (bytes < 1024) {
|
|
||||||
return `${bytes} B`
|
|
||||||
}
|
}
|
||||||
if (bytes < 1024 * 1024) {
|
|
||||||
return `${(bytes / 1024).toFixed(1)} KB`
|
|
||||||
}
|
|
||||||
if (bytes < 1024 * 1024 * 1024) {
|
|
||||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadFileProgress: function(options, file, reporter) {
|
|
||||||
api.loading = true
|
|
||||||
|
|
||||||
return new Promise(function(res, rej) {
|
|
||||||
let report = reporter || function() {}
|
|
||||||
let formdata = new FormData()
|
|
||||||
|
|
||||||
formdata.append('file', file)
|
|
||||||
|
|
||||||
let request = new XMLHttpRequest()
|
|
||||||
|
|
||||||
let finished = false
|
|
||||||
let lastMarker = new Date()
|
|
||||||
let lastMarkerLoaded = 0
|
|
||||||
let lastMarkerSpeed = '...'
|
|
||||||
|
|
||||||
request.abortRequest = function() {
|
|
||||||
finished = true
|
|
||||||
request.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
request.upload.addEventListener('progress', function (e) {
|
|
||||||
let check = new Date()
|
|
||||||
if (check - lastMarker >= 1000) {
|
|
||||||
let loaded = e.loaded - lastMarkerLoaded
|
|
||||||
lastMarkerSpeed = api.prettyFormatBytes(loaded / ((check - lastMarker) / 1000))
|
|
||||||
lastMarker = check
|
|
||||||
lastMarkerLoaded = e.loaded
|
|
||||||
}
|
|
||||||
report(request, Math.min(e.loaded / file.size * 100, 100), lastMarkerSpeed)
|
|
||||||
})
|
|
||||||
request.addEventListener('abort', function(e) {
|
|
||||||
finished = true
|
|
||||||
window.requestAnimationFrame(m.redraw.bind(m))
|
|
||||||
rej()
|
|
||||||
})
|
|
||||||
|
|
||||||
request.addEventListener('readystatechange', function(e) {
|
|
||||||
if (finished) return
|
|
||||||
if (request.readyState !== 4) return
|
|
||||||
|
|
||||||
finished = true
|
|
||||||
let out = safeParseReponse(request.responseText, request.status, options.url)
|
|
||||||
if (out instanceof Error || request.status >= 300) {
|
|
||||||
return rej(out)
|
|
||||||
}
|
|
||||||
return res(out)
|
|
||||||
})
|
|
||||||
|
|
||||||
request.open(options.method || 'POST', options.url)
|
|
||||||
request.send(formdata)
|
|
||||||
|
|
||||||
report(request, 0)
|
|
||||||
})
|
|
||||||
.then(function(res) {
|
|
||||||
api.loading = false
|
|
||||||
window.requestAnimationFrame(m.redraw.bind(m))
|
|
||||||
return res
|
|
||||||
}, function(err) {
|
|
||||||
api.loading = false
|
|
||||||
window.requestAnimationFrame(m.redraw.bind(m))
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = api
|
module.exports = api
|
||||||
|
|
|
@ -35,17 +35,6 @@ const Authentication = {
|
||||||
return localStorage.getItem(storageName)
|
return localStorage.getItem(storageName)
|
||||||
},
|
},
|
||||||
|
|
||||||
getTokenDecoded: function() {
|
|
||||||
let token = Authentication.getToken()
|
|
||||||
var base64Url = token.split('.')[1];
|
|
||||||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
|
|
||||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
|
||||||
}).join(''));
|
|
||||||
|
|
||||||
return JSON.parse(jsonPayload);
|
|
||||||
},
|
|
||||||
|
|
||||||
requiresLogin: function() {
|
requiresLogin: function() {
|
||||||
if (Authentication.currentUser) return
|
if (Authentication.currentUser) return
|
||||||
m.route.set('/')
|
m.route.set('/')
|
||||||
|
|
|
@ -7,17 +7,15 @@ const Menu = {
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
this.currentActive = 'home'
|
this.currentActive = 'home'
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.browsing = true
|
|
||||||
if (!videos.Tree.length) {
|
|
||||||
videos.refreshTree()
|
|
||||||
}
|
|
||||||
this.onbeforeupdate()
|
this.onbeforeupdate()
|
||||||
},
|
},
|
||||||
|
|
||||||
onbeforeupdate: function() {
|
onbeforeupdate: function() {
|
||||||
videos.calculateActiveBranches()
|
|
||||||
let currentPath = m.route.get()
|
let currentPath = m.route.get()
|
||||||
this.browsing = currentPath === '/' || currentPath === '/browse' || videos.year
|
|
||||||
|
/*if (currentPath === '/') this.currentActive = 'home'
|
||||||
|
else if (currentPath === '/login') this.currentActive = 'login'
|
||||||
|
else if (currentPath && currentPath.startsWith('/page')) this.currentActive = currentPath.slice(currentPath.lastIndexOf('/') + 1)*/
|
||||||
},
|
},
|
||||||
|
|
||||||
logOut: function() {
|
logOut: function() {
|
||||||
|
@ -26,10 +24,6 @@ const Menu = {
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function() {
|
view: function() {
|
||||||
let tree = videos.Tree
|
|
||||||
let last = videos.Tree[videos.Tree.length - 1]
|
|
||||||
let hasId = m.route.param('id')
|
|
||||||
|
|
||||||
return Authentication.currentUser
|
return Authentication.currentUser
|
||||||
? [
|
? [
|
||||||
m('nav', [
|
m('nav', [
|
||||||
|
@ -38,33 +32,8 @@ const Menu = {
|
||||||
Authentication.currentUser.rank > 10
|
Authentication.currentUser.rank > 10
|
||||||
? m(m.route.Link, { class: 'upload', href: '/upload' }, lang.upload_goto) // Upload
|
? m(m.route.Link, { class: 'upload', href: '/upload' }, lang.upload_goto) // Upload
|
||||||
: null,
|
: null,
|
||||||
// m('button.logout', { onclick: this.logOut }, lang.header_logout), // Log out
|
m('button.logout', { onclick: this.logOut }, lang.header_logout), // Log out
|
||||||
]),
|
|
||||||
!this.browsing && videos.error
|
|
||||||
? m('div.error', { onclick: videos.refreshTree }, [
|
|
||||||
videos.error, m('br'), 'Click here to try again'
|
|
||||||
])
|
])
|
||||||
: null,
|
|
||||||
this.browsing && !videos.error
|
|
||||||
? [
|
|
||||||
m('.nav', m('.inner', tree.map(year => {
|
|
||||||
return m(m.route.Link, {
|
|
||||||
class: [videos.year === year ? 'active' : '',
|
|
||||||
!year.videos.length ? 'empty' : ''].join(' '),
|
|
||||||
href: ['', (videos.year !== year && year !== last) || hasId ? year.title : 'browse' ].filter(x => x).join('/'),
|
|
||||||
}, year.title)
|
|
||||||
}))),
|
|
||||||
videos.year
|
|
||||||
? m('.nav', m('.inner', videos.year.branches.map(month => {
|
|
||||||
return m(m.route.Link, {
|
|
||||||
class: [videos.month === month ? 'active' : '',
|
|
||||||
!month.videos.length ? 'empty' : ''].join(' '),
|
|
||||||
href: ['', videos.year.title, videos.month !== month || hasId ? month.title : null ].filter(x => x).join('/'),
|
|
||||||
}, lang.months[month.title])
|
|
||||||
})))
|
|
||||||
: null,
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
]
|
]
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,6 @@ const Header = require('./header')
|
||||||
const Login = require('./page_login')
|
const Login = require('./page_login')
|
||||||
const Browse = require('./page_browse')
|
const Browse = require('./page_browse')
|
||||||
const Upload = require('./page_upload')
|
const Upload = require('./page_upload')
|
||||||
const Article = require('./page_article')
|
|
||||||
window.m = m
|
window.m = m
|
||||||
|
|
||||||
let css = [
|
let css = [
|
||||||
|
@ -39,9 +38,6 @@ const allRoutes = {
|
||||||
'/': Login,
|
'/': Login,
|
||||||
'/browse': Browse,
|
'/browse': Browse,
|
||||||
'/upload': Upload,
|
'/upload': Upload,
|
||||||
'/:year': Browse,
|
|
||||||
'/:year/:month': Browse,
|
|
||||||
'/:year/:month/:id': Article,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.
|
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.
|
||||||
|
|
|
@ -1,8 +1,29 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
const api = require('./api')
|
const popper = require('@popperjs/core')
|
||||||
const tempus = require('@eonasdan/tempus-dominus')
|
const tempus = require('@eonasdan/tempus-dominus')
|
||||||
|
|
||||||
const tempusLocalization = {
|
const Input = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.tempus = null
|
||||||
|
this.subscription = null
|
||||||
|
},
|
||||||
|
|
||||||
|
onremove: function(vnode) {
|
||||||
|
if (!this.tempus) return
|
||||||
|
this.tempus.dispose()
|
||||||
|
this.tempus = null
|
||||||
|
},
|
||||||
|
|
||||||
|
getInput: function(vnode) {
|
||||||
|
switch (vnode.attrs.utility) {
|
||||||
|
case 'datetime':
|
||||||
|
return m('input', {
|
||||||
|
type: 'text',
|
||||||
|
oncreate: (e) => {
|
||||||
|
this.tempus = new tempus.TempusDominus(e.dom, {
|
||||||
|
defaultDate: vnode.attrs.form[vnode.attrs.formKey],
|
||||||
|
viewDate: vnode.attrs.form[vnode.attrs.formKey],
|
||||||
|
localization: {
|
||||||
locale: 'is',
|
locale: 'is',
|
||||||
startOfTheWeek: 0,
|
startOfTheWeek: 0,
|
||||||
hourCycle: 'h23',
|
hourCycle: 'h23',
|
||||||
|
@ -14,62 +35,15 @@ const tempusLocalization = {
|
||||||
LLL: 'd [de] MMMM [de] yyyy H:mm',
|
LLL: 'd [de] MMMM [de] yyyy H:mm',
|
||||||
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
|
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
const Input = {
|
|
||||||
oninit: function(vnode) {
|
|
||||||
this.tempus = null
|
|
||||||
this.subscription = null
|
|
||||||
this.input = null
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onremove: function(vnode) {
|
|
||||||
if (!this.tempus) return
|
|
||||||
if (this.subscription) this.subscription.unsubscribe()
|
|
||||||
this.tempus.dispose()
|
|
||||||
this.tempus = null
|
|
||||||
},
|
|
||||||
|
|
||||||
getInput: function(vnode) {
|
|
||||||
switch (vnode.attrs.utility) {
|
|
||||||
case 'file':
|
|
||||||
return m('div.form-row', [
|
|
||||||
m('input', {
|
|
||||||
type: 'text',
|
|
||||||
disabled: api.loading,
|
|
||||||
value: vnode.attrs.form[vnode.attrs.formKey]?.name || '',
|
|
||||||
}),
|
|
||||||
m('button.fal', { class: vnode.attrs.button || 'file' }),
|
|
||||||
m('input.cover', {
|
|
||||||
type: 'file',
|
|
||||||
accept: vnode.attrs.accept,
|
|
||||||
disabled: api.loading,
|
|
||||||
oninput: (e) => { vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.files?.[0] || null },
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
case 'datetime':
|
|
||||||
return m('div.form-row', [
|
|
||||||
m('input', {
|
|
||||||
type: 'text',
|
|
||||||
disabled: api.loading,
|
|
||||||
oncreate: (e) => {
|
|
||||||
this.tempus = new tempus.TempusDominus(e.dom, {
|
|
||||||
defaultDate: vnode.attrs.form[vnode.attrs.formKey],
|
|
||||||
viewDate: vnode.attrs.form[vnode.attrs.formKey],
|
|
||||||
localization: tempusLocalization,
|
|
||||||
})
|
})
|
||||||
this.subscription = this.tempus.subscribe(tempus.Namespace.events.change, (e) => {
|
this.subscription = this.tempus.subscribe(tempus.Namespace.events.change, (e) => {
|
||||||
vnode.attrs.form[vnode.attrs.formKey] = e.date
|
console.log(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
m('button.fal.fa-calendar', {
|
|
||||||
onclick: () => { this.tempus.toggle(); return false },
|
|
||||||
})
|
})
|
||||||
])
|
|
||||||
default:
|
default:
|
||||||
return m('input', {
|
return m('input', {
|
||||||
disabled: api.loading,
|
|
||||||
type: vnode.attrs.type || 'text',
|
type: vnode.attrs.type || 'text',
|
||||||
value: vnode.attrs.form[vnode.attrs.formKey],
|
value: vnode.attrs.form[vnode.attrs.formKey],
|
||||||
oninput: (e) => { vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.value },
|
oninput: (e) => { vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.value },
|
||||||
|
|
|
@ -33,44 +33,8 @@ const i18n = {
|
||||||
'Lykilorð'],
|
'Lykilorð'],
|
||||||
login_submit: ['Log in',
|
login_submit: ['Log in',
|
||||||
'Skrá inn'],
|
'Skrá inn'],
|
||||||
upload_missing_title: ['Title is missing',
|
login_footer: ['Photo by {0} on {1}',
|
||||||
'Titill vantar'],
|
|
||||||
upload_missing_date: ['Date is missing',
|
|
||||||
'Dagsetning vantar'],
|
|
||||||
upload_missing_file: ['Video file missing',
|
|
||||||
'Myndaskrá vantar'],
|
|
||||||
upload_error: ['Error while uploading: {0}',
|
|
||||||
'Villa við að hlaða upp myndefni: {0}'],
|
|
||||||
unsplash: ['Photo by {0} on {1}',
|
|
||||||
'Mynd eftir {0} frá {1}'],
|
'Mynd eftir {0} frá {1}'],
|
||||||
api_down: ['No internet or browser blocked the request.',
|
|
||||||
'Ekkert net eða vafri blockaði fyrirspurn.'],
|
|
||||||
months: {
|
|
||||||
'1': ['January',
|
|
||||||
'Janúar'],
|
|
||||||
'2': ['February',
|
|
||||||
'Febrúar'],
|
|
||||||
'3': ['March',
|
|
||||||
'Mars'],
|
|
||||||
'4': ['April',
|
|
||||||
'Apríl'],
|
|
||||||
'5': ['May',
|
|
||||||
'Maí'],
|
|
||||||
'6': ['June',
|
|
||||||
'Júní'],
|
|
||||||
'7': ['July',
|
|
||||||
'Júlí'],
|
|
||||||
'8': ['August',
|
|
||||||
'Ágúst'],
|
|
||||||
'9': ['September',
|
|
||||||
'September'],
|
|
||||||
'10': ['Oktober',
|
|
||||||
'Október'],
|
|
||||||
'11': ['November',
|
|
||||||
'Nóvember'],
|
|
||||||
'12': ['December',
|
|
||||||
'Desember'],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const langs = {
|
const langs = {
|
||||||
'en': 0,
|
'en': 0,
|
||||||
|
@ -82,17 +46,10 @@ const regexNumber = new RegExp('^\\d+$')
|
||||||
out.langset = function(lang) {
|
out.langset = function(lang) {
|
||||||
out.currentlang = lang
|
out.currentlang = lang
|
||||||
let index = langs[lang]
|
let index = langs[lang]
|
||||||
|
let keys = Object.keys(i18n)
|
||||||
for (let key of Object.keys(i18n)) {
|
for (let key of keys) {
|
||||||
if (!Array.isArray(i18n[key])) {
|
|
||||||
out[key] = {}
|
|
||||||
for (let subKey of Object.keys(i18n[key])) {
|
|
||||||
out[key][subKey] = i18n[key][subKey][index]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out[key] = i18n[key][index]
|
out[key] = i18n[key][index]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.langtoggle = function() {
|
out.langtoggle = function() {
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
const m = require('mithril')
|
|
||||||
const api = require('./api')
|
|
||||||
const Authentication = require('./authentication')
|
|
||||||
const lang = require('./lang')
|
|
||||||
|
|
||||||
const Article = {
|
|
||||||
oninit: function(vnode) {
|
|
||||||
Authentication.requiresLogin()
|
|
||||||
this.error = ''
|
|
||||||
this.path = ''
|
|
||||||
this.data = null
|
|
||||||
this.onbeforeupdate(vnode)
|
|
||||||
},
|
|
||||||
|
|
||||||
onbeforeupdate: function(vnode) {
|
|
||||||
let path = m.route.param('id')
|
|
||||||
if (this.path === path) return
|
|
||||||
|
|
||||||
this.fetchArticle(vnode, path)
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchArticle: function(vnode, path) {
|
|
||||||
this.error = ''
|
|
||||||
this.data = null
|
|
||||||
this.path = path
|
|
||||||
|
|
||||||
api.sendRequest({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/auth/articles/' + this.path,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
this.data = result.article
|
|
||||||
this.afterData()
|
|
||||||
}, (err) => {
|
|
||||||
this.error = err.message
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
afterData: function() {
|
|
||||||
if (!this.data) {
|
|
||||||
this.error = 'Article not found'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
view: function(vnode) {
|
|
||||||
console.log(this.data)
|
|
||||||
return [
|
|
||||||
api.loading ? m('div.loading-spinner') : null,
|
|
||||||
this.error
|
|
||||||
? m('div.full-error', { onclick: this.fetchArticle.bind(this, vnode, this.path) }, [
|
|
||||||
this.error, m('br'), 'Click here to try again'
|
|
||||||
])
|
|
||||||
: null,
|
|
||||||
this.data?.media_path
|
|
||||||
? [
|
|
||||||
m('.player', [
|
|
||||||
m('video', {
|
|
||||||
crossorigin: '',
|
|
||||||
controls: true,
|
|
||||||
preload: 'none',
|
|
||||||
poster: '/assets/placeholder.avif',
|
|
||||||
}, [
|
|
||||||
m('source', {
|
|
||||||
src: this.data.media_path
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Article
|
|
|
@ -1,58 +1,26 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
const api = require('./api')
|
|
||||||
const Authentication = require('./authentication')
|
const Authentication = require('./authentication')
|
||||||
const videos = require('./videos')
|
const videos = require('./videos')
|
||||||
const lang = require('./lang')
|
|
||||||
|
|
||||||
const Browse = {
|
const Browse = {
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
Authentication.requiresLogin()
|
Authentication.requiresLogin()
|
||||||
|
if (!videos.Tree.length) {
|
||||||
|
this.refreshTree()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mArticles: function(vnode, articles) {
|
refreshTree: function(vnode) {
|
||||||
return articles.map(article => {
|
videos.refreshTree()
|
||||||
return m(m.route.Link, { href: ['', article.publish_at.getFullYear(), article.publish_at.getMonth() + 1, article.id].join('/') }, [
|
|
||||||
m('span', article.publish_at.toUTCString()),
|
|
||||||
m('span', article.name),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
mMonth: function(vnode, year) {
|
|
||||||
return year.branches.map(month => {
|
|
||||||
return [
|
|
||||||
m('.gallery-month', lang.months[month.title]),
|
|
||||||
m('.group', this.mArticles(vnode, month.videos))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
let articles = videos.month?.videos || videos.year?.videos || videos.Articles
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
api.loading ? m('div.loading-spinner') : null,
|
|
||||||
videos.error
|
videos.error
|
||||||
? m('div.full-error', { onclick: videos.refreshTree }, [
|
? m('div.full-error', { onclick: this.refreshTree.bind(this) }, [
|
||||||
videos.error, m('br'), 'Click here to try again'
|
videos.error, m('br'), 'Click here to try again'
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
m('.gallery', [
|
|
||||||
videos.month
|
|
||||||
? m('.group', this.mArticles(vnode, articles))
|
|
||||||
: null,
|
|
||||||
videos.year && !videos.month
|
|
||||||
? this.mMonth(vnode, videos.year)
|
|
||||||
: null,
|
|
||||||
!videos.year
|
|
||||||
? videos.Tree.slice(-2).map(year => {
|
|
||||||
return [
|
|
||||||
m('.gallery-year', year.title),
|
|
||||||
this.mMonth(vnode, year),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ const Authentication = require('./authentication')
|
||||||
const api = require('./api')
|
const api = require('./api')
|
||||||
const Input = require('./input')
|
const Input = require('./input')
|
||||||
const lang = require('./lang')
|
const lang = require('./lang')
|
||||||
const videos = require('./videos')
|
|
||||||
|
|
||||||
const Login = {
|
const Login = {
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
|
@ -35,7 +34,6 @@ const Login = {
|
||||||
if (!result.token) return Promise.reject(new Error(lang.login_error_auth)) // Unknown error from server. Try again later
|
if (!result.token) return Promise.reject(new Error(lang.login_error_auth)) // Unknown error from server. Try again later
|
||||||
Authentication.updateToken(result.token)
|
Authentication.updateToken(result.token)
|
||||||
m.route.set(this.redirect || '/browse')
|
m.route.set(this.redirect || '/browse')
|
||||||
videos.refreshTree()
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.error = lang.format(lang.login_error, error.message) // Error while logging in:
|
this.error = lang.format(lang.login_error, error.message) // Error while logging in:
|
||||||
|
@ -75,7 +73,7 @@ const Login = {
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
m('footer', lang.mformat(
|
m('footer', lang.mformat(
|
||||||
lang.unsplash, // Photo by X on Y
|
lang.login_footer, // Photo by X on Y
|
||||||
m('a', { href: 'https://unsplash.com/@franhotchin?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Francesca Hotchin'),
|
m('a', { href: 'https://unsplash.com/@franhotchin?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Francesca Hotchin'),
|
||||||
m('a', { href: 'https://unsplash.com/photos/landscape-photo-of-mountain-covered-with-snow-FN-cedy6NHA?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Unsplash'),
|
m('a', { href: 'https://unsplash.com/photos/landscape-photo-of-mountain-covered-with-snow-FN-cedy6NHA?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Unsplash'),
|
||||||
)),
|
)),
|
||||||
|
|
|
@ -2,8 +2,6 @@ const m = require('mithril')
|
||||||
const Authentication = require('./authentication')
|
const Authentication = require('./authentication')
|
||||||
const api = require('./api')
|
const api = require('./api')
|
||||||
const Input = require('./input')
|
const Input = require('./input')
|
||||||
const lang = require('./lang')
|
|
||||||
const videos = require('./videos')
|
|
||||||
|
|
||||||
const Upload = {
|
const Upload = {
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
|
@ -16,95 +14,15 @@ const Upload = {
|
||||||
d.setSeconds(0)
|
d.setSeconds(0)
|
||||||
d.setMilliseconds(0)
|
d.setMilliseconds(0)
|
||||||
|
|
||||||
this.cache = null
|
|
||||||
this.uploading = null
|
|
||||||
this.form = {
|
this.form = {
|
||||||
title: 'Sunnudagssamkoma',
|
title: '',
|
||||||
date: d,
|
date: d,
|
||||||
file: null,
|
file: null,
|
||||||
metadata: {
|
|
||||||
speaker: '',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadvideo: function(vnode, e) {
|
uploadvideo: function(vnode, e) {
|
||||||
this.error = ''
|
console.log(this.form)
|
||||||
|
|
||||||
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.error) return false
|
|
||||||
|
|
||||||
api.sendRequest({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/auth/uploadToken',
|
|
||||||
body: this.form,
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
if (this.cache?.file === this.form.file) {
|
|
||||||
return this.cache
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.uploadFileProgress({
|
|
||||||
url: res.path + '?token=' + res.token,
|
|
||||||
}, this.form.file, (xhr, progress, perSecond) => {
|
|
||||||
this.uploading = {
|
|
||||||
progress,
|
|
||||||
xhr,
|
|
||||||
perSecond
|
|
||||||
}
|
|
||||||
m.redraw()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
this.cache = {
|
|
||||||
file: this.form.file,
|
|
||||||
filename: res.filename,
|
|
||||||
path: res.path,
|
|
||||||
}
|
|
||||||
this.uploading = null
|
|
||||||
|
|
||||||
return api.sendRequest({
|
|
||||||
method: 'PUT',
|
|
||||||
url: '/api/auth/articles/0',
|
|
||||||
body: {
|
|
||||||
name: this.form.title,
|
|
||||||
page_id: 'null',
|
|
||||||
path: this.form.date.toISOString().replace('T', '_').replace(/:/g, '').split('.')[0],
|
|
||||||
content: JSON.stringify(this.form.metadata),
|
|
||||||
publish_at: this.form.date,
|
|
||||||
admin_id: Authentication.getTokenDecoded().user_id,
|
|
||||||
is_featured: false,
|
|
||||||
media: {
|
|
||||||
filename: res.filename,
|
|
||||||
type: this.form.file.type,
|
|
||||||
path: res.path,
|
|
||||||
size: this.form.file.size,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
console.log(res)
|
|
||||||
videos.refreshTree()
|
|
||||||
m.route.set('/browse')
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.uploading = null
|
|
||||||
if (!error) return
|
|
||||||
|
|
||||||
this.error = lang.format(lang.upload_error, error.message) // Error while uploading:
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelUpload(e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.uploading.xhr.abortRequest()
|
|
||||||
this.uploading = null
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -123,47 +41,20 @@ const Upload = {
|
||||||
formKey: 'title',
|
formKey: 'title',
|
||||||
}),
|
}),
|
||||||
m(Input, {
|
m(Input, {
|
||||||
label: 'Date (dd.mm.yyyy)',
|
label: 'Date',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
utility: 'datetime',
|
utility: 'datetime',
|
||||||
form: this.form,
|
form: this.form,
|
||||||
formKey: 'date',
|
formKey: 'date',
|
||||||
}),
|
}),
|
||||||
m(Input, {
|
|
||||||
label: 'Video',
|
|
||||||
type: 'file',
|
|
||||||
accept: '.webm',
|
|
||||||
utility: 'file',
|
|
||||||
button: 'fa-video',
|
|
||||||
form: this.form,
|
|
||||||
formKey: 'file',
|
|
||||||
}),
|
|
||||||
m('p.separator', 'Optional'),
|
|
||||||
m(Input, {
|
|
||||||
label: 'Speaker',
|
|
||||||
form: this.form.metadata,
|
|
||||||
formKey: 'speaker',
|
|
||||||
}),
|
|
||||||
m('input.spinner', {
|
m('input.spinner', {
|
||||||
hidden: api.loading,
|
hidden: api.loading,
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
value: 'Begin upload',
|
value: 'Begin upload',
|
||||||
}),
|
}),
|
||||||
api.loading ? m('div.loading-spinner') : null,
|
api.loading ? m('div.loading-spinner') : null,
|
||||||
this.uploading ? [
|
|
||||||
m('p', `${Math.floor(this.uploading.progress)}% (${this.uploading.perSecond}/s)`),
|
|
||||||
m('.loading-bar', { style: `--progress: ${this.uploading.progress}%` }),
|
|
||||||
m('button.button.button-alert', {
|
|
||||||
onclick: this.cancelUpload.bind(this),
|
|
||||||
}, 'Cancel upload'),
|
|
||||||
] : null,
|
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
m('footer', lang.mformat(
|
|
||||||
lang.unsplash, // Photo by X on Y
|
|
||||||
m('a', { href: 'https://unsplash.com/@guzmanbarquin?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Guzmán Barquín'),
|
|
||||||
m('a', { href: 'https://unsplash.com/photos/sea-under-full-moon-Qd688l1yDOI?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Unsplash'),
|
|
||||||
)),
|
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,79 +2,13 @@ const m = require('mithril')
|
||||||
const api = require('./api')
|
const api = require('./api')
|
||||||
|
|
||||||
const Tree = []
|
const Tree = []
|
||||||
const Articles = []
|
|
||||||
|
|
||||||
exports.Tree = Tree
|
exports.Tree = Tree
|
||||||
exports.Articles = Articles
|
|
||||||
|
|
||||||
exports.loading = false
|
exports.loading = false
|
||||||
exports.error = ''
|
exports.error = ''
|
||||||
exports.year = null
|
|
||||||
exports.month = null
|
|
||||||
|
|
||||||
const matcher = /\/(\d+)(\/\d+)?/
|
exports.refreshTree = function() {
|
||||||
|
|
||||||
function calculateActiveBranches() {
|
|
||||||
let path = matcher.exec(m.route.get())
|
|
||||||
if (path && path[1] !== exports.year?.title) {
|
|
||||||
for (let year of Tree) {
|
|
||||||
if (year.title === path[1]) {
|
|
||||||
exports.year = year
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!path && (m.route.get() === '/' || (m.route.get() || '').startsWith('/browse'))) {
|
|
||||||
exports.year = Tree[Tree.length - 1]
|
|
||||||
} else if (!path) {
|
|
||||||
exports.year = null
|
|
||||||
}
|
|
||||||
if (path && exports.year && path[2]) {
|
|
||||||
exports.month = exports.year.branches[Number(path[2].slice(1)) - 1] || null
|
|
||||||
} else if (!path?.[2]) {
|
|
||||||
exports.month = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildTree() {
|
|
||||||
Tree.splice(0, Tree.length)
|
|
||||||
|
|
||||||
if (!Articles.length) return
|
|
||||||
|
|
||||||
let startYear = Articles[0].publish_at
|
|
||||||
let target = new Date()
|
|
||||||
let articleIndex = 0
|
|
||||||
|
|
||||||
for (let year = startYear.getFullYear(); year <= target.getFullYear(); year++) {
|
|
||||||
let branchYear = {
|
|
||||||
title: year.toString(),
|
|
||||||
type: 'year',
|
|
||||||
branches: [],
|
|
||||||
videos: []
|
|
||||||
}
|
|
||||||
Tree.push(branchYear)
|
|
||||||
let lastMonth = year === target.getFullYear() ? target.getMonth() + 1 : 12
|
|
||||||
|
|
||||||
for (let month = 1; month <= lastMonth; month++) {
|
|
||||||
let branchMonth = {
|
|
||||||
title: month.toString(),
|
|
||||||
type: 'month',
|
|
||||||
branches: [],
|
|
||||||
videos: []
|
|
||||||
}
|
|
||||||
branchYear.branches.push(branchMonth)
|
|
||||||
|
|
||||||
let start = new Date(year, month - 1)
|
|
||||||
let end = new Date(year, month)
|
|
||||||
|
|
||||||
for (; Articles[articleIndex] && Articles[articleIndex].publish_at >= start && Articles[articleIndex].publish_at < end; articleIndex++) {
|
|
||||||
branchYear.videos.push(Articles[articleIndex])
|
|
||||||
branchMonth.videos.push(Articles[articleIndex])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshTree() {
|
|
||||||
exports.error = ''
|
exports.error = ''
|
||||||
|
|
||||||
if (exports.loading) return Promise.resolve()
|
if (exports.loading) return Promise.resolve()
|
||||||
|
@ -85,27 +19,17 @@ function refreshTree() {
|
||||||
|
|
||||||
return api.sendRequest({
|
return api.sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/articles',
|
url: '/api/videos',
|
||||||
})
|
})
|
||||||
.then(result => {
|
.then(pages => {
|
||||||
result.videos.forEach(video => {
|
console.log(pages)
|
||||||
video.publish_at = new Date(video.publish_at)
|
Tree.splice(0, Tree.length)
|
||||||
video.path_short = video.path.split('-')[2]
|
Tree.push.apply(Tree, pages.videos)
|
||||||
})
|
|
||||||
|
|
||||||
Articles.splice(0, Articles.length)
|
|
||||||
Articles.push.apply(Articles, result.videos)
|
|
||||||
|
|
||||||
rebuildTree()
|
|
||||||
}, err => {
|
|
||||||
exports.error = 'Error fetching videos: ' + err.message
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
exports.loading = false
|
exports.loading = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
|
}, err => {
|
||||||
|
exports.loading = false
|
||||||
|
m.redraw()
|
||||||
|
exports.error = 'Error fetching videos: ' + err.message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.rebuildTree = rebuildTree
|
|
||||||
exports.refreshTree = refreshTree
|
|
||||||
exports.calculateActiveBranches = calculateActiveBranches
|
|
||||||
|
|
24
filadelfia_web/dev.mjs
Normal file
24
filadelfia_web/dev.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import { ServiceCore } from 'service-core'
|
||||||
|
import * as index from './index.mjs'
|
||||||
|
|
||||||
|
const port = 4130
|
||||||
|
|
||||||
|
var core = new ServiceCore('filadelfia_web', import.meta.url, port, '')
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
frontend: {
|
||||||
|
url: 'http://localhost:' + port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
config.port = port
|
||||||
|
|
||||||
|
core.setConfig(config)
|
||||||
|
core.init(index).then(function() {
|
||||||
|
return core.run()
|
||||||
|
})
|
|
@ -1,5 +1,3 @@
|
||||||
import fs from 'fs'
|
|
||||||
import { pathToFileURL } from 'url'
|
|
||||||
import config from './base/config.mjs'
|
import config from './base/config.mjs'
|
||||||
|
|
||||||
export function start(http, port, ctx) {
|
export function start(http, port, ctx) {
|
||||||
|
@ -11,28 +9,3 @@ export function start(http, port, ctx) {
|
||||||
return server.run()
|
return server.run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
||||||
import('service-core').then(core => {
|
|
||||||
const port = 4130
|
|
||||||
|
|
||||||
var core = new core.ServiceCore('filadelfia_web', import.meta.url, port, '')
|
|
||||||
|
|
||||||
let config = {
|
|
||||||
frontend: {
|
|
||||||
url: 'http://localhost:' + port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
config = JSON.parse(fs.readFileSync('./config.json'))
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
config.port = port
|
|
||||||
|
|
||||||
core.setConfig(config)
|
|
||||||
core.init({ start }).then(function() {
|
|
||||||
return core.run()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@
|
||||||
"dev:server": "eltro --watch server --npm server",
|
"dev:server": "eltro --watch server --npm server",
|
||||||
"dev:build:old": "npm-watch build",
|
"dev:build:old": "npm-watch build",
|
||||||
"dev:server:old": "npm-watch server",
|
"dev:server:old": "npm-watch server",
|
||||||
"server": "node index.mjs | bunyan"
|
"server": "node dev.mjs | bunyan"
|
||||||
},
|
},
|
||||||
"watch": {
|
"watch": {
|
||||||
"server": {
|
"server": {
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
Before Width: | Height: | Size: 252 KiB |
|
@ -16,7 +16,6 @@
|
||||||
--bg-component-half: #f3f7ff77;
|
--bg-component-half: #f3f7ff77;
|
||||||
--bg-component-alt: #ffd99c;
|
--bg-component-alt: #ffd99c;
|
||||||
--color: #031131;
|
--color: #031131;
|
||||||
--color-alt: #7a9ad3;
|
|
||||||
--main: #1066ff;
|
--main: #1066ff;
|
||||||
--main-fg: #fff;
|
--main-fg: #fff;
|
||||||
--error: red;
|
--error: red;
|
||||||
|
@ -93,46 +92,6 @@ input[type=datetime] {
|
||||||
width: 100%;
|
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] {
|
.button, input[type=submit] {
|
||||||
background: var(--main);
|
background: var(--main);
|
||||||
color:var(--main-fg);
|
color:var(--main-fg);
|
||||||
|
@ -142,35 +101,14 @@ input[type=datetime]:disabled {
|
||||||
margin: 1rem 0 2rem;
|
margin: 1rem 0 2rem;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.spinner, input[type=submit].spinner {
|
.button.spinner, input[type=submit].spinner {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
margin-top: 2rem;
|
margin-top: 1.5rem;
|
||||||
margin-bottom: 1.5rem;
|
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=text]:focus,
|
||||||
input[type=password]:focus,
|
input[type=password]:focus,
|
||||||
input[type=datetime]:focus {
|
input[type=datetime]:focus {
|
||||||
|
@ -209,17 +147,10 @@ h1 {
|
||||||
form p, label {
|
form p, label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0.75rem 0 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
display: block;
|
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 {
|
.error {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
}
|
}
|
||||||
|
@ -239,7 +170,6 @@ form p.separator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
margin-top: 0.5rem;
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
.loading-spinner:after {
|
.loading-spinner:after {
|
||||||
|
@ -262,37 +192,11 @@ form p.separator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-upload {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-upload footer {
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
background: var(--bg-component-half);
|
|
||||||
align-self: center;
|
|
||||||
padding: 0.25rem 2rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avifsupport .page-upload {
|
|
||||||
background-image: url('./assets/bg_admin.avif');
|
|
||||||
}
|
|
||||||
.jpegonly .page-upload {
|
|
||||||
background-image: url('./assets/bg_admin.jpg');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nav */
|
/* Nav */
|
||||||
|
|
||||||
#header {
|
#header nav {
|
||||||
background: var(--bg-component);
|
|
||||||
}
|
|
||||||
|
|
||||||
#header nav,
|
|
||||||
#header .nav {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
background: var(--bg-component);
|
||||||
padding: 0.5rem 1rem 0.5rem 0;
|
padding: 0.5rem 1rem 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,49 +238,6 @@ form p.separator {
|
||||||
color: var(--main-fg);
|
color: var(--main-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#header .nav {
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 0.75rem 1rem 0.25rem;
|
|
||||||
min-height: 4rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-bottom: 1px solid #0001;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header .nav:hover {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header .nav .inner {
|
|
||||||
flex: 2 1 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header .nav a {
|
|
||||||
margin: 0 0.25rem;
|
|
||||||
padding: 0.25rem 1.5rem;
|
|
||||||
border-radius: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header .nav a.empty {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header .nav a.active {
|
|
||||||
background: var(--bg-component-alt);
|
|
||||||
color: var(--color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header .error {
|
|
||||||
background: var(--error-bg);
|
|
||||||
color: var(--color);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main */
|
/* Main */
|
||||||
|
|
||||||
.full-error {
|
.full-error {
|
||||||
|
@ -427,65 +288,6 @@ footer a {
|
||||||
background-image: url('./assets/bg.jpg');
|
background-image: url('./assets/bg.jpg');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* browse */
|
|
||||||
|
|
||||||
.gallery {
|
|
||||||
margin: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-year {
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 0 0 1rem;
|
|
||||||
border-bottom: 1px solid var(--main);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-month {
|
|
||||||
margin: 0rem 1rem 1rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
border-bottom: 1px solid #0003;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery .group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery .group a {
|
|
||||||
width: 40vw;
|
|
||||||
max-width: 320px;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Player */
|
|
||||||
|
|
||||||
.player {
|
|
||||||
background: black;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player video {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 1280px;
|
|
||||||
max-width: 100vw;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 435 KiB |
|
@ -14,7 +14,7 @@
|
||||||
"build": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js",
|
"build": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js",
|
||||||
"dev:build": "npm-watch build",
|
"dev:build": "npm-watch build",
|
||||||
"dev:server": "node index.mjs | bunyan",
|
"dev:server": "node index.mjs | bunyan",
|
||||||
"dev": "npm-watch dev:server"
|
"dev": "npm-watch dev:server",
|
||||||
},
|
},
|
||||||
"watch": {
|
"watch": {
|
||||||
"dev:server": {
|
"dev:server": {
|
||||||
|
|
Loading…
Reference in a new issue