mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Merge pull request #72 from Pandipipas/add-about-section-and-update-system
feat(about): redesign About and add GitHub release update checker
This commit is contained in:
@@ -1,9 +1,124 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useHead } from '@unhead/vue';
|
import { useHead } from '@unhead/vue';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
defineOptions({ name: 'AboutView' });
|
defineOptions({ name: 'AboutView' });
|
||||||
|
|
||||||
useHead({ title: 'About' });
|
useHead({ title: 'About' });
|
||||||
|
|
||||||
|
type ReleaseResponse = {
|
||||||
|
html_url: string;
|
||||||
|
name: string | null;
|
||||||
|
tag_name: string;
|
||||||
|
published_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appName = 'Scoreko Dev';
|
||||||
|
const currentVersion = import.meta.env.PACKAGE_VERSION;
|
||||||
|
const repoOwner = ref(localStorage.getItem('scoreko:repoOwner') ?? 'Pandipipas');
|
||||||
|
const repoName = ref(localStorage.getItem('scoreko:repoName') ?? 'scoreko-dev');
|
||||||
|
|
||||||
|
const checkingUpdates = ref(false);
|
||||||
|
const updateError = ref('');
|
||||||
|
const latestRelease = ref<ReleaseResponse | null>(null);
|
||||||
|
|
||||||
|
const collaborators = [
|
||||||
|
{
|
||||||
|
name: 'zoton2',
|
||||||
|
role: 'Template base (NodeCG + Vue + TS)',
|
||||||
|
url: 'https://github.com/zoton2/nodecg-vue-ts-template'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Dan Shields',
|
||||||
|
role: 'nodecg-vue-composable helper',
|
||||||
|
url: 'https://github.com/Dan-Shields/nodecg-vue-composable'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NodeCG',
|
||||||
|
role: 'Broadcast graphics framework',
|
||||||
|
url: 'https://github.com/nodecg/nodecg'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const releaseLabel = computed(() => {
|
||||||
|
if (!latestRelease.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestRelease.value.name?.trim().length
|
||||||
|
? latestRelease.value.name
|
||||||
|
: latestRelease.value.tag_name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasUpdate = computed(() => {
|
||||||
|
if (!latestRelease.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareVersions(normalizeVersion(latestRelease.value.tag_name), normalizeVersion(currentVersion)) > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const repoUrl = computed(() => `https://github.com/${repoOwner.value}/${repoName.value}`);
|
||||||
|
const releaseUrl = computed(() => latestRelease.value?.html_url ?? `${repoUrl.value}/releases`);
|
||||||
|
|
||||||
|
function normalizeVersion(version: string) {
|
||||||
|
return version.replace(/^v/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(a: string, b: string) {
|
||||||
|
const aParts = a.split('.').map((value) => Number(value));
|
||||||
|
const bParts = b.split('.').map((value) => Number(value));
|
||||||
|
const max = Math.max(aParts.length, bParts.length);
|
||||||
|
|
||||||
|
for (let index = 0; index < max; index += 1) {
|
||||||
|
const aPart = Number.isFinite(aParts[index]) ? aParts[index]! : 0;
|
||||||
|
const bPart = Number.isFinite(bParts[index]) ? bParts[index]! : 0;
|
||||||
|
|
||||||
|
if (aPart > bPart) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aPart < bPart) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
checkingUpdates.value = true;
|
||||||
|
updateError.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('scoreko:repoOwner', repoOwner.value.trim());
|
||||||
|
localStorage.setItem('scoreko:repoName', repoName.value.trim());
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.github.com/repos/${encodeURIComponent(repoOwner.value.trim())}/${encodeURIComponent(repoName.value.trim())}/releases/latest`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.github+json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub respondió con estado ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
latestRelease.value = await response.json() as ReleaseResponse;
|
||||||
|
} catch (error) {
|
||||||
|
latestRelease.value = null;
|
||||||
|
updateError.value = error instanceof Error ? error.message : 'Error desconocido revisando releases.';
|
||||||
|
} finally {
|
||||||
|
checkingUpdates.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void checkForUpdates();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -11,8 +126,183 @@ useHead({ title: 'About' });
|
|||||||
<div class="text-h4 q-mb-md">
|
<div class="text-h4 q-mb-md">
|
||||||
About
|
About
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body1">
|
|
||||||
Bundle information and relevant links.
|
<div class="row q-col-gutter-lg">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<QCard
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<QCardSection class="row items-center q-col-gutter-md">
|
||||||
|
<div class="col-auto">
|
||||||
|
<QImg
|
||||||
|
src="../image.png"
|
||||||
|
alt="Scoreko logo"
|
||||||
|
width="72px"
|
||||||
|
height="72px"
|
||||||
|
fit="contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-h6">
|
||||||
|
{{ appName }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-7">
|
||||||
|
Versión {{ currentVersion }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
|
||||||
|
<QSeparator />
|
||||||
|
|
||||||
|
<QCardSection>
|
||||||
|
<p class="q-mb-sm">
|
||||||
|
Dashboard para producción de overlays de fighting games usando NodeCG, Vue y Quasar.
|
||||||
|
</p>
|
||||||
|
<div class="column q-gutter-sm">
|
||||||
|
<QBtn
|
||||||
|
:href="repoUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
icon="open_in_new"
|
||||||
|
label="Repositorio en GitHub"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
align="left"
|
||||||
|
/>
|
||||||
|
<QBtn
|
||||||
|
href="https://github.com/nodecg/nodecg"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
icon="open_in_new"
|
||||||
|
label="Framework NodeCG"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
align="left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
|
||||||
|
<QSeparator />
|
||||||
|
|
||||||
|
<QCardSection>
|
||||||
|
<div class="text-subtitle2 q-mb-sm">
|
||||||
|
Colaboradores y agradecimientos
|
||||||
|
</div>
|
||||||
|
<QList dense>
|
||||||
|
<QItem
|
||||||
|
v-for="person in collaborators"
|
||||||
|
:key="person.name"
|
||||||
|
tag="a"
|
||||||
|
:href="person.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<QItemSection>
|
||||||
|
<QItemLabel>{{ person.name }}</QItemLabel>
|
||||||
|
<QItemLabel caption>
|
||||||
|
{{ person.role }}
|
||||||
|
</QItemLabel>
|
||||||
|
</QItemSection>
|
||||||
|
</QItem>
|
||||||
|
</QList>
|
||||||
|
</QCardSection>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<QCard
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<QCardSection>
|
||||||
|
<div class="text-h6">
|
||||||
|
Sistema de updates (GitHub Releases)
|
||||||
|
</div>
|
||||||
|
<div class="text-body2 text-grey-7 q-mt-xs">
|
||||||
|
Este chequeo consulta el último release del repo y lo compara con la versión actual.
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
|
||||||
|
<QSeparator />
|
||||||
|
|
||||||
|
<QCardSection class="q-gutter-md">
|
||||||
|
<QInput
|
||||||
|
v-model="repoOwner"
|
||||||
|
label="GitHub owner"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
<QInput
|
||||||
|
v-model="repoName"
|
||||||
|
label="GitHub repo"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QBtn
|
||||||
|
label="Buscar updates"
|
||||||
|
color="primary"
|
||||||
|
icon="sync"
|
||||||
|
:loading="checkingUpdates"
|
||||||
|
no-caps
|
||||||
|
@click="checkForUpdates"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QBanner
|
||||||
|
v-if="latestRelease"
|
||||||
|
rounded
|
||||||
|
class="bg-grey-2"
|
||||||
|
>
|
||||||
|
<template #avatar>
|
||||||
|
<QIcon
|
||||||
|
:name="hasUpdate ? 'system_update_alt' : 'check_circle'"
|
||||||
|
:color="hasUpdate ? 'warning' : 'positive'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
Último release: {{ releaseLabel }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-7">
|
||||||
|
Publicado: {{ new Date(latestRelease.published_at).toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="q-mt-sm">
|
||||||
|
{{ hasUpdate ? 'Hay una versión más nueva disponible.' : 'Tu versión está actualizada frente al último release.' }}
|
||||||
|
</div>
|
||||||
|
<template #action>
|
||||||
|
<QBtn
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
label="Ver release"
|
||||||
|
:href="releaseUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
no-caps
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</QBanner>
|
||||||
|
|
||||||
|
<QBanner
|
||||||
|
v-if="updateError"
|
||||||
|
rounded
|
||||||
|
class="bg-red-1 text-red-10"
|
||||||
|
>
|
||||||
|
{{ updateError }}
|
||||||
|
</QBanner>
|
||||||
|
|
||||||
|
<QBanner
|
||||||
|
rounded
|
||||||
|
class="bg-blue-1 text-blue-10"
|
||||||
|
>
|
||||||
|
Nota para Electron: este panel implementa solo <strong>detección y aviso</strong>. Para updates
|
||||||
|
automáticos reales en desktop, hay que integrar `autoUpdater` en el proceso principal de Electron
|
||||||
|
y publicar artefactos firmados por plataforma.
|
||||||
|
</QBanner>
|
||||||
|
</QCardSection>
|
||||||
|
</QCard>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</QPage>
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Vendored
+8
@@ -1 +1,9 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly PACKAGE_VERSION: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
|
import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import checker from 'vite-plugin-checker';
|
import checker from 'vite-plugin-checker';
|
||||||
import NodeCGPlugin from 'vite-plugin-nodecg';
|
import NodeCGPlugin from 'vite-plugin-nodecg';
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8')) as { version: string };
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue({ template: { transformAssetUrls } }),
|
vue({ template: { transformAssetUrls } }),
|
||||||
quasar({
|
quasar({
|
||||||
|
|||||||
Reference in New Issue
Block a user