Compare commits

..

42 commits

Author SHA1 Message Date
7a6eb69d9d discord_embed: Fix when updating old discord links, it updates the input as well
Some checks failed
/ deploy (push) Failing after 1m4s
2024-11-21 12:57:03 +00:00
86eff6d033 forgejo: Fix ci
Some checks failed
/ deploy (push) Failing after 1m10s
2024-11-21 12:46:48 +00:00
61b8ba4bd0 discord_embed: Deploy new version
Some checks are pending
/ deploy (push) Waiting to run
2024-11-21 12:37:33 +00:00
38381f3bad discord_embed: Add discord link refresher support. Add width and height support
Some checks are pending
/ deploy (push) Waiting to run
2024-11-21 12:37:13 +00:00
28dd7e77fb new combined
Some checks are pending
/ deploy (push) Waiting to run
2024-11-20 14:50:39 +00:00
852f37dc8d filadelfia_archive: Fix header
All checks were successful
/ deploy (push) Successful in -22h4m50s
2023-12-06 05:06:01 +00:00
14a3bc3123 filadelfia_archive: Fix mobile experience a bit, make it at least usable.
All checks were successful
/ deploy (push) Successful in -22h5m43s
2023-12-06 04:54:37 +00:00
ab2e7b93c4 filadelfia_archive: Rename website
All checks were successful
/ deploy (push) Successful in -22h35m14s
2023-12-05 23:01:48 +00:00
2e7b3be8d5 filadelfia_archive: When generating a banner, do incremental lower quality to fit 8000 character limit
All checks were successful
/ deploy (push) Successful in -22h35m19s
2023-12-05 22:57:59 +00:00
533e279b0b filadelfia_archive: Add another missing dependency
All checks were successful
/ deploy (push) Successful in -34h4m28s
2023-11-30 04:31:01 +00:00
e75719682e filadelfia_archive: Fix missing dependency
All checks were successful
/ deploy (push) Successful in -34h4m36s
2023-11-30 04:29:41 +00:00
c55f0c9a02 filadelfia_archive: Update msnodesqlv8
All checks were successful
/ deploy (push) Successful in -34h4m35s
2023-11-30 04:27:15 +00:00
0b686a462f filadelfia_archive: Fix name in package.json
Some checks failed
/ deploy (push) Failing after -34h5m19s
2023-11-30 04:22:35 +00:00
329a3e267c filadelfia_archive: Rename
Some checks reported warnings
/ deploy (push) Has been cancelled
2023-11-30 04:21:05 +00:00
ab9ed32196 filadelfia_archive: Finished implementing 1.0 2023-11-30 04:14:42 +00:00
3a6996bfbb base: Fix logging of sql parameter errors 2023-11-30 04:13:08 +00:00
bdeeff3794 dev
All checks were successful
/ deploy (push) Successful in -34h50m18s
2023-11-29 19:19:41 +00:00
fad7acd5f7 nfp_moe: Fix typo in package.json
All checks were successful
/ deploy (push) Successful in -53h46m37s
2023-11-20 07:12:40 +00:00
2d7101d666 filadelfia_web: More development 2023-11-20 07:12:08 +00:00
16b87aabcf base: Slight changes 2023-11-20 07:11:57 +00:00
857a087410 discord_ember, allow crossorigin for video
Some checks failed
/ deploy (push) Failing after -53h56m14s
2023-11-20 05:14:02 +00:00
ec7ade938f More dev
Some checks failed
/ deploy (push) Failing after -63h57m0s
2023-11-15 04:43:05 +00:00
638e6cc435 filadelfia_web: More development
Some checks failed
/ deploy (push) Failing after -65h42m51s
2023-11-14 07:27:04 +00:00
531c7acefe base: Fix error logging and reporting, make it better.
Some checks failed
/ deploy (push) Failing after -65h42m50s
2023-11-14 07:26:54 +00:00
a5c7e53802 base: Add feature flag for making articles api private
Some checks failed
/ deploy (push) Failing after -75h28m39s
2023-11-09 09:46:43 +00:00
d1730974dc nfp_moe: cleanup 2023-11-09 09:46:43 +00:00
4216e036e4 heimaerbest: dev 2023-11-09 09:45:37 +00:00
825cd7ef2d filadelfia_web: init 2023-11-09 09:45:37 +00:00
5baf1823f4 discord_embed: Fix wording again -.-
All checks were successful
/ deploy (push) Successful in -78h31m0s
2023-11-07 21:09:51 +00:00
285ad3dc64 discord_embed: Clarify a bit better dropbox, improve video size
All checks were successful
/ deploy (push) Successful in -78h31m16s
2023-11-07 21:05:24 +00:00
9e56095773 discord_embed: Fix slight CSS bug.
All checks were successful
/ deploy (push) Successful in -78h32m27s
2023-11-07 20:52:06 +00:00
de0a8b6f00 discord_embed: Tweak ui, tweak UX, remove strip.
All checks were successful
/ deploy (push) Successful in -78h32m42s
2023-11-07 20:48:03 +00:00
86394efb1f base: Make run smarter when creating server 2023-11-07 20:47:45 +00:00
3dbcc18da8 discord_embed: Trigger release after bug in db
All checks were successful
/ deploy (push) Successful in -82h24m57s
2023-11-05 22:10:36 +00:00
f6d87c647f base: Fix a bug in database loader
All checks were successful
/ deploy (push) Successful in -82h27m41s
nfp_moe: Trigger new release from above bug.
2023-11-05 21:36:40 +00:00
e5a1432443 nfp_moe: Update msnodesqlv8
All checks were successful
/ deploy (push) Successful in -82h28m12s
2023-11-05 21:30:51 +00:00
324623d717 deploy: Deploy to the new machine.
All checks were successful
/ deploy (push) Successful in -82h29m3s
discord_embed: Update msnodesqlv8
2023-11-05 21:22:13 +00:00
aad55ae9f0 nfp_is: Add more missing dependencies
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
/ deploy (push) Successful in -83h41m45s
2023-11-05 06:27:58 +00:00
70d14b24aa deploy: Fix a tiny bug
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
/ deploy (push) Successful in -83h42m53s
2023-11-05 06:25:29 +00:00
5e31e785df Combine all actions to one action
Some checks reported warnings
continuous-integration/appveyor/branch AppVeyor build succeeded
/ deploy (push) Has been cancelled
2023-11-05 06:24:01 +00:00
d9ca9bcc52 base: Don't load database/mssql unless we need to 2023-11-05 06:23:43 +00:00
6f39e97977 nfp_is: Add missing dependency
All checks were successful
/ discord_embed (push) Successful in -83h44m41s
continuous-integration/appveyor/branch AppVeyor build succeeded
/ nfp_is (push) Successful in -83h43m59s
/ nfp_moe (push) Successful in -83h44m39s
/ saproxy (push) Successful in -83h44m39s
2023-11-05 06:11:17 +00:00
79 changed files with 135309 additions and 389 deletions

View file

@ -0,0 +1,79 @@
on:
push:
branches:
- master
jobs:
deploy:
runs-on: arch
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Check for new release
run: |
chmod +x ./7zas
echo ""
echo "Checking following projects:"
for f in *; do
[ -d "$f" ] && [ ! -L "$f" ] && [ ! "$f" = "base" ] && echo " * $f";
done
echo ""
for f in *; do
[ ! -d "$f" ] || [ -L "$f" ] || [ "$f" = "base" ] && continue;
echo ""
echo "------------------------------------"
echo ""
echo "checking $f";
cd $f
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ github.job }} since $CURR_NAME already exists";
cd ..
continue;
fi
echo "New release ${CURR_VER} found, running npm install..."
mv package.json fuck-you-npm-package.json
mv build-package.json package.json
npm install && npm run build
mv package.json build-package.json
mv fuck-you-npm-package.json package.json
../7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating ${CURR_VER} release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ github.sha }} :\n\n${{ github.event.head_commit.message }}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
echo "Deplying to production"
echo "curl -X POST http://192.168.93.52:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.52:$MAN_PORT/update/$MAN_NAME
cd ..
done

View file

@ -1,59 +0,0 @@
on:
push:
branches:
- master
jobs:
discord_embed:
runs-on: alpine
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Check for new release
run: |
cd ${{ gitea.job }}
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ gitea.job }} since $CURR_NAME already exists";
exit 0
fi
echo "New release ${CURR_VER} found, running npm install..."
mv package.json fuck-you-npm-package.json
mv build-package.json package.json
npm install && npm run build
mv package.json build-package.json
mv fuck-you-npm-package.json package.json
chmod +x ../7zas
../7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating ${CURR_VER} release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ gitea.sha }} :\n\n${{ gitea.event.head_commit.message }}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
echo "Deplying to production"
echo "curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME

View file

@ -1,59 +0,0 @@
on:
push:
branches:
- master
jobs:
nfp_is:
runs-on: alpine
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Check for new release
run: |
cd ${{ gitea.job }}
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ gitea.job }} since $CURR_NAME already exists";
exit 0
fi
echo "New release ${CURR_VER} found, running npm install..."
mv package.json fuck-you-npm-package.json
mv build-package.json package.json
npm install && npm run build
mv package.json build-package.json
mv fuck-you-npm-package.json package.json
chmod +x ../7zas
../7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating ${CURR_VER} release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ gitea.sha }} :\n\n${{ gitea.event.head_commit.message }}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
echo "Deplying to production"
echo "curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME

View file

@ -1,59 +0,0 @@
on:
push:
branches:
- master
jobs:
nfp_moe:
runs-on: alpine
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Check for new release
run: |
cd ${{ gitea.job }}
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ gitea.job }} since $CURR_NAME already exists";
exit 0
fi
echo "New release ${CURR_VER} found, running npm install..."
mv package.json fuck-you-npm-package.json
mv build-package.json package.json
npm install && npm run build
mv package.json build-package.json
mv fuck-you-npm-package.json package.json
chmod +x ../7zas
../7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating ${CURR_VER} release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ gitea.sha }} :\n\n${{ gitea.event.head_commit.message }}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
echo "Deplying to production"
echo "curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME

View file

@ -1,59 +0,0 @@
on:
push:
branches:
- master
jobs:
saproxy:
runs-on: alpine
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Check for new release
run: |
cd ${{ gitea.job }}
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ gitea.job }} since $CURR_NAME already exists";
exit 0
fi
echo "New release ${CURR_VER} found, running npm install..."
mv package.json fuck-you-npm-package.json
mv build-package.json package.json
npm install && npm run build
mv package.json build-package.json
mv fuck-you-npm-package.json package.json
chmod +x ../7zas
../7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating ${CURR_VER} release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ gitea.sha }} :\n\n${{ gitea.event.head_commit.message }}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
echo "Deplying to production"
echo "curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME

View file

@ -8,11 +8,14 @@ export default class ArticleRoutes {
uploadMedia: uploadMedia,
uploadFile: uploadFile,
deleteFile: deleteFile,
requireAuth: opts.requireAuth,
})
}
register(server) {
server.flaska.get('/api/articles/:path', this.getArticle.bind(this))
if (!this.requireAuth) {
server.flaska.get('/api/articles/:path', this.getArticle.bind(this))
}
server.flaska.get('/api/auth/articles', server.authenticate(), this.auth_getAllArticles.bind(this))
server.flaska.get('/api/auth/articles/:id', server.authenticate(), this.auth_getSingleArticle.bind(this))
server.flaska.put('/api/auth/articles/:id', [
@ -74,6 +77,7 @@ export default class ArticleRoutes {
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
ctx.body = this.private_getUpdateArticle_resOutput(res)
@ -111,6 +115,10 @@ export default class ArticleRoutes {
)
}
if (ctx.req.body.media && ctx.req.body.media.filename && ctx.req.body.media.type && ctx.req.body.media.path && ctx.req.body.media.size) {
newMedia = ctx.req.body.media
}
await Promise.all(promises)
return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia)

View file

@ -18,8 +18,8 @@ export default class AuthenticationRoutes {
/** GET: /api/authentication/login */
async login(ctx) {
let res = await ctx.db.safeCallProc('auth_login', [
ctx.req.body.email,
ctx.req.body.password,
ctx.req.body.email || '',
ctx.req.body.password || '',
])
let out = res.results[0][0]

View file

@ -53,6 +53,7 @@ nconf.defaults({
"iss": "dev",
"path": "https://media.nfp.is/media/resize",
"filePath": "https://media.nfp.is/media",
"directFilePath": "https://media.nfp.is/media/noprefix",
"removePath": "https://media.nfp.is/media/",
"preview": {
"out": "base64",

View file

@ -1,5 +1,6 @@
import MSSQL from 'msnodesqlv8'
import { HttpError } from 'flaska'
import { HttpErrorInternal } from './error.mjs'
export function initPool(core, config) {
let pool = new MSSQL.Pool(config)
@ -32,7 +33,10 @@ export function initPool(core, config) {
if (err.lineNumber && err.procName) {
message = `Error at ${err.procName}:${err.lineNumber} => ${message}`
}
throw new HttpError(500, message)
throw new HttpErrorInternal(message, err, !err.lineNumber ? {
name,
params
} : null)
})
},
promises: pool.promises,

15
base/error.mjs Normal file
View file

@ -0,0 +1,15 @@
import { HttpError } from 'flaska'
export class HttpErrorInternal extends HttpError {
constructor(message, inner, extra) {
super(500, message);
Error.captureStackTrace(this, HttpError);
let proto = Object.getPrototypeOf(this);
proto.name = 'HttpErrorInternal';
this.inner = inner
this.extra = extra
}
}

View file

@ -98,6 +98,10 @@ export default class Client {
}
createJwt(body, secret) {
return Client.createJwt(body, secret)
}
static createJwt(body, secret) {
let header = {
typ: 'JWT',
alg: 'HS256',

View file

@ -15,8 +15,6 @@ export function uploadMedia(file) {
}
}
console.log(media)
let body = {}
if (media.preview) {

View file

@ -5,9 +5,9 @@ export function mediaToDatabase(media, removeFlag) {
media.type,
media.path,
media.size,
media.preview.base64,
media.sizes.small.avif.path.replace(/_small\.avif$/, ''),
JSON.stringify(media.sizes),
media.preview?.base64 || null,
media.sizes?.small?.avif?.path?.replace(/_small\.avif$/, '') || null,
JSON.stringify(media.sizes || {}),
removeFlag ? 1 : 0,
]
} else {

View file

@ -3,7 +3,7 @@
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.2",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.7.0",
"msnodesqlv8": "^4.4.0",
"nconf-lite": "^2.0.0"
}
}

View file

@ -1,7 +1,6 @@
import { Flaska, QueryHandler, JsonHandler, FormidableHandler } from 'flaska'
import { Flaska, QueryHandler, JsonHandler, FormidableHandler, HttpError } from 'flaska'
import formidable from 'formidable'
import { initPool } from './db.mjs'
import config from './config.mjs'
import PageRoutes from './page/routes.mjs'
import ArticleRoutes from './article/routes.mjs'
@ -15,6 +14,7 @@ export default class Server {
this.http = http
this.port = port
this.core = core
this.pool = null
this.flaskaOptions = {
appendHeaders: {
@ -43,22 +43,36 @@ export default class Server {
// Create our server
this.flaska = new Flaska(this.flaskaOptions, this.http)
// Create our database pool
let pool = this.runCreateDatabase()
// configure our server
if (config.get('NODE_ENV') === 'development') {
this.flaska.devMode()
}
this.flaska.onerror((err, ctx) => {
if (err instanceof HttpError && err.status !== 500) {
ctx.status = err.status
ctx.log.warn(err.message)
} else {
ctx.log.error(err.inner || err)
if (err.extra) {
ctx.log.error({ extra: err.extra }, 'Database parameters')
}
ctx.status = 500
}
ctx.body = {
status: ctx.status,
message: err.message,
}
})
this.flaska.before(function(ctx) {
ctx.state.started = new Date().getTime()
ctx.req.ip = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress
ctx.log = ctx.log.child({
id: Math.random().toString(36).substring(2, 14),
})
ctx.db = pool
})
ctx.db = this.pool
}.bind(this))
this.flaska.before(QueryHandler())
let healthChecks = 0
@ -109,7 +123,9 @@ export default class Server {
}
runCreateDatabase() {
return initPool(this.core, config.get('mssql'))
return import('./db.mjs').then(db => {
this.pool = db.initPool(this.core, config.get('mssql'))
})
}
runStartListen() {
@ -119,9 +135,13 @@ export default class Server {
}
run() {
this.runCreateServer()
this.runRegisterRoutes()
return this.runStartListen()
return Promise.all([
this.runCreateServer(),
this.runCreateDatabase(),
]).then(() => {
return this.runRegisterRoutes()
}).then(() => {
return this.runStartListen()
})
}
}

View file

@ -23,11 +23,11 @@ export function decode(base64StringUrlSafe) {
export function parseMediaAndBanner(item) {
if (item.banner_path) {
item.banner_path = 'https://cdn.nfp.is' + item.banner_path
item.banner_alt_prefix = 'https://cdn.nfp.is' + item.banner_alt_prefix
item.banner_alt_prefix = 'https://cdn.nfp.is' + (item.banner_alt_prefix || '')
}
if (item.media_path) {
item.media_path = 'https://cdn.nfp.is' + item.media_path
item.media_alt_prefix = 'https://cdn.nfp.is' + item.media_alt_prefix
item.media_alt_prefix = 'https://cdn.nfp.is' + (item.media_alt_prefix || '')
}
}

View file

@ -2,6 +2,8 @@ import { uploadMedia, deleteFile } from '../base/media/upload.mjs'
import config from '../base/config.mjs'
import AlphabeticID from './id.mjs'
const ImageRegex = /\.(jpg|png|gif)(\?|$)/
export default class IndexPost {
constructor(opts = {}) {
Object.assign(this, {
@ -16,40 +18,6 @@ export default class IndexPost {
server.flaska.post('/', [
server.formidable({ maxFileSize: 8 * 1024 * 1024, }),
], this.createNewLink.bind(this))
server.flaska.get('/video/:id', this.videoRedirect.bind(this))
}
async videoRedirect(ctx) {
try {
let id = AlphabeticID.decode(ctx.params.id.slice(0,-5))
let videoLink = null
if (id) {
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
if (res.first.length) {
videoLink = res.first[0].video_link
} else {
ctx.status = 404
}
}
if (videoLink) {
ctx.status = 302
ctx.headers['Location'] = videoLink
ctx.type = 'text/html; charset=utf-8'
ctx.body = `
Redirecting
<a href="${videoLink}">Click here if it doesn't redirect</a>
`
return
} else {
ctx.status = 404
ctx.state.error = 'Video not found.'
}
} catch (err) {
ctx.log.error(err, 'Unable to fetch resource ' + ctx.url.slice(1))
ctx.state.error = 'Unknown error while fetching link.'
}
return this.serve.serveIndex(ctx)
}
hasErrors(ctx, hasMedia) {
@ -61,6 +29,24 @@ Redirecting
return 'Video link has to be a valid full url'
}
if (ImageRegex.exec(ctx.req.body.video)) {
return 'Image links are not gonna work, that is not how embedding video works. What are you even doing mate?'
}
if (ctx.req.body.width) {
let n = Number(ctx.req.body.width)
if (isNaN(n) || n < 10 || n > 5000) {
return 'The video width does not look right'
}
}
if (ctx.req.body.height) {
let n = Number(ctx.req.body.height)
if (isNaN(n) || n < 10 || n > 3000) {
return 'The video height does not look right'
}
}
if (ctx.req.body.image) {
if (!ctx.req.body.image.startsWith('http')) {
return 'Image link has to be a valid full url'
@ -77,6 +63,8 @@ Redirecting
async createNewLink(ctx) {
ctx.state.video = ctx.req.body.video
ctx.state.image = ctx.req.body.image || 'https://cdn.nfp.is/av1/empty.png'
ctx.state.width = ctx.req.body.width || null
ctx.state.height = ctx.req.body.height || null
let rateLimited = false
let redisKey = 'ratelimit_' + ctx.req.ip.replace(/:/g, '-')
@ -129,6 +117,8 @@ Redirecting
ctx.state.video,
ctx.state.image,
ctx.req.ip,
ctx.state.width,
ctx.state.height,
]
let res = await ctx.db.safeCallProc('discord_embed.link_add', params)
let id = AlphabeticID.encode(res.first[0].id + 3843)

View file

@ -7,11 +7,15 @@ import dot from 'dot'
import config from '../base/config.mjs'
import AlphabeticID from './id.mjs'
const ExpirationRegex = /ex=([0-9a-fA-F]{8})/
export default class ServeHandler extends Parent {
loadTemplate(indexFile) {
this.template = dot.template(indexFile.toString(), { argName: [
'imageLink',
'videoLink',
'width',
'height',
'error',
'siteUrl',
'siteUrlBase',
@ -19,12 +23,28 @@ export default class ServeHandler extends Parent {
'nonce',
'in_debug',
'inputVideo',
'inputImage'
] })
'inputImage',
'inputWidth',
'inputHeight'
], strip: false })
}
async refreshUrl(link) {
var res = await fetch('https://discord.com/api/v10/attachments/refresh-urls', {
method: 'POST',
headers: {
'Authorization': `Bot ${config.get('discord_token')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ attachment_urls: [link] })
})
let output = await res.json()
return output?.refreshed_urls?.[0].refreshed
}
register(server) {
super.register(server)
server.flaska.get('/video/:id', this.videoRedirect.bind(this))
server.flaska.onerror(this.serveError.bind(this))
}
@ -41,13 +61,19 @@ export default class ServeHandler extends Parent {
let videoLink = ctx.query.get('v') || ''
let imageLink = ctx.query.get('i') || ''
let width = ctx.query.get('w') || 1280
let height = ctx.query.get('h') || 720
ctx.body = this.template({
videoLink: videoLink,
imageLink: imageLink,
width: width,
height: height,
error: ctx.state.error || '',
inputVideo: ctx.state.video || videoLink || '',
inputImage: ctx.state.image || imageLink || '',
inputWidth: width,
inputHeight: height,
siteUrl: this.frontend + ctx.url,
siteUrlBase: this.frontend + '/',
version: this.version,
@ -57,6 +83,39 @@ export default class ServeHandler extends Parent {
ctx.type = 'text/html; charset=utf-8'
}
async videoRedirect(ctx) {
try {
let id = AlphabeticID.decode(ctx.params.id.slice(0,-5))
let videoLink = null
if (id) {
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
if (res.first.length) {
videoLink = res.first[0].video_link
} else {
ctx.status = 404
}
}
if (videoLink) {
ctx.status = 302
ctx.headers['Location'] = videoLink
ctx.type = 'text/html; charset=utf-8'
ctx.body = `
Redirecting
<a href="${videoLink}">Click here if it doesn't redirect</a>
`
return
} else {
ctx.status = 404
ctx.state.error = 'Video not found.'
}
} catch (err) {
ctx.log.error(err, 'Unable to fetch resource ' + ctx.url.slice(1))
ctx.state.error = 'Unknown error while fetching link.'
}
return this.serveIndex(ctx)
}
async serveIndex(ctx) {
if (config.get('NODE_ENV') === 'development') {
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
@ -65,11 +124,14 @@ export default class ServeHandler extends Parent {
let videoLink = ctx.query.get('v') || ''
let imageLink = ctx.query.get('i') || (videoLink ? 'https://cdn.nfp.is/av1/empty.png' : '')
let width = ctx.query.get('w') || 1280
let height = ctx.query.get('h') || 720
let id = null
if (!ctx.state.error) {
if (ctx.url.match(/^\/[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]+$/) && ctx.url.length < 7) {
try {
let id = AlphabeticID.decode(ctx.url.slice(1))
id = AlphabeticID.decode(ctx.url.slice(1))
if (id) {
let res = await ctx.db.safeCallProc('discord_embed.link_get', [id - 3843])
if (res.first.length) {
@ -80,6 +142,8 @@ export default class ServeHandler extends Parent {
videoLink = this.frontend + '/video/' + ctx.url.slice(1) + '.webm'
}
imageLink = res.first[0].image_link
width = res.first[0].width || width
height = res.first[0].height || height
} else {
ctx.status = 404
}
@ -94,15 +158,40 @@ export default class ServeHandler extends Parent {
}
if (videoLink.startsWith('https://cdn.discordapp.com')) {
if (id) {
let match = ExpirationRegex.exec(videoLink)
if (match && match[1]) {
try {
let expiration = Number('0x' + match[1]) * 1000
if (new Date() > new Date(expiration)) {
let newLink = await this.refreshUrl(videoLink)
if (newLink) {
ctx.log.info({
old: videoLink,
new: newLink,
}, 'Updating link')
videoLink = ctx.state.video = newLink
await ctx.db.safeCallProc('discord_embed.link_update', [id - 3843, videoLink])
}
}
} catch (err) {
ctx.log.error(err)
}
}
}
videoLink = videoLink.replace('https://cdn.discordapp.com', 'https://discordproxy.nfp.is')
}
let payload = {
videoLink: videoLink,
imageLink: imageLink,
width: width,
height: height,
error: ctx.state.error || '',
inputVideo: ctx.state.video || videoLink || '',
inputImage: ctx.state.image || imageLink || '',
inputWidth: width,
inputHeight: height,
siteUrl: this.frontend + ctx.url,
siteUrlBase: this.frontend + '/',
version: this.version,

View file

@ -20,7 +20,7 @@ export default class Server extends Parent {
static: new StaticRoutes(),
post: new IndexPost({
frontend: config.get('frontend:url'),
})
}),
}
this.routes.serve = new ServeHandler({
root: localUtil.getPathFromRoot('../public'),
@ -32,7 +32,6 @@ export default class Server extends Parent {
runCreateServer() {
super.runCreateServer()
this.flaska.before((ctx) => {
ctx.redis = this.redis
})

View file

@ -1,6 +1,6 @@
{
"name": "discord_embed",
"version": "1.0.19",
"version": "1.0.28",
"port": 4120,
"description": "AV1 discord server embed helper",
"main": "index.js",
@ -40,7 +40,7 @@
"flaska": "^1.3.4",
"formidable": "^1.2.6",
"ioredis": "^5.2.3",
"msnodesqlv8": "^2.4.7",
"msnodesqlv8": "^4.1.1",
"nconf-lite": "^2.0.0"
},
"devDependencies": {

View file

@ -2,15 +2,15 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Discord Embedder from AV1 server 1.0.18</title>
<title>Discord Embedder from AV1 server 1.0.28</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ if (imageLink) { }}
<meta property="og:image" content="{{=imageLink}}">
<meta property="og:type" content="video.other">
<meta property="og:video:url" content="{{=videoLink}}">
<meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720">
<meta property="og:video:width" content="{{=width}}">
<meta property="og:video:height" content="{{=height}}">
{{ } else { }}
<meta property="og:type" content="website" />
<meta property="og:url" content="{{=siteUrl}}" />
@ -47,7 +47,7 @@ body {
text-rendering: optimizeSpeed;
line-height: 1.5;
font-size: 16px;
padding: 1rem 0.5rem;
padding: 1rem 0.5rem 3rem;
font-family: sans-serif;
background: var(--bg);
color: var(--color);
@ -117,14 +117,7 @@ input[type=submit] {
background: var(--button-bg);
color: var(--button-fg);
padding: 0.25rem 1rem;
margin: 1rem 0 2rem;
}
pre {
background: var(--bg-content-alt);
padding: 0.5rem;
white-space: break-spaces;
word-break: break-all;
margin: 0rem 0 2rem;
}
.row {
@ -174,6 +167,10 @@ h1 {
margin-bottom: 1rem;
}
p {
margin-bottom: 0.5rem;
}
.warning {
font-size: 0.9rem;
color: #ffe69c;
@ -182,8 +179,8 @@ h1 {
.alert {
font-size: 0.9rem;
color: #f1aeb5;
margin-bottom: 0.5rem;
margin: 0.5rem 0 1rem;
color: hsl(353.7, 70.5%, 87%);
}
.info {
@ -196,18 +193,36 @@ h1 {
text-decoration: underline;
}
.row.optional {
background: #333;
margin: 1rem -0.5rem 0;
.optional {
margin: 0 -0.5rem 0.5rem;
padding: 0rem 0.5rem 1rem;
background: var(--bg-content-alt);
}
#testing {
display: flex;
flex-direction: column;
align-items: stretch;
}
.hidden {
display: none;
display: none !important;
}
pre {
padding: 0.5rem !important;
white-space: break-spaces;
word-break: break-all;
}
#mainplayer {
max-width: calc(100vw - 1rem);
max-height: 95vh;
}
#previewVideo {
border: 1px solid white;
max-width: 100%;
}
</style>
@ -217,52 +232,75 @@ h1 {
<p>Your link is:</p>
<pre>{{=siteUrl}}</pre>
<div class="center">
<video controls poster="{{=imageLink}}">
<video id=mainplayer" crossorigin controls poster="{{=imageLink}}">
<source src="{{=videoLink}}">
</video>
</div>
{{ } }}
<form action="/" method="post" enctype="multipart/form-data" class="inside">
<h1>Create/generate embed url</h1>
<p class="warning">Please don't try to use <b>youtube.com</b> links or file sharing sites that don't allow direct link to the video. They will show a download page and won't embed properly (for example <b>mediafire.com</b>, <b>megaupload</b>, <b>drive.google.com</b>, etc.)<br><b>Those will all not work</b>. (You can test if embedding works by clicking play below in the video player after filling out the video link.)</p>
<p class="info">Video upload sites that work are sites like <a href="https://catbox.moe/" target="_blank">catbox.moe</a> and other sites that allow direct link to video file.</p>
<p class="alert"><b>HEVC/h.265 files will not embed or work</b>!</p>
<p class="alert">Video files of .mkv <b>will also not work</b>! Only files like *.mov, *.mp4 and *.webm will work.</p>
<h1>Embed {{ if (videoLink && imageLink) { }} another {{ } }} video for discord:</h1>
<p>Use this tool to generate links that forces discord to try and play a video or movie directly inside discord. By default, discord will not show video playback for video links that are too large or not in proper format. This tool forces discord to at least try.</p>
<p class="warning">Only sites and filesharing sites that have direct link to the video will work and play in discord. Youtube has it's own player and will not work. Many video sites will have their own player and will not work.</p>
<p class="info">Video upload sites that work are sites like <a href="https://catbox.moe/" target="_blank">catbox.moe</a> and other sites that allow <b>direct link to video file</b> will work.</p>
{{ if (error) { }}<p class="error">{{=error}}</p>{{ } }}
<label>Video link*</label>
<p> </p>
<label>Link to video file you want to play in discord*</label>
<input id="inputVideo" type="text" name="video" value="{{=inputVideo}}">
<p class="alert">
<b>.mkv</b> video files will <b>not work</b>! <b>HEVC/h.265</b> will also <b>not work</b>! File hosting sites like <b>mediafire/google drive/dropbox</b> will also probably <b>not work</b>!<br>
</p>
<div class="row optional">
<div class="row-item">
<label>Optional: Image link (recommended, defaults to black cover)</label>
<label>Optional: Link to image to show before discord user presses play</label>
<input id="inputImage" type="text" name="image" value="{{=inputImage}}">
</div>
<span class="row-inbetween">or</span>
<div class="row-item">
<label>Optional: Upload image file (max 8MiB, recommended)</label>
<input type="file" name="media">
<label>Optional: Upload image to show before discord user presses play</label>
<input type="file" name="media" type="image">
</div>
</div>
<input type="submit" value="Generate short url">
<div class="row optional">
<div class="row-item">
<label>Optional: Overwrite aspect ratio from default 16:9 (1280x720)</label>
<input id="inputWidth" type="text" name="width" value="{{=inputWidth}}">
</div>
<span class="row-inbetween">x</span>
<div class="row-item">
<label>&nbsp;</label>
<input id="inputHeight" type="text" name="height" value="{{=inputHeight}}">
</div>
</div>
<div id="testing" class="hidden">
<p>Test area (does the video load and play? If not, it might not work on discord)</p>
<div class="center">
<video class="hidden" id="previewVideo" crossorigin controls poster="" preload="none">
<source src="">
</video>
</div>
<pre class="optional" id="generateurl">{{=siteUrlBase}}?v=&lt;video link&gt;</pre>
<input type="submit" value="Generate shorter url">
</div>
</form>
<p>
Preview:
</p>
<pre id="generateurl">
{{=siteUrlBase}}?v=&lt;video link&gt;
</pre>
<video class="hidden" id="previewVideo" controls poster="" preload="none">
<source src="">
</video>
<script type="text/javascript" nonce="{{=nonce}}">
var defaultPoster = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABQAAAALQAQAAAADnBuD7AAAAh0lEQVR42u3BMQEAAADCIPuntsROYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkDsTfAAFMFnd/AAAAAElFTkSuQmCC';
var baseSite = '{{=siteUrlBase}}';
var existingVideoUrl = '{{=inputVideo}}';
var testing = document.getElementById('testing');
var generateurl = document.getElementById('generateurl');
var inputVideo = document.getElementById('inputVideo');
var inputImage = document.getElementById('inputImage');
var inputWidth = document.getElementById('inputWidth');
var inputHeight = document.getElementById('inputHeight');
var previewVideo = document.getElementById('previewVideo');
var isExample = true;
previewVideo.poster = defaultPoster;
@ -278,17 +316,35 @@ h1 {
generateurl.innerText = baseSite + '?v=<video link>';
previewVideo.classList.add('hidden')
} else {
generateurl.innerText = baseSite + '?v=' + encodeURIComponent(inputVideo.value) + (inputImage.value ? '&i=' + encodeURIComponent(inputImage.value) : '');
previewVideo.poster = inputImage.value || defaultPoster;
previewVideo.children[0].src = inputVideo.value;
previewVideo.classList.remove('hidden')
generateurl.innerText = baseSite + '?v=' + encodeURIComponent(inputVideo.value)
+ (inputImage.value ? '&i=' + encodeURIComponent(inputImage.value) : '')
+ (inputWidth.value !== '1280' ? '&w=' + encodeURIComponent(inputWidth.value) : '')
+ (inputHeight.value !== '720' ? '&h=' + encodeURIComponent(inputHeight.value) : '');
previewVideo.pause();
if (inputVideo.value !== existingVideoUrl) {
previewVideo.setAttribute('poster', inputImage.value || defaultPoster);
previewVideo.children[0].setAttribute('src', inputVideo.value);
previewVideo.classList.remove('hidden');
testing.classList.remove('hidden');
} else {
previewVideo.removeAttribute('poster');
previewVideo.children[0].removeAttribute('src');
previewVideo.classList.add('hidden')
testing.classList.add('hidden');
}
previewVideo.load();
}
};
inputVideo.addEventListener('change', checkChange);
inputImage.addEventListener('change', checkChange);
inputWidth.addEventListener('change', checkChange);
inputHeight.addEventListener('change', checkChange);
inputVideo.addEventListener('keyup', checkChange);
inputImage.addEventListener('keyup', checkChange);
inputWidth.addEventListener('keyup', checkChange);
inputHeight.addEventListener('keyup', checkChange);
checkChange()
</script>
</body>

View file

@ -0,0 +1 @@
package-lock=false

View file

@ -0,0 +1,52 @@
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'
import { RankLevels } from '../../base/authentication/security.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/articles', this.getVideos.bind(this))
server.flaska.get('/api/articles/:path', this.getArticle.bind(this))
server.flaska.put('/api/auth/articles/:id', [server.authenticate(), server.jsonHandler()], this.updateCreateArticle.bind(this))
server.flaska.get('/api/auth/uploadToken', server.authenticate(RankLevels.Admin), this.getUploadToken.bind(this))
server.flaska.delete('/api/auth/articles/:id', server.authenticate(RankLevels.Admin), this.auth_removeSingleArticle.bind(this))
}
/** GET: /api/auth/articles */
async getVideos(ctx) {
let res = await ctx.db.safeCallProc('filadelfia_archive.article_get_all', [])
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'),
resize: config.get('media:path'),
delete: config.get('media:removePath'),
}
}
/** PUT: /api/auth/articles/:id */
async updateCreateArticle(ctx) {
return this.private_getUpdateArticle(ctx, ctx.req.body, ctx.req.body.banner, ctx.req.body.media)
}
}

View file

@ -0,0 +1,19 @@
import { parseMediaAndBanner } from "../../base/util.mjs"
export function parseVideos(videos) {
for (let i = 0; i < videos.length; i++) {
parseVideo(videos[i])
}
return videos
}
export function parseVideo(video) {
if (!video) {
return null
}
parseMediaAndBanner(video)
video.metadata = JSON.parse(video.metadata || '{}')
delete video.banner_alt_prefix
delete video.media_alt_prefix
return video
}

View file

@ -0,0 +1,24 @@
import fs from 'fs/promises'
import dot from 'dot'
import Parent from '../base/serve.mjs'
import path from 'path'
export default class ServeHandler extends Parent {
loadTemplate(indexFile) {
this.template = dot.template(indexFile.toString(), { argName: [
'siteUrl',
] })
}
async serveIndex(ctx) {
if (process.env.NODE_ENV !== 'production') {
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
this.loadTemplate(indexFile)
}
ctx.body = this.template({
siteUrl: this.frontend + ctx.url,
})
ctx.type = 'text/html; charset=utf-8'
}
}

View file

@ -0,0 +1,26 @@
import config from '../base/config.mjs'
import Parent from '../base/server.mjs'
import StaticRoutes from '../base/static_routes.mjs'
import ServeHandler from './serve.mjs'
import AuthenticationRoutes from '../base/authentication/routes.mjs'
import ArticleRoutes from './article/routes.mjs'
export default class Server extends Parent {
init() {
super.init()
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/`,
this.flaskaOptions.nonce = []
this.routes = {
static: new StaticRoutes(),
auth: new AuthenticationRoutes(),
article: new ArticleRoutes(),
}
this.routes.serve = new ServeHandler({
root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running,
frontend: config.get('frontend:url'),
})
}
}

View file

@ -0,0 +1,284 @@
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)
}
}
let requests = new Set()
let requestIndex = 0
function clearLoading(request) {
requests.delete(request)
if (!requests.size) {
api.loading = false
window.requestAnimationFrame(m.redraw.bind(m))
}
}
const api = {
loading: false,
sendRequest: function(options, isPagination) {
let request = requestIndex++
requests.add(request)
api.loading = true
let token = Authentication.getToken()
let pagination = isPagination
if (token) {
options.headers = options.headers || {}
options.headers['Authorization'] = 'Bearer ' + token
}
options.extract = function(xhr) {
let out = safeParseReponse(xhr.responseText, xhr.status, this.url)
if (out instanceof Error) {
throw out
}
if (xhr.status >= 300) {
if (out.message) {
throw out
}
console.error('Got error ' + xhr.status + ' but no error message:', out)
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 m.request(options)
.then(function(res) {
clearLoading(request)
return res
})
.catch(function (error) {
clearLoading(request)
window.requestAnimationFrame(m.redraw.bind(m))
if (error.status === 403) {
Authentication.clearToken()
m.route.set('/', { redirect: m.route.get() })
}
if (error.response && error.response.status) {
return Promise.reject(error.response)
}
return Promise.reject(error)
})
},
uploadBanner: function(bannerFile, token, reporter) {
let request = requestIndex++
requests.add(request)
api.loading = true
var report = reporter || function() {}
var data = new FormData()
data.append('file', bannerFile)
/*data.append('preview', JSON.stringify({
"out": "base64",
"format": "avif",
"resize": {
"width": 360,
"height": 203,
"fit": "cover",
"withoutEnlargement": true,
"kernel": "mitchell"
},
"avif": {
"quality": 40,
"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
}
}))
report(lang.api_banner_upload)
return api.sendRequest({
method: 'POST',
url: token.resize + '?token=' + token.token,
body: data,
})
.then(async (banner) => {
let preview = null
let quality = 60
while (!preview && quality > 10) {
report(lang.format(lang.api_banner_generate, quality))
let check = await api.sendRequest({
method: 'POST',
url: token.resize + '/' + banner.filename + '?token=' + token.token,
body: {
"preview": {
"out": "base64",
"format": "avif",
"resize": {
"width": 360,
"height": 203,
"fit": "cover",
"withoutEnlargement": true,
"kernel": "mitchell"
},
"avif": {
"quality": quality,
"effort": 9
}
},
},
})
if (check.preview.base64.length < 8000) {
preview = check.preview.base64
} else {
quality -= 5
}
}
report(null)
api.sendRequest({
method: 'DELETE',
url: token.delete + banner.filename + '?token=' + token.token,
}).catch(err => console.error(err))
return {
file: bannerFile,
size: bannerFile.size,
medium: {
filename: banner.medium.filename,
path: banner.medium.path,
},
preview: {
base64: preview,
},
}
})
.then(
function(res) {
clearLoading(request)
return res
},
function(err) {
clearLoading(request)
return Promise.reject(err)
}
)
},
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) {
let request = requestIndex++
requests.add(request)
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) {
clearLoading(request)
return res
}, function(err) {
clearLoading(request)
return Promise.reject(err)
})
},
}
module.exports = api

View file

@ -0,0 +1,64 @@
const m = require('mithril')
const storageName = 'nfp_sites_filadelfia_web_logintoken'
const Authentication = {
currentUser: null,
isAdmin: false,
loadingListeners: [],
authListeners: [],
updateToken: function(token) {
if (!token) return Authentication.clearToken()
localStorage.setItem(storageName, token)
Authentication.currentUser = JSON.parse(atob(token.split('.')[1]))
if (Authentication.authListeners.length) {
Authentication.authListeners.forEach(function(x) { x(Authentication.currentUser) })
}
},
clearToken: function() {
Authentication.currentUser = null
localStorage.removeItem(storageName)
Authentication.isAdmin = false
},
addEvent: function(event) {
Authentication.authListeners.push(event)
},
setAdmin: function(item) {
Authentication.isAdmin = item
},
getToken: function() {
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() {
if (Authentication.currentUser) return
m.route.set('/login')
},
requiresNotLogin: function() {
if (!Authentication.currentUser) return
m.route.set('/')
},
}
Authentication.updateToken(localStorage.getItem(storageName))
window.Authentication = Authentication
module.exports = Authentication

View file

@ -0,0 +1,72 @@
const m = require('mithril')
const videos = require('./videos')
const Authentication = require('./authentication')
const lang = require('./lang')
const Menu = {
oninit: function(vnode) {
this.currentActive = 'home'
this.loading = false
if (!videos.Tree.length) {
videos.refreshTree()
}
this.onbeforeupdate()
},
onbeforeupdate: function() {
videos.calculateActiveBranches()
let currentPath = m.route.get()
},
logOut: function() {
Authentication.clearToken()
m.route.set('/')
},
view: function() {
let tree = videos.Tree
let last = videos.Tree[videos.Tree.length - 1]
let hasId = m.route.param('id')
return [
m('nav', [
m('h4', m(m.route.Link, { href: '/' }, lang.header_title /* Filadelfia archival center */)),
m('a.link.changelang', { onclick: lang.langtoggle }, lang.lang_current),
Authentication.currentUser?.rank >= 100
? m(m.route.Link, { class: 'upload', href: '/upload' }, lang.upload_goto) // Upload
: null,
Authentication.currentUser
? m(m.route.Link, { class: 'link', href: '/logout' }, lang.logout)
: m(m.route.Link, { class: 'upload', href: '/login' }, lang.login_submit) // Upload,
// m('button.logout', { onclick: this.logOut }, lang.header_logout), // Log out
]),
videos.error
? m('div.error', { onclick: videos.refreshTree }, [
videos.error, m('br'), 'Click here to try again'
])
: null,
!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 : '' ].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,
]
},
}
module.exports = Menu

View file

@ -0,0 +1,68 @@
const m = require('mithril')
const HoldButton = {
oninit: function(vnode) {
this.timer = null
this.holding = false
this.windowBlur = () => {
this.timerStop()
m.redraw()
}
window.addEventListener('blur', this.windowBlur)
},
onremove: function(vnode) {
this.timerStop()
window.removeEventListener('blur', this.windowBlur)
},
keydown(vnode, e) {
if (e.key === " " || e.key === "Enter" || e.key === "Spacebar") {
this.timerStart(vnode)
m.redraw()
}
},
keyup(e) {
if (e.key === " " || e.key === "Enter" || e.key === "Spacebar") {
this.timerStop()
m.redraw()
}
},
timerStart(vnode) {
if (this.timer) return
this.timer = setTimeout(this.timerConfirmed.bind(this), 2000, vnode)
this.holding = true
},
timerStop() {
clearTimeout(this.timer)
this.timer = null
this.holding = false
},
timerConfirmed(vnode) {
this.timerStop()
vnode.attrs.onclick()
m.redraw()
},
view: function(vnode) {
return m('button.holdbutton', {
style: '--hold-bg: var(--error-bg); --hold-color: var(--error); --hold-fill: var(--error);',
hidden: vnode.attrs.hidden,
class: (vnode.attrs.class || '')
+ (this.holding ? ' holdbutton-active' : ''),
onpointerdown: this.timerStart.bind(this, vnode),
onpointerup: this.timerStop.bind(this),
onpointerleave: this.timerStop.bind(this),
onkeydown: this.keydown.bind(this, vnode),
onkeyup: this.keyup.bind(this),
onclick: e => { return false },
}, m('div.inner', vnode.attrs.text))
},
}
module.exports = HoldButton

View file

@ -0,0 +1,62 @@
const m = require('mithril')
const Authentication = require('./authentication')
const Header = require('./header')
const Login = require('./page_login')
const Logout = require('./page_logout')
const Browse = require('./page_browse')
const Upload = require('./page_upload')
const Article = require('./page_article')
window.m = m
let css = [
'/assets/app.css?v=2',
'/assets/tempus-dominus.css',
'/assets/fontawesome.css',
]
for (let item of css) {
var fileref = document.createElement("link");
fileref.setAttribute("rel", "stylesheet");
fileref.setAttribute("type", "text/css");
fileref.setAttribute("href", item);
document.head.appendChild(fileref)
}
m.route.setOrig = m.route.set
m.route.set = function(path, data, options){
m.route.setOrig(path, data, options)
window.scrollTo(0, 0)
}
m.route.linkOrig = m.route.link
m.route.link = function(vnode){
m.route.linkOrig(vnode)
window.scrollTo(0, 0)
}
m.route.prefix = ''
const allRoutes = {
'/': Browse,
'/login': Login,
'/logout': Logout,
'/upload': Upload,
'/:year': Browse,
'/:year/:month': Browse,
'/:year/:month/:path': Article,
}
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.
/*
* imgsupport.js from leechy/imgsupport
*/
const AVIF = new Image();
AVIF.onload = AVIF.onerror = function () {
window.supportsavif = (AVIF.height === 2)
document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
m.mount(document.getElementById('header'), Header)
m.route(document.getElementById('main'), '/', allRoutes)
}
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAABAA0ABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgAPkgIaDQgMgkf8AAAQAAAr3A=';

View file

@ -0,0 +1,143 @@
const m = require('mithril')
const api = require('./api')
const tempus = require('@eonasdan/tempus-dominus')
const tempusLocalization = {
locale: 'is',
startOfTheWeek: 0,
hourCycle: 'h23',
dateFormats: {
LTS: 'H:mm:ss',
LT: 'H:mm',
L: 'dd.MM.yyyy',
LL: 'd [de] MMMM [de] yyyy',
LLL: '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
this.preview = null
},
onremove: function(vnode) {
if (this.subscription) this.subscription.unsubscribe()
if (this.tempus) {
this.tempus.dispose()
this.tempus = null
}
if (this.preview) {
this.preview.clear()
}
},
onupdate: function(vnode) {
if (this.tempus && vnode.attrs.form[vnode.attrs.formKey]) {
if (vnode.attrs.form[vnode.attrs.formKey].getTime() !== this.tempus.viewDate?.getTime()) {
this.tempus.dates.setValue(new tempus.DateTime(vnode.attrs.form[vnode.attrs.formKey]))
}
}
},
imageChanged: function(vnode, e) {
let file = e.currentTarget.files?.[0] || null
this.updateValue(vnode, file)
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)
},
}
}
},
updateValue: function(vnode, value) {
vnode.attrs.form[vnode.attrs.formKey] = value
if (typeof(vnode.attrs.oninput) === 'function') {
vnode.attrs.oninput(vnode.attrs.form[vnode.attrs.formKey])
}
return false
},
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) => this.updateValue(vnode, 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, {
localization: tempusLocalization,
})
this.tempus.dates.setValue(new tempus.DateTime(vnode.attrs.form[vnode.attrs.formKey]))
this.subscription = this.tempus.subscribe(tempus.Namespace.events.change, (e) => {
this.updateValue(vnode, e.date)
});
},
}),
m('button.fal.fa-calendar', {
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,
type: vnode.attrs.type || 'text',
value: vnode.attrs.form[vnode.attrs.formKey],
oninput: (e) => this.updateValue(vnode, e.currentTarget.value),
})
}
},
view: function(vnode) {
return [
vnode.attrs.label ? m('label', vnode.attrs.label) : null,
this.getInput(vnode),
]
},
}
module.exports = Input

View file

@ -0,0 +1,156 @@
const out = {
currentlang: 'en',
}
const i18n = {
lang_change_long: ['Skipta yfir á íslensku',
'Change to english'],
lang_current: ['en',
'is'],
header_title: ['Fíladelfia archival center',
'Fíladelfia myndhvelfing'],
header_logout: ['Log out',
'Skrá út'],
title: ['Title',
'Titill'],
date: ['Date',
'Dagsetning'],
language: ['EN',
'IS'],
upload_goto: ['Upload',
'Upphlaða'],
login_error: ['Error while logging in: {0}',
'Villa við innskráningu: {0}'],
login_error_auth: ['Unknown error from server. Try again later.',
'Óþekkt villa frá vefþjóni. Reyndu aftur seinna.'],
login_missing_email: ['Email is missing',
'Email eða nafn vantar'],
login_missing_password:['Password is missing',
'Lykilorð vantar'],
login_email: ['Email or name',
'Email eða nafn'],
login_password: ['Password',
'Lykilorð'],
login_submit: ['Log in',
'Skrá inn'],
logout: ['Log out',
'Skrá út'],
upload_missing_title: ['Title is missing',
'Titill vantar'],
upload_missing_date: ['Date is missing',
'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}',
'Mynd eftir {0} frá {1}'],
api_down: ['No internet or browser blocked the request.',
'Ekkert net eða vafri blockaði fyrirspurn.'],
edit: ['Edit',
'Breyta'],
delete: ['Delete',
'Eyða'],
article_speaker: ['Speaker',
'Ræðumaður'],
delete_error: ['Error while deleting: {0}',
'Villa við að eyða efni: {0}'],
article_error: ['Error while saving: {0}',
'Villa við að vista: {0}'],
api_banner_upload: ['Uploading banner image',
'Er að senda mynd'],
api_banner_generate:['Generating preview, testing quality {0}%',
'Bý til forsíðumynd, prufa {0}% gæði'],
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 = {
'en': 0,
'is': 1,
}
const regexNumber = new RegExp('^\\d+$')
out.printdate = function(date) {
let day = date.getDate().toString()
if (out.currentlang === 'en') {
let last = day[day.length - 1]
if (last === '1') {
day += 'st'
} else if (last === '2') {
day += 'nd'
} else if (last === '3') {
day += 'rd'
} else {
day += 'th'
}
} else {
day += '.'
}
return `${day} ${out.months[date.getMonth() + 1]} ${date.getFullYear()}, ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
out.langset = function(lang) {
out.currentlang = lang
let index = langs[lang]
for (let key of Object.keys(i18n)) {
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.langtoggle = function() {
out.langset(out.currentlang === 'en' ? 'is' : 'en')
return false
}
out.format = function(str, ...args) {
return out.mformat(str, ...args).join('')
}
out.mformat = function(str, ...args) {
let split = (str || '').split(/\{|\}/)
return split.map(function(item) {
if (regexNumber.test(item)) {
return args[Number(item)] || item
}
return item
})
}
out.langset('is')
module.exports = out

View file

@ -0,0 +1,265 @@
const m = require('mithril')
const api = require('./api')
const Authentication = require('./authentication')
const Input = require('./input')
const lang = require('./lang')
const videos = require('./videos')
const HoldButton = require('./holdbutton')
const Article = {
oninit: function(vnode) {
this.error = ''
this.path = ''
this.data = null
this.editing = false
this.cacheImage = null
this.form = {
title: 'Sunnudagssamkoma',
date: new Date(),
banner: null,
metadata: {
speaker: '',
},
}
this.onbeforeupdate(vnode)
},
onbeforeupdate: function(vnode) {
let path = m.route.param('year').padStart(4, '0')
+ '-' + m.route.param('month').padStart(2, '0')
+ '-' + m.route.param('path')
if (this.path === path) return
this.fetchArticle(vnode, path)
},
fetchArticle: function(vnode, path) {
this.error = ''
this.data = null
this.path = path
this.cacheImage = null
this.editing = false
api.sendRequest({
method: 'GET',
url: '/api/articles/' + this.path,
})
.then((result) => {
this.data = result.article
this.gotArticle(vnode)
}, (err) => {
this.error = err.message
})
},
gotArticle: function(vnode) {
if (!this.data) {
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.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.error) return false
let promise = Promise.resolve()
if (Authentication.currentUser && (typeof(this.form.banner) !== 'string' && this.cacheImage?.file !== this.form.banner)) {
promise = api.sendRequest({
method: 'GET',
url: '/api/auth/uploadToken',
})
.then(res => {
return api.uploadBanner(this.form.banner, res)
.then(imageData => {
this.cacheImage = imageData
if (this.data.banner_path) {
api.sendRequest({
method: 'DELETE',
url: res.delete + this.data.banner_path.slice(this.data.banner_path.lastIndexOf('/') + 1) + '?token=' + res.token,
}).catch(err => console.error(err))
}
return res
})
})
}
promise.then(() => {
return api.sendRequest({
method: 'PUT',
url: '/api/auth/articles/' + this.data.id,
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: null,
banner: this.cacheImage ? {
filename: this.cacheImage.medium.filename,
path: this.cacheImage.medium.path,
type: 'image/avif',
size: this.cacheImage.size,
preview: {
base64: this.cacheImage.preview.base64,
},
} : null,
},
})
})
.then(res => {
this.fetchArticle(vnode, this.path)
})
.catch((error) => {
console.error(error)
this.error = lang.format(lang.article_error, error.message) // Error while saving:
})
return false
},
deletevideo: function(vnode) {
api.sendRequest({
method: 'GET',
url: '/api/auth/uploadToken',
body: this.form,
})
.then(res => {
return Promise.all([
this.data.banner_path ? api.sendRequest({
method: 'DELETE',
url: res.delete + this.data.banner_path.slice(this.data.banner_path.lastIndexOf('/') + 1) + '?token=' + res.token,
}).catch(err => console.error(err)) : Promise.resolve(),
this.data.media_path ? api.sendRequest({
method: 'DELETE',
url: res.delete + this.data.media_path.slice(this.data.media_path.lastIndexOf('/') + 1) + '?token=' + res.token,
}).catch(err => console.error(err)) : Promise.resolve(),
])
})
.then(() => {
return api.sendRequest({
method: 'DELETE',
url: '/api/auth/articles/' + this.data.id,
})
})
.then(res => {
videos.removeArticle(this.data.id)
m.route.set('/')
})
.catch((error) => {
if (!error) return
this.error = lang.format(lang.delete_error, error.message) // Error while uploading:
})
},
view: function(vnode) {
return [
api.loading && !this.data ? m('div.loading-spinner') : null,
this.data ? [
this.data.media_path
? [
m('.player', [
m('video', {
crossorigin: '',
controls: true,
preload: 'none',
poster: this.data.banner_path || '/assets/placeholder.avif',
}, [
m('source', {
src: this.data.media_path
})
]),
]),
]
: null,
this.editing
? m('form.article', {
onsubmit: this.updatevideo.bind(this, vnode),
}, [
m('div.form-row', [
m('div.form-columns', [
m(Input, {
label: '',
type: 'file',
accept: 'image/*',
utility: 'image',
form: this.form,
formKey: 'banner',
}),
]),
m('div.form-columns.article-name', [
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',
}),
this.error ? m('div.full-error', this.error) : null,
m('div.row', [
m('input.spinner', {
hidden: api.loading,
type: 'submit',
value: lang.edit,
}),
m('div.filler', {
hidden: api.loading,
}),
api.loading ? m('div.loading-spinner') : null,
m(HoldButton, {
class: 'button spinner',
onclick: () => this.deletevideo(vnode),
hidden: api.loading,
text: lang.delete,
}),
]),
])
: m('div.article', [
m('h1', this.data.name),
m('p', [
lang.printdate(this.form.date),
]),
m('div.table', [
m('div.table-row', [
m('div.table-item', lang.article_speaker),
m('div.table-item', this.data.content.speaker || '...'),
]),
]),
Authentication.currentUser?.rank >= 10
? m('button.button', { onclick: () => this.editing = true },lang.edit)
: null,
]),
] : [
this.error ? m('div.full-error', this.error) : null,
],
]
},
}
module.exports = Article

View file

@ -0,0 +1,62 @@
const m = require('mithril')
const api = require('./api')
const Authentication = require('./authentication')
const videos = require('./videos')
const lang = require('./lang')
const Browse = {
oninit: function(vnode) {
},
mArticles: function(vnode, articles) {
return articles.map(article => {
return m(m.route.Link, {
href: ['', article.publish_at.getFullYear(), article.publish_at.getMonth() + 1, article.path_short].join('/'),
style: article.avif_preview ? `background-image: url('${article.avif_preview}')` : null,
}, [
m('span', lang.printdate(article.publish_at)),
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) {
let articles = videos.month?.videos || videos.year?.videos || videos.Articles
return [
api.loading ? m('div.loading-spinner') : null,
videos.error
? m('div.full-error', { onclick: videos.refreshTree }, [
videos.error, m('br'), 'Click here to try again'
])
: 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(-1).map(year => {
return [
m('.gallery-year', year.title),
this.mMonth(vnode, year),
]
})
: null
]),
]
},
}
module.exports = Browse

View file

@ -0,0 +1,86 @@
const m = require('mithril')
const Authentication = require('./authentication')
const api = require('./api')
const Input = require('./input')
const lang = require('./lang')
const videos = require('./videos')
const Login = {
oninit: function(vnode) {
this.redirect = vnode.attrs.redirect || ''
Authentication.requiresNotLogin()
this.error = ''
this.form = {
email: '',
password: '',
}
},
loginuser: function(vnode, e) {
e.preventDefault()
this.error = ''
if (!this.form.email) this.error = lang.login_missing_email // Email is missing
if (!this.form.password) this.error = lang.login_missing_password // Password is missing
if (this.error) return false
api.sendRequest({
method: 'POST',
url: '/api/authentication/login',
body: this.form,
})
.then((result) => {
if (!result.token) return Promise.reject(new Error(lang.login_error_auth)) // Unknown error from server. Try again later
Authentication.updateToken(result.token)
m.route.set(this.redirect || '/')
videos.refreshTree()
})
.catch((error) => {
this.error = lang.format(lang.login_error, error.message) // Error while logging in:
this.form.password = ''
})
return false
},
view: function(vnode) {
return [
m('div.page.page-login', [
m('div.modal', [
m('form', {
onsubmit: this.loginuser.bind(this, vnode),
}, [
m('h3', lang.header_title /* Filadelfia archival center */),
this.error ? m('p.error', this.error) : null,
m(Input, {
label: lang.login_email, // Email or name
form: this.form,
formKey: 'email',
}),
m(Input, {
label: lang.login_password, // Password
type: 'password',
form: this.form,
formKey: 'password',
}),
m('input.spinner', {
hidden: api.loading,
type: 'submit',
value: lang.login_submit, // Log in
}),
api.loading ? m('div.loading-spinner') : null,
]),
]),
m('footer', lang.mformat(
lang.unsplash, // 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/photos/landscape-photo-of-mountain-covered-with-snow-FN-cedy6NHA?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Unsplash'),
)),
]),
]
},
}
module.exports = Login

View file

@ -0,0 +1,15 @@
const m = require('mithril')
const Authentication = require('./authentication')
const Logout = {
oninit: function(vnode) {
Authentication.clearToken()
m.route.set(vnode.attrs.redirect || '/')
},
view: function(vnode) {
return []
},
}
module.exports = Logout

View file

@ -0,0 +1,227 @@
const m = require('mithril')
const Authentication = require('./authentication')
const api = require('./api')
const Input = require('./input')
const lang = require('./lang')
const videos = require('./videos')
const Upload = {
oninit: function(vnode) {
Authentication.requiresLogin()
this.error = ''
let d = new Date()
d.setDate(d.getDate() - d.getDay())
d.setHours(11)
d.setMinutes(0)
d.setSeconds(0)
d.setMilliseconds(0)
this.cacheVideo = null
this.cacheImage = null
this.uploading = null
this.bannerStatus = null
this.form = {
title: 'Sunnudagssamkoma',
date: d,
file: null,
banner: null,
metadata: {
speaker: '',
},
}
},
uploadvideo: function(vnode, e) {
this.error = ''
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.banner) this.error = lang.upload_missing_banner // Poster image missing
if (this.error) return false
api.sendRequest({
method: 'GET',
url: '/api/auth/uploadToken',
})
.then(res => {
if (this.cacheImage?.file === this.form.banner) {
return this.cacheImage
}
return api.uploadBanner(this.form.banner, res, (status) => {
this.bannerStatus = status
m.redraw()
})
.then(imageData => {
this.cacheImage = imageData
return res
})
})
.then(res => {
if (this.cacheVideo?.file === this.form.file) {
return this.cacheVideo
}
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.cacheVideo = {
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,
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.cacheImage.size,
preview: {
base64: this.cacheImage.preview.base64,
},
},
},
})
})
.then(res => {
videos.refreshTree()
m.route.set('/')
})
.catch((error) => {
this.bannerStatus = null
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
},
filechanged(file) {
if (!file || !file.name) return
let matches = /^(\d{4})-(\d\d)-(\d\d)_(\d\d)-(\d\d)/.exec(file.name)
if (!matches) return
var date = new Date(matches.slice(1, 4).join('-') + 'T' + matches.slice(4,6).join(':') + ':00')
if (isNaN(date.getTime())) return
if (date.getMinutes() >= 30 || date.getHours() === 10) {
date.setHours(date.getHours() + 1)
}
date.setMinutes(0)
this.form.date = date
},
view: function(vnode) {
return [
m('div.page.page-upload', [
m('div.modal', [
m('form', {
onsubmit: this.uploadvideo.bind(this, vnode),
}, [
m('h3', 'Upload new video'),
this.error ? m('p.error', this.error) : null,
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(Input, {
label: 'Video',
type: 'file',
accept: '.webm',
utility: 'file',
button: 'fa-video',
form: this.form,
formKey: 'file',
oninput: (file) => this.filechanged(file),
}),
m(Input, {
label: 'Mynd',
type: 'file',
accept: 'image/*',
utility: 'image',
form: this.form,
formKey: 'banner',
}),
m('p.separator', 'Optional'),
m(Input, {
label: 'Speaker',
form: this.form.metadata,
formKey: 'speaker',
}),
m('input.spinner', {
hidden: api.loading,
type: 'submit',
value: 'Begin upload',
}),
api.loading ? m('div.loading-spinner') : null,
this.bannerStatus ? [
m('p', this.bannerStatus),
m('.loading-bar', { style: `--progress: 0%` }),
] : 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/@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'),
)),
]),
]
},
}
module.exports = Upload

View file

@ -0,0 +1,120 @@
const m = require('mithril')
const api = require('./api')
const Tree = []
const Articles = []
exports.Tree = Tree
exports.Articles = Articles
exports.loading = false
exports.error = ''
exports.year = null
exports.month = null
const matcher = /\/(\d+)(\/\d+)?/
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() === '/') {
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 removeArticle(id) {
let index = Articles.findIndex(article => article.id === id)
if (index >= 0) {
Articles.splice(index, 1)
rebuildTree()
}
}
function refreshTree() {
exports.error = ''
if (exports.loading) return Promise.resolve()
exports.loading = true
m.redraw()
return api.sendRequest({
method: 'GET',
url: '/api/articles',
})
.then(result => {
result.videos.forEach(video => {
video.publish_at = new Date(video.publish_at)
video.path_short = video.path.split('-')[2]
})
Articles.splice(0, Articles.length)
Articles.push.apply(Articles, result.videos)
rebuildTree()
}, err => {
exports.error = 'Error fetching videos: ' + err.message
})
.then(() => {
exports.loading = false
m.redraw()
})
}
exports.removeArticle = removeArticle
exports.rebuildTree = rebuildTree
exports.refreshTree = refreshTree
exports.calculateActiveBranches = calculateActiveBranches

1
filadelfia_archive/base Symbolic link
View file

@ -0,0 +1 @@
../base

View file

@ -0,0 +1,13 @@
{
"scripts": {
"build": "esbuild app/index.js --bundle --outfile=public/assets/app.js"
},
"dependencies": {
"@eonasdan/tempus-dominus": "^6.7.19",
"@popperjs/core": "^2.11.8",
"eltro": "^1.4.4",
"esbuild": "^0.19.5",
"mithril": "^2.2.2",
"service-core": "^3.0.0-beta.17"
}
}

View file

@ -0,0 +1,38 @@
import fs from 'fs'
import { pathToFileURL } from 'url'
import config from './base/config.mjs'
export function start(http, port, ctx) {
config.sources[1].store = ctx.config
return import('./api/server.mjs')
.then(function(module) {
let server = new module.default(http, port, ctx)
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()
})
})
}

View file

@ -0,0 +1,66 @@
{
"name": "filadelfia_archive",
"version": "1.0.8",
"port": 4130,
"description": "Filadelfia archive",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"start": "node index.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"build:prod": "asbundle app/index.js public/assets/app.js",
"build": "esbuild app/index.js --bundle --outfile=public/assets/app.js",
"dev:build": "eltro --watch build --npm build",
"dev:server": "eltro --watch server --npm server",
"dev:build:old": "npm-watch build",
"dev:server:old": "npm-watch server",
"server": "node index.mjs | bunyan"
},
"watch": {
"server": {
"patterns": [
"api",
"base",
"../base"
],
"extensions": "js,mjs",
"quiet": true,
"inherit": true
},
"build": {
"patterns": [
"app"
],
"extensions": "js",
"quiet": true,
"inherit": true
}
},
"repository": {
"type": "git",
"url": "https://git.nfp.is/nfp/nfp_sites.git"
},
"author": "Jonatan Nilsson",
"license": "WTFPL",
"bugs": {
"url": "https://git.nfp.is/nfp/nfp_sites/issues"
},
"homepage": "https://git.nfp.is/nfp/nfp_sites",
"dependencies": {
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.2",
"formidable": "^1.2.6",
"msnodesqlv8": "^4.1.1",
"nconf-lite": "^2.0.0"
},
"devDependencies": {
"@eonasdan/tempus-dominus": "^6.7.19",
"@popperjs/core": "^2.11.8",
"eltro": "^1.4.4",
"esbuild": "^0.19.5",
"mithril": "^2.2.2",
"service-core": "^3.0.0-beta.17"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="26.656599mm"
height="26.65659mm"
viewBox="0 0 26.656599 26.65659"
version="1.1"
id="svg1"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showguides="true"
showgrid="false"
inkscape:zoom="4.2083894"
inkscape:cx="-36.712382"
inkscape:cy="66.890197"
inkscape:window-width="2560"
inkscape:window-height="1377"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<sodipodi:guide
position="21.496619,297"
orientation="0,-1"
id="guide1"
inkscape:locked="false" />
<inkscape:grid
id="grid2"
units="mm"
originx="-90.44421"
originy="-127.62701"
spacingx="0.99999998"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="false" />
<sodipodi:guide
position="-2.3731349e-07,282.22545"
orientation="1,0"
id="guide2"
inkscape:locked="false" />
<sodipodi:guide
position="26.656599,279.39628"
orientation="1,0"
id="guide3"
inkscape:locked="false" />
<sodipodi:guide
position="8.8910958,270.34341"
orientation="0,-1"
id="guide4"
inkscape:locked="false" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.444211,-127.62701)">
<path
id="path1"
d="M 90.444211,154.2836 H 117.10081 V 127.62701 H 90.444211 Z"
style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.159661" />
<path
id="path224"
d="m 103.77216,127.62701 c 7.36141,0 13.32865,5.96724 13.32865,13.3283 0,7.36106 -5.96724,13.32829 -13.32865,13.32829 -7.360713,0 -13.327947,-5.96723 -13.327947,-13.32829 0,-7.36106 5.967234,-13.3283 13.327947,-13.3283"
style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
<path
id="path225"
d="m 103.77216,128.3629 c -6.954663,0 -12.592405,5.63774 -12.592405,12.5924 0,6.95466 5.637742,12.5924 12.592405,12.5924 6.95501,0 12.59275,-5.63774 12.59275,-12.5924 0,-6.95466 -5.63774,-12.5924 -12.59275,-12.5924"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
<path
id="path226"
d="m 103.74393,129.68123 c 6.19478,0 11.21586,5.02179 11.21586,11.21622 0,6.19477 -5.02108,11.21621 -11.21586,11.21621 -6.19442,0 -11.216219,-5.02144 -11.216219,-11.21621 0,-6.19443 5.021799,-11.21622 11.216219,-11.21622"
style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
<path
id="path227"
d="m 102.98229,131.02708 v 5.04931 h 3.34469 c 0.56162,-1.17581 2.25918,-2.76684 3.26989,-3.64631 l -0.45332,1.06045 c 0.0794,-0.067 -0.70238,1.29046 0.009,2.81799 0.63217,1.35713 1.07032,2.09867 1.2894,3.73097 0.35278,-0.94015 0.56691,-1.62736 1.04034,-2.17628 0.27552,-0.31856 0.31538,-0.93698 0.27023,-1.63971 0.38488,0.42192 0.93556,2.09126 0.82726,3.4364 -0.0536,0.67134 0.10936,2.83175 -2.17523,3.46358 -0.36794,1.33879 -1.24283,2.57704 -2.68993,3.37961 -1.78646,0.9906 -2.58057,0.20249 -3.81987,2.15512 -0.0596,-0.33691 -0.0677,-0.71932 -0.0773,-0.99554 -0.12065,-0.1584 -0.20885,-0.26988 -0.26494,-0.57397 -0.0603,-0.16052 0.001,-0.51082 0.26423,-0.57256 -0.0127,-1.36984 1.36384,-1.15641 1.60585,-1.95474 0.52422,-1.24848 -1.57445,-1.39877 -2.44052,-2.12513 v 6.70877 c 0.30127,0.17286 0.58526,0.44062 0.84772,0.85478 1.29082,-2.03412 3.09175,-0.54151 4.95265,-1.57339 l 1.83021,0.63077 -0.1337,0.1076 c -2.52412,1.23119 -4.95406,-0.67381 -6.70772,2.0902 -1.78223,-2.80846 -4.264368,-0.79586 -6.832603,-2.15229 l 1.937463,-0.67628 c 0.670632,0.37183 1.33315,0.41593 1.96744,0.41522 v -10.6306 h -5.036613 c -0.20179,0.0473 -0.21449,-2.18899 0,-2.13713 l 5.036613,0.002 -0.001,-5.04931 c -0.0483,-0.21449 2.18969,-0.19614 2.1396,0 z m 3.31576,7.18397 h -1.01177 c 0.004,0.32773 10e-4,0.62194 10e-4,0.85901 0,0.78317 1.59526,1.75789 2.0387,1.95827 0.14958,-1.2125 -0.12029,-1.60549 -0.8128,-2.50049 -0.0832,-0.10689 -0.15451,-0.21167 -0.21519,-0.31679"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100.749" height="100.749" viewBox="0 0 26.657 26.657"><path d="M103.772 127.627c7.362 0 13.329 5.967 13.329 13.328s-5.967 13.329-13.329 13.329c-7.36 0-13.328-5.968-13.328-13.329 0-7.36 5.967-13.328 13.328-13.328" style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/><path d="M103.772 128.363c-6.955 0-12.592 5.638-12.592 12.592 0 6.955 5.637 12.593 12.592 12.593 6.955 0 12.593-5.638 12.593-12.593 0-6.954-5.638-12.592-12.593-12.592" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/><path d="M103.744 129.681c6.195 0 11.216 5.022 11.216 11.216 0 6.195-5.021 11.217-11.216 11.217-6.194 0-11.216-5.022-11.216-11.217 0-6.194 5.022-11.216 11.216-11.216" style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/><path d="M102.982 131.027v5.05h3.345c.562-1.176 2.26-2.767 3.27-3.647l-.453 1.06c.079-.066-.703 1.291.009 2.819.632 1.357 1.07 2.098 1.289 3.73.353-.94.567-1.627 1.04-2.176.276-.318.316-.937.27-1.64.385.422.936 2.092.828 3.437-.054.671.11 2.832-2.175 3.463-.368 1.34-1.243 2.578-2.69 3.38-1.787.99-2.581.203-3.82 2.155-.06-.337-.068-.72-.078-.995-.12-.159-.208-.27-.264-.574-.06-.16 0-.511.264-.573-.013-1.37 1.364-1.156 1.606-1.955.524-1.248-1.575-1.398-2.44-2.125v6.709c.3.173.584.44.847.855 1.29-2.034 3.092-.542 4.952-1.574l1.83.631-.133.108c-2.524 1.231-4.954-.674-6.708 2.09-1.782-2.808-4.264-.796-6.832-2.152l1.937-.677c.67.372 1.333.416 1.968.416v-10.63h-5.037c-.202.046-.215-2.19 0-2.138l5.037.002-.001-5.05c-.049-.214 2.19-.196 2.14 0zm3.316 7.184h-1.012c.004.328.001.622.001.86 0 .782 1.596 1.757 2.039 1.957.15-1.212-.12-1.605-.813-2.5a2.774 2.774 0 0 1-.215-.317" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

View file

@ -0,0 +1,700 @@
.visually-hidden, .tempus-dominus-widget [data-action]::after {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.tempus-dominus-widget {
list-style: none;
padding: 4px;
width: 19rem;
border-radius: 4px;
display: none;
z-index: 9999;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
}
.tempus-dominus-widget.calendarWeeks {
width: 21rem;
}
.tempus-dominus-widget.calendarWeeks .date-container-days {
grid-auto-columns: 12.5%;
grid-template-areas: "a a a a a a a a";
}
.tempus-dominus-widget [data-action] {
cursor: pointer;
}
.tempus-dominus-widget [data-action]::after {
content: attr(title);
}
.tempus-dominus-widget [data-action].disabled, .tempus-dominus-widget [data-action].disabled:hover {
background: none;
cursor: not-allowed;
}
.tempus-dominus-widget .arrow {
display: none;
}
.tempus-dominus-widget.show {
display: block;
}
.tempus-dominus-widget.show.date-container {
min-height: 315px;
}
.tempus-dominus-widget.show.time-container {
min-height: 217px;
}
.tempus-dominus-widget .td-collapse:not(.show) {
display: none;
}
.tempus-dominus-widget .td-collapsing {
height: 0;
overflow: hidden;
transition: height 0.35s ease;
}
@media (min-width: 576px) {
.tempus-dominus-widget.timepicker-sbs {
width: 38em;
}
}
@media (min-width: 768px) {
.tempus-dominus-widget.timepicker-sbs {
width: 38em;
}
}
@media (min-width: 992px) {
.tempus-dominus-widget.timepicker-sbs {
width: 38em;
}
}
.tempus-dominus-widget.timepicker-sbs .td-row {
display: flex;
}
.tempus-dominus-widget.timepicker-sbs .td-row .td-half {
flex: 0 0 auto;
width: 50%;
}
.tempus-dominus-widget div[data-action]:active {
box-shadow: none;
}
.tempus-dominus-widget .timepicker-hour,
.tempus-dominus-widget .timepicker-minute,
.tempus-dominus-widget .timepicker-second {
width: 54px;
font-weight: bold;
font-size: 1.2em;
margin: 0;
}
.tempus-dominus-widget button[data-action] {
padding: 6px;
}
.tempus-dominus-widget .toggleMeridiem {
text-align: center;
height: 38px;
}
.tempus-dominus-widget .calendar-header {
display: grid;
grid-template-areas: "a a a";
margin-bottom: 10px;
font-weight: bold;
}
.tempus-dominus-widget .calendar-header .next {
text-align: right;
padding-right: 10px;
}
.tempus-dominus-widget .calendar-header .previous {
text-align: left;
padding-left: 10px;
}
.tempus-dominus-widget .calendar-header .picker-switch {
text-align: center;
}
.tempus-dominus-widget .toolbar {
display: grid;
grid-auto-flow: column;
grid-auto-rows: 40px;
}
.tempus-dominus-widget .toolbar div {
border-radius: 999px;
align-items: center;
justify-content: center;
box-sizing: border-box;
display: flex;
}
.tempus-dominus-widget .date-container-days {
display: grid;
grid-template-areas: "a a a a a a a";
grid-auto-rows: 40px;
grid-auto-columns: 14.2857142857%;
}
.tempus-dominus-widget .date-container-days .range-in {
background-color: #01419e !important;
border: none;
border-radius: 0 !important;
box-shadow: -5px 0 0 #01419e, 5px 0 0 #01419e;
}
.tempus-dominus-widget .date-container-days .range-end {
border-radius: 0 50px 50px 0 !important;
}
.tempus-dominus-widget .date-container-days .range-start {
border-radius: 50px 0 0 50px !important;
}
.tempus-dominus-widget .date-container-days .dow {
align-items: center;
justify-content: center;
text-align: center;
}
.tempus-dominus-widget .date-container-days .cw {
width: 90%;
height: 90%;
align-items: center;
justify-content: center;
display: flex;
font-size: 0.8em;
line-height: 20px;
cursor: default;
}
.tempus-dominus-widget .date-container-decades,
.tempus-dominus-widget .date-container-years,
.tempus-dominus-widget .date-container-months {
display: grid;
grid-template-areas: "a a a";
grid-auto-rows: calc((19rem - 8px) / 7);
}
.tempus-dominus-widget .time-container-hour,
.tempus-dominus-widget .time-container-minute,
.tempus-dominus-widget .time-container-second {
display: grid;
grid-template-areas: "a a a a";
grid-auto-rows: calc((19rem - 8px) / 7);
}
.tempus-dominus-widget .time-container-clock {
display: grid;
grid-auto-rows: calc((19rem - 8px) / 7);
}
.tempus-dominus-widget .time-container-clock .no-highlight {
width: 90%;
height: 90%;
align-items: center;
justify-content: center;
display: flex;
}
.tempus-dominus-widget .date-container-decades div:not(.no-highlight),
.tempus-dominus-widget .date-container-years div:not(.no-highlight),
.tempus-dominus-widget .date-container-months div:not(.no-highlight),
.tempus-dominus-widget .date-container-days div:not(.no-highlight),
.tempus-dominus-widget .time-container-clock div:not(.no-highlight),
.tempus-dominus-widget .time-container-hour div:not(.no-highlight),
.tempus-dominus-widget .time-container-minute div:not(.no-highlight),
.tempus-dominus-widget .time-container-second div:not(.no-highlight) {
width: 90%;
height: 90%;
border-radius: 999px;
align-items: center;
justify-content: center;
box-sizing: border-box;
display: flex;
}
.tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled, .tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled,
.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled,
.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled,
.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled,
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled,
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled,
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled,
.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled:hover {
background: none;
cursor: not-allowed;
}
.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today,
.tempus-dominus-widget .date-container-years div:not(.no-highlight).today,
.tempus-dominus-widget .date-container-months div:not(.no-highlight).today,
.tempus-dominus-widget .date-container-days div:not(.no-highlight).today,
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today,
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today,
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today,
.tempus-dominus-widget .time-container-second div:not(.no-highlight).today {
position: relative;
}
.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today:before,
.tempus-dominus-widget .date-container-years div:not(.no-highlight).today:before,
.tempus-dominus-widget .date-container-months div:not(.no-highlight).today:before,
.tempus-dominus-widget .date-container-days div:not(.no-highlight).today:before,
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today:before,
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today:before,
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today:before,
.tempus-dominus-widget .time-container-second div:not(.no-highlight).today:before {
content: "";
display: inline-block;
border: solid transparent;
border-width: 0 0 7px 7px;
position: absolute;
bottom: 6px;
right: 6px;
}
.tempus-dominus-widget .time-container {
margin-bottom: 0.5rem;
}
.tempus-dominus-widget button {
display: inline-block;
font-weight: 400;
line-height: 1.5;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
padding: 0.375rem 0.75rem;
font-size: 1rem;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds],
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod] {
pointer-events: none;
cursor: default;
}
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds]:hover,
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod]:hover {
background: none;
}
.tempus-dominus-widget.light {
color: #000;
background-color: #fff;
}
.tempus-dominus-widget.light [data-action].disabled, .tempus-dominus-widget.light [data-action].disabled:hover {
color: #6c757d;
}
.tempus-dominus-widget.light .toolbar div:hover {
background: #e9ecef;
}
.tempus-dominus-widget.light .date-container-days .dow {
color: rgba(0, 0, 0, 0.5);
}
.tempus-dominus-widget.light .date-container-days .cw {
color: rgba(0, 0, 0, 0.38);
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight):hover,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight):hover,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight):hover,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight):hover,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight):hover,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight):hover,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight):hover,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight):hover {
background: #e9ecef;
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active,
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight),
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight),
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight),
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active {
background-color: #0d6efd;
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.old, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.new, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.old,
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.new,
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).new {
color: #fff;
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).today:before,
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).today:before,
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).today:before,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.today:before {
border-bottom-color: #fff;
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).new,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).old,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).new,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).old,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).new {
color: rgba(0, 0, 0, 0.38);
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled, .tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled:hover {
color: #6c757d;
}
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).today:before,
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).today:before {
border-bottom-color: #0d6efd;
border-top-color: rgba(0, 0, 0, 0.2);
}
.tempus-dominus-widget.light button {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
.tempus-dominus-widget.dark {
color: #e3e3e3;
background-color: #1b1b1b;
}
.tempus-dominus-widget.dark [data-action].disabled, .tempus-dominus-widget.dark [data-action].disabled:hover {
color: #6c757d;
}
.tempus-dominus-widget.dark .toolbar div:hover {
background: rgb(35, 38, 39);
}
.tempus-dominus-widget.dark .date-container-days .dow {
color: rgba(232, 230, 227, 0.5);
}
.tempus-dominus-widget.dark .date-container-days .range-in {
background-color: #0071c7 !important;
box-shadow: -5px 0 0 #0071c7, 5px 0 0 #0071c7;
}
.tempus-dominus-widget.dark .date-container-days .cw {
color: rgba(232, 230, 227, 0.38);
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight):hover,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight):hover {
background: rgb(35, 38, 39);
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active,
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight),
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight),
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight),
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active {
background-color: #4db2ff;
color: #fff;
text-shadow: 0 -1px 0 rgba(232, 230, 227, 0.25);
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.old, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.new, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.old,
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.new,
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).new {
color: #fff;
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.today:before,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.today:before {
border-bottom-color: #1b1b1b;
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).new,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).old,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).new,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).old,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).new {
color: rgba(232, 230, 227, 0.38);
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled, .tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled:hover,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled:hover {
color: #6c757d;
}
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).today:before,
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).today:before {
border-bottom-color: #4db2ff;
border-top-color: rgba(232, 230, 227, 0.2);
}
.tempus-dominus-widget.dark button {
color: #fff;
background-color: #4db2ff;
border-color: #4db2ff;
}
/*# sourceMappingURL=tempus-dominus.css.map */

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,213 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Filadelfia myndhvelfing</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
<link rel="icon" type="image/png" href="/assets/favicon.png">
<style>
[hidden] { display: none !important; }
:root { --bg: #fff; --bg-component: #f3f7ff; --bg-component-half: #f3f7ff77; --bg-component-alt: #ffd99c; --color: #031131; --color-alt: #7a9ad3; --main: #18597d; --main-fg: #fff; --error: red; --error-bg: hsl(0, 75%, 80%); } /* Box sizing rules */
*, *::before, *::after { box-sizing: border-box;
}
/* Remove default margin */
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; }
body { min-height: 100vh; text-rendering: optimizeSpeed; line-height: 1.5; font-size: 16px; font-family: 'Inter var', Helvetica, Arial, sans-serif; font-variation-settings: "slnt" 0; font-feature-settings: "case", "frac", "tnum", "ss02", "calt", "ccmp", "kern"; background: var(--bg); color: var(--color); display: flex; flex-direction: column; }
.italic { font-variation-settings: "slnt" 10deg; }
input, button, textarea, select { font: inherit; }
h1 { font-size: 1.88rem; }
h2 { font-size: 1.66rem; }
h3 { font-size: 1.44rem; }
h4 { font-size: 1.22rem; }
h5 { font-size: 1.0rem; }
a, a:visited, button { text-decoration: underline; border: none; padding: 0; margin: 0; font-weight: bold; cursor: pointer; color: var(--main); background: transparent; }
h1 { margin-bottom: 1rem; }
#main { flex: 2 1 auto; display: flex; flex-direction: column; }
.page { flex: 2 1 auto; display: flex; flex-direction: column; }
.modal { flex: 2 1 auto; display: flex; flex-direction: column; justify-content: center; align-items: center; }
.modal h3 { text-align: center; margin-bottom: 1rem; }
.error { color: var(--error); }
.modal form { background: var(--bg-component); border-radius: 20px; width: 100%; max-width: 500px; margin: 2rem; padding: 1rem; display: flex; flex-direction: column; }
.loading-spinner { display: inline-block; width: 80px; height: 80px; margin-top: 0.5rem; align-self: center; }
.loading-spinner:after { content: " "; display: block; width: 64px; height: 64px; margin: 8px; border-radius: 50%; border: 6px solid var(--main); border-color: var(--main) transparent var(--main) transparent; animation: loading-spinner 1.2s linear infinite; }
@keyframes loading-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Common components */
.row { display: flex; }
.column { display: flex; flex-direction: column; }
.filler { flex-grow: 2; }
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; flex-wrap: wrap; }
.form-row input { flex: 2 1 auto; }
.form-row > input { width: 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 */
#header { background: var(--bg-component); }
#header nav { display: flex; }
#header nav,
#header .nav { text-align: center; padding: 0.5rem 1rem 0.5rem 0; justify-content: flex-end; flex-wrap: wrap; }
#header nav a,
#header nav button { margin-left: 1rem; }
#header h4 { flex: 2 1 auto; margin: -0.5rem; }
#header h4 a,
#header h4 a:visited { background: url('/assets/logo_nobg.svg') left center no-repeat; background-size: auto calc(3rem - 4px); height: 3rem; display: inline-block; padding-left: 3rem; line-height: 3rem; }
#header .link { font-size: 1.0rem; line-height: 2rem; font-weight: normal; padding: 0 0.5rem; }
#header .changelang { font-size: 1.2rem; }
#header .logout,
#header .upload { padding: 0.25rem 1.5rem; border-radius: 2rem; text-decoration: none; }
#header .logout { background: var(--bg-component-alt); color: var(--color); }
#header .upload { background: var(--main); 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 { padding-top: 0.5rem; }
#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 */
.full-error { background: var(--error-bg); color: var(--color); font-size: 0.8rem; text-align: center; padding: 0.25rem; cursor: pointer; flex: 2 1 auto; display: flex; justify-content: center; align-items: center; }
footer { text-align: center; padding: 1rem; }
footer a { font-size: 0.8rem; }
/* login */
/* upload */
.page-login,
.page-upload { background-image: url('./assets/bg.avif'); background-repeat: no-repeat; background-position: center; background-size: cover; }
.page-login .modal form,
.page-upload .modal form { backdrop-filter: blur(10px); background: var(--bg-component-half); }
/* 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: calc(50vw - 4rem); 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; margin: 0 1rem 1rem; text-align: center; border: 1px solid var(--main); }
.gallery .group a span { align-self: stretch; text-align: center; background: #fffb; }
/* Player */
.player { background: black; text-align: center; margin-bottom: 1rem; }
.player video { margin: 0 auto; width: 1280px; max-width: 100%; aspect-ratio: 16 / 9; }
/* article */
.article { width: 100%; max-width: 1280px; padding: 0.5rem; align-self: center; margin-bottom: 5rem; }
.article .full-error { margin-top: 1rem; }
.article-name { flex: 2 1 auto; margin-left: 1rem; }
.article h1 { margin: 0; }
.article h1,
.article p { padding: 0 0.5rem 0.5rem; }
/* table */
.table { display: grid; column-gap: 0; row-gap: 0; grid-template-columns: minmax(150px, 1.33fr) minmax(150px, 2.33fr); }
.table-row { display: contents; }
.table-row:nth-child(odd) .table-item { background: #f8f6ff; }
.table-item { padding: 0.5rem; }
/* holdbutton */
.holdbutton {
--hold-bg: var(--bg-component);
--hold-color: var(--main);
--hold-fill: var(--main);
--hold-fill-fg: white;
display: inline-block;
background: var(--hold-bg);
position: relative;
padding: 0;
}
.holdbutton .inner {
padding: 0.25rem 1rem;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
height: 100%;
background: var(--hold-color);
background-clip: text;
color: transparent;
}
.holdbutton.holdbutton-active {
background: linear-gradient( var(--hold-fill) , var(--hold-fill)) var(--hold-bg) no-repeat 0 0;
background-size: 0 100%;
animation: stripes 2s linear 1 forwards;
}
.holdbutton.holdbutton-active div.inner {
background: linear-gradient( var(--hold-fill-fg), var(--hold-fill-fg)) var(--hold-color) no-repeat 0 0;
background-size: 0 100%;
animation: stripes 2s linear 1 forwards;
background-clip: text;
}
@keyframes stripes { to { background-size: 100% 100%; } }
@media (pointer:coarse) {
#header .nav { overflow-x: scroll; }
}
@media screen and (max-width: 700px){
.gallery, .gallery-year { margin: 0.25rem; }
.gallery-month { margin: 0rem 0.25rem 0.25rem; }
.gallery .group a { margin: 0 0.25rem 0.25rem; width: calc(50vw - 1rem); font-size: min(3vw, 1rem); }
}
</style>
</head>
<body>
<div id="header"></div>
<main id="main"></main>
<script type="text/javascript" src="/assets/app.js?v=2"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

File diff suppressed because it is too large Load diff

144
heimaerbest/app/combobox.js Normal file
View file

@ -0,0 +1,144 @@
const m = require('mithril')
const util = require('./util')
let activeBox = null
let boxIndex = 1
document.body.addEventListener('click', function() {
activeBox = null
m.redraw()
})
const Combobox = {
oninit: function(vnode) {
this.filtered = []
this.open = false
this.input = null
this.id = boxIndex++
this.onbeforeupdate(vnode)
this.focus = this.onFocus.bind(this, vnode)
},
onbeforeupdate: function(vnode) {
if (!vnode.attrs.value) {
this.filtered = vnode.attrs.items || []
return
}
let val = vnode.attrs.value.toLocaleLowerCase()
this.filtered = vnode.attrs.items.filter(item => {
return item.toLocaleLowerCase().indexOf(val) >= 0
})
},
onInput: function(vnode, e) {
this.smartOpen(vnode)
if (vnode.attrs.oninput) {
vnode.attrs.oninput(e)
}
},
onDone: function(vnode) {
if (vnode.attrs.ondone) {
vnode.attrs.ondone()
}
},
onFocus: function(vnode) {
this.input.focus()
},
selectText: function(vnode, text) {
this.input.value = text
this.onInput(vnode, { target: this.input })
activeBox = null
this.onDone(vnode)
return false
},
onKeyPress: function(vnode, e) {
if (e.key === 'ArrowDown') {
if (e.target.dataset.type === 'input' && e.target.nextElementSibling && e.target.nextElementSibling.childNodes) {
e.target.nextElementSibling.childNodes[0].focus()
} else if (e.target.dataset.type === 'item') {
if (e.target.nextElementSibling) {
e.target.nextElementSibling.focus()
} else {
this.input.focus()
}
}
return false
}
if (e.key === 'ArrowUp') {
if (e.target.dataset.type === 'input' && e.target.nextElementSibling && e.target.nextElementSibling.lastChild) {
e.target.nextElementSibling.scrollTop = e.target.nextElementSibling.scrollHeight
e.target.nextElementSibling.lastChild.focus()
} else if (e.target.dataset.type === 'item') {
if (e.target.previousElementSibling) {
e.target.previousElementSibling.focus()
} else {
this.input.focus()
}
}
return false
}
if (e.key === 'Enter') {
let newVal = ''
if (e.target.dataset.type === 'input' && this.filtered.length) {
if (e.target.value && e.target.value !== this.filtered[0]) {
return this.selectText(vnode, this.filtered[0])
}
this.onDone(vnode)
return false
}
if (e.target.dataset.type === 'item') {
return this.selectText(vnode, e.target.dataset.value)
}
}
},
smartOpen: function(vnode) {
if (this.input.value && this.input.value === this.filtered[0]) {
activeBox = null
} else {
activeBox = this.id
}
m.redraw()
},
view: function(vnode) {
closeBox = false
return m('div.form-item.combobox', {
class: vnode.attrs.class,
onclick: util.cancelPropagation,
}, [
m('label', vnode.attrs.label),
m('input', {
'data-type': 'input',
oncreate: (e) => { this.input = e.dom },
onkeydown: (e) => this.onKeyPress(vnode, e),
type: vnode.attrs.type || 'text',
value: vnode.attrs.value,
onfocus: () => this.smartOpen(vnode),
placeholder: vnode.attrs.placeholder || '',
oninput: this.onInput.bind(this, vnode),
}),
activeBox === this.id
? m('div.combobox-list', [
this.filtered.slice(0, 50).map(item => {
return m('div.combobox-list-item', {
'data-type': 'item',
'data-value': item,
onkeydown: (e) => this.onKeyPress(vnode, e),
onclick: (e) => this.selectText(vnode, item),
tabindex: '0',
}, item)
})
])
: null,
])
}
}
module.exports = Combobox

272
heimaerbest/app/consts.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,18 @@
const m = require('mithril')
const Combobox = require('./combobox')
const Constants = require('./consts')
const Frontpage = {
oninit: function(vnode) {
this.error = ''
this.showAddLocation = false
this.showAddLocation = true
this.form = {
city: '',
zip: '',
street_name: '',
locations: [
'Hverfisgata, 101 - Reykjavík',
// 'Hverfisgata, 101 - Reykjavík',
],
type: [ true, false, false, false, false ],
size: [ true, false, false, false, false ],
@ -20,11 +23,23 @@ const Frontpage = {
size: [ 'Alveg sama', '0 - 50fm', '50 - 80fm', '80 - 120fm', '120fm +'],
rooms: [ 'Alveg sama', 'Stúdíó', '2 - 3 herb.', '3 - 4 herb.', '5 + herb.' ],
}
this.inputs = {
zip: null,
street: null,
}
this.cities = Object.keys(Constants.Locations)
this.zips = Object.keys(Constants.Streets)
this.streets = []
},
onFormUpdate: function(vnode, key, index, event) {
if (['city', 'zip', 'street_name'].includes(key)) {
this.form[key] = event.target.value
if (key === 'city') {
this.zips = Constants.Locations[this.form.city] || []
} else if (key === 'zip') {
this.streets = Constants.Streets[this.form.zip] || []
}
} else if (key === 'type' || key === 'size' || key === 'rooms') {
if (index > 0) {
this.form[key][0] = false
@ -58,6 +73,9 @@ const Frontpage = {
this.form.zip = ''
this.form.street_name = ''
this.form.locations.push(entry)
this.cities = Object.keys(Constants.Locations)
this.zips = Object.keys(Constants.Streets)
this.streets = []
return false
},
@ -99,30 +117,33 @@ const Frontpage = {
hidden: !this.showAddLocation,
onsubmit: (e) => this.onLocationAdd(e),
}, [
m('div.form-item', [
m('label', 'City*'),
m('input', {
type: 'text',
placeholder: 'Reykjavík',
oninput: (e) => this.onFormUpdate(vnode, 'city', null, e),
}),
]),
m('div.form-item', [
m('label', 'Postal code (optional)'),
m('input', {
type: 'text',
placeholder: '000',
oninput: (e) => this.onFormUpdate(vnode, 'zip', null, e),
}),
]),
m('div.form-item.form-fill', [
m('label', 'Street name (optional)'),
m('input', {
type: 'text',
placeholder: 'Enter your dream street adress',
oninput: (e) => this.onFormUpdate(vnode, 'street_name', null, e),
}),
]),
m(Combobox, {
label: 'City*',
items: this.cities,
value: this.form.city,
placeholder: 'Reykjavík',
oninput: (e) => this.onFormUpdate(vnode, 'city', null, e),
ondone: () => { this.inputs.zip.state.focus() },
}),
m(Combobox, {
label: 'Postal code (optional)',
items: this.zips,
value: this.form.zip,
placeholder: '000',
oninput: (e) => this.onFormUpdate(vnode, 'zip', null, e),
oncreate: (e) => { this.inputs.zip = e },
ondone: () => { this.inputs.street.state.focus() },
}),
m(Combobox, {
class: 'form-fill',
label: 'Street name (optional)',
items: this.streets,
value: this.form.street_name,
placeholder: 'Enter your dream street adress',
oninput: (e) => this.onFormUpdate(vnode, 'street_name', null, e),
oncreate: (e) => { this.inputs.street = e },
ondone: () => { this.onLocationAdd(vnode) },
}),
m('div.form-item.form-small.form-no-label', [
m('input', {
class: this.form.city ? 'button-active' : 'button-outline',

0
heimaerbest/app/temp.txt Normal file
View file

4
heimaerbest/app/util.js Normal file
View file

@ -0,0 +1,4 @@
export function cancelPropagation(event) {
event.stopPropagation()
return false
}

195
heimaerbest/app/zipcode.txt Normal file
View file

@ -0,0 +1,195 @@
101 Reykjavík Reykjavík (Miðborg) Þéttbýli Hagatorgi 1
102 Reykjavík Reykjavík (Vatnsmýri og Skerjafjörður) Þéttbýli Hagatorgi 1
103 Reykjavík Reykjavík (Háaleitis- og Bústaðahverfi) Þéttbýli Síðumúla 3-5, 108 Reykjavík
104 Reykjavík Reykjavík (Laugardalur) Þéttbýli Síðumúla 3-5, 108 Reykjavík
105 Reykjavík Reykjavík (Hlíðar) Þéttbýli Síðumúla 3-5, 108 Reykjavík
107 Reykjavík Reykjavík (Vesturbær) Þéttbýli Hagatorgi 1
108 Reykjavík Reykjavík (Múlar) Þéttbýli Síðumúla 3-5, 108 Reykjavík
109 Reykjavík Reykjavík (Breiðholt) Þéttbýli Þönglabakka 4
110 Reykjavík Reykjavík (Árbær) Þéttbýli Höfðabakka 9, 110 Reykjavík
111 Reykjavík Reykjavík (Breiðholt) Þéttbýli Þönglabakka 4, 109 Reykjavík
112 Reykjavík Reykjavík (Grafarvogur) Þéttbýli Höfðabakka 9, 110 Reykjavík
113 Reykjavík Reykjavík (Grafarholt og Úlfarsárdalur) Þéttbýli Höfðabakka 9, 110 Reykjavík
116 Reykjavík Reykjavík (Grundarhverfi) Þéttbýli Háholti 14, 270 Mosfellsbæ
121 Reykjavík Reykjavík, pósthólf Pósthólf Pósthússtræti 5, 101 Reykjavík
123 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúla 3-5, 108 Reykjavík
124 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúla 3-5, 108 Reykjavík
125 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúla 3-5, 108 Reykjavík
127 Reykjavík Reykjavík, pósthólf Pósthólf Eiðistorgi 15, 170 Seltjarnarnesi
128 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúli 3-5, 108 Reykjavík
129 Reykjavík Reykjavík, pósthólf Pósthólf Þönglabakka 4, 109 Reykjavík
130 Reykjavík Reykjavík, pósthólf Pósthólf Höfðabakka 9, 110 Reykjavík
132 Reykjavík Reykjavík, pósthólf Pósthólf Hverafold 1-3, 112 Reykjavík
150 Reykjavík Annað Opinberar stofnanir, eins og ráðuneyti og ríkisstofnanir.
155 Reykjavík Annað Einkafyrirtæki, eins og viðskiptabankar.
161 Reykjavík Reykjavík, dreifbýli (ofan Elliðavatns) Dreifbýli Höfðabakka 9, 110 Reykjavík
162 Reykjavík - Dreifbýli Kjalarnes, dreifbýli Dreifbýli Höfðabakka 9, 110 Reykjavík
170 Seltjarnarnesi Seltjarnarnes Þéttbýli Hagatorg 1
172 Seltjarnarnesi Seltjarnarnes, pósthólf Pósthólf Hagatorg 1
200 Kópavogi Kópavogur (Miðbær) Þéttbýli Dalvegi 18, 201 Kópavogi
201 Kópavogi Kópavogur (Smárar, Lindir, Salir) Þéttbýli Dalvegi 18
202 Kópavogi Kópavogur, pósthólf Pósthólf Dalvegi 18, 201 Kópavogi
203 Kópavogi Kópavogur (Hvörf, Kórar) Þéttbýli Dalvegi 18, 201 Kópavogi
206 Kópavogi Kópavogur, dreifbýli Dreifbýli Dalvegi 18
210 Garðabæ Garðabær Þéttbýli Fjarðargötu 13-15, 220 Hafnarfirði
212 Garðabæ Garðabær, pósthólf Pósthólf Fjarðargötu 13-15, 220 Hafnarfirði
220 Hafnarfirði Hafnarfjörður (Miðbær) Þéttbýli Fjarðargötu 13-15
221 Hafnarfirði Hafnarfjörður (Vellir) Þéttbýli Fjarðargötu 13-15, 220 Hafnarfirði
222 Hafnarfirði Hafnarfjörður, pósthólf Pósthólf Fjarðargötu 13-15, 220 Hafnarfirði
225 Garðabæ Garðabær (Álftanes) Þéttbýli Fjarðargötu 13-15, 220 Hafnarfirði
270 Mosfellsbæ Mosfellsbær Þéttbýli Höfðabakka 9, 110 Reykjavík
271 Mosfellsbæ Mosfellssveit, dreifbýli Dreifbýli Höfðabakka 9, 110 Reykjavík
276 Mosfellsbæ Hvalfjörður og Kjós, dreifbýli Dreifbýli Höfðabakka 9, 110 Reykjavík
190 Vogum Vogar Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
191 Vogum Vatnsleysuströnd, dreifbýli Dreifbýli Hafnargötu 89, 230 Reykjanesbæ
230 Reykjanesbæ Reykjanesbær (Keflavík) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
232 Reykjanesbæ Reykjanesbær, pósthólf Pósthólf Hafnargötu 89, 230 Reykjanesbæ
233 Reykjanesbæ Reykjanesbær (Hafnir) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
235 Reykjanesbæ Keflavíkurflugvöllur Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
240 Grindavík Grindavík Þéttbýli Víkurbraut 25
241 Grindavík Grindavík, dreifbýli Dreifbýli Víkurbraut 25, 240 Grindavík
245 Suðurnesjabæ Sandgerði Þéttbýli Suðurgötu 2-4
246 Suðurnesjabæ Sandgerði, dreifbýli Dreifbýli Suðurgötu 2-4, 245 Sandgerði
250 Suðurnesjabæ Garður Þéttbýli Garðbraut 69
251 Suðurnesjabæ Garður, dreifbýli Dreifbýli Garðbraut 69, 250 Garði
260 Reykjanesbæ Reykjanesbær (Njarðvík) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
262 Reykjanesbæ Reykjanesbær (Ásbrú) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
300 Akranesi Akranes Þéttbýli Smiðjuvöllum 30
301 Akranesi Akranes, dreifbýli Dreifbýli Smiðjuvöllum 30, 300 Akranesi
302 Akranesi Akranes, pósthólf Pósthólf Smiðjuvöllum 30, 300 Akranesi
310 Borgarnesi Borgarnes Þéttbýli Borgarbraut 12
311 Borgarnesi Borgarnes, dreifbýli Dreifbýli Borgarbraut 12, 310 Borgarnesi
320 Reykholti í Borgarfirði Reykholt í Borgarfirði, dreifbýli Dreifbýli Borgarbraut 12, 310 Borgarnesi
340 Stykkishólmi Stykkishólmur Þéttbýli Aðalgötu 31
341 Stykkishólmi Stykkishólmur, dreifbýli Dreifbýli Aðalgötu 31, 340 Stykkilshólmi
342 Stykkishólmi Eyja og Miklaholtshreppur Dreifbýli Aðalgötu 31, 340 Stykkilshólmi
345 Flatey á Breiðafirði Flatey á Breiðafirði Dreifbýli Aðalgötu 31, 340 Stykkishólmi
350 Grundarfirði Grundarfjörður Þéttbýli Grundargötu 50
351 Grundarfirði Grundarfjörður, dreifbýli Dreifbýli Grundargötu 50, 350 Grundarfirði
355 Ólafsvík Ólafsvík Þéttbýli Bæjartúni 5
356 Snæfellsbæ Snæfellsbær, dreifbýli Dreifbýli Bæjartúni 5, 355 Ólafsvík
360 Hellissandi Hellissandur Þéttbýli Bæjartúni 5, 355 Ólafsvík
370 Búðardal Búðardalur Þéttbýli Miðbraut 13
371 Búðardal Búðardalur, dreifbýli Dreifbýli Miðbraut 13, 370 Búðardal
380 Reykhólahreppi Reykhólar Þéttbýli Miðbraut 13, 370 Búðardal
381 Reykhólahreppi Reykhólahreppur, dreifbýli Dreifbýli Miðbraut 13, 370 Búðardal
400 Ísafirði Ísafjörður Þéttbýli Hafnarstræti 9-13
401 Ísafirði Ísafjarðardjúp, dreifbýli (frá Ögri til Laugarholts) Dreifbýli Hafnarstræti 9-13, 400 Ísafirði
410 Hnífsdal Hnífsdalur Þéttbýli Hafnarstræti 9-13, 400 Ísafirði
415 Bolungarvík Bolungarvík Þéttbýli Aðalstræti 14
416 Bolungarvík Bolungarvík, dreifbýli Dreifbýli Aðalstræti 14, 415 Bolungarvík
420 Súðavík Súðavík Þéttbýli Grundarstræti 3-5
421 Súðavík Súðavík, dreifbýli Dreifbýli Grundarstræti 3-5, Súðavík
425 Flateyri Flateyri Þéttbýli Hafnarstræti 9-13, Ísafirði
426 Flateyri Flateyri, dreifbýli Dreifbýli Hafnarstræti 9-13, Ísafirði
430 Suðureyri Suðureyri Þéttbýli Hafnarstræti 9-13, Ísafirði
431 Suðureyri Súgandafjörður, dreifbýli Dreifbýli Hafnarstræti 9-13, Ísafirði
450 Patreksfirði Patreksfjörður Þéttbýli Bjarkargötu 4
451 Patreksfirði Patreksfjörður, dreifbýli Dreifbýli Bjarkargötu 4, Patreksfirði
460 Tálknafirði Tálknafjörður Þéttbýli Bjarkargötu 4, Patreksfirði
461 Tálknafirði Tálknafjörður, dreifbýli Dreifbýli Bjarkargötu 4, Patreksfirði
465 Bíldudal Bíldudalur Þéttbýli Bjarkargötu 4, Patreksfirði
466 Bíldudal Bíldudalur, dreifbýli Dreifbýli Bjarkargötu 4, Patreksfirði
470 Þingeyri Þingeyri Þéttbýli Hafnarstræti 9-13, Ísafirði
471 Þingeyri Dýrafjörður, dreifbýli Dreifbýli Hafnarstræti 9-13, Ísafirði
500 Stað Staður Dreifbýli Lækjargötu 2, 530 Hvammstanga
510 Hólmavík Hólmavík Þéttbýli Hafnarbraut 19
511 Hólmavík Hólmavík, dreifbýli Dreifbýli Hafnarbraut 19, 510 Hólmavík
512 Hólmavík Ísafjarðardjúp, dreifbýli (nær Hólmavík) Dreifbýli Hafnarbraut 19, 510 Hólmavík
520 Drangsnesi Drangsnes Þéttbýli Hafnarbraut 19, 510 Hólmavík
524 Árneshreppi Árneshreppur Dreifbýli Hafnarbraut 19, 510 Hólmavík
530 Hvammstanga Hvammstangi Þéttbýli Lækjargötu 2
531 Hvammstanga Hvammstangi, dreifbýli Dreifbýli Lækjargötu 2, Hvammstanga
540 Blönduósi Blönduós Þéttbýli Hnjúkabyggð 32
541 Blönduósi Blönduós, dreifbýli Dreifbýli Hnjúkabyggð 32, Blönduósi
545 Skagaströnd Skagaströnd Þéttbýli Höfða
546 Skagaströnd Skagaströnd, dreifbýli Dreifbýli Hnjúkabyggð 32, Blönduósi
550 Sauðárkróki Sauðárkrókur Þéttbýli Kirkjutorgi 5
551 Sauðárkróki Sauðárkrókur, dreifbýli Dreifbýli Kirkjutorgi 5, Sauðárkróki
560 Varmahlíð Varmahlíð Þéttbýli Kirkjutorgi 5, Sauðárkróki
561 Varmahlíð Varmahlíð, dreifbýli Dreifbýli Kirkjutorgi 5, Sauðárkróki
565 Hofsósi Hofsós Þéttbýli Kirkjutorgi 5, Sauðárkróki
566 Hofsósi Hofsós, dreifbýli Dreifbýli Kirkjutorgi 5, Sauðárkróki
570 Fljótum Fljót Dreifbýli Kirkjutorgi 5, Sauðárkróki
580 Siglufirði Siglufjörður Þéttbýli Aðalgötu 24
581 Siglufirði Siglufjörður, dreifbýli Dreifbýli Aðalgötu 24, Siglufirði
600 Akureyri Akureyri Þéttbýli Strandgötu 3
601 Akureyri Akureyri, dreifbýli Dreifbýli Strandgötu 3, 600 Akureyri
602 Akureyri Akureyri, pósthólf Pósthólf Strandgötu 3, 600 Akureyri
603 Akureyri Akureyri Þéttbýli Norðurtanga 3, 600 Akureyri
604 Akureyri Akureyri, dreifbýli (Hörgársveit) Dreifbýli Norðurtanga 3
605 Akureyri Akureyri, dreifbýli (Eyjafjarðarsveit) Dreifbýli Norðurtanga 3
606 Akureyri Akureyri, dreifbýli (Svalbarðsströnd) Dreifbýli Norðurtanga 3
607 Akureyri Akureyri, dreifbýli (Þingeyjarsveit) Dreifbýli Norðurtanga 3
610 Grenivík Grenivík Þéttbýli Túngötu 3
611 Grímsey Grímsey Þéttbýli Vallargata 9
616 Grenivík Grenivík, dreifbýli Dreifbýli Túngötu 3, 610 Grenivík
620 Dalvík Dalvík Þéttbýli Hafnarbraut 26
621 Dalvík Dalvík, dreifbýli Dreifbýli Hafnarbraut 26, 620 Dalvík
625 Ólafsfirði Ólafsfjörður Þéttbýli Aðalgötu 14
626 Ólafsfirði Ólafsfjörður, dreifbýli Dreifbýli Aðalgötu 14, 625 Ólafsfirði
630 Hrísey Hrísey Þéttbýli Norðurvegi 6-8
640 Húsavík Húsavík Þéttbýli Garðarsbraut 70
641 Húsavík Húsavík, dreifbýli Dreifbýli Garðarsbraut 70, 640 Húsavík
645 Fosshóli Fosshóll, dreifbýli Dreifbýli Garðarsbraut 70, 640 Húsavík
650 Laugum Laugar Þéttbýli Kjarna
660 Mývatni Mývatn Dreifbýli Helluhrauni 3
670 Kópaskeri Kópasker Þéttbýli Bakkagötu 2
671 Kópaskeri Kópasker, dreifbýli Dreifbýli Bakkagötu 2, 670 Kópaskeri
675 Raufarhöfn Raufarhöfn Þéttbýli Aðalbraut 19
676 Raufarhöfn Raufarhöfn, dreifbýli Dreifbýli Aðalbraut 19, 675 Raufarhöfn
680 Þórshöfn Þórshöfn Þéttbýli Fjarðarvegi 5
681 Þórshöfn Þórshöfn, dreifbýli Dreifbýli Fjarðarvegi 5, 680 Þórshöfn
685 Bakkafirði Bakkafjörður Þéttbýli Fjarðarvegi 5, 680 Þórshöfn
686 Bakkafirði Bakkafjörður, dreifbýli Dreifbýli Fjarðarvegi 5, 680 Þórshöfn
690 Vopnafirði Vopnafjörður Þéttbýli Kolbeinsgötu 10
691 Vopnafirði Vopnafjörður, dreifbýli Dreifbýli Kolbeinsgötu 10, 690 Vopnafirði
700 Egilsstöðum Egilsstaðir Þéttbýli Kaupvangi 6
701 Egilsstöðum Egilsstaðir, dreifbýli Dreifbýli Kaupvangi 6, 700 Egilsstöðum
710 Seyðisfirði Seyðisfjörður Þéttbýli Hafnargötu 4
711 Seyðisfirði Seyðisfjörður, dreifbýli Dreifbýli Hafnargötu 6, 710 Seyðisfirði
715 Mjóafirði Mjóifjörður, dreifbýli Dreifbýli Brekku
720 Borgarfirði (eystri) Bakkagerði Þéttbýli Kaupvangi 6, 700 Egilsstöðum
721 Borgarfirði (eystri) Borgarfjörður eystri Dreifbýli Kaupvangi 6, 700 Egilsstöðum
730 Reyðarfirði Reyðarfjörður Þéttbýli Búðareyri 35
731 Reyðarfirði Reyðarfjörður, dreifbýli Dreifbýli Búðareyri 35, 720 Reyðarfirði
735 Eskifirði Eskifjörður Þéttbýli Strandgötu 55
736 Eskifirði Eskifjörður, dreifbýli Dreifbýli Strandgötu 55, 735 Eskifirði
740 Neskaupstað Neskaupstaður Þéttbýli Miðstræti 26
741 Neskaupstað Neskaupstaður, dreifbýli Dreifbýli Miðstræti 26, 740 Neskaupstað
750 Fáskrúðsfirði Fáskrúðsfjörður Þéttbýli Skólavegi 59
751 Fáskrúðsfirði Fáskrúðsfjörður, dreifbýli Dreifbýli Skólavegi 59, 750, Fáskrúðsfirði
755 Stöðvarfirði Stöðvarfjörður Þéttbýli Búðareyri 35, 720 Reiðarfirði
756 Stöðvarfirði Stöðvarfjörður, dreifbýli Dreifbýli Búðareyri 35, 720 Reiðarfirði
760 Breiðdalsvík Breiðdalsvík Þéttbýli Selnesi 38
761 Breiðdalsvík Breiðdalsvík, dreifbýli Dreifbýli Selnesi 38, 760 Breiðdalsvík
765 Djúpavogi Djúpivogur Þéttbýli Kambi 1
766 Djúpavogi Djúpivogur, dreifbýli Dreifbýli Kambi 1, 765 Djúpavog
780 Höfn í Hornafirði Höfn Þéttbýli Hafnarbraut 21
781 Höfn í Hornafirði Höfn, dreifbýli Dreifbýli Hafnarbraut 21, 780 Höfn
785 Öræfum Öræfi, dreifbýli Dreifbýli Hafnarbraut 21, 780 Höfn
800 Selfossi Selfoss Þéttbýli Larsenstræti 1
801 Selfossi Selfoss, dreifbyli (Árborg) Dreifbýli Larsenstræti 1, 800 Selfossi
802 Selfossi Selfoss, pósthólf Pósthólf Larsenstræti 1, 800 Selfossi
803 Selfossi Selfoss, dreifbýli (Flóahreppur) Dreifbýli Larsenstræti 1, 800 Selfossi
804 Selfossi Selfoss, dreifbýli (Skeiða- og Gnúpverjahreppur) Dreifbýli Larsenstræti 1, 800 Selfossi
805 Selfossi Selfoss, dreifbýli (Grímsnes- og Grafningshreppur) Dreifbýli Larsenstræti 1, 800 Selfossi
806 Selfossi Selfoss (Bláskógabyggð) Dreifbýli Larsenstræti 1, 800 Selfossi
810 Hveragerði Hveragerði Þéttbýli Sunnumörk 2-4
815 Þorlákshöfn Þorlákshöfn Þéttbýli Hafnarberg 1
816 Ölfusi Ölfus, dreifbýli Dreifbýli Larsenstræti 1, 800 Selfossi
820 Eyrarbakka Eyrarbakki Þéttbýli Larsenstræti 1, 800 Selfossi
825 Stokkseyri Stokkseyri Þéttbýli Larsenstræti 1, 800 Selfossi
840 Laugarvatni Laugarvatn Þéttbýli Larsenstræti 1, 800 Selfossi
845 Flúðum Flúðir Þéttbýli Larsenstræti 1, 800 Selfossi
846 Flúðum Flúðir, dreifbýli Dreifbýli Larsenstræti 1, 800 Selfossi
850 Hellu Hella Þéttbýli Þrúðvangi 10
851 Hellu Hella, dreifbyli Dreifbýli Þrúðvangi 10, 850 Hella
860 Hvolsvelli Hvolsvöllur Þéttbýli Austurvegi 2
861 Hvolsvelli Hvolsvöllur, dreifbýli Dreifbýli Austurvegi 2, 860 Hvolsvelli
870 Vík Vík Þéttbýli Austurvegi 2, 860 Hvolsvelli
871 Vík Vík, dreifbýli Dreifbýli Austurvegi 2, 860 Hvolsvelli
880 Kirkjubæjarklaustri Kirkjubæjarklaustur Þéttbýli Austurvegi 2, 860 Hvolsvelli
881 Kirkjubæjarklaustri Kirkjubæjarklaustur, dreifbýli Dreifbýli Austurvegi 2, 860 Hvolsvelli
900 Vestmannaeyjum Vestmannaeyjar Þéttbýli Vestmannabraut 22
902 Vestmannaeyjum Vestmannaeyjar, pósthólf Pósthólf Vestmannabraut 22, 900 Vestmannaeyjar

View file

@ -50,7 +50,7 @@
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.0",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"msnodesqlv8": "^2.7.0",
"nconf-lite": "^2.0.0",
"striptags": "^3.2.0"
},

View file

@ -571,6 +571,38 @@ i.ic-plus {
background-size: contain;
}
/* ---------------- icons ---------------- */
.combobox {
position: relative;
}
.combobox-list {
position: absolute;
top: 100%;
left: 0;
width: 100%;
border-radius: var(--form-radius);
border: var(--form-border);
background: var(--main-bg);
z-index: 5;
max-height: 530px;
overflow-y: scroll;
box-shadow: 0px 3px 10px #0004;
}
.combobox-list-item {
padding: 0.9rem 0.5rem;
cursor: pointer;
}
.combobox-list-item:hover,
.combobox-list-item:active,
.combobox-list-item:focus {
background: var(--main-accent);
color: var(--main-accent-fg);
}
</style>
<link id="headstyle" rel="Stylesheet" href="/assets/app.css?v=2" type="text/css" media="none" />
</head>

View file

@ -1,6 +1,6 @@
{
"name": "nfp_is",
"version": "1.0.0",
"version": "1.0.3",
"port": 4220,
"description": "nfp.is website",
"main": "index.mjs",
@ -37,7 +37,9 @@
},
"homepage": "https://git.nfp.is/nfp/nfp_sites",
"dependencies": {
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.4",
"formidable": "^1.2.6",
"nconf-lite": "^2.0.0"
},
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "nfp_moe",
"version": "2.1.4",
"version": "2.1.6",
"port": 4110,
"description": "NFP Moe website",
"main": "index.js",
@ -8,17 +8,13 @@
"test": "test"
},
"scripts": {
"start": "node --experimental-modules index.mjs",
"start": "node index.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"build:prod": "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:server": "node index.mjs | bunyan",
"dev": "npm-watch dev:server",
"watch:sass:public": "sass --watch app/app.scss public/assets/app.css",
"watch:sass:admin": "sass --watch app/admin.scss public/assets/admin.css",
"prod": "npm run build && npm start",
"temp": "asbundle"
"dev": "npm-watch dev:server"
},
"watch": {
"dev:server": {
@ -55,7 +51,7 @@
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.0",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"msnodesqlv8": "^4.1.1",
"nconf-lite": "^2.0.0",
"striptags": "^3.2.0"
},

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

View file

@ -0,0 +1,159 @@
Usage: SvtAv1EncApp <options> <-b dst_filename> -i src_filename
Examples:
Multi-pass encode (VBR):
SvtAv1EncApp <--stats svtav1_2pass.log> --passes 2 --rc 1 --tbr 1000 -b dst_filename -i src_filename
Multi-pass encode (CRF):
SvtAv1EncApp <--stats svtav1_2pass.log> --passes 2 --rc 0 --crf 43 -b dst_filename -i src_filename
Single-pass encode (VBR):
SvtAv1EncApp --passes 1 --rc 1 --tbr 1000 -b dst_filename -i src_filename
Options:
--help Shows the command line options currently available
--color-help Extra help for adding AV1 metadata to the bitstream
--version Shows the version of the library that's linked to the library
-i, --input Input raw video (y4m and yuv) file path, use `stdin` or `-` to read from pipe
-b, --output Output compressed (ivf) file path, use `stdout` or `-` to write to pipe
-c, --config Configuration file path
--errlog Error file path, defaults to stderr
-o, --recon Reconstructed yuv file path
--stat-file PSNR / SSIM per picture stat output file path, requires `--enable-stat-report 1`
--progress Verbosity of the output, default is 1 [0: no progress is printed, 2: aomenc style output, 3: fancy progress]
--no-progress Do not print out progress, default is 0 [1: `--progress 0`, 0: `--progress 1`]
--preset Encoder preset, presets < 0 are for debugging. Higher presets means faster encodes, but with a quality tradeoff, default is 10 [-1-13]
--svtav1-params colon separated list of key=value pairs of parameters with keys based on config file options
Encoder Global Options:
-w, --width Frame width in pixels, inferred if y4m, default is 0 [4-16384]
-h, --height Frame height in pixels, inferred if y4m, default is 0 [4-8704]
--forced-max-frame-width Maximum frame width value to force, default is 0 [4-16384]
--forced-max-frame-height Maximum frame height value to force, default is 0 [4-8704]
--n Number of frames to encode. If `n` is larger than the input, the encoder will loop back and continue encoding, default is 0 [0: until EOF, 1-`(2^63)-1`]
--skip Number of frames to skip. Default is 0 [0: don`t skip, 1-`(2^63)-1`]
--frames Number of frames to encode. If `n` is larger than the input, the encoder will loop back and continue encoding, default is 0 [0: until EOF, 1-`(2^63)-1`]
--nb Buffer `n` input frames into memory and use them to encode, default is -1 [-1: no frames buffered, 1-`(2^31)-1`]
--color-format Color format, only yuv420 is supported at this time, default is 1 [0: yuv400, 1: yuv420, 2: yuv422, 3: yuv444]
--profile Bitstream profile, default is 0 [0: main, 1: high, 2: professional]
--level Bitstream level, defined in A.3 of the av1 spec, default is 0 [0: autodetect from input, 2.0-7.3]
--enable-hdr Enable writing of HDR metadata in the bitstream, default is 0 [0-1]
--fps Input video frame rate, integer values only, inferred if y4m, default is 60 [1-240]
--fps-num Input video frame rate numerator, default is 60000 [0-2^32-1]
--fps-denom Input video frame rate denominator, default is 1000 [0-2^32-1]
--input-depth Input video file and output bitstream bit-depth, default is 10 [8, 10]
--inj Inject pictures to the library at defined frame rate, default is 0 [0-1]
--inj-frm-rt Set injector frame rate, only applicable with `--inj 1`, default is 60 [0-240]
--enable-stat-report Calculates and outputs PSNR SSIM metrics at the end of encoding, default is 0 [0-1]
--asm Limit assembly instruction set, only applicable to x86, default is max [c, mmx, sse, sse2, sse3, ssse3, sse4_1, sse4_2, avx, avx2, avx512, max]
--lp Target (best effort) number of logical cores to be used. 0 means all. Refer to Appendix A.1 of the user guide, default is 0 [0, core count of the machine]
--pin Pin the execution to the first --lp cores. Overwritten to 1 when `--ss` is set. Refer to Appendix A.1 of the user guide, default is 0 [0-1]
--ss Specifies which socket to run on, assumes a max of two sockets. Refer to Appendix A.1 of the user guide, default is -1 [-1, 0, -1]
Rate Control Options:
--rc Rate control mode, default is 0 [0: CRF or CQP (if `--aq-mode` is 0), 1: VBR, 2: CBR]
-q, --qp Initial QP level value, default is 35 [1-63]
--crf Constant Rate Factor value, setting this value is equal to `--rc 0 --aq-mode 2 --qp x`, default is 35 [1-70]
--tbr Target Bitrate (kbps), only applicable for VBR and CBR encoding, default is 7000 [1-100000]
--mbr Maximum Bitrate (kbps) only applicable for CRF encoding, default is 0 [1-100000]
--use-q-file Overwrite the encoder default picture based QP assignments and use QP values from `--qp-file`, default is 0 [0-1]
--qpfile Path to a file containing per picture QP value separated by newlines
--max-qp Maximum (highest) quantizer, only applicable for VBR and CBR, default is 63 [1-63]
--min-qp Minimum (lowest) quantizer, only applicable for VBR and CBR, default is 1 [1-63]
--aq-mode Set adaptive QP level, default is 2 [0: off, 1: variance base using AV1 segments, 2: deltaq pred efficiency]
--use-fixed-qindex-offsets Overwrite the encoder default hierarchical layer based QP assignment and use fixed Q index offsets, default is 0 [0-2]
--key-frame-qindex-offset Overwrite the encoder default keyframe Q index assignment, default is 0 [-256-255]
--key-frame-chroma-qindex-offset Overwrite the encoder default chroma keyframe Q index assignment, default is 0 [-256-255]
--qindex-offsets list of luma Q index offsets per hierarchical layer, separated by `,` with each offset in the range of [-256-255], default is `0,0,..,0`
--chroma-qindex-offsets list of chroma Q index offsets per hierarchical layer, separated by `,` with each offset in the range of [-256-255], default is `0,0,..,0`
--luma-y-dc-qindex-offset Luma Y DC Qindex Offset
--chroma-u-dc-qindex-offset Chroma U DC Qindex Offset
--chroma-u-ac-qindex-offset Chroma U AC Qindex Offset
--chroma-v-dc-qindex-offset Chroma V DC Qindex Offset
--chroma-v-ac-qindex-offset Chroma V AC Qindex Offset
--lambda-scale-factors list of scale factor for lambda values used for different frame types defined by SvtAv1FrameUpdateType, separated by `,` with each scale factor as integer. value divided by 128 is the actual scale factor in float, default is `128,128,..,128`
--undershoot-pct Only for VBR and CBR, allowable datarate undershoot (min) target (percentage), default is 25, but can change based on rate control [0-100]
--overshoot-pct Only for VBR and CBR, allowable datarate overshoot (max) target (percentage), default is 25, but can change based on rate control [0-100]
--mbr-overshoot-pct Only for Capped CRF, allowable datarate overshoot (max) target (percentage), default is 50, but can change based on rate control [0-100]
--gop-constraint-rc Enable GoP constraint rc. When enabled, the rate control matches the target rate for each GoP, default is 0 [0-1]
--buf-sz Client buffer size (ms), only applicable for CBR, default is 6000 [0-10000]
--buf-initial-sz Client initial buffer size (ms), only applicable for CBR, default is 4000 [0-10000]
--buf-optimal-sz Client optimal buffer size (ms), only applicable for CBR, default is 5000 [0-10000]
--recode-loop Recode loop level, refer to "Recode loop level table" in the user guide for more info [0: off, 4: preset based]
--minsection-pct GOP min bitrate (expressed as a percentage of the target rate), default is 0 [0-100]
--maxsection-pct GOP max bitrate (expressed as a percentage of the target rate), default is 2000 [0-10000]
--enable-qm Enable quantisation matrices, default is 1 [0-1]
--qm-min Min quant matrix flatness, default is 0 [0-15]
--qm-max Max quant matrix flatness, default is 15 [0-15]
--roi-map-file Enable Region Of Interest and specify a picture based QP Offset map file, default is off
Multi-pass Options:
--pass Multi-pass selection, pass 2 is only available for VBR, default is 0 [0: single pass encode, 1: first pass, 2: second pass]
--stats Filename for multi-pass encoding, default is "svtav1_2pass.log"
--passes Number of encoding passes, default is preset dependent but generally 1 [1: one pass encode, 2: multi-pass encode]
GOP Size & Type Options:
--keyint GOP size (frames), default is -2 [-2: ~5 seconds, -1: "infinite" and only applicable for CRF, 0: same as -1]
--irefresh-type Intra refresh type, default is 2 [1: FWD Frame (Open GOP), 2: KEY Frame (Closed GOP)]
--scd Scene change detection control, default is 0 [0-1]
--lookahead Number of frames in the future to look ahead, not including minigop, temporal filtering, and rate control, default is -1 [-1: auto, 0-120]
--hierarchical-levels Set hierarchical levels beyond the base layer, default is <=M12: 5, else: 4 [2: 3 temporal layers, 3: 4 temporal layers, 4: 5 layers, 5: 6 layers]
--pred-struct Set prediction structure, default is 2 [1: low delay frames, 2: random access]
--force-key-frames Force key frames at the comma separated specifiers. `#f` for frames, `#.#s` for seconds
--startup-mg-size Specify another mini-gop configuration for the first mini-gop after the key-frame, default is 0 [0: OFF, 2: 3 temporal layers, 3: 4 temporal layers, 4: 5 temporal layers]
AV1 Specific Options:
--tile-rows Number of tile rows to use, `TileRow == log2(x)`, default changes per resolution but is 1 [0-6]
--tile-columns Number of tile columns to use, `TileCol == log2(x)`, default changes per resolution but is 1 [0-4]
--enable-dlf Deblocking loop filter control, default is 1 [0-2]
--enable-cdef Enable Constrained Directional Enhancement Filter, default is 1 [0-1]
--enable-restoration Enable loop restoration filter, default is 1 [0-1]
--enable-tpl-la Temporal Dependency model control, currently forced on library side, only applicable for CRF/CQP, default is 1 [0-1]
--enable-mfmv Motion Field Motion Vector control, default is -1 [-1: auto, 0-1]
--enable-dg Dynamic GoP control, default is 1 [0-1]
--fast-decode Fast Decoder levels, default is 0 [0-2]
--enable-tf Enable ALT-REF (temporally filtered) frames, default is 1 [0-1]
--enable-overlays Enable the insertion of overlayer pictures which will be used as an additional reference frame for the base layer picture, default is 0 [0-1]
--tune Optimize the encoding process for different desired outcomes [0 = VQ, 1 = PSNR, 2 = SSIM, 3 = Subjective SSIM, 4 = Still Picture], default is 2 [0-4]
--scm Set screen content detection level, default is 2 [0: off, 1: on, 2: content adaptive]
--rmv Restrict motion vectors from reaching outside the picture boundary, default is 0 [0-1]
--film-grain Enable film grain, default is 0 [0: off, 1-50: level of denoising for film grain]
--film-grain-denoise Apply denoising when film grain is ON, default is 0 [0: no denoising, film grain data is still in frame header, 1: level of denoising is set by the film-grain parameter]
--fgs-table Set the film grain model table path
--superres-mode Enable super-resolution mode, refer to the super-resolution section in the user guide, default is 0 [0: off, 1-3, 4: auto-select mode]
--superres-denom Super-resolution denominator, only applicable for mode == 1, default is 8 [8: no scaling, 9-15, 16: half-scaling]
--superres-kf-denom Super-resolution denominator for key frames, only applicable for mode == 1, default is 8 [8: no scaling, 9-15, 16: half-scaling]
--superres-qthres Super-resolution q-threshold, only applicable for mode == 3, default is 43 [0-63]
--superres-kf-qthres Super-resolution q-threshold for key frames, only applicable for mode == 3, default is 43 [0-63]
--sframe-dist S-Frame interval (frames) (0: OFF[default], > 0: ON)
--sframe-mode S-Frame insertion mode ([1-2], 1: the considered frame will be made into an S-Frame only if it is an altref frame, 2: the next altref frame will be made into an S-Frame[default])
--resize-mode Enable resize mode [0: none, 1: fixed scale, 2: random scale, 3: dynamic scale, 4: random access]
--resize-denom Resize denominator, only applicable for mode == 1 [8-16]
--resize-kf-denom Resize denominator for key frames, only applicable for mode == 1 [8-16]
--frame-resz-events Resize frame events, in a list separated by ',', a reference scaling process starts from the given frame number with new denominators, only applicable for mode == 4
--frame-resz-kf-denoms Resize denominator for key frames in event, in a list separated by ',', only applicable for mode == 4
--frame-resz-denoms Resize denominator in event, in a list separated by ',', only applicable for mode == 4
Color Description Options:
--color-help [PSY] Metadata help from user guide Appendix A.2
--color-primaries Color primaries, refer to --color-help. Default is 2 [0-12, 22]
--transfer-characteristics Transfer characteristics, refer to --color-help. Default is 2 [0-22]
--matrix-coefficients Matrix coefficients, refer to --color-help. Default is 2 [0-14]
--color-range Color range, default is 0 [0: Studio, 1: Full]
--chroma-sample-position Chroma sample position, default is 'unknown' ['unknown', 'vertical'/'left', 'colocated'/'topleft']
--mastering-display Mastering display metadata in the format of "G(x,y)B(x,y)R(x,y)WP(x,y)L(max,min)", refer to the user guide Appendix A.2
--content-light Set content light level in the format of "max_cll,max_fall", refer to the user guide Appendix A.2
Psychovisual Options:
--enable-variance-boost Enable variance boost, default is 1 [0-1]
--variance-boost-strength Variance boost strength, default is 2 [1-4]
--variance-octile Octile for variance boost, default is 6 [1-8]
--enable-alt-curve [PSY] Enable alternative curve for variance boost (different boosting trade-offs), default is 0 [0-1]
--sharpness [PSY] Affects loopfilter deblock sharpness and rate distortion, default is 0 [-7 to 7]
--qp-scale-compress-strength [PSY] QP scale compress strength, default is 1 [0-3]
--frame-luma-bias [PSY] Adjusts the frame's QP based on the frame's average luma value, default is 0 [0 to 100]
--max-32-tx-size [PSY] Limits the allowed transform sizes to a maximum of 32x32, default is 0 [0-1]
--adaptive-film-grain [PSY] Adapts film grain blocksize based on video resolution, default is 1 [0-1]
--tf-strength [PSY] Adjust temporal filtering strength, default is 1 [0-4]
--kf-tf-strength [PSY] Adjust TF strength on keyframes, default is 1 [0-4]
--chroma-qm-min [PSY] Min chroma quant matrix flatness, default is 8 [0-15]
--chroma-qm-max [PSY] Max chroma quant matrix flatness, default is 15 [0-15]
--noise-norm-strength [PSY] Noise normalization strength, default is 0 [0-4]

View file

@ -1027,10 +1027,10 @@ footer .middle {
}
footer .asuna {
flex: 0 0 119px;
height: 150px;
width: 119px;
background-position: 0px 0px;
flex: 0 0 171px;
height: 200px;
width: 171px;
background-position-x: -1000px;
}
footer ul {
@ -1071,7 +1071,7 @@ footer a {
}
footer .asuna {
flex: 0 0 150px;
flex: 0 0 200px;
}
}