add About page, refactor updates

This commit is contained in:
Alexander Drozdov
2023-01-08 10:43:48 +02:00
parent 8ed0a8c451
commit da6ed60d07
13 changed files with 254 additions and 104 deletions

View File

@@ -34,6 +34,24 @@ export interface ShortcutAction {
}
}
export type UpdateInfo =
{
state: 'initial' | 'checking-for-update'
} | {
state: 'update-available' | 'update-downloaded'
version: string
} | {
state: 'update-not-available' | 'error'
checkedAt: number
}
export interface HostState {
contents: string | null
version: string
portable: boolean
updater: UpdateInfo
}
export type IpcEvent =
// events that have meaning only in Overlay mode:
IpcOverlayAttached |
@@ -44,7 +62,7 @@ export type IpcEvent =
IpcTrackArea |
// events used by any type of Client:
IpcSaveConfig |
IpcUpdateInfo |
IpcUpdaterState |
IpcStashSearch |
IpcGameLog |
IpcClientIsActive |
@@ -52,7 +70,8 @@ export type IpcEvent =
IpcHostConfig |
IpcWidgetAction |
IpcItemText |
IpcConfigChanged
IpcConfigChanged |
IpcUserAction
export type IpcEventPayload<Name extends IpcEvent['name'], T extends IpcEvent = IpcEvent> =
T extends { name: Name, payload: infer P } ? P : never
@@ -134,10 +153,14 @@ type IpcGameLog =
lines: string[]
}>
export type IpcUpdateInfo =
Event<'MAIN->CLIENT::update-available', {
auto: boolean
version: string
type IpcUpdaterState =
Event<'MAIN->CLIENT::updater-state', UpdateInfo>
// Hotkeyable actions are defined in `ShortcutAction`.
// Actions below are triggered by user interaction with the UI.
type IpcUserAction =
Event<'CLIENT->MAIN::user-action', {
action: 'check-for-update' | 'update-and-restart'
}>
interface Event<TName extends string, TPayload = undefined> {

View File

@@ -10,7 +10,7 @@ export class AppTray {
this.tray = new Tray(
nativeImage.createFromPath(path.join(__dirname, process.env.STATIC!, process.platform === 'win32' ? 'icon.ico' : 'icon.png'))
)
this.tray.setToolTip('Awakened PoE Trade')
this.tray.setToolTip(`Awakened PoE Trade v${app.getVersion()}`)
this.rebuildMenu()
}
@@ -32,38 +32,12 @@ export class AppTray {
}
},
{ type: 'separator' },
{
label: `APT v${app.getVersion()}`,
click: () => {
shell.openExternal('https://github.com/SnosMe/awakened-poe-trade/releases')
}
},
{
label: 'Open config folder',
click: () => {
shell.openPath(path.join(app.getPath('userData'), 'apt-data'))
}
},
{ type: 'separator' },
{
label: 'Patreon (Donate)',
click: () => {
shell.openExternal('https://patreon.com/awakened_poe_trade')
}
},
{
label: 'Discord',
submenu: [
{
label: 'The Forbidden Trove',
click: () => { shell.openExternal('https://discord.gg/KNpmhvk') }
},
{
label: 'r/pathofexile',
click: () => { shell.openExternal('https://discord.gg/fSwfqN5') }
}
]
},
{
label: 'Quit',
click: () => {

View File

@@ -1,66 +1,63 @@
import { autoUpdater } from 'electron-updater'
import type { ServerEvents } from './server'
import type { UpdateInfo } from '../../ipc/types'
export class AppUpdater {
private canCheck = true
private checkedAtStartup = false
private _checkedAtStartup = false
private _info: UpdateInfo = { state: 'initial' }
get info () { return this._info }
set info (info: UpdateInfo) {
this._info = info
this.server.sendEventTo('broadcast', {
name: 'MAIN->CLIENT::updater-state',
payload: info
})
}
constructor (
private server: ServerEvents
) {
setInterval(this.check, 16 * 60 * 60 * 1000)
autoUpdater.on('update-available', async (info: { version: string }) => {
this.canCheck = false
if (autoUpdater.autoDownload) {
// this.status = `Downloading v${info.version} ...`
} else {
// this.status = `Update v${info.version} available on GitHub`
this.server.sendEventTo('broadcast', {
name: 'MAIN->CLIENT::update-available',
payload: { auto: false, version: info.version }
})
this.server.onEventAnyClient('CLIENT->MAIN::user-action', ({ action }) => {
if (action === 'check-for-update') {
this.check()
} else if (action === 'update-and-restart') {
autoUpdater.quitAndInstall(false)
}
})
autoUpdater.on('checking-for-update', () => {
this.info = { state: 'checking-for-update' }
})
autoUpdater.on('update-available', (info: { version: string }) => {
this.info = { state: 'update-available', version: info.version }
})
autoUpdater.on('update-not-available', () => {
this.canCheck = true
// this.status = 'No updates available'
this.info = { state: 'update-not-available', checkedAt: Date.now() }
})
autoUpdater.on('error', () => {
this.canCheck = true
// this.status = 'Something went wrong, check logs'
this.info = { state: 'error', checkedAt: Date.now() }
})
autoUpdater.on('update-downloaded', async (info: { version: string }) => {
this.canCheck = false
// this.status = `v${info.version} will be installed on exit`
this.server.sendEventTo('broadcast', {
name: 'MAIN->CLIENT::update-available',
payload: { auto: true, version: info.version }
})
autoUpdater.on('update-downloaded', (info: { version: string }) => {
this.info = { state: 'update-downloaded', version: info.version }
})
// on('download-progress') https://github.com/electron-userland/electron-builder/issues/2521
}
updateOps (autoDownload: boolean) {
updateOpts (autoDownload: boolean) {
autoUpdater.autoDownload = (
!process.env.PORTABLE_EXECUTABLE_DIR && // https://www.electron.build/configuration/nsis.html#portable
autoDownload
)
if (!this.checkedAtStartup) {
this.checkedAtStartup = true
if (!this._checkedAtStartup) {
this._checkedAtStartup = true
this.check()
}
}
private check = async () => {
if (!this.canCheck) return
this.canCheck = false
// this.status = 'Checking for update...'
try {
await autoUpdater.checkForUpdates()
} catch {

View File

@@ -40,11 +40,11 @@ app.on('ready', async () => {
shortcuts.updateActions(cfg.shortcuts, cfg.stashScroll, cfg.restoreClipboard)
gameLogWatcher.restart(cfg.clientLog)
gameConfig.readConfig(cfg.gameConfig)
appUpdater.updateOps(!cfg.disableUpdateDownload)
appUpdater.updateOpts(!cfg.disableUpdateDownload)
tray.overlayKey = cfg.overlayKey
})
uIOhook.start()
const port = await startServer()
const port = await startServer(appUpdater)
overlay.loadAppPage(port)
tray.serverPort = port
},

View File

@@ -6,9 +6,11 @@ import fastifyStatic from '@fastify/static'
import type { WebSocket } from 'ws'
import type { AddressInfo } from 'net'
import { EventEmitter } from 'events'
import { IpcEvent, IpcEventPayload } from '../../ipc/types'
import { app } from 'electron'
import { IpcEvent, IpcEventPayload, HostState } from '../../ipc/types'
import { ConfigStore } from './host-files/ConfigStore'
import { addFileUploadRoutes } from './host-files/file-uploads'
import type { AppUpdater } from './AppUpdater'
const server = fastify()
let lastActiveClient: WebSocket
@@ -81,12 +83,6 @@ export const eventPipe = {
sendEventTo
}
const configStore = new ConfigStore(eventPipe)
server.get('/config', async (_req) => {
return { contents: await configStore.load() }
})
server.register(async (instance) => {
instance.get('/events', { websocket: true }, (connection) => {
lastActiveClient = connection.socket
@@ -107,7 +103,22 @@ server.register(async (instance) => {
})
})
export async function startServer (): Promise<number> {
export async function startServer (
appUpdater: AppUpdater
): Promise<number> {
const configStore = new ConfigStore(eventPipe)
server.get<{
Reply: HostState
}>('/config', async (_req) => {
return {
version: app.getVersion(),
portable: Boolean(process.env.PORTABLE_EXECUTABLE_DIR),
updater: appUpdater.info,
contents: await configStore.load()
}
})
await server.listen({
port: (process.env.VITE_DEV_SERVER_URL) ? 8584 : 0
})

View File

@@ -1,5 +1,22 @@
{
"please_wait": "Please wait\u2026",
"map.mods.heist": "heist",
"map.mods.outdated": "outdated",
"Support development on": "Support development\u00A0on"
"Support development on": "Support development\u00A0on",
"updates.maybe_outdated": "You may have an outdated version",
"updates.latest": "You have the latest version",
"updates.error": "Error while checking for updates",
"updates.checking": "Checking for updates",
"updates.downloading": "Downloading\u2026",
"updates.available": "Update available: {0}",
"updates.never_checked": "Last checked: never",
"updates.last_checked": "Last checked: {0}",
"updates.check_now": "Check now",
"updates.install_now": "Install now",
"updates.downloads_page": "Open the Downloads page",
"updates.installed_on_exit": "It will be installed automatically on exit",
"updates.download_manually": "You can download it from GitHub",
"updates.download_disabled": "You have disabled automatic updates download"
}

View File

@@ -25,10 +25,26 @@
"scourge": "преображен",
"Not recognized modifier": "Нераспознанное свойство",
"Refresh": "Обновить",
"please_wait": "Пожалуйста, подождите\u2026",
"map.mods.heist": "кража",
"map.mods.outdated": "устарело",
"Support development on": "Поддержите разработку\u00A0на",
"updates.maybe_outdated": "У вас может быть устаревшая версия",
"updates.latest": "У вас самая последняя версия",
"updates.error": "Ошибка при проверке наличия обновлений",
"updates.checking": "Проверка наличия обновлений",
"updates.downloading": "Скачивание\u2026",
"updates.available": "Доступно обновление: {0}",
"updates.never_checked": "Последняя проверка: никогда",
"updates.last_checked": "Последняя проверка: {0}",
"updates.check_now": "Проверить сейчас",
"updates.install_now": "Установить сейчас",
"updates.downloads_page": "Открыть страницу скачивания",
"updates.installed_on_exit": "Оно будет установлено автоматически при выходе",
"updates.download_manually": "Вы можете загрузить его с GitHub",
"updates.download_disabled": "Вы отключили автоматическую загрузку обновлений",
"Anomalous": "Аномальный",
"Divergent": "Искривлённый",
"Phantasmal": "Фантомный"

View File

@@ -1,9 +0,0 @@
import type { IpcUpdateInfo } from '@ipc/types'
import { MainProcess } from '@/web/background/IPC'
import { shallowRef } from 'vue'
export const updateInfo = shallowRef<IpcUpdateInfo['payload'] | null>(null)
MainProcess.onEvent('MAIN->CLIENT::update-available', (e) => {
updateInfo.value = e
})

View File

@@ -1,4 +1,4 @@
import type { IpcEvent, IpcEventPayload } from '@ipc/types'
import type { IpcEvent, IpcEventPayload, UpdateInfo, HostState } from '@ipc/types'
import { shallowRef } from 'vue'
import Sockette from 'sockette'
@@ -6,6 +6,9 @@ class HostTransport {
private evBus = new EventTarget()
private socket: Sockette
logs = shallowRef('')
version = shallowRef('0.0.00000')
isPortable = shallowRef(false)
updateInfo = shallowRef<UpdateInfo>({ state: 'initial' })
constructor () {
this.socket = new Sockette(`ws://${window.location.host}/events`, {
@@ -16,6 +19,9 @@ class HostTransport {
this.onEvent('MAIN->CLIENT::log-entry', (entry) => {
this.logs.value += (entry.message + '\n')
})
this.onEvent('MAIN->CLIENT::updater-state', (info) => {
this.updateInfo.value = info
})
}
selfDispatch (event: IpcEvent) {
@@ -45,7 +51,11 @@ class HostTransport {
async getConfig (): Promise<string | null> {
const response = await fetch('/config')
const config = await response.json() as { contents: string | null }
const config = await response.json() as HostState
// TODO: 1) refactor this 2) add logs
this.version.value = config.version
this.updateInfo.value = config.updater
this.isPortable.value = config.portable
return config.contents
}

View File

@@ -43,7 +43,6 @@ import WidgetSettings from '../settings/SettingsWindow.vue'
import { AppConfig, saveConfig, pushHostConfig } from '@/web/Config'
import LoadingAnimation from './LoadingAnimation.vue'
// ---
import '@/web/background/AutoUpdates'
import '@/web/background/Prices'
import { load as loadLeagues } from '@/web/background/Leagues'
import { handleLine } from '@/web/client-log/client-log'

View File

@@ -1,12 +1,7 @@
<template>
<div v-if="updateInfo" class="bg-gray-900 mt-2 mx-4 rounded flex items-baseline py-1 px-2">
<div class="flex-1 font-sans">
<div>{{ t('Update available: {0}', [updateInfo.version]) }}</div>
<p class="text-gray-500">{{ t(updateInfo.auto
? 'It will be installed automatically on exit'
: 'You can download it from GitHub') }}</p>
</div>
<button @click="updateInfo = null"><i class="fas fa-times"></i></button>
<div v-if="updateInfo" class="bg-gray-900 mt-2 mx-4 rounded py-1 px-2">
<p>{{ updateInfo.str1 }}</p>
<p class="text-gray-500">{{ updateInfo.str2 }}</p>
</div>
<div v-if="loadingLeagues" class="pt-2 px-4">
<i class="fas fa-info-circle text-gray-600"></i> {{ t('Loading leagues...') }}</div>
@@ -21,11 +16,11 @@
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue'
import { defineComponent, computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import * as Leagues from '@/web/background/Leagues'
import { updateInfo } from '@/web/background/AutoUpdates'
import { poeWebApi } from '@/web/Config'
import { Host } from '@/web/background/IPC'
import { poeWebApi, AppConfig } from '@/web/Config'
export default defineComponent({
setup () {
@@ -33,6 +28,20 @@ export default defineComponent({
const { t } = useI18n()
const updateInfo = computed(() => {
const rawInfo = Host.updateInfo.value
switch (rawInfo.state) {
case 'update-downloaded':
return { str1: t('updates.available', [rawInfo.version]), str2: t('updates.installed_on_exit') }
case 'update-available':
return (Host.isPortable.value || AppConfig().disableUpdateDownload)
? { str1: t('updates.available', [rawInfo.version]), str2: (Host.isPortable.value) ? t('updates.download_manually') : t('updates.download_disabled') }
: null
default:
return null
}
})
return {
t,
updateInfo,
@@ -55,9 +64,6 @@ export default defineComponent({
"leagues_failed": "Make sure the realm is not under maintenance. Also try clicking on the \"Browser\" button, you may need to complete a CAPTCHA there."
},
"ru": {
"Update available: {0}": "Доступно обновление: {0}",
"It will be installed automatically on exit": "Оно будет установлено автоматически при выходе",
"You can download it from GitHub": "Вы можете загрузить его с GitHub",
"Loading leagues...": "Загрузка лиг...",
"Failed to load leagues": "Не удалось загрузить лиги",
"leagues_failed": "Убедитесь, что сервера не находятся на обслуживании. Попробуйте нажать на кнопку \"Браузер\", возможно, там будет CAPTCHA."

View File

@@ -55,6 +55,7 @@ import type { Widget, WidgetManager } from '@/web/overlay/interfaces'
import SettingsHotkeys from './hotkeys.vue'
import SettingsChat from './chat.vue'
import SettingsGeneral from './general.vue'
import SettingsAbout from './about.vue'
import SettingsPricecheck from './price-check.vue'
import SettingsItemcheck from './item-check.vue'
import SettingsDebug from './debug.vue'
@@ -183,7 +184,7 @@ function menuByType (type?: string) {
[SettingsHotkeys, SettingsChat],
[SettingsGeneral],
[SettingsPricecheck, SettingsMaps, SettingsItemcheck],
[SettingsDebug]
[SettingsDebug, SettingsAbout]
]
}
}
@@ -324,6 +325,7 @@ function flatJoin<T, J> (arr: T[][], joinEl: () => J) {
"Settings - Awakened PoE Trade": "Настройки - Awakened PoE Trade",
"Hotkeys": "Быстрые клавиши",
"General": "Общие",
"About": "О программе",
"Price check": "Прайс-чек",
"Maps": "Карты",
"Item info": "Проверка предмета",

View File

@@ -0,0 +1,104 @@
<template>
<div class="p-2 flex flex-col h-full items-center">
<div class="flex flex-col items-center p-2 mb-4">
<img class="w-12 h-12" src="/images/TransferOrb.png">
<p class="text-base">Awakened PoE Trade</p>
<p class="">{{ t('Version {0}', [version]) }}</p>
<div class="flex gap-2">
<a class="border-b" href="https://github.com/SnosMe/awakened-poe-trade/releases" target="_blank">{{ t('Release notes') }}</a>
<a class="border-b" href="https://github.com/SnosMe/awakened-poe-trade/issues" target="_blank">{{ t('Report a bug on GitHub') }}</a>
</div>
</div>
<div class="border border-gray-600 rounded p-2 whitespace-nowrap min-w-min w-72">
<p>{{ info.str1 }}</p>
<p>{{ info.str2 }}</p>
<button v-if="info.action" @click="info.action"
class="btn w-full mt-1">{{ info.actionText }}</button>
</div>
<div class="text-center mt-auto py-8">
<p>{{ t('contact_in_discord') }} <br><span class="font-sans text-gray-500 select-all">&lt;@295216259795124225&gt;</span></p>
<ul class="flex gap-4">
<li><img class="rounded inline" src="https://cdn.discordapp.com/icons/645607528297922560/a_c177897c1751abde48b893844811a1a7.gif?size=32"> <a class="border-b" href="https://discord.gg/tftrove" target="_blank">The Forbidden Trove</a></li>
<li><img class="rounded inline" src="https://cdn.discordapp.com/icons/174993814845521922/53a393aa8b4f386bad2b41878dc324b5.webp?size=32"> <a class="border-b" href="https://discord.gg/pathofexile" target="_blank">r/pathofexile</a></li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Host } from '@/web/background/IPC'
import { AppConfig } from '@/web/Config'
import { DateTime } from 'luxon'
function checkForUpdates () {
Host.sendEvent({
name: 'CLIENT->MAIN::user-action',
payload: { action: 'check-for-update' }
})
}
function openDownloadPage () {
window.open('https://snosme.github.io/awakened-poe-trade/download')
}
function quitAndInstall () {
Host.sendEvent({
name: 'CLIENT->MAIN::user-action',
payload: { action: 'update-and-restart' }
})
}
function fmtTime (millis: number) {
return DateTime.fromMillis(millis).toRelative({ style: 'long' }) ?? 'n/a'
}
export default defineComponent({
name: 'About',
inheritAttrs: false,
setup () {
const { t } = useI18n()
const info = computed(() => {
const rawInfo = Host.updateInfo.value
switch (rawInfo.state) {
case 'initial':
return { str1: t('updates.maybe_outdated'), str2: t('updates.never_checked'), action: checkForUpdates, actionText: t('updates.check_now') }
case 'checking-for-update':
return { str1: t('updates.checking'), str2: t('please_wait') }
case 'update-not-available':
return { str1: t('updates.latest'), str2: t('updates.last_checked', [fmtTime(rawInfo.checkedAt)]), action: checkForUpdates, actionText: t('updates.check_now') }
case 'error':
return { str1: t('updates.maybe_outdated'), str2: t('updates.error'), action: openDownloadPage, actionText: t('updates.downloads_page') }
case 'update-downloaded':
return { str1: t('updates.available', [rawInfo.version]), str2: t('updates.installed_on_exit'), action: quitAndInstall, actionText: t('updates.install_now') }
case 'update-available':
return (Host.isPortable.value || AppConfig().disableUpdateDownload)
? { str1: t('updates.available', [rawInfo.version]), str2: (Host.isPortable.value) ? t('updates.download_manually') : t('updates.download_disabled'), action: openDownloadPage, actionText: t('updates.downloads_page') }
: { str1: t('updates.available', [rawInfo.version]), str2: t('updates.downloading') }
}
})
return {
t,
info,
version: Host.version
}
}
})
</script>
<i18n>
{
"en": {
"contact_in_discord": "Contact me on one of the PoE Discords,"
},
"ru": {
"Version {0}": "Версия {0}",
"Release notes": "Список изменений",
"Report a bug on GitHub": "Сообщить о баге на GitHub",
"contact_in_discord": "Пишите мне в дискорде по PoE,"
}
}
</i18n>