Browse Source

Add timeago. Fix locales

pull/937/head
Sergei Birukov 2 years ago
parent
commit
e03026e1d2
  1. 53
      webui/package-lock.json
  2. 4
      webui/package.json
  3. 36
      webui/src/App.vue
  4. 20
      webui/src/components/ClientAddress.vue
  5. 13
      webui/src/components/ClientInfo.vue
  6. 4
      webui/src/components/ClientTrafficStat.vue
  7. 29
      webui/src/composables/useTimeAgo.js
  8. 17
      webui/src/main.js
  9. 5
      webui/src/store/store.js

53
webui/package-lock.json

@ -10,9 +10,11 @@
"dependencies": { "dependencies": {
"apexcharts": "^3.48.0", "apexcharts": "^3.48.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-i18n": "^9.10.2", "vue-i18n": "^9.10.2",
"vue-timeago3": "^2.3.2",
"vue3-apexcharts": "^1.5.2" "vue3-apexcharts": "^1.5.2"
}, },
"devDependencies": { "devDependencies": {
@ -60,6 +62,17 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.19.11", "version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
@ -1523,6 +1536,15 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -3050,6 +3072,11 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -3829,6 +3856,32 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vue-timeago3": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/vue-timeago3/-/vue-timeago3-2.3.2.tgz",
"integrity": "sha512-yxb++H9ekLS8bSt3in7fMwIfW1ex9ceMYVCcfLiOppBYpp8hatlOTLqQyCmLXeDYB098uwgfnCewEPKVh/gi6A==",
"dependencies": {
"date-fns": "^2.28.0"
},
"peerDependencies": {
"vue": "^3.3.6"
}
},
"node_modules/vue-timeago3/node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/vue3-apexcharts": { "node_modules/vue3-apexcharts": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.2.tgz", "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.2.tgz",

4
webui/package.json

@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vite build", "build": "vite build",
"build:watch": "vite build --watch", "build:watch": "vite build --watch",
"preview": "vite preview" "preview": "vite preview"
@ -12,9 +12,11 @@
"dependencies": { "dependencies": {
"apexcharts": "^3.48.0", "apexcharts": "^3.48.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-i18n": "^9.10.2", "vue-i18n": "^9.10.2",
"vue-timeago3": "^2.3.2",
"vue3-apexcharts": "^1.5.2" "vue3-apexcharts": "^1.5.2"
}, },
"devDependencies": { "devDependencies": {

36
webui/src/App.vue

@ -64,6 +64,7 @@
<script setup> <script setup>
import { ref, onBeforeUnmount, onBeforeMount, watch } from 'vue'; import { ref, onBeforeUnmount, onBeforeMount, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import * as Locales from 'date-fns/locale';
import Auth from '@/components/Auth.vue'; import Auth from '@/components/Auth.vue';
import UpdateNotification from '@/components/UpdateNotification.vue'; import UpdateNotification from '@/components/UpdateNotification.vue';
@ -81,7 +82,10 @@ 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 i18n = useI18n({
inheritLocale: true,
useScope: 'global',
});
const store = useStore(); const store = useStore();
@ -96,6 +100,8 @@ const {
uiTheme, uiTheme,
uiChartType, uiChartType,
uiTrafficStats, uiTrafficStats,
lang,
dateFnsLocale,
} = storeToRefs(store); } = storeToRefs(store);
const currentRelease = ref(null); const currentRelease = ref(null);
@ -131,6 +137,7 @@ onBeforeMount(() => {
getChartType(); getChartType();
getRelease(); getRelease();
getLang(); getLang();
getDateFnsLocale();
getUiTrafficStats(); getUiTrafficStats();
}); });
@ -139,14 +146,29 @@ onBeforeUnmount(() => {
}); });
async function getLang() { async function getLang() {
const lang = await api.getLang(); try {
const configLang = await api.getLang();
if (lang !== localStorage.getItem('lang') && i18n.availableLocales.includes(lang)) {
localStorage.setItem('lang', lang); if (i18n.availableLocales.includes(configLang)) {
i18n.global.locale = lang; localStorage.setItem('lang', configLang);
i18n.locale.value = configLang;
lang.value = configLang;
} else {
lang.value = 'en';
localStorage.setItem('lang', lang.value);
console.warn(`Configured language '${configLang}' is not available. Using default locale.`);
}
} catch (error) {
lang.value = 'en';
console.error(`Failed to get lang. Using default. ${error.message}`);
} }
} }
function getDateFnsLocale() {
const localeCode = Object.keys(Locales).find((key) => key.slice(0, 2).includes(lang.value));
dateFnsLocale.value = Locales[localeCode] ?? Locales.enUS;
}
async function getRelease() { async function getRelease() {
try { try {
const currentReleaseData = await api.getRelease(); const currentReleaseData = await api.getRelease();
@ -181,8 +203,6 @@ async function getChartType() {
.getChartType() .getChartType()
.then((res) => { .then((res) => {
uiChartType.value = parseInt(res, 10); uiChartType.value = parseInt(res, 10);
console.log(res);
console.log(uiChartType.value);
}) })
.catch(() => { .catch(() => {
uiChartType.value = 0; uiChartType.value = 0;

20
webui/src/components/ClientAddress.vue

@ -26,17 +26,25 @@
<script setup> <script setup>
import { ref, nextTick } from 'vue'; import { ref, nextTick } from 'vue';
import IconEdit from './icons/IconEdit.vue'; import IconEdit from './icons/IconEdit.vue';
import API from '@/services/api';
const props = defineProps(['client']); const api = new API();
const props = defineProps({
client: {
type: Object,
required: true,
},
});
const editAddress = ref(null); const editAddress = ref(null);
const editAddressId = ref(null); const editAddressId = ref(null);
const emit = defineEmits(['inFocus', 'submit', 'update-address']);
const updateAddress = () => { const updateAddress = () => {
console.log('emit'); api.updateClientAddress({
clientId: editAddressId.value,
address: editAddress.value,
});
emit('update-address', props.client, editAddress.value);
editAddress.value = null; editAddress.value = null;
editAddressId.value = null; editAddressId.value = null;
}; };
@ -49,6 +57,8 @@ const cancelEdit = () => {
const edit = () => { const edit = () => {
editAddress.value = props.client.address; editAddress.value = props.client.address;
editAddressId.value = props.client.id; editAddressId.value = props.client.id;
// ? remove this ?
nextTick(() => { nextTick(() => {
const clientAddressRef = ['client-' + props.client.id + '-address']; const clientAddressRef = ['client-' + props.client.id + '-address'];
if (clientAddressRef.value) { if (clientAddressRef.value) {

13
webui/src/components/ClientInfo.vue

@ -26,9 +26,9 @@
</ClientTransfer> </ClientTransfer>
<!-- Last seen --> <!-- Last seen -->
<span v-if="client.latestHandshakeAt" :title="'Last seen on ' + dateTime(new Date(client.latestHandshakeAt))"> <span v-if="client.latestHandshakeAt" :title="'Last seen on ' + dateTime(new Date(client.latestHandshakeAt))">
<!-- FIXME: add "timeago" --> {{ !uiTrafficStats ? ' · ' : '' }}
<!-- · {{ new Date(client.latestHandshakeAt) | timeago }} --> <!-- eslint-disable-next-line vue/no-undef-components -->
{{ !uiTrafficStats ? ' · ' : '' }}{{ timeago(client.latestHandshakeAt) }} <Timeago :datetime="client.latestHandshakeAt" :locale="dateFnsLocale" />
</span> </span>
</div> </div>
</div> </div>
@ -53,7 +53,6 @@
<script setup> <script setup>
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import API from '@/services/api';
import { useStore } from '@/store/store'; import { useStore } from '@/store/store';
import ClientName from '@/components/ClientName.vue'; import ClientName from '@/components/ClientName.vue';
@ -64,7 +63,6 @@ import ClientTransfer from '@/components/ClientTransfer.vue';
import ClientTrafficStat from '@/components/ClientTrafficStat.vue'; import ClientTrafficStat from '@/components/ClientTrafficStat.vue';
import { useDateTime } from '@/composables/useDateTime'; import { useDateTime } from '@/composables/useDateTime';
import { useTimeAgo } from '@/composables/useTimeAgo';
import { useBytes } from '@/composables/useBytes'; import { useBytes } from '@/composables/useBytes';
defineProps({ defineProps({
@ -72,11 +70,8 @@ defineProps({
}); });
const store = useStore(); const store = useStore();
const { uiTrafficStats } = storeToRefs(store); const { uiTrafficStats, dateFnsLocale } = storeToRefs(store);
const { dateTime } = useDateTime(); const { dateTime } = useDateTime();
const { timeago } = useTimeAgo();
const { bytes } = useBytes(); const { bytes } = useBytes();
const api = new API();
</script> </script>

4
webui/src/components/ClientTrafficStat.vue

@ -24,11 +24,11 @@ const { bytes } = useBytes();
defineProps({ defineProps({
transferData: { transferData: {
type: Object, type: Number,
default: null, default: null,
}, },
transferDataCurrent: { transferDataCurrent: {
type: Object, type: Number,
default: null, default: null,
}, },
type: { type: {

29
webui/src/composables/useTimeAgo.js

@ -1,29 +0,0 @@
export function useTimeAgo() {
const timeago = (date) => {
const now = new Date();
const diff = now - date;
if (diff < 1000) {
return 'just now';
} else if (diff < 60000) {
const seconds = Math.floor(diff / 1000);
return `${seconds} ${plural('second', seconds)} ago`;
} else if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes} ${plural('minute', minutes)} ago`;
} else if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} ${plural('hour', hours)} ago`;
} else {
const days = Math.floor(diff / 86400000);
return `${days} ${plural('day', days)} ago`;
}
};
// #278 -- this function might need to be rewritten to support l18n
const plural = (word, count) => {
return count !== 1 ? `${word}s` : word;
};
return { timeago };
}

17
webui/src/main.js

@ -5,12 +5,27 @@ import { createApp } from 'vue';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { messages } from '@/utils/i18n'; import { messages } from '@/utils/i18n';
import timeago from 'vue-timeago3';
const i18n = createI18n({ const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('lang') || 'en', locale: localStorage.getItem('lang') || 'en',
allowComposition: true, allowComposition: true,
fallbackLocale: 'en', fallbackLocale: 'en',
messages, messages,
}); });
const pinia = createPinia(); const pinia = createPinia();
createApp(App).use(i18n).use(pinia).mount('#app');
const timeagoOptions = {
converterOptions: {
includeSeconds: false,
addSuffix: true,
},
};
const app = createApp(App);
app.use(i18n);
app.use(pinia);
app.use(timeago, timeagoOptions);
app.mount('#app');

5
webui/src/store/store.js

@ -18,6 +18,8 @@ export const useStore = defineStore('store', () => {
const clientCreateShowModal = ref(null); const clientCreateShowModal = ref(null);
const clientCreateName = ref(''); const clientCreateName = ref('');
const qrcode = ref(null); const qrcode = ref(null);
const lang = ref('en');
const dateFnsLocale = ref();
const uiTheme = ref(localStorage.theme || 'auto'); const uiTheme = ref(localStorage.theme || 'auto');
const prefersDarkScheme = ref(window.matchMedia('(prefers-color-scheme: dark)')); const prefersDarkScheme = ref(window.matchMedia('(prefers-color-scheme: dark)'));
@ -107,7 +109,6 @@ export const useStore = defineStore('store', () => {
// Debug // Debug
client.transferRx = clientsPersist[client.id].transferRxPrevious + Math.random() * 1000; client.transferRx = clientsPersist[client.id].transferRxPrevious + Math.random() * 1000;
client.transferTx = clientsPersist[client.id].transferTxPrevious + Math.random() * 1000; client.transferTx = clientsPersist[client.id].transferTxPrevious + Math.random() * 1000;
client.latestHandshakeAt = new Date('2024-03-20');
updateCharts = true; // DEV TODO: Update. Get from settings updateCharts = true; // DEV TODO: Update. Get from settings
if (updateCharts) { if (updateCharts) {
@ -179,6 +180,8 @@ export const useStore = defineStore('store', () => {
qrcode, qrcode,
uiTheme, uiTheme,
prefersDarkScheme, prefersDarkScheme,
lang,
dateFnsLocale,
uiChartType, uiChartType,
uiShowCharts, uiShowCharts,
uiTrafficStats, uiTrafficStats,

Loading…
Cancel
Save