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": {
"apexcharts": "^3.48.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"pinia": "^2.1.7",
"vue": "^3.3.11",
"vue-i18n": "^9.10.2",
"vue-timeago3": "^2.3.2",
"vue3-apexcharts": "^1.5.2"
},
"devDependencies": {
@ -60,6 +62,17 @@
"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": {
"version": "0.19.11",
"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",
"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": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -3050,6 +3072,11 @@
"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": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -3829,6 +3856,32 @@
"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": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.2.tgz",

4
webui/package.json

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

36
webui/src/App.vue

@ -64,6 +64,7 @@
<script setup>
import { ref, onBeforeUnmount, onBeforeMount, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import * as Locales from 'date-fns/locale';
import Auth from '@/components/Auth.vue';
import UpdateNotification from '@/components/UpdateNotification.vue';
@ -81,7 +82,10 @@ import Client from '@/components/Client.vue';
import { useStore } from '@/store/store';
import { storeToRefs } from 'pinia';
const i18n = useI18n();
const i18n = useI18n({
inheritLocale: true,
useScope: 'global',
});
const store = useStore();
@ -96,6 +100,8 @@ const {
uiTheme,
uiChartType,
uiTrafficStats,
lang,
dateFnsLocale,
} = storeToRefs(store);
const currentRelease = ref(null);
@ -131,6 +137,7 @@ onBeforeMount(() => {
getChartType();
getRelease();
getLang();
getDateFnsLocale();
getUiTrafficStats();
});
@ -139,14 +146,29 @@ onBeforeUnmount(() => {
});
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;
try {
const configLang = await api.getLang();
if (i18n.availableLocales.includes(configLang)) {
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() {
try {
const currentReleaseData = await api.getRelease();
@ -181,8 +203,6 @@ async function getChartType() {
.getChartType()
.then((res) => {
uiChartType.value = parseInt(res, 10);
console.log(res);
console.log(uiChartType.value);
})
.catch(() => {
uiChartType.value = 0;

20
webui/src/components/ClientAddress.vue

@ -26,17 +26,25 @@
<script setup>
import { ref, nextTick } from '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 editAddressId = ref(null);
const emit = defineEmits(['inFocus', 'submit', 'update-address']);
const updateAddress = () => {
console.log('emit');
api.updateClientAddress({
clientId: editAddressId.value,
address: editAddress.value,
});
emit('update-address', props.client, editAddress.value);
editAddress.value = null;
editAddressId.value = null;
};
@ -49,6 +57,8 @@ const cancelEdit = () => {
const edit = () => {
editAddress.value = props.client.address;
editAddressId.value = props.client.id;
// ? remove this ?
nextTick(() => {
const clientAddressRef = ['client-' + props.client.id + '-address'];
if (clientAddressRef.value) {

13
webui/src/components/ClientInfo.vue

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

4
webui/src/components/ClientTrafficStat.vue

@ -24,11 +24,11 @@ const { bytes } = useBytes();
defineProps({
transferData: {
type: Object,
type: Number,
default: null,
},
transferDataCurrent: {
type: Object,
type: Number,
default: null,
},
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 { createPinia } from 'pinia';
import { messages } from '@/utils/i18n';
import timeago from 'vue-timeago3';
const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('lang') || 'en',
allowComposition: true,
fallbackLocale: 'en',
messages,
});
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 clientCreateName = ref('');
const qrcode = ref(null);
const lang = ref('en');
const dateFnsLocale = ref();
const uiTheme = ref(localStorage.theme || 'auto');
const prefersDarkScheme = ref(window.matchMedia('(prefers-color-scheme: dark)'));
@ -107,7 +109,6 @@ export const useStore = defineStore('store', () => {
// Debug
client.transferRx = clientsPersist[client.id].transferRxPrevious + 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
if (updateCharts) {
@ -179,6 +180,8 @@ export const useStore = defineStore('store', () => {
qrcode,
uiTheme,
prefersDarkScheme,
lang,
dateFnsLocale,
uiChartType,
uiShowCharts,
uiTrafficStats,

Loading…
Cancel
Save