mirror of
https://github.com/open-goal/launcher.git
synced 2024-10-19 14:47:36 -04:00
settings: gracefully handle github failures and paginate releases (#566)
Fixes #365
This commit is contained in:
parent
543f937dfa
commit
7b9ade5def
|
@ -141,6 +141,7 @@
|
|||
"settings_versions_table_header_changes": "Changes",
|
||||
"settings_versions_table_header_date": "Date",
|
||||
"settings_versions_table_header_version": "Version",
|
||||
"settings_versions_noReleasesFound": "No releases could be retrieved from GitHub!",
|
||||
"setup_button_continue": "Continue",
|
||||
"setup_button_getSupportPackage": "Get Support Package",
|
||||
"setup_button_installViaISO": "Install via ISO",
|
||||
|
@ -221,5 +222,7 @@
|
|||
"toasts_modSourceUnreachable": "Mod source unreachable",
|
||||
"toasts_couldNotRemoveModSource": "Unable to remove mod source",
|
||||
"toasts_modSourceDuplicateName": "Mod source has the same display name as one you already have added",
|
||||
"toasts_unableToRetrieveModDownloadURL": "Unable to retrieve mod download URL"
|
||||
"toasts_unableToRetrieveModDownloadURL": "Unable to retrieve mod download URL",
|
||||
"toasts_githubRateLimit": "Unable to hit GitHub's API, you are rate-limited",
|
||||
"toasts_githubUnexpectedError": "Unexpected error when hitting GitHub's API"
|
||||
}
|
||||
|
|
|
@ -75,7 +75,10 @@
|
|||
$VersionStore.activeVersionType === "official"
|
||||
) {
|
||||
const latestToolingVersion = await getLatestOfficialRelease();
|
||||
if ($VersionStore.activeVersionName !== latestToolingVersion.version) {
|
||||
if (
|
||||
latestToolingVersion !== undefined &&
|
||||
$VersionStore.activeVersionName !== latestToolingVersion.version
|
||||
) {
|
||||
// Check that we havn't already downloaded it
|
||||
let alreadyHaveRelease = false;
|
||||
const downloadedOfficialVersions =
|
||||
|
@ -127,11 +130,6 @@
|
|||
{$VersionStore.activeVersionName === null
|
||||
? "not set!"
|
||||
: $VersionStore.activeVersionName}
|
||||
{#if $VersionStore.activeVersionType === "unofficial"}
|
||||
(unf)
|
||||
{:else if $VersionStore.activeVersionType === "devel"}
|
||||
(dev)
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -215,5 +215,7 @@ export async function initLocales(async: boolean) {
|
|||
});
|
||||
if (!async) {
|
||||
await initPromise;
|
||||
} else {
|
||||
return initPromise;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { invoke_rpc } from "./rpc";
|
||||
|
||||
export type VersionFolders = null | "official" | "unofficial" | "devel";
|
||||
export type VersionFolders = null | "official";
|
||||
|
||||
export async function listDownloadedVersions(
|
||||
versionFolder: VersionFolders,
|
||||
|
|
|
@ -1,12 +1,28 @@
|
|||
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
type Mock,
|
||||
} from "vitest";
|
||||
import { arch, platform } from "@tauri-apps/api/os";
|
||||
import { listOfficialReleases } from "./github";
|
||||
import { init } from "svelte-i18n";
|
||||
import { initLocales } from "$lib/i18n/i18n";
|
||||
|
||||
vi.mock("@tauri-apps/api/os");
|
||||
global.fetch = vi.fn();
|
||||
|
||||
function createFetchResponse(data: any) {
|
||||
return { json: () => new Promise((resolve) => resolve(data)) };
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Map(),
|
||||
json: () => new Promise((resolve) => resolve(data)),
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeGithubReleaseAsset(assetName) {
|
||||
|
@ -121,6 +137,10 @@ function createFakeGithubRelease(assetNames: string[]) {
|
|||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initLocales(true);
|
||||
});
|
||||
|
||||
describe("listOfficialReleases", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { toastStore } from "$lib/stores/ToastStore";
|
||||
import { arch, platform } from "@tauri-apps/api/os";
|
||||
import semver from "semver";
|
||||
import { unwrapFunctionStore, format } from "svelte-i18n";
|
||||
|
||||
const $format = unwrapFunctionStore(format);
|
||||
|
||||
export interface ReleaseInfo {
|
||||
releaseType: "official" | "unofficial" | "devel";
|
||||
releaseType: "official";
|
||||
version: string;
|
||||
date: string | undefined;
|
||||
githubLink: string | undefined;
|
||||
|
@ -26,17 +28,12 @@ function isIntelMacOsRelease(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO - go back and fix old asset names so windows/linux can be simplified
|
||||
function isWindowsRelease(
|
||||
platform: string,
|
||||
architecture: string,
|
||||
assetName: string,
|
||||
): boolean {
|
||||
return (
|
||||
platform === "win32" &&
|
||||
(assetName.startsWith("opengoal-windows-v") ||
|
||||
(assetName.startsWith("opengoal-v") && assetName.includes("windows")))
|
||||
);
|
||||
return platform === "win32" && assetName.startsWith("opengoal-windows-v");
|
||||
}
|
||||
|
||||
function isLinuxRelease(
|
||||
|
@ -44,11 +41,7 @@ function isLinuxRelease(
|
|||
architecture: string,
|
||||
assetName: string,
|
||||
): boolean {
|
||||
return (
|
||||
platform === "linux" &&
|
||||
(assetName.startsWith("opengoal-linux-v") ||
|
||||
(assetName.startsWith("opengoal-v") && assetName.includes("linux")))
|
||||
);
|
||||
return platform === "linux" && assetName.startsWith("opengoal-linux-v");
|
||||
}
|
||||
|
||||
async function getDownloadLinkForCurrentPlatform(
|
||||
|
@ -99,35 +92,53 @@ async function parseGithubRelease(githubRelease: any): Promise<ReleaseInfo> {
|
|||
}
|
||||
|
||||
export async function listOfficialReleases(): Promise<ReleaseInfo[]> {
|
||||
return listReleases("official", "open-goal/jak-project");
|
||||
}
|
||||
|
||||
export async function listReleases(
|
||||
releaseType: string,
|
||||
repo: string,
|
||||
): Promise<ReleaseInfo[]> {
|
||||
const nextUrlPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i;
|
||||
let releases = [];
|
||||
// TODO - handle rate limiting
|
||||
// TODO - long term - handle pagination (more than 100 releases)
|
||||
const resp = await fetch(
|
||||
"https://api.github.com/repos/open-goal/jak-project/releases?per_page=100",
|
||||
);
|
||||
// TODO - handle error
|
||||
const githubReleases = await resp.json();
|
||||
let urlToHit =
|
||||
"https://api.github.com/repos/open-goal/jak-project/releases?per_page=100";
|
||||
|
||||
for (const release of githubReleases) {
|
||||
releases.push(await parseGithubRelease(release));
|
||||
while (urlToHit !== undefined) {
|
||||
const resp = await fetch(urlToHit);
|
||||
if (resp.status === 403 || resp.status === 429) {
|
||||
toastStore.makeToast($format("toasts_githubRateLimit"), "error");
|
||||
return [];
|
||||
} else if (!resp.ok) {
|
||||
toastStore.makeToast($format("toasts_githubUnexpectedError"), "error");
|
||||
return [];
|
||||
}
|
||||
|
||||
const githubReleases = await resp.json();
|
||||
for (const release of githubReleases) {
|
||||
releases.push(await parseGithubRelease(release));
|
||||
}
|
||||
|
||||
if (
|
||||
resp.headers.has("link") &&
|
||||
resp.headers.get("link").includes(`rel=\"next\"`)
|
||||
) {
|
||||
// we must paginate!
|
||||
urlToHit = resp.headers.get("link").match(nextUrlPattern)[0];
|
||||
} else {
|
||||
urlToHit = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return releases.sort((a, b) => b.date.localeCompare(a.date));
|
||||
}
|
||||
|
||||
export async function getLatestOfficialRelease(): Promise<ReleaseInfo> {
|
||||
// TODO - handle rate limiting
|
||||
export async function getLatestOfficialRelease(): Promise<
|
||||
ReleaseInfo | undefined
|
||||
> {
|
||||
const resp = await fetch(
|
||||
"https://api.github.com/repos/open-goal/jak-project/releases/latest",
|
||||
);
|
||||
// TODO - handle error
|
||||
if (resp.status === 403 || resp.status === 429) {
|
||||
toastStore.makeToast($format("toasts_githubRateLimit"), "error");
|
||||
return undefined;
|
||||
} else if (!resp.ok) {
|
||||
toastStore.makeToast($format("toasts_githubUnexpectedError"), "error");
|
||||
return undefined;
|
||||
}
|
||||
const githubRelease = await resp.json();
|
||||
return await parseGithubRelease(githubRelease);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
},
|
||||
];
|
||||
}
|
||||
// TODO - "no releases found"
|
||||
|
||||
// Merge that with the actual current releases on github
|
||||
const githubReleases = await listOfficialReleases();
|
||||
|
@ -111,8 +110,6 @@
|
|||
if (success) {
|
||||
$VersionStore.activeVersionType = "official";
|
||||
$VersionStore.activeVersionName = $VersionStore.selectedVersions.official;
|
||||
$VersionStore.selectedVersions.unofficial = null;
|
||||
$VersionStore.selectedVersions.devel = null;
|
||||
toastStore.makeToast($_("toasts_savedToolingVersion"), "info");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import IconDownload from "~icons/mdi/download";
|
||||
import IconDeleteForever from "~icons/mdi/delete-forever";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Radio,
|
||||
Spinner,
|
||||
|
@ -60,144 +61,152 @@
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_version")}</TableHeadCell
|
||||
>
|
||||
<TableHeadCell>{$_("settings_versions_table_header_date")}</TableHeadCell>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_changes")}</TableHeadCell
|
||||
>
|
||||
</TableHead>
|
||||
<TableBody tableBodyClass="divide-y">
|
||||
{#each releaseList as release (release.version)}
|
||||
<TableBodyRow>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.isDownloaded}
|
||||
<Radio
|
||||
class="disabled:cursor-not-allowed p-0"
|
||||
bind:group={$VersionStore.selectedVersions[releaseType]}
|
||||
on:change={() => dispatch("versionChange")}
|
||||
value={release.version}
|
||||
disabled={!release.isDownloaded}
|
||||
name={`${releaseType}-release`}
|
||||
/>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell
|
||||
class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
style="line-height: 0;"
|
||||
>
|
||||
<Button
|
||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||
disabled={release.pendingAction ||
|
||||
(!release.isDownloaded &&
|
||||
release.downloadUrl !== undefined &&
|
||||
release.invalid)}
|
||||
on:click={async () => {
|
||||
if (release.isDownloaded) {
|
||||
dispatch("removeVersion", { version: release.version });
|
||||
} else {
|
||||
dispatch("downloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if releaseList === undefined || releaseList.length <= 0}
|
||||
<Alert color="red" class="dark:bg-slate-900 flex-grow">
|
||||
{$_("settings_versions_noReleasesFound")}
|
||||
</Alert>
|
||||
{:else}
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_version")}</TableHeadCell
|
||||
>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_date")}</TableHeadCell
|
||||
>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_changes")}</TableHeadCell
|
||||
>
|
||||
</TableHead>
|
||||
<TableBody tableBodyClass="divide-y">
|
||||
{#each releaseList as release (release.version)}
|
||||
<TableBodyRow>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.isDownloaded}
|
||||
<IconDeleteForever
|
||||
class="text-xl"
|
||||
color="red"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_removeVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{:else if release.downloadUrl === undefined}
|
||||
<span>{$_("settings_versions_incompatibleVersion")}</span>
|
||||
{:else if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else if release.releaseType === "official" && release.downloadUrl !== undefined}
|
||||
<IconDownload
|
||||
class="text-xl"
|
||||
color="#00d500"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_downloadVersion_altText",
|
||||
)}
|
||||
<Radio
|
||||
class="disabled:cursor-not-allowed p-0"
|
||||
bind:group={$VersionStore.selectedVersions[releaseType]}
|
||||
on:change={() => dispatch("versionChange")}
|
||||
value={release.version}
|
||||
disabled={!release.isDownloaded}
|
||||
name={`${releaseType}-release`}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if release.invalid}
|
||||
<Tooltip color="red">
|
||||
{#if release.invalidationReasons.length > 0}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{#each release.invalidationReasons as reason}
|
||||
<br />
|
||||
- {reason}
|
||||
{/each}
|
||||
{:else}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{/if}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if release.isDownloaded && release.releaseType == "official"}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell
|
||||
class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
style="line-height: 0;"
|
||||
>
|
||||
<Button
|
||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||
disabled={release.pendingAction}
|
||||
disabled={release.pendingAction ||
|
||||
(!release.isDownloaded &&
|
||||
release.downloadUrl !== undefined &&
|
||||
release.invalid)}
|
||||
on:click={async () => {
|
||||
dispatch("redownloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
if (release.isDownloaded) {
|
||||
dispatch("removeVersion", { version: release.version });
|
||||
} else {
|
||||
dispatch("downloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else}
|
||||
<IconRefresh
|
||||
{#if release.isDownloaded}
|
||||
<IconDeleteForever
|
||||
class="text-xl"
|
||||
color="red"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_redownloadVersion_altText",
|
||||
"settings_versions_icon_removeVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{:else if release.downloadUrl === undefined}
|
||||
<span>{$_("settings_versions_incompatibleVersion")}</span>
|
||||
{:else if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else if release.releaseType === "official" && release.downloadUrl !== undefined}
|
||||
<IconDownload
|
||||
class="text-xl"
|
||||
color="#00d500"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_downloadVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
>{release.version}</TableBodyCell
|
||||
>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.date}
|
||||
{new Date(release.date).toLocaleDateString()}
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.githubLink}
|
||||
<a
|
||||
class="inline-block"
|
||||
href={release.githubLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<IconGitHub
|
||||
class="text-xl"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_githubRelease_altText",
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
</TableBodyRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{#if release.invalid}
|
||||
<Tooltip color="red">
|
||||
{#if release.invalidationReasons.length > 0}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{#each release.invalidationReasons as reason}
|
||||
<br />
|
||||
- {reason}
|
||||
{/each}
|
||||
{:else}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{/if}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if release.isDownloaded && release.releaseType == "official"}
|
||||
<Button
|
||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||
disabled={release.pendingAction}
|
||||
on:click={async () => {
|
||||
dispatch("redownloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{#if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else}
|
||||
<IconRefresh
|
||||
class="text-xl"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_redownloadVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
>{release.version}</TableBodyCell
|
||||
>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.date}
|
||||
{new Date(release.date).toLocaleDateString()}
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.githubLink}
|
||||
<a
|
||||
class="inline-block"
|
||||
href={release.githubLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<IconGitHub
|
||||
class="text-xl"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_githubRelease_altText",
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
</TableBodyRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
Loading…
Reference in a new issue