mirror of
https://github.com/Luzifer/share.git
synced 2024-12-20 10:31:16 +00:00
Port frontend to Vue 3 / Bootstrap 5.3
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
2c2605d016
commit
549f0d1f36
14 changed files with 4849 additions and 1437 deletions
10
.eslintrc.js
10
.eslintrc.js
|
@ -8,7 +8,7 @@
|
||||||
const Module = require('module')
|
const Module = require('module')
|
||||||
|
|
||||||
const hacks = [
|
const hacks = [
|
||||||
'babel-eslint',
|
'@babel/eslint-parser',
|
||||||
'eslint-plugin-vue',
|
'eslint-plugin-vue',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:vue/recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'eslint:recommended', // https://eslint.org/docs/rules/
|
'eslint:recommended', // https://eslint.org/docs/rules/
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -44,12 +44,14 @@ module.exports = {
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
parser: 'babel-eslint',
|
parser: '@typescript-eslint/parser',
|
||||||
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
// required to lint *.vue files
|
// required to lint *.vue files
|
||||||
'vue',
|
'vue',
|
||||||
|
'@typescript-eslint',
|
||||||
],
|
],
|
||||||
|
|
||||||
reportUnusedDisableDirectives: true,
|
reportUnusedDisableDirectives: true,
|
||||||
|
@ -134,6 +136,7 @@ module.exports = {
|
||||||
'switch-colon-spacing': ['error'],
|
'switch-colon-spacing': ['error'],
|
||||||
'template-curly-spacing': ['error', 'never'],
|
'template-curly-spacing': ['error', 'never'],
|
||||||
'unicode-bom': ['error', 'never'],
|
'unicode-bom': ['error', 'never'],
|
||||||
|
'vue/comment-directive': 'off',
|
||||||
'vue/new-line-between-multi-line-property': ['error'],
|
'vue/new-line-between-multi-line-property': ['error'],
|
||||||
'vue/no-empty-component-block': ['error'],
|
'vue/no-empty-component-block': ['error'],
|
||||||
'vue/no-reserved-component-names': ['error'],
|
'vue/no-reserved-component-names': ['error'],
|
||||||
|
@ -141,6 +144,7 @@ module.exports = {
|
||||||
'vue/no-unused-properties': ['error'],
|
'vue/no-unused-properties': ['error'],
|
||||||
'vue/no-unused-refs': ['error'],
|
'vue/no-unused-refs': ['error'],
|
||||||
'vue/no-useless-mustaches': ['error'],
|
'vue/no-useless-mustaches': ['error'],
|
||||||
|
'vue/no-v-html': 'off', // Disabled for this project
|
||||||
'vue/order-in-components': ['off'], // Collides with sort-keys
|
'vue/order-in-components': ['off'], // Collides with sort-keys
|
||||||
'vue/require-name-property': ['error'],
|
'vue/require-name-property': ['error'],
|
||||||
'vue/v-for-delimiter-style': ['error'],
|
'vue/v-for-delimiter-style': ['error'],
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
|
frontend/fa-*
|
||||||
|
frontend/app.*
|
||||||
|
node_modules
|
||||||
share
|
share
|
||||||
src/node_modules
|
|
||||||
|
|
|
@ -4,7 +4,12 @@ COPY . /src/share
|
||||||
WORKDIR /src/share
|
WORKDIR /src/share
|
||||||
|
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk add --update git \
|
&& apk add --update \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
git \
|
||||||
|
make \
|
||||||
|
&& make frontend \
|
||||||
&& go install \
|
&& go install \
|
||||||
-ldflags "-X main.version=$(git describe --tags --always || echo dev)" \
|
-ldflags "-X main.version=$(git describe --tags --always || echo dev)" \
|
||||||
-mod=readonly \
|
-mod=readonly \
|
||||||
|
|
24
Makefile
24
Makefile
|
@ -7,24 +7,12 @@ lint:
|
||||||
node:12-alpine \
|
node:12-alpine \
|
||||||
npx eslint --ext .js --fix frontend/app.js
|
npx eslint --ext .js --fix frontend/app.js
|
||||||
|
|
||||||
assets: frontend/bundle.css
|
.PHONY: frontend
|
||||||
assets: frontend/bundle.js
|
frontend: node_modules
|
||||||
|
node ci/build.mjs
|
||||||
|
|
||||||
frontend/bundle.css:
|
node_modules:
|
||||||
./ci/combine.sh $@ \
|
npm ci
|
||||||
npm/bootstrap@4/dist/css/bootstrap.min.css \
|
|
||||||
npm/bootstrap-vue@2/dist/bootstrap-vue.min.css \
|
|
||||||
npm/bootswatch@5/dist/darkly/bootstrap.min.css \
|
|
||||||
gh/highlightjs/cdn-release@11.2.0/build/styles/androidstudio.min.css
|
|
||||||
|
|
||||||
frontend/bundle.js:
|
publish: frontend
|
||||||
./ci/combine.sh $@ \
|
|
||||||
npm/axios@0.21.1 \
|
|
||||||
npm/vue@2 \
|
|
||||||
npm/vue-i18n@8.25.0/dist/vue-i18n.min.js \
|
|
||||||
npm/bootstrap-vue@2/dist/bootstrap-vue.min.js \
|
|
||||||
npm/showdown@1 \
|
|
||||||
gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js
|
|
||||||
|
|
||||||
publish:
|
|
||||||
bash ci/build.sh
|
bash ci/build.sh
|
||||||
|
|
34
ci/build.mjs
Normal file
34
ci/build.mjs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import esbuild from 'esbuild'
|
||||||
|
import { sassPlugin } from 'esbuild-sass-plugin'
|
||||||
|
import vuePlugin from 'esbuild-plugin-vue3'
|
||||||
|
|
||||||
|
const buildOpts = {
|
||||||
|
assetNames: '[name]-[hash]',
|
||||||
|
bundle: true,
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'dev'),
|
||||||
|
},
|
||||||
|
entryPoints: ['frontend-src/main.ts'],
|
||||||
|
legalComments: 'none',
|
||||||
|
loader: {
|
||||||
|
'.ttf': 'file',
|
||||||
|
'.woff2': 'file',
|
||||||
|
},
|
||||||
|
minify: true,
|
||||||
|
outfile: 'frontend/app.js',
|
||||||
|
plugins: [
|
||||||
|
sassPlugin(),
|
||||||
|
vuePlugin(),
|
||||||
|
],
|
||||||
|
target: [
|
||||||
|
'chrome109',
|
||||||
|
'edge116',
|
||||||
|
'es2020',
|
||||||
|
'firefox115',
|
||||||
|
'safari15',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buildOpts }
|
||||||
|
|
||||||
|
esbuild.build(buildOpts)
|
220
frontend-src/display.vue
Normal file
220
frontend-src/display.vue
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a
|
||||||
|
class="navbar-brand"
|
||||||
|
href="#"
|
||||||
|
@click.prevent=""
|
||||||
|
>
|
||||||
|
<i class="fas fa-fw fa-share-alt-square mr-1" /> Share
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2><i class="fas fa-spinner fa-pulse" /></h2>
|
||||||
|
{{ $t('loading') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="card text-bg-danger"
|
||||||
|
>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2><i class="fas fa-exclamation-circle" /></h2>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="fileType.startsWith('image/')"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<a :href="path">
|
||||||
|
<img
|
||||||
|
:src="path"
|
||||||
|
class="img-fluid"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="fileType.startsWith('video/')"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<video controls>
|
||||||
|
<source :src="path">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="fileType.startsWith('audio/')"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<audio
|
||||||
|
controls
|
||||||
|
:src="path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="fileType.startsWith('text/markdown')"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="card-body"
|
||||||
|
v-html="renderMarkdown(text)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="fileType.startsWith('text/')"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre><code>{{ text }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2><i class="fas fa-cloud-download-alt" /></h2>
|
||||||
|
<button class="btn btn-success">
|
||||||
|
{{ fileName }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
import showdown from 'showdown'
|
||||||
|
|
||||||
|
const rewrites = {
|
||||||
|
'application/javascript': 'text/javascript',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
fileName: '',
|
||||||
|
fileType: null,
|
||||||
|
loading: true,
|
||||||
|
path: '',
|
||||||
|
text: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
hashChange() {
|
||||||
|
const hash = window.location.hash
|
||||||
|
|
||||||
|
if (hash.length > 0) {
|
||||||
|
this.path = hash.substring(1)
|
||||||
|
} else {
|
||||||
|
this.error = this.$i18n.t('fileNotFound')
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMarkdown(text) {
|
||||||
|
return new showdown.Converter().makeHtml(text)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
window.onhashchange = this.hashChange
|
||||||
|
this.hashChange()
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'ShareContentDisplay',
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
fileType(v) {
|
||||||
|
// Rewrite known file types not matching the expectations above
|
||||||
|
if (rewrites[v]) {
|
||||||
|
this.fileType = rewrites[v]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load text files directly and highlight them
|
||||||
|
if (v.startsWith('text/')) {
|
||||||
|
this.loading = true
|
||||||
|
fetch(this.path)
|
||||||
|
.then(resp => resp.text())
|
||||||
|
.then(text => {
|
||||||
|
this.text = text
|
||||||
|
|
||||||
|
if (this.text.length < 200 * 1024 && v !== 'text/plain') {
|
||||||
|
// Only highlight up to 200k and not on text/plain
|
||||||
|
window.setTimeout(() => hljs.initHighlighting(), 100)
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
path() {
|
||||||
|
if (this.path.indexOf('://') >= 0) {
|
||||||
|
// Strictly disallow loading files having any protocol in them
|
||||||
|
this.error = this.$i18n.t('notPermitted')
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(this.path, {
|
||||||
|
method: 'HEAD',
|
||||||
|
})
|
||||||
|
.then(resp => {
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
switch (resp.status) {
|
||||||
|
case 200:
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
this.error = this.$i18n.t('notPermitted')
|
||||||
|
return
|
||||||
|
case 404:
|
||||||
|
this.error = this.$i18n.t('fileNotFound')
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
this.error = this.$i18n.t('genericError', { status: resp.status })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileType = resp?.headers?.get('content-type') || 'application/octet-stream'
|
||||||
|
this.fileName = this.path.substring(this.path.lastIndexOf('/') + 1)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
39
frontend-src/main.ts
Normal file
39
frontend-src/main.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/* eslint-disable sort-imports */
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css'
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.css'
|
||||||
|
|
||||||
|
import { createApp, h } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import ContentDisplay from './display.vue'
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: {
|
||||||
|
fileNotFound: 'The requested file has not been found.',
|
||||||
|
genericError: 'Something went wrong (Status {status})',
|
||||||
|
loading: 'Loading file details...',
|
||||||
|
notPermitted: 'Access to this file was denied.',
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
fileNotFound: 'Die angegebene Datei wurde nicht gefunden.',
|
||||||
|
genericError: 'Irgendwas lief schief... (Status {status})',
|
||||||
|
loading: 'Lade Datei-Informationen...',
|
||||||
|
notPermitted: 'Der Zugriff auf diese Datei wurde verweigert.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
name: 'Share',
|
||||||
|
render() {
|
||||||
|
return h(ContentDisplay)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(createI18n({
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
locale: new URLSearchParams(window.location.search).get('hl') || navigator.languages?.[0].split('-')[0] || navigator.language?.split('-')[0] || 'en',
|
||||||
|
messages,
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.mount('#app')
|
123
frontend/app.js
123
frontend/app.js
|
@ -1,123 +0,0 @@
|
||||||
/* global axios, hljs, showdown, Vue */
|
|
||||||
|
|
||||||
const rewrites = {
|
|
||||||
'application/javascript': 'text/javascript',
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
en: {
|
|
||||||
fileNotFound: 'The requested file has not been found.',
|
|
||||||
genericError: 'Something went wrong (Status {status})',
|
|
||||||
loading: 'Loading file details...',
|
|
||||||
notPermitted: 'Access to this file was denied.',
|
|
||||||
},
|
|
||||||
de: {
|
|
||||||
fileNotFound: 'Die angegebene Datei wurde nicht gefunden.',
|
|
||||||
genericError: 'Irgendwas lief schief... (Status {status})',
|
|
||||||
loading: 'Lade Datei-Informationen...',
|
|
||||||
notPermitted: 'Der Zugriff auf diese Datei wurde verweigert.',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
data: {
|
|
||||||
error: null,
|
|
||||||
fileName: '',
|
|
||||||
fileType: null,
|
|
||||||
loading: true,
|
|
||||||
path: '',
|
|
||||||
text: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
el: '#app',
|
|
||||||
|
|
||||||
i18n: new VueI18n({
|
|
||||||
fallbackLocale: 'en',
|
|
||||||
locale: new URLSearchParams(window.location.search).get('hl') || navigator.languages?.[0].split('-')[0] || navigator.language?.split('-')[0] || 'en',
|
|
||||||
messages,
|
|
||||||
}),
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
hashChange() {
|
|
||||||
const hash = window.location.hash
|
|
||||||
|
|
||||||
if (hash.length > 0) {
|
|
||||||
this.path = hash.substring(1)
|
|
||||||
} else {
|
|
||||||
this.error = this.$i18n.t('fileNotFound')
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderMarkdown(text) {
|
|
||||||
return new showdown.Converter().makeHtml(text)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
window.onhashchange = this.hashChange
|
|
||||||
this.hashChange()
|
|
||||||
},
|
|
||||||
|
|
||||||
name: 'App',
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
fileType(v) {
|
|
||||||
// Rewrite known file types not matching the expectations above
|
|
||||||
if (rewrites[v]) {
|
|
||||||
this.fileType = rewrites[v]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load text files directly and highlight them
|
|
||||||
if (v.startsWith('text/')) {
|
|
||||||
this.loading = true
|
|
||||||
axios.get(this.path)
|
|
||||||
.then(resp => {
|
|
||||||
this.text = resp.data
|
|
||||||
|
|
||||||
if (this.text.length < 200 * 1024 && v !== 'text/plain') {
|
|
||||||
// Only highlight up to 200k and not on text/plain
|
|
||||||
window.setTimeout(() => hljs.initHighlighting(), 100)
|
|
||||||
}
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
path() {
|
|
||||||
if (this.path.indexOf('://') >= 0) {
|
|
||||||
// Strictly disallow loading files having any protocol in them
|
|
||||||
this.error = this.$i18n.t('notPermitted')
|
|
||||||
this.loading = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
axios.head(this.path)
|
|
||||||
.then(resp => {
|
|
||||||
let contentType = 'application/octet-stream'
|
|
||||||
if (resp && resp.headers && resp.headers['content-type']) {
|
|
||||||
contentType = resp.headers['content-type']
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false
|
|
||||||
this.fileType = contentType
|
|
||||||
this.fileName = this.path.substring(this.path.lastIndexOf('/') + 1)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
switch (err.response.status) {
|
|
||||||
case 403:
|
|
||||||
this.error = this.$i18n.t('notPermitted')
|
|
||||||
break
|
|
||||||
case 404:
|
|
||||||
this.error = this.$i18n.t('fileNotFound')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this.error = this.$i18n.t('genericError', { status: err.response.status })
|
|
||||||
}
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
File diff suppressed because one or more lines are too long
1160
frontend/bundle.js
1160
frontend/bundle.js
File diff suppressed because one or more lines are too long
|
@ -1,15 +1,12 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<!-- Required meta tags -->
|
<!-- Required meta tags -->
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
<link rel="stylesheet" href="bundle.css">
|
<link rel="stylesheet" href="app.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
|
|
||||||
integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] { display: none; }
|
[v-cloak] { display: none; }
|
||||||
|
@ -19,98 +16,8 @@
|
||||||
<title>Share</title>
|
<title>Share</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak></div>
|
||||||
<b-navbar
|
|
||||||
variant="primary"
|
|
||||||
type="dark"
|
|
||||||
>
|
|
||||||
<b-navbar-brand href="#">
|
|
||||||
<i class="fas fa-fw fa-share-alt-square mr-1"></i> Share
|
|
||||||
</b-navbar-brand>
|
|
||||||
</b-navbar>
|
|
||||||
|
|
||||||
<b-container class="mt-4">
|
|
||||||
<b-row>
|
|
||||||
<b-col>
|
|
||||||
<b-card v-if="loading">
|
|
||||||
<b-card-text class="text-center">
|
|
||||||
<h2><i class="fas fa-spinner fa-pulse"></i></h2>
|
|
||||||
{{ $t('loading') }}
|
|
||||||
</b-card-text>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<b-card
|
|
||||||
v-if="error"
|
|
||||||
bg-variant="danger"
|
|
||||||
text-variant="white"
|
|
||||||
>
|
|
||||||
<b-card-text class="text-center">
|
|
||||||
<h2><i class="fas fa-exclamation-circle"></i></h2>
|
|
||||||
{{ error }}
|
|
||||||
</b-card-text>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<b-card v-else-if="fileType.startsWith('image/')">
|
|
||||||
<b-card-text class="text-center">
|
|
||||||
<a :href="path">
|
|
||||||
<b-img
|
|
||||||
:src="path"
|
|
||||||
fluid
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</b-card-text>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<b-card v-else-if="fileType.startsWith('video/')">
|
|
||||||
<b-embed
|
|
||||||
type="video"
|
|
||||||
:src="path"
|
|
||||||
allowfullscreen
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<b-card v-else-if="fileType.startsWith('audio/')">
|
|
||||||
<b-card-text class="text-center">
|
|
||||||
<audio
|
|
||||||
:src="path"
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
</b-card-text>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<b-card
|
|
||||||
v-else-if="fileType.startsWith('text/markdown')"
|
|
||||||
no-body
|
|
||||||
>
|
|
||||||
<b-card-body
|
|
||||||
v-html="renderMarkdown(text)"
|
|
||||||
/>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<b-card v-else-if="fileType.startsWith('text/')">
|
|
||||||
<pre><code>{{ text }}</code></pre>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<b-card v-else>
|
|
||||||
<b-card-text class="text-center">
|
|
||||||
<h2><i class="fas fa-cloud-download-alt"></i></h2>
|
|
||||||
<b-button
|
|
||||||
:href="path"
|
|
||||||
variant="success"
|
|
||||||
>
|
|
||||||
{{ fileName }}
|
|
||||||
</b-button>
|
|
||||||
</b-card-text>
|
|
||||||
</b-card>
|
|
||||||
</template>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="bundle.js"></script>
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
11
main.go
11
main.go
|
@ -135,13 +135,18 @@ func doCLIUpload() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func doBootstrap() error {
|
func doBootstrap() error {
|
||||||
for _, asset := range []string{"index.html", "app.js", "bundle.css", "bundle.js"} {
|
files, err := frontend.ReadDir("frontend")
|
||||||
content, err := frontend.ReadFile(strings.Join([]string{"frontend", asset}, "/"))
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing embedded files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, asset := range files {
|
||||||
|
content, err := frontend.ReadFile(strings.Join([]string{"frontend", asset.Name()}, "/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "reading baked asset")
|
return errors.Wrap(err, "reading baked asset")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := executeUpload(asset, bytes.NewReader(content), false, "", true); err != nil {
|
if _, err := executeUpload(asset.Name(), bytes.NewReader(content), false, "", true); err != nil {
|
||||||
return errors.Wrapf(err, "uploading bootstrap asset %q", asset)
|
return errors.Wrapf(err, "uploading bootstrap asset %q", asset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4500
package-lock.json
generated
Normal file
4500
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
package.json
Normal file
23
package.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.23.10",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.3.0",
|
||||||
|
"@typescript-eslint/parser": "^7.3.0",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
|
"esbuild-plugin-vue3": "^0.4.2",
|
||||||
|
"esbuild-sass-plugin": "^3.2.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
|
"typescript": "^5.4.2"
|
||||||
|
},
|
||||||
|
"name": "share",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"highlight.js": "^11.9.0",
|
||||||
|
"showdown": "^2.1.0",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-i18n": "^9.10.2"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue