mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
parent
2dce3076ee
commit
d931690802
13 changed files with 155 additions and 25 deletions
14
app.go
14
app.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/strimertul/strimertul/docs"
|
"github.com/strimertul/strimertul/docs"
|
||||||
|
@ -143,3 +144,16 @@ func (a *App) GetLastLogs() []LogEntry {
|
||||||
func (a *App) GetDocumentation() map[string]docs.KeyObject {
|
func (a *App) GetDocumentation() map[string]docs.KeyObject {
|
||||||
return docs.Keys
|
return docs.Keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VersionInfo struct {
|
||||||
|
Release string `json:"release"`
|
||||||
|
BuildInfo *debug.BuildInfo `json:"build"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetAppVersion() VersionInfo {
|
||||||
|
info, _ := debug.ReadBuildInfo()
|
||||||
|
return VersionInfo{
|
||||||
|
Release: appVersion,
|
||||||
|
BuildInfo: info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const blankTemplate = (slug: string) => `// ==Extension==
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @author Put your name here!
|
// @author Put your name here!
|
||||||
// @description A new extension for strimertul
|
// @description A new extension for strimertul
|
||||||
|
// @apiversion 3.1.0
|
||||||
// ==/Extension==
|
// ==/Extension==
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
// ==/Extension==
|
// ==/Extension==
|
||||||
|
|
||||||
interface ExtensionMetadata {
|
interface ExtensionMetadata {
|
||||||
name: string;
|
name?: string;
|
||||||
version: string;
|
version?: string;
|
||||||
author: string;
|
author?: string;
|
||||||
description: string;
|
description?: string;
|
||||||
|
apiversion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseExtensionMetadata(
|
export function parseExtensionMetadata(
|
||||||
|
@ -40,6 +41,7 @@ export function parseExtensionMetadata(
|
||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
author: metadata.author,
|
author: metadata.author,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
|
apiversion: metadata.apiversion ?? '3.1.0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -332,7 +332,9 @@
|
||||||
"error": "Error encountered",
|
"error": "Error encountered",
|
||||||
"terminated": "Stopped"
|
"terminated": "Stopped"
|
||||||
},
|
},
|
||||||
"error-alert": "Error details for {{name}}"
|
"error-alert": "Error details for {{name}}",
|
||||||
|
"incompatible-body": "This extension requires {{APPNAME}} version {{version}} and up, you are currently running {{appversion}}, which may be too old and miss required features",
|
||||||
|
"incompatible-warning": "This extension is not compatible"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-actions": {
|
"form-actions": {
|
||||||
|
|
|
@ -5,12 +5,14 @@ import thunkMiddleware from 'redux-thunk';
|
||||||
import apiReducer from './api/reducer';
|
import apiReducer from './api/reducer';
|
||||||
import loggingReducer from './logging/reducer';
|
import loggingReducer from './logging/reducer';
|
||||||
import extensionsReducer from './extensions/reducer';
|
import extensionsReducer from './extensions/reducer';
|
||||||
|
import serverReducer from './server/reducer';
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
api: apiReducer.reducer,
|
api: apiReducer.reducer,
|
||||||
logging: loggingReducer.reducer,
|
logging: loggingReducer.reducer,
|
||||||
extensions: extensionsReducer.reducer,
|
extensions: extensionsReducer.reducer,
|
||||||
|
server: serverReducer.reducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
|
|
31
frontend/src/store/server/reducer.ts
Normal file
31
frontend/src/store/server/reducer.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { GetAppVersion } from '@wailsapp/go/main/App';
|
||||||
|
import { main } from '@wailsapp/go/models';
|
||||||
|
|
||||||
|
interface ServerState {
|
||||||
|
version: main.VersionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ServerState = {
|
||||||
|
version: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverReducer = createSlice({
|
||||||
|
name: 'server',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
loadedVersionData(state, { payload }: PayloadAction<main.VersionInfo>) {
|
||||||
|
state.version = payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const initializeServerInfo = createAsyncThunk(
|
||||||
|
'server/init-info',
|
||||||
|
async (_: void, { dispatch }) => {
|
||||||
|
dispatch(serverReducer.actions.loadedVersionData(await GetAppVersion()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default serverReducer;
|
|
@ -26,6 +26,7 @@ import { createWSClient, useAuthBypass } from '~/store/api/reducer';
|
||||||
import { ConnectionStatus } from '~/store/api/types';
|
import { ConnectionStatus } from '~/store/api/types';
|
||||||
import loggingReducer from '~/store/logging/reducer';
|
import loggingReducer from '~/store/logging/reducer';
|
||||||
import { initializeExtensions } from '~/store/extensions/reducer';
|
import { initializeExtensions } from '~/store/extensions/reducer';
|
||||||
|
import { initializeServerInfo } from '~/store/server/reducer';
|
||||||
|
|
||||||
import LogViewer from './components/LogViewer';
|
import LogViewer from './components/LogViewer';
|
||||||
import Sidebar, { RouteSection } from './components/Sidebar';
|
import Sidebar, { RouteSection } from './components/Sidebar';
|
||||||
|
@ -165,6 +166,11 @@ export default function App(): JSX.Element {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fill application info
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(initializeServerInfo());
|
||||||
|
});
|
||||||
|
|
||||||
// Get application logs
|
// Get application logs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void GetLastLogs().then((logs) => {
|
void GetLastLogs().then((logs) => {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { styled } from '@stitches/react';
|
import { styled } from '@stitches/react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
||||||
|
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import logo from '~/assets/icon-logo.svg';
|
import logo from '~/assets/icon-logo.svg';
|
||||||
|
|
||||||
import { RootState } from '~/store';
|
import { useAppSelector } from '~/store';
|
||||||
import { APPNAME, APPREPO } from '../theme';
|
import { APPNAME, APPREPO } from '../theme';
|
||||||
import BrowserLink from './BrowserLink';
|
import BrowserLink from './BrowserLink';
|
||||||
import Scrollbar from './utils/Scrollbar';
|
import Scrollbar from './utils/Scrollbar';
|
||||||
|
@ -160,18 +159,12 @@ export default function Sidebar({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const resolved = useResolvedPath('/about');
|
const resolved = useResolvedPath('/about');
|
||||||
const matchApp = useMatch({ path: resolved.pathname, end: true });
|
const matchApp = useMatch({ path: resolved.pathname, end: true });
|
||||||
const client = useSelector((state: RootState) => state.api.client);
|
const version = useAppSelector((state) => state.server.version?.release);
|
||||||
const [version, setVersion] = useState<string>(null);
|
|
||||||
const [lastVersion, setLastVersion] = useState<{ url: string; name: string }>(
|
const [lastVersion, setLastVersion] = useState<{ url: string; name: string }>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const dev = version && version.startsWith('v0.0.0');
|
const dev = version && version.startsWith('v0.0.0');
|
||||||
|
|
||||||
async function fetchVersion() {
|
|
||||||
const versionString = await client.getKey('stul-meta/version');
|
|
||||||
setVersion(versionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLastVersion() {
|
async function fetchLastVersion() {
|
||||||
try {
|
try {
|
||||||
const req = await fetch(
|
const req = await fetch(
|
||||||
|
@ -197,12 +190,6 @@ export default function Sidebar({
|
||||||
void fetchLastVersion();
|
void fetchLastVersion();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (client) {
|
|
||||||
void fetchVersion();
|
|
||||||
}
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
|
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
|
||||||
|
@ -219,11 +206,14 @@ export default function Sidebar({
|
||||||
{version && !dev ? version : t('debug.dev-build')}
|
{version && !dev ? version : t('debug.dev-build')}
|
||||||
</VersionLabel>
|
</VersionLabel>
|
||||||
</AppLink>
|
</AppLink>
|
||||||
{!dev && lastVersion && !version.startsWith(lastVersion.name) && (
|
{!dev &&
|
||||||
<UpdateButton href={lastVersion.url}>
|
version &&
|
||||||
{t('menu.messages.update-available')}
|
lastVersion &&
|
||||||
</UpdateButton>
|
!version.startsWith(lastVersion.name) && (
|
||||||
)}
|
<UpdateButton href={lastVersion.url}>
|
||||||
|
{t('menu.messages.update-available')}
|
||||||
|
</UpdateButton>
|
||||||
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
{sections.map(({ title: sectionTitle, links }) => (
|
{sections.map(({ title: sectionTitle, links }) => (
|
||||||
<MenuSection key={sectionTitle}>
|
<MenuSection key={sectionTitle}>
|
||||||
|
|
|
@ -145,6 +145,11 @@ type ExtensionListItemProps = {
|
||||||
function ExtensionListItem(props: ExtensionListItemProps) {
|
function ExtensionListItem(props: ExtensionListItemProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const metadata = parseExtensionMetadata(props.entry.source);
|
const metadata = parseExtensionMetadata(props.entry.source);
|
||||||
|
const version = useAppSelector((state) => state.server.version?.release);
|
||||||
|
const isDev = version && version.startsWith('v0.0.0');
|
||||||
|
const showIncompatibleWarning =
|
||||||
|
!isDev && version && version < `v${metadata.apiversion}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtensionRow
|
<ExtensionRow
|
||||||
status={props.enabled && isRunning(props.status) ? 'enabled' : 'disabled'}
|
status={props.enabled && isRunning(props.status) ? 'enabled' : 'disabled'}
|
||||||
|
@ -180,6 +185,24 @@ function ExtensionListItem(props: ExtensionListItemProps) {
|
||||||
<ExtensionStatusNote color={colorByStatus(props.status)}>
|
<ExtensionStatusNote color={colorByStatus(props.status)}>
|
||||||
{t(`pages.extensions.statuses.${props.status}`)}
|
{t(`pages.extensions.statuses.${props.status}`)}
|
||||||
</ExtensionStatusNote>
|
</ExtensionStatusNote>
|
||||||
|
{showIncompatibleWarning ? (
|
||||||
|
<Alert>
|
||||||
|
<AlertTrigger asChild>
|
||||||
|
<Button variation="warning" size="small">
|
||||||
|
<ExclamationTriangleIcon />
|
||||||
|
</Button>
|
||||||
|
</AlertTrigger>
|
||||||
|
<AlertContent
|
||||||
|
title={t('pages.extensions.incompatible-warning')}
|
||||||
|
showCancel={false}
|
||||||
|
>
|
||||||
|
{t('pages.extensions.incompatible-body', {
|
||||||
|
version: metadata.apiversion,
|
||||||
|
appversion: version,
|
||||||
|
})}
|
||||||
|
</AlertContent>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
{props.error ? (
|
{props.error ? (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTrigger asChild>
|
<AlertTrigger asChild>
|
||||||
|
|
|
@ -231,6 +231,19 @@ const button = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
warning: {
|
||||||
|
border: '1px solid $yellow6',
|
||||||
|
backgroundColor: '$yellow4',
|
||||||
|
'&:not(:disabled)': {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '$yellow5',
|
||||||
|
borderColor: '$yellow8',
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
background: '$yellow6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
danger: {
|
danger: {
|
||||||
border: '1px solid $red6',
|
border: '1px solid $red6',
|
||||||
backgroundColor: '$red4',
|
backgroundColor: '$red4',
|
||||||
|
|
5
frontend/wailsjs/go/main/App.d.ts
vendored
5
frontend/wailsjs/go/main/App.d.ts
vendored
|
@ -1,10 +1,15 @@
|
||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {main} from '../models';
|
import {main} from '../models';
|
||||||
|
import {map[string]docs} from '../models';
|
||||||
import {helix} from '../models';
|
import {helix} from '../models';
|
||||||
|
|
||||||
export function AuthenticateKVClient(arg1:string):Promise<void>;
|
export function AuthenticateKVClient(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function GetAppVersion():Promise<main.VersionInfo>;
|
||||||
|
|
||||||
|
export function GetDocumentation():Promise<map[string]docs.KeyObject>;
|
||||||
|
|
||||||
export function GetKilovoltBind():Promise<string>;
|
export function GetKilovoltBind():Promise<string>;
|
||||||
|
|
||||||
export function GetLastLogs():Promise<Array<main.LogEntry>>;
|
export function GetLastLogs():Promise<Array<main.LogEntry>>;
|
||||||
|
|
|
@ -6,6 +6,14 @@ export function AuthenticateKVClient(arg1) {
|
||||||
return window['go']['main']['App']['AuthenticateKVClient'](arg1);
|
return window['go']['main']['App']['AuthenticateKVClient'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetAppVersion() {
|
||||||
|
return window['go']['main']['App']['GetAppVersion']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetDocumentation() {
|
||||||
|
return window['go']['main']['App']['GetDocumentation']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetKilovoltBind() {
|
export function GetKilovoltBind() {
|
||||||
return window['go']['main']['App']['GetKilovoltBind']();
|
return window['go']['main']['App']['GetKilovoltBind']();
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,39 @@ export namespace main {
|
||||||
this.data = source["data"];
|
this.data = source["data"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class VersionInfo {
|
||||||
|
release: string;
|
||||||
|
// Go type: debug.BuildInfo
|
||||||
|
build?: any;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new VersionInfo(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.release = source["release"];
|
||||||
|
this.build = this.convertValues(source["build"], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue