Browse Source

Add i18n. Add lang. Add charts. Add theme switch

pull/937/head
Sergei Birukov 2 years ago
parent
commit
051833923d
  1. 77
      webui/package-lock.json
  2. 5
      webui/package.json
  3. 42
      webui/src/App.vue
  4. 64
      webui/src/components/ClientCharts.vue
  5. 46
      webui/src/components/Header.vue
  6. 15
      webui/src/components/icons/IconChart.vue
  7. 54
      webui/src/components/icons/IconUITheme.vue
  8. 10
      webui/src/main.js
  9. 22
      webui/src/services/api.js
  10. 40
      webui/src/store/store.js
  11. 36
      webui/src/utils/chartOptions.js
  12. 559
      webui/src/utils/i18n.js
  13. 35
      webui/tailwind.config.js

77
webui/package-lock.json

@ -8,11 +8,12 @@
"name": "webui", "name": "webui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"apexcharts": "^3.45.1", "apexcharts": "^3.48.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue3-apexcharts": "^1.4.4" "vue-i18n": "^9.10.2",
"vue3-apexcharts": "^1.5.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "^7.3.1", "@typescript-eslint/parser": "^7.3.1",
@ -560,6 +561,47 @@
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true "dev": true
}, },
"node_modules/@intlify/core-base": {
"version": "9.10.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.10.2.tgz",
"integrity": "sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg==",
"dependencies": {
"@intlify/message-compiler": "9.10.2",
"@intlify/shared": "9.10.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.10.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.10.2.tgz",
"integrity": "sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA==",
"dependencies": {
"@intlify/shared": "9.10.2",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "9.10.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.10.2.tgz",
"integrity": "sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1159,9 +1201,9 @@
} }
}, },
"node_modules/apexcharts": { "node_modules/apexcharts": {
"version": "3.45.1", "version": "3.48.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.1.tgz", "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
"integrity": "sha512-pPjj/SA6dfPvR/IKRZF0STdfBGpBh3WRt7K0DFuW9P8erypYkX17EHu3/molPRfo2zSiQwTVpshHC5ncysqfkA==", "integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
"dependencies": { "dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3", "@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2", "svg.draggable.js": "^2.2.2",
@ -3768,10 +3810,29 @@
"eslint": ">=6.0.0" "eslint": ">=6.0.0"
} }
}, },
"node_modules/vue-i18n": {
"version": "9.10.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.10.2.tgz",
"integrity": "sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw==",
"dependencies": {
"@intlify/core-base": "9.10.2",
"@intlify/shared": "9.10.2",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue3-apexcharts": { "node_modules/vue3-apexcharts": {
"version": "1.4.4", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.4.4.tgz", "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.2.tgz",
"integrity": "sha512-TH89uZrxGjaDvkaYAISvj8+k6Bf1rUKFillc8oJirs5XZEPiwM1ELKZQ786wz0rfPqkSHHny2lqqUCK7Rw+LcQ==", "integrity": "sha512-rGbgUJDjtsyjfRF0uzwDjzt8+M7ICSRAbm1N9KCDiczW8BSpbEZuaEsJDJYnJuLFIIVXIGilYzIcjNBf6NbeYA==",
"peerDependencies": { "peerDependencies": {
"apexcharts": "> 3.0.0", "apexcharts": "> 3.0.0",
"vue": "> 3.0.0" "vue": "> 3.0.0"

5
webui/package.json

@ -10,11 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"apexcharts": "^3.45.1", "apexcharts": "^3.48.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue3-apexcharts": "^1.4.4" "vue-i18n": "^9.10.2",
"vue3-apexcharts": "^1.5.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "^7.3.1", "@typescript-eslint/parser": "^7.3.1",

42
webui/src/App.vue

@ -1,6 +1,6 @@
<template> <template>
<main> <main>
<div v-cloak class="container mx-auto max-w-3xl px-5 md:px-0"> <div v-cloak class="container mx-auto max-w-3xl px-3 md:px-0 mt-4 xs:mt-6">
<div v-if="authenticated === true"> <div v-if="authenticated === true">
<Header /> <Header />
<UpdateNotification :latest-release="latestRelease" :current-release="currentRelease" /> <UpdateNotification :latest-release="latestRelease" :current-release="currentRelease" />
@ -62,7 +62,8 @@
</template> </template>
<script setup> <script setup>
import { ref, onBeforeUnmount, onBeforeMount } from 'vue'; import { ref, onBeforeUnmount, onBeforeMount, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import Auth from '@/components/Auth.vue'; import Auth from '@/components/Auth.vue';
import UpdateNotification from '@/components/UpdateNotification.vue'; import UpdateNotification from '@/components/UpdateNotification.vue';
@ -80,10 +81,13 @@ import Client from '@/components/Client.vue';
import { useStore } from '@/store/store'; import { useStore } from '@/store/store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const i18n = useI18n();
const store = useStore(); const store = useStore();
const { authenticated, requiresPassword } = storeToRefs(store); const { authenticated, requiresPassword } = storeToRefs(store);
const { clients, clientCreateShowModal, clientToDelete, clientCreateName, qrcode } = storeToRefs(store); const { clients, clientCreateShowModal, clientToDelete, qrcode, prefersDarkScheme, uiTheme, uiChartType } =
storeToRefs(store);
const currentRelease = ref(null); const currentRelease = ref(null);
const latestRelease = ref(null); const latestRelease = ref(null);
@ -93,8 +97,15 @@ const refreshInterval = ref(null);
const api = new API(); const api = new API();
const refresh = store.refresh; const refresh = store.refresh;
const setTheme = store.setTheme;
watch(prefersDarkScheme.value, () => {
setTheme(uiTheme.value);
});
onBeforeMount(() => { onBeforeMount(() => {
setTheme(uiTheme.value);
api api
.getSession() .getSession()
.then((session) => { .then((session) => {
@ -107,13 +118,25 @@ onBeforeMount(() => {
.catch((err) => { .catch((err) => {
console.log(err.message || err.toString()); console.log(err.message || err.toString());
}); });
getChartType();
getRelease(); getRelease();
getLang();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(refreshInterval.value); clearInterval(refreshInterval.value);
}); });
async function getLang() {
const lang = await api.getLang();
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
localStorage.setItem('lang', lang);
i18n.global.locale = lang;
}
}
async function getRelease() { async function getRelease() {
try { try {
const currentReleaseData = await api.getRelease(); const currentReleaseData = await api.getRelease();
@ -142,4 +165,17 @@ async function getRelease() {
console.log('Something went wrong', error); console.log('Something went wrong', error);
} }
} }
async function getChartType() {
api
.getChartType()
.then((res) => {
uiChartType.value = parseInt(res, 10);
console.log(res);
console.log(uiChartType.value);
})
.catch(() => {
uiChartType.value = 0;
});
}
</script> </script>

64
webui/src/components/ClientCharts.vue

@ -1,22 +1,20 @@
<template> <template>
<div v-if="client"> <div>
<!-- TODO: Individual bars are too wide --> <!-- TODO: Individual bars are too wide -->
<div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%"> <div v-if="uiChartType && uiShowCharts" class="absolute z-0 bottom-0 left-0 right-0 h-6">
<VueApexCharts <VueApexCharts
width="100%" width="100%"
height="100%" height="100%"
type="bar" :options="chartOptionsTX"
:options="chartOptions" :series="[{ name: 'Upload (TX)', data: client?.transferTxHistory }]"
:series="[{ name: 'Upload (TX)', data: client?.transferTxHistory, },]"
/> />
</div> </div>
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%"> <div v-if="uiChartType && uiShowCharts" class="absolute z-0 top-0 left-0 right-0 h-6">
<VueApexCharts <VueApexCharts
width="100%" width="100%"
height="100%" height="100%"
type="bar" :options="chartOptionsRX"
:options="chartOptions" :series="[{ name: 'Download (RX)', data: client?.transferRxHistory }]"
:series="[{ name: 'Download (RX)', data: client?.transferRxHistory, },]"
style="transform: scaleY(-1)" style="transform: scaleY(-1)"
/> />
</div> </div>
@ -25,10 +23,56 @@
<script setup> <script setup>
import VueApexCharts from 'vue3-apexcharts'; import VueApexCharts from 'vue3-apexcharts';
import chartOptions from '@/utils/chartOptions'; import { chartOptions } from '@/utils/chartOptions';
import { useStore } from '@/store/store';
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';
const UI_CHART_TYPES = [
{ type: false, strokeWidth: 0 },
{ type: 'line', strokeWidth: 3 },
{ type: 'area', strokeWidth: 0 },
{ type: 'bar', strokeWidth: 0 },
];
const CHART_COLORS = {
rx: { light: 'rgba(128,128,128,0.3)', dark: 'rgba(255,255,255,0.3)' },
tx: { light: 'rgba(128,128,128,0.4)', dark: 'rgba(255,255,255,0.3)' },
gradient: { light: ['rgba(0,0,0,0.1)', 'rgba(0,0,0,1.0)'], dark: ['rgba(128,128,128,0)', 'rgba(128,128,128,0)'] },
};
defineProps({ defineProps({
client: {}, client: {},
}); });
const store = useStore();
const { uiShowCharts, uiChartType, uiTheme, prefersDarkScheme } = storeToRefs(store);
const darkOrLight = computed(() => {
if (uiTheme.value === 'auto') {
return prefersDarkScheme.value.matches ? 'dark' : 'light';
}
return uiTheme.value;
});
const chartOptionsTX = computed(() => {
const opts = {
...chartOptions,
colors: [CHART_COLORS.tx[darkOrLight.value]],
};
opts.chart.type = UI_CHART_TYPES[uiChartType.value].type || false;
opts.stroke.width = UI_CHART_TYPES[uiChartType.value].strokeWidth;
opts.fill.gradient.gradientToColors = CHART_COLORS.gradient[darkOrLight.value];
return opts;
});
const chartOptionsRX = computed(() => {
const opts = {
...chartOptions,
colors: [CHART_COLORS.rx[darkOrLight.value]],
};
opts.chart.type = UI_CHART_TYPES[uiChartType.value].type || false;
opts.stroke.width = UI_CHART_TYPES[uiChartType.value].strokeWidth;
opts.fill.gradient.gradientToColors = CHART_COLORS.gradient[darkOrLight.value];
return opts;
});
</script> </script>

46
webui/src/components/Header.vue

@ -1,5 +1,39 @@
<template> <template>
<div> <div class="flex flex-col-reverse xxs:flex-row flex-auto items-center items-end gap-3">
<h1 class="text-4xl dark:text-neutral-200 font-medium flex-grow self-start mb-4">
<img src="@/assets/img/logo.png" width="32" class="inline align-middle dark:bg mr-2" /><span class="align-middle"
>WireGuard</span
>
</h1>
<div class="flex items-center grow-0 gap-3 items-end self-end xxs:self-center">
<!-- Dark / light theme -->
<button
class="flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 transition"
:title="$t(`theme.${uiTheme}`)"
@click="toggleTheme"
>
<IconUITheme :theme="uiTheme" />
</button>
<!-- Show / hide charts -->
<label
v-if="uiChartType > 0"
class="inline-flex items-center justify-center cursor-pointer w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600 whitespace-nowrap transition group"
:title="$t('toggleCharts')"
>
<input v-model="uiShowCharts" type="checkbox" value="" class="sr-only peer" @change="toggleCharts" />
<IconChart />
</label>
<span
v-if="requiresPassword"
class="text-sm text-gray-400 dark:text-neutral-400 cursor-pointer hover:underline"
@click="logout"
>
{{ $t('logout') }}
<IconLogout />
</span>
</div>
</div>
<!-- <div>
<span <span
v-if="requiresPassword" v-if="requiresPassword"
class="text-sm text-gray-400 dark:text-neutral-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right" class="text-sm text-gray-400 dark:text-neutral-400 mb-10 mr-2 mt-3 cursor-pointer hover:underline float-right"
@ -13,19 +47,23 @@
<span class="align-middle">WireGuard</span> <span class="align-middle">WireGuard</span>
</h1> </h1>
<h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2> <h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2>
</div> </div> -->
</template> </template>
<script setup> <script setup>
import IconLogout from './icons/IconLogout.vue'; import IconLogout from '@/components/icons/IconLogout.vue';
import IconUITheme from '@/components/icons/IconUITheme.vue';
import IconChart from './icons/IconChart.vue';
import { useStore } from '@/store/store'; import { useStore } from '@/store/store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const store = useStore(); const store = useStore();
const { requiresPassword } = storeToRefs(store); const { requiresPassword, uiTheme, uiShowCharts, uiChartType } = storeToRefs(store);
const logout = store.logout; const logout = store.logout;
const toggleTheme = store.toggleTheme;
const toggleCharts = store.toggleCharts;
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

15
webui/src/components/icons/IconChart.vue

@ -0,0 +1,15 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke-width="1.5"
fill="currentColor"
class="w-5 h-5 peer fill-gray-400 peer-checked:fill-gray-600 dark:fill-neutral-600 peer-checked:dark:fill-neutral-400 group-hover:dark:fill-neutral-500 transition"
>
<path
d="M18.375 2.25c-1.035 0-1.875.84-1.875 1.875v15.75c0 1.035.84 1.875 1.875 1.875h.75c1.035 0 1.875-.84 1.875-1.875V4.125c0-1.036-.84-1.875-1.875-1.875h-.75ZM9.75 8.625c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v11.25c0 1.035-.84 1.875-1.875 1.875h-.75a1.875 1.875 0 0 1-1.875-1.875V8.625ZM3 13.125c0-1.036.84-1.875 1.875-1.875h.75c1.036 0 1.875.84 1.875 1.875v6.75c0 1.035-.84 1.875-1.875 1.875h-.75A1.875 1.875 0 0 1 3 19.875v-6.75Z"
/>
</svg>
</template>
<script setup></script>

54
webui/src/components/icons/IconUITheme.vue

@ -0,0 +1,54 @@
<template>
<div>
<svg
v-if="theme === 'light'"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
/>
</svg>
<svg
v-else-if="theme === 'dark'"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 text-neutral-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
/>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
class="w-5 h-5 fill-gray-600 dark:fill-neutral-400"
>
<path
d="M12,2.2c-5.4,0-9.8,4.4-9.8,9.8s4.4,9.8,9.8,9.8s9.8-4.4,9.8-9.8S17.4,2.2,12,2.2z M3.8,12c0-4.5,3.7-8.2,8.2-8.2v16.5C7.5,20.2,3.8,16.5,3.8,12z"
/>
</svg>
</div>
</template>
<script setup>
defineProps({
theme: {
type: String,
default: 'auto',
},
});
</script>

10
webui/src/main.js

@ -2,7 +2,15 @@ import './assets/style.css';
import App from './App.vue'; import App from './App.vue';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { messages } from '@/utils/i18n';
const i18n = createI18n({
locale: localStorage.getItem('lang') || 'en',
allowComposition: true,
fallbackLocale: 'en',
messages,
});
const pinia = createPinia(); const pinia = createPinia();
createApp(App).use(pinia).mount('#app'); createApp(App).use(i18n).use(pinia).mount('#app');

22
webui/src/services/api.js

@ -32,6 +32,28 @@ export default class API {
}); });
} }
async getLang() {
const res = await this.call({
method: 'get',
path: '/lang',
});
return res.slice(0, 2);
}
async getuiTrafficStats() {
return this.call({
method: 'get',
path: '/ui-traffic-stats',
});
}
async getChartType() {
return this.call({
method: 'get',
path: '/ui-chart-type',
});
}
async getSession() { async getSession() {
return this.call({ return this.call({
method: 'get', method: 'get',

40
webui/src/store/store.js

@ -19,6 +19,12 @@ export const useStore = defineStore('store', () => {
const clientCreateName = ref(''); const clientCreateName = ref('');
const qrcode = ref(null); const qrcode = ref(null);
const uiTheme = ref(localStorage.theme || 'auto');
const prefersDarkScheme = ref(window.matchMedia('(prefers-color-scheme: dark)'));
const uiChartType = ref(0);
const uiShowCharts = ref(localStorage.getItem('uiShowCharts') === '1');
function login(e) { function login(e) {
e.preventDefault(); e.preventDefault();
@ -34,7 +40,7 @@ export const useStore = defineStore('store', () => {
const session = await api.getSession(); const session = await api.getSession();
authenticated.value = session.authenticated; authenticated.value = session.authenticated;
requiresPassword.value = session.requiresPassword; requiresPassword.value = session.requiresPassword;
return this.refresh(); return refresh();
}) })
.catch((err) => { .catch((err) => {
console.log(err.message || err.toString()); console.log(err.message || err.toString());
@ -135,6 +141,30 @@ export const useStore = defineStore('store', () => {
} }
} }
function toggleTheme() {
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(uiTheme.value);
const newIndex = (currentIndex + 1) % themes.length;
uiTheme.value = themes[newIndex];
localStorage.theme = uiTheme.value;
setTheme(uiTheme.value);
}
function setTheme(theme) {
const { classList } = document.documentElement;
const shouldAddDarkClass = theme === 'dark' || (theme === 'auto' && prefersDarkScheme.value.matches);
classList.toggle('dark', shouldAddDarkClass);
}
function handlePrefersChange(e) {
if (localStorage.theme === 'auto') {
setTheme(e.matches ? 'dark' : 'light');
}
}
function toggleCharts() {
localStorage.setItem('uiShowCharts', uiShowCharts.value ? 1 : 0);
}
return { return {
authenticated, authenticated,
authenticating, authenticating,
@ -146,10 +176,18 @@ export const useStore = defineStore('store', () => {
clientCreateShowModal, clientCreateShowModal,
clientCreateName, clientCreateName,
qrcode, qrcode,
uiTheme,
prefersDarkScheme,
uiChartType,
uiShowCharts,
login, login,
logout, logout,
createClient, createClient,
deleteClient, deleteClient,
refresh, refresh,
toggleTheme,
setTheme,
handlePrefersChange,
toggleCharts,
}; };
}); });

36
webui/src/utils/chartOptions.js

@ -1,9 +1,6 @@
import { reactive } from 'vue'; export const chartOptions = {
const chartOptions = reactive({
chart: { chart: {
background: 'transparent', background: 'transparent',
type: 'bar',
stacked: false, stacked: false,
toolbar: { toolbar: {
show: false, show: false,
@ -11,11 +8,26 @@ const chartOptions = reactive({
animations: { animations: {
enabled: false, enabled: false,
}, },
parentHeightOffset: 0,
sparkline: {
enabled: true,
},
},
colors: [],
stroke: {
curve: 'smooth',
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'vertical',
shadeIntensity: 0,
inverseColors: false,
opacityTo: 0,
stops: [0, 100],
},
}, },
colors: [
'#DDDDDD', // rx
'#EEEEEE', // tx
],
dataLabels: { dataLabels: {
enabled: false, enabled: false,
}, },
@ -29,10 +41,10 @@ const chartOptions = reactive({
show: false, show: false,
}, },
axisTicks: { axisTicks: {
show: true, show: false,
}, },
axisBorder: { axisBorder: {
show: true, show: false,
}, },
}, },
yaxis: { yaxis: {
@ -64,6 +76,4 @@ const chartOptions = reactive({
}, },
}, },
}, },
}); };
export default chartOptions;

559
webui/src/utils/i18n.js

@ -0,0 +1,559 @@
'use strict';
export const messages = {
// eslint-disable-line no-unused-vars
en: {
name: 'Name',
password: 'Password',
signIn: 'Sign In',
logout: 'Logout',
updateAvailable: 'There is an update available!',
update: 'Update',
clients: 'Clients',
new: 'New',
deleteClient: 'Delete Client',
deleteDialog1: 'Are you sure you want to delete',
deleteDialog2: 'This action cannot be undone.',
cancel: 'Cancel',
create: 'Create',
createdOn: 'Created on ',
lastSeen: 'Last seen on ',
totalDownload: 'Total Download: ',
totalUpload: 'Total Upload: ',
newClient: 'New Client',
disableClient: 'Disable Client',
enableClient: 'Enable Client',
noClients: 'There are no clients yet.',
noPrivKey: 'This client has no known private key. Cannot create Configuration.',
showQR: 'Show QR Code',
downloadConfig: 'Download Configuration',
madeBy: 'Made by',
donate: 'Donate',
toggleCharts: 'Show/hide Charts',
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
},
ua: {
name: 'Ім`я',
password: 'Пароль',
signIn: 'Увійти',
logout: 'Вихід',
updateAvailable: 'Доступне оновлення!',
update: 'Оновити',
clients: 'Клієнти',
new: 'Новий',
deleteClient: 'Видалити клієнта',
deleteDialog1: 'Ви впевнені, що бажаєте видалити',
deleteDialog2: 'Цю дію неможливо скасувати.',
cancel: 'Скасувати',
create: 'Створити',
createdOn: 'Створено ',
lastSeen: 'Останнє підключення в ',
totalDownload: 'Всього завантажено: ',
totalUpload: 'Всього відправлено: ',
newClient: 'Новий клієнт',
disableClient: 'Вимкнути клієнта',
enableClient: 'Увімкнути клієнта',
noClients: 'Ще немає клієнтів.',
showQR: 'Показати QR-код',
downloadConfig: 'Завантажити конфігурацію',
madeBy: 'Зроблено',
donate: 'Пожертвувати',
},
ru: {
name: 'Имя',
password: 'Пароль',
signIn: 'Войти',
logout: 'Выйти',
updateAvailable: 'Доступно обновление!',
update: 'Обновить',
clients: 'Клиенты',
new: 'Создать',
deleteClient: 'Удалить клиента',
deleteDialog1: 'Вы уверены, что хотите удалить',
deleteDialog2: 'Это действие невозможно отменить.',
cancel: 'Закрыть',
create: 'Создать',
createdOn: 'Создано в ',
lastSeen: 'Последнее подключение в ',
totalDownload: 'Всего скачано: ',
totalUpload: 'Всего загружено: ',
newClient: 'Создать клиента',
disableClient: 'Выключить клиента',
enableClient: 'Включить клиента',
noClients: 'Пока нет клиентов.',
showQR: 'Показать QR-код',
downloadConfig: 'Скачать конфигурацию',
madeBy: 'Автор',
donate: 'Поблагодарить',
},
tr: {
// Müslüm Barış Korkmazer @babico
name: 'İsim',
password: 'Şifre',
signIn: 'Giriş Yap',
logout: 'Çıkış Yap',
updateAvailable: 'Mevcut bir güncelleme var!',
update: 'Güncelle',
clients: 'Kullanıcılar',
new: 'Yeni',
deleteClient: 'Kullanıcı Sil',
deleteDialog1: 'Silmek istediğine emin misin',
deleteDialog2: 'Bu işlem geri alınamaz.',
cancel: 'İptal',
create: 'Oluştur',
createdAt: 'Şu saatte oluşturuldu: ',
lastSeen: 'Son görülme tarihi: ',
totalDownload: 'Toplam İndirme: ',
totalUpload: 'Toplam Yükleme: ',
newClient: 'Yeni Kullanıcı',
disableClient: 'İstemciyi Devre Dışı Bırak',
enableClient: 'İstemciyi Etkinleştir',
noClients: 'Henüz kullanıcı yok.',
showQR: 'QR Kodunu Göster',
downloadConfig: 'Yapılandırmayı İndir',
madeBy: 'Yapan Kişi: ',
donate: 'Bağış Yap',
changeLang: 'Dil Değiştir',
},
no: {
// github.com/digvalley
name: 'Navn',
password: 'Passord',
signIn: 'Logg Inn',
logout: 'Logg Ut',
updateAvailable: 'En ny oppdatering er tilgjengelig!',
update: 'Oppdater',
clients: 'Klienter',
new: 'Ny',
deleteClient: 'Slett Klient',
deleteDialog1: 'Er du sikker på at du vil slette?',
deleteDialog2: 'Denne handlingen kan ikke angres',
cancel: 'Avbryt',
create: 'Opprett',
createdOn: 'Opprettet ',
lastSeen: 'Sist sett ',
totalDownload: 'Total Nedlasting: ',
totalUpload: 'Total Opplasting: ',
newClient: 'Ny Klient',
disableClient: 'Deaktiver Klient',
enableClient: 'Aktiver Klient',
noClients: 'Ingen klienter opprettet enda.',
showQR: 'Vis QR Kode',
downloadConfig: 'Last Ned Konfigurasjon',
madeBy: 'Laget av',
donate: 'Doner',
},
pl: {
// github.com/archont94
name: 'Nazwa',
password: 'Hasło',
signIn: 'Zaloguj się',
logout: 'Wyloguj się',
updateAvailable: 'Dostępna aktualizacja!',
update: 'Aktualizuj',
clients: 'Klienci',
new: 'Stwórz klienta',
deleteClient: 'Usuń klienta',
deleteDialog1: 'Jesteś pewny że chcesz usunąć',
deleteDialog2: 'Tej akcji nie da się cofnąć.',
cancel: 'Anuluj',
create: 'Stwórz',
createdOn: 'Utworzono ',
lastSeen: 'Ostatnio widziany ',
totalDownload: 'Całkowite pobieranie: ',
totalUpload: 'Całkowite wysyłanie: ',
newClient: 'Nowy klient',
disableClient: 'Wyłączenie klienta',
enableClient: 'Włączenie klienta',
noClients: 'Nie ma jeszcze klientów.',
showQR: 'Pokaż kod QR',
downloadConfig: 'Pobierz konfigurację',
madeBy: 'Stworzone przez',
donate: 'Wsparcie autora',
},
fr: {
// github.com/clem3109
name: 'Nom',
password: 'Mot de passe',
signIn: 'Se Connecter',
logout: 'Se déconnecter',
updateAvailable: 'Une mise à jour est disponible !',
update: 'Mise à jour',
clients: 'Clients',
new: 'Nouveau',
deleteClient: 'Supprimer ce client',
deleteDialog1: 'Êtes-vous que vous voulez supprimer',
deleteDialog2: 'Cette action ne peut pas être annulée.',
cancel: 'Annuler',
create: 'Créer',
createdOn: 'Créé le ',
lastSeen: 'Dernière connexion le ',
totalDownload: 'Téléchargement total : ',
totalUpload: 'Téléversement total : ',
newClient: 'Nouveau client',
disableClient: 'Désactiver ce client',
enableClient: 'Activer ce client',
noClients: 'Aucun client pour le moment.',
showQR: 'Afficher le code à réponse rapide (QR Code)',
downloadConfig: 'Télécharger la configuration',
madeBy: 'Développé par',
donate: 'Soutenir',
},
de: {
// github.com/florian-asche
name: 'Name',
password: 'Passwort',
signIn: 'Anmelden',
logout: 'Abmelden',
updateAvailable: 'Eine Aktualisierung steht zur Verfügung!',
update: 'Aktualisieren',
clients: 'Clients',
new: 'Neu',
deleteClient: 'Client löschen',
deleteDialog1: 'Möchtest du wirklich löschen?',
deleteDialog2: 'Diese Aktion kann nicht rückgängig gemacht werden.',
cancel: 'Abbrechen',
create: 'Erstellen',
createdOn: 'Erstellt am ',
lastSeen: 'Zuletzt Online ',
totalDownload: 'Gesamt Download: ',
totalUpload: 'Gesamt Upload: ',
newClient: 'Neuer Client',
disableClient: 'Client deaktivieren',
enableClient: 'Client aktivieren',
noClients: 'Es wurden noch keine Clients konfiguriert.',
noPrivKey: 'Es ist kein Private Key für diesen Client bekannt. Eine Konfiguration kann nicht erstellt werden.',
showQR: 'Zeige den QR Code',
downloadConfig: 'Konfiguration herunterladen',
madeBy: 'Erstellt von',
donate: 'Spenden',
},
ca: {
// github.com/guillembonet
name: 'Nom',
password: 'Contrasenya',
signIn: 'Iniciar sessió',
logout: 'Tanca sessió',
updateAvailable: 'Hi ha una actualització disponible!',
update: 'Actualitza',
clients: 'Clients',
new: 'Nou',
deleteClient: 'Esborra client',
deleteDialog1: 'Estàs segur que vols esborrar aquest client?',
deleteDialog2: 'Aquesta acció no es pot desfer.',
cancel: 'Cancel·la',
create: 'Crea',
createdOn: 'Creat el ',
lastSeen: 'Última connexió el ',
totalDownload: 'Baixada total: ',
totalUpload: 'Pujada total: ',
newClient: 'Nou client',
disableClient: 'Desactiva client',
enableClient: 'Activa client',
noClients: 'Encara no hi ha cap client.',
showQR: 'Mostra codi QR',
downloadConfig: 'Descarrega configuració',
madeBy: 'Fet per',
donate: 'Donatiu',
},
es: {
// github.com/amarqz
name: 'Nombre',
password: 'Contraseña',
signIn: 'Iniciar sesión',
logout: 'Cerrar sesión',
updateAvailable: '¡Hay una actualización disponible!',
update: 'Actualizar',
clients: 'Clientes',
new: 'Nuevo',
deleteClient: 'Eliminar cliente',
deleteDialog1: '¿Estás seguro de que quieres borrar este cliente?',
deleteDialog2: 'Esta acción no podrá ser revertida.',
cancel: 'Cancelar',
create: 'Crear',
createdOn: 'Creado el ',
lastSeen: 'Última conexión el ',
totalDownload: 'Total descargado: ',
totalUpload: 'Total subido: ',
newClient: 'Nuevo cliente',
disableClient: 'Desactivar cliente',
enableClient: 'Activar cliente',
noClients: 'Aún no hay ningún cliente.',
showQR: 'Mostrar código QR',
downloadConfig: 'Descargar configuración',
madeBy: 'Hecho por',
donate: 'Donar',
},
ko: {
name: '이름',
password: '암호',
signIn: '로그인',
logout: '로그아웃',
updateAvailable: '업데이트가 있습니다!',
update: '업데이트',
clients: '클라이언트',
new: '추가',
deleteClient: '클라이언트 삭제',
deleteDialog1: '삭제 하시겠습니까?',
deleteDialog2: '이 작업은 취소할 수 없습니다.',
cancel: '취소',
create: '생성',
createdOn: '생성일: ',
lastSeen: '마지막 사용 날짜: ',
totalDownload: '총 다운로드: ',
totalUpload: '총 업로드: ',
newClient: '새로운 클라이언트',
disableClient: '클라이언트 비활성화',
enableClient: '클라이언트 활성화',
noClients: '아직 클라이언트가 없습니다.',
showQR: 'QR 코드 표시',
downloadConfig: '구성 다운로드',
madeBy: '만든 사람',
donate: '기부',
},
vi: {
name: 'Tên',
password: 'Mật khẩu',
signIn: 'Đăng nhập',
logout: 'Đăng xuất',
updateAvailable: 'Có bản cập nhật mới!',
update: 'Cập nhật',
clients: 'Danh sách người dùng',
new: 'Mới',
deleteClient: 'Xóa người dùng',
deleteDialog1: 'Bạn có chắc chắn muốn xóa',
deleteDialog2: 'Thao tác này không thể hoàn tác.',
cancel: 'Huỷ',
create: 'Tạo',
createdOn: 'Được tạo lúc ',
lastSeen: 'Lần xem cuối vào ',
totalDownload: 'Tổng dung lượng tải xuống: ',
totalUpload: 'Tổng dung lượng tải lên: ',
newClient: 'Người dùng mới',
disableClient: 'Vô hiệu hóa người dùng',
enableClient: 'Kích hoạt người dùng',
noClients: 'Hiện chưa có người dùng nào.',
showQR: 'Hiển thị mã QR',
downloadConfig: 'Tải xuống cấu hình',
madeBy: 'Được tạo bởi',
donate: 'Ủng hộ',
},
nl: {
name: 'Naam',
password: 'Wachtwoord',
signIn: 'Inloggen',
logout: 'Uitloggen',
updateAvailable: 'Nieuw update beschikbaar!',
update: 'update',
clients: 'clients',
new: 'Nieuw',
deleteClient: 'client verwijderen',
deleteDialog1: 'Weet je zeker dat je wilt verwijderen',
deleteDialog2: 'Deze actie kan niet ongedaan worden gemaakt.',
cancel: 'Annuleren',
create: 'Creëren',
createdOn: 'Gemaakt op ',
lastSeen: 'Laatst gezien op ',
totalDownload: 'Totaal Gedownload: ',
totalUpload: 'Totaal Geupload: ',
newClient: 'Nieuwe client',
disableClient: 'client uitschakelen',
enableClient: 'client inschakelen',
noClients: 'Er zijn nog geen clients.',
showQR: 'QR-code weergeven',
downloadConfig: 'Configuratie downloaden',
madeBy: 'Gemaakt door',
donate: 'Doneren',
},
is: {
name: 'Nafn',
password: 'Lykilorð',
signIn: 'Skrá inn',
logout: 'Útskráning',
updateAvailable: 'Það er uppfærsla í boði!',
update: 'Uppfæra',
clients: 'Viðskiptavinir',
new: 'Nýtt',
deleteClient: 'Eyða viðskiptavin',
deleteDialog1: 'Ertu viss um að þú viljir eyða',
deleteDialog2: 'Þessi aðgerð getur ekki verið afturkallað.',
cancel: 'Hætta við',
create: 'Búa til',
createdOn: 'Búið til á ',
lastSeen: 'Síðast séð á ',
totalDownload: 'Samtals Niðurhlaða: ',
totalUpload: 'Samtals Upphlaða: ',
newClient: 'Nýr Viðskiptavinur',
disableClient: 'Gera viðskiptavin óvirkan',
enableClient: 'Gera viðskiptavin virkan',
noClients: 'Engir viðskiptavinir ennþá.',
showQR: 'Sýna QR-kóða',
downloadConfig: 'Niðurhal Stillingar',
madeBy: 'Gert af',
donate: 'Gefa',
},
pt: {
name: 'Nome',
password: 'Palavra Chave',
signIn: 'Entrar',
logout: 'Sair',
updateAvailable: 'Existe uma atualização disponível!',
update: 'Atualizar',
clients: 'Clientes',
new: 'Novo',
deleteClient: 'Apagar Clientes',
deleteDialog1: 'Tem certeza que pretende apagar',
deleteDialog2: 'Esta ação não pode ser revertida.',
cancel: 'Cancelar',
create: 'Criar',
createdOn: 'Criado em ',
lastSeen: 'Último acesso em ',
totalDownload: 'Total Download: ',
totalUpload: 'Total Upload: ',
newClient: 'Novo Cliente',
disableClient: 'Desativar Cliente',
enableClient: 'Ativar Cliente',
noClients: 'Não existem ainda clientes.',
showQR: 'Apresentar o código QR',
downloadConfig: 'Descarregar Configuração',
madeBy: 'Feito por',
donate: 'Doar',
},
chs: {
name: '名称',
password: '密码',
signIn: '登录',
logout: '退出',
updateAvailable: '有新版本可用!',
update: '更新',
clients: '客户端',
new: '新建',
deleteClient: '删除客户端',
deleteDialog1: '您确定要删除',
deleteDialog2: '此操作无法撤销。',
cancel: '取消',
create: '创建',
createdOn: '创建于 ',
lastSeen: '最后访问于 ',
totalDownload: '总下载: ',
totalUpload: '总上传: ',
newClient: '新建客户端',
disableClient: '禁用客户端',
enableClient: '启用客户端',
noClients: '目前没有客户端。',
showQR: '显示二维码',
downloadConfig: '下载配置',
madeBy: '由',
donate: '捐赠',
},
cht: {
name: '名字',
password: '密碼',
signIn: '登入',
logout: '登出',
updateAvailable: '有新版本可用!',
update: '更新',
clients: '客戶',
new: '新建',
deleteClient: '刪除客戶',
deleteDialog1: '您確定要刪除',
deleteDialog2: '此操作無法撤銷。',
cancel: '取消',
create: '建立',
createdOn: '建立於 ',
lastSeen: '最後訪問於 ',
totalDownload: '總下載: ',
totalUpload: '總上傳: ',
newClient: '新客戶',
disableClient: '禁用客戶',
enableClient: '啟用客戶',
noClients: '目前沒有客戶。',
showQR: '顯示二維碼',
downloadConfig: '下載配置',
madeBy: '由',
donate: '捐贈',
},
it: {
name: 'Nome',
password: 'Password',
signIn: 'Accedi',
logout: 'Esci',
updateAvailable: 'È disponibile un aggiornamento!',
update: 'Aggiorna',
clients: 'Client',
new: 'Nuovo',
deleteClient: 'Elimina Client',
deleteDialog1: 'Sei sicuro di voler eliminare',
deleteDialog2: 'Questa azione non può essere annullata.',
cancel: 'Annulla',
create: 'Crea',
createdOn: 'Creato il ',
lastSeen: "Visto l'ultima volta il ",
totalDownload: 'Totale Download: ',
totalUpload: 'Totale Upload: ',
newClient: 'Nuovo Client',
disableClient: 'Disabilita Client',
enableClient: 'Abilita Client',
noClients: 'Non ci sono ancora client.',
showQR: 'Mostra codice QR',
downloadConfig: 'Scarica configurazione',
madeBy: 'Realizzato da',
donate: 'Donazione',
},
th: {
name: 'ชื่อ',
password: 'รหัสผ่าน',
signIn: 'ลงชื่อเข้าใช้',
logout: 'ออกจากระบบ',
updateAvailable: 'มีอัปเดตพร้อมใช้งาน!',
update: 'อัปเดต',
clients: 'Clients',
new: 'ใหม่',
deleteClient: 'ลบ Client',
deleteDialog1: 'คุณแน่ใจหรือไม่ว่าต้องการลบ',
deleteDialog2: 'การกระทำนี้;ไม่สามารถยกเลิกได้',
cancel: 'ยกเลิก',
create: 'สร้าง',
createdOn: 'สร้างเมื่อ ',
lastSeen: 'เห็นครั้งสุดท้ายเมื่อ ',
totalDownload: 'ดาวน์โหลดทั้งหมด: ',
totalUpload: 'อัพโหลดทั้งหมด: ',
newClient: 'Client ใหม่',
disableClient: 'ปิดการใช้งาน Client',
enableClient: 'เปิดการใช้งาน Client',
noClients: 'ยังไม่มี Clients เลย',
showQR: 'แสดงรหัส QR',
downloadConfig: 'ดาวน์โหลดการตั้งค่า',
madeBy: 'สร้างโดย',
donate: 'บริจาค',
},
hi: {
// github.com/rahilarious
name: 'नाम',
password: 'पासवर्ड',
signIn: 'लॉगिन',
logout: 'लॉगआउट',
updateAvailable: 'अपडेट उपलब्ध है!',
update: 'अपडेट',
clients: 'उपयोगकर्ताये',
new: 'नया',
deleteClient: 'उपयोगकर्ता हटाएँ',
deleteDialog1: 'क्या आपको पक्का हटाना है',
deleteDialog2: 'यह निर्णय पलट नहीं सकता।',
cancel: 'कुछ ना करें',
create: 'बनाएं',
createdOn: 'सर्जन तारीख ',
lastSeen: 'पिछली बार देखे गए थे ',
totalDownload: 'कुल डाउनलोड: ',
totalUpload: 'कुल अपलोड: ',
newClient: 'नया उपयोगकर्ता',
disableClient: 'उपयोगकर्ता स्थगित कीजिये',
enableClient: 'उपयोगकर्ता शुरू कीजिये',
noClients: 'अभी तक कोई भी उपयोगकर्ता नहीं है।',
noPrivKey: 'ये उपयोगकर्ता की कोई भी गुप्त चाबी नहीं हे। बना नहीं सकते।',
showQR: 'क्यू आर कोड देखिये',
downloadConfig: 'डाउनलोड कॉन्फीग्यूरेशन',
madeBy: 'सर्जक',
donate: 'दान करें',
},
};

35
webui/tailwind.config.js

@ -1,11 +1,30 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default {
content: [ 'use strict';
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}", module.exports = {
], darkMode: 'selector',
content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
theme: { theme: {
extend: {}, screens: {
xxs: '450px',
xs: '576px',
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
}, },
plugins: [], plugins: [
} function addDisabledClass({ addUtilities }) {
const newUtilities = {
'.is-disabled': {
opacity: '0.25',
cursor: 'default',
},
};
addUtilities(newUtilities);
},
],
};

Loading…
Cancel
Save