feat: add release update checks for v1.0.1
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "holodream-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "holodream-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.1",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "holodream-tools",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import packageInfo from '../../../package.json';
|
||||
|
||||
type ReleaseAsset = {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
};
|
||||
|
||||
type GitHubRelease = {
|
||||
tag_name: string;
|
||||
html_url: string;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
assets: ReleaseAsset[];
|
||||
};
|
||||
|
||||
type CachedRelease = {
|
||||
checkedAt: number;
|
||||
release: GitHubRelease;
|
||||
};
|
||||
|
||||
const currentVersion = packageInfo.version;
|
||||
const releaseApiUrl = 'https://api.github.com/repos/DD4SN/HolodoriCalc/releases/latest';
|
||||
const releasesUrl = 'https://github.com/DD4SN/HolodoriCalc/releases';
|
||||
const cacheKey = 'holodori-calc:latest-release';
|
||||
const cacheDuration = 6 * 60 * 60 * 1000;
|
||||
|
||||
let status: 'checking' | 'current' | 'available' | 'failed' = 'checking';
|
||||
let latestRelease: GitHubRelease | null = null;
|
||||
|
||||
function versionParts(version: string): number[] {
|
||||
return version.replace(/^v/i, '').split('-', 1)[0].split('.').map((part) => Number.parseInt(part, 10) || 0);
|
||||
}
|
||||
|
||||
function isNewerVersion(candidate: string, current: string): boolean {
|
||||
const candidateParts = versionParts(candidate);
|
||||
const currentParts = versionParts(current);
|
||||
const length = Math.max(candidateParts.length, currentParts.length, 3);
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const difference = (candidateParts[index] ?? 0) - (currentParts[index] ?? 0);
|
||||
if (difference !== 0) return difference > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isRelease(value: unknown): value is GitHubRelease {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const release = value as Partial<GitHubRelease>;
|
||||
return typeof release.tag_name === 'string'
|
||||
&& typeof release.html_url === 'string'
|
||||
&& Array.isArray(release.assets);
|
||||
}
|
||||
|
||||
function applyRelease(release: GitHubRelease): void {
|
||||
latestRelease = release;
|
||||
status = isNewerVersion(release.tag_name, currentVersion) ? 'available' : 'current';
|
||||
}
|
||||
|
||||
function readCachedRelease(): CachedRelease | null {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(cacheKey) ?? 'null') as Partial<CachedRelease> | null;
|
||||
if (!parsed || typeof parsed.checkedAt !== 'number' || !isRelease(parsed.release)) return null;
|
||||
return parsed as CachedRelease;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLatestRelease(): Promise<void> {
|
||||
const cached = readCachedRelease();
|
||||
if (cached && Date.now() - cached.checkedAt < cacheDuration) {
|
||||
applyRelease(cached.release);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), 6000);
|
||||
|
||||
try {
|
||||
const response = await fetch(releaseApiUrl, {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
signal: controller.signal
|
||||
});
|
||||
if (!response.ok) throw new Error(`GitHub release lookup failed: ${response.status}`);
|
||||
|
||||
const release: unknown = await response.json();
|
||||
if (!isRelease(release) || release.draft || release.prerelease) throw new Error('Invalid GitHub release response');
|
||||
|
||||
localStorage.setItem(cacheKey, JSON.stringify({ checkedAt: Date.now(), release } satisfies CachedRelease));
|
||||
applyRelease(release);
|
||||
} catch {
|
||||
if (cached) applyRelease(cached.release);
|
||||
else status = 'failed';
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadUrl(release: GitHubRelease): string {
|
||||
const zip = release.assets.find((asset) => /^HolodoriCalc-offline-v.+\.zip$/i.test(asset.name));
|
||||
return zip?.browser_download_url ?? release.html_url;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void checkLatestRelease();
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside class:available={status === 'available'} class="update-notice" aria-live="polite">
|
||||
<div class="version-summary">
|
||||
<span class="label">현재 버전</span>
|
||||
<strong>v{currentVersion}</strong>
|
||||
{#if status === 'checking'}
|
||||
<span class="status">최신 버전을 확인하고 있습니다.</span>
|
||||
{:else if status === 'available' && latestRelease}
|
||||
<span class="status"><b>v{latestRelease.tag_name.replace(/^v/i, '')}</b> 업데이트를 사용할 수 있습니다.</span>
|
||||
{:else if status === 'current'}
|
||||
<span class="status">최신 버전입니다.</span>
|
||||
{:else}
|
||||
<span class="status">업데이트를 확인할 수 없습니다.</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if status === 'available' && latestRelease}
|
||||
<a class="action primary" href={downloadUrl(latestRelease)} target="_blank" rel="noreferrer">최신 버전 다운로드 <span aria-hidden="true">→</span></a>
|
||||
{:else}
|
||||
<a class="action" href={releasesUrl} target="_blank" rel="noreferrer">릴리즈 보기 <span aria-hidden="true">→</span></a>
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.update-notice { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-bottom: 12px; padding: 14px 18px; border: 1px solid #2c3440; border-radius: 12px; background: #151a22; }
|
||||
.update-notice.available { border-color: rgb(237 139 181 / 38%); background: rgb(237 139 181 / 8%); }
|
||||
.version-summary { display: flex; align-items: baseline; min-width: 0; gap: 10px; }
|
||||
.label { color: #7f8998; font-size: 13px; font-weight: 800; }
|
||||
strong { color: #eef1f5; font-size: 15px; }
|
||||
.status { color: #a4adba; font-size: 14px; }
|
||||
.status b { color: #f0a1c3; }
|
||||
.action { flex: 0 0 auto; color: #aab3c0; font-size: 14px; font-weight: 800; text-decoration: none; }
|
||||
.action.primary { color: #f2a6c6; }
|
||||
.action span { margin-left: 4px; }
|
||||
.action:hover { color: #f2a6c6; }
|
||||
.action:focus-visible { outline: 3px solid rgb(239 139 182 / 28%); outline-offset: 3px; }
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.update-notice { align-items: flex-start; flex-direction: column; gap: 8px; }
|
||||
.version-summary { align-items: flex-start; flex-wrap: wrap; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
:root {
|
||||
--page-max-width: 1520px;
|
||||
--page-gutter: clamp(40px, 5vw, 96px);
|
||||
--page-title-size: 72px;
|
||||
--page-content-top: 48px;
|
||||
--page-title-size: 40px;
|
||||
--page-content-top: 24px;
|
||||
--page-header-bottom: 28px;
|
||||
--page-description-size: 16px;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<script lang="ts">
|
||||
import UpdateNotice from '$lib/components/UpdateNotice.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>홀로도리 계산기</title>
|
||||
<meta name="description" content="홀로라이브 드림즈의 편성, 스킬 순서와 멤버 보드를 분석하는 계산기" />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<UpdateNotice />
|
||||
|
||||
<aside class="start-guide" aria-label="시작 안내">
|
||||
<p><span>처음 사용한다면</span><strong>사용자 데이터를 먼저 설정하세요.</strong> 보유 카드와 개화 단계, 멤버별 보드를 실제 분석에 반영할 수 있습니다.</p>
|
||||
<a href="/data">설정하기 <span aria-hidden="true">→</span></a>
|
||||
|
||||
Reference in New Issue
Block a user