From dd923ea3e60564aca0ed59bf3e2f4ab08ae078d6 Mon Sep 17 00:00:00 2001 From: Tipx-L <138244655+Tipx-L@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:45:02 -0700 Subject: [PATCH 1/2] Update the API of downloading assets. --- noname/library/update.js | 74 ++++++++++- noname/ui/create/menu/pages/otherMenu.js | 159 ++++++++++------------- 2 files changed, 142 insertions(+), 91 deletions(-) diff --git a/noname/library/update.js b/noname/library/update.js index 9ac35b83e..511abdc08 100644 --- a/noname/library/update.js +++ b/noname/library/update.js @@ -1,4 +1,4 @@ -import { lib, ui, game } from "../../noname.js"; +import { ui, game } from "../../noname.js"; // https://github.com/libccy/noname/archive/refs/tags/v1.10.10.zip @@ -386,7 +386,7 @@ export async function request(url, onProgress, options = {}) { try { // @ts-ignore filename = response.headers.get("Content-Disposition").split(";")[1].split("=")[1]; - } catch {} + } catch { /* empty */ } let receivedBytes = 0; let chunks = []; @@ -516,3 +516,73 @@ export function createProgress(title, max, fileName, value) { }; return parent; } + +/** + * Retrieves the latest version tag from a GitHub repository, excluding a specific tag. + * This function fetches the list of tags from the GitHub repository specified by + * the owner and repository name, then returns the latest tag name that is not “v1998”. + * @param {string} owner - The username or organization name on GitHub that owns the repository. + * @param {string} repo - The name of the repository from which to fetch tags. + * @returns {Promise} A promise that resolves with the name of the latest version tag, + * or rejects with an error if the operation fails. + * @throws {Error} Will throw an error if the fetch operation fails or if no valid tags are found. + */ +export async function getLatestVersionFromGitHub(owner = 'libccy', repo = 'noname') { + if (!localStorage.getItem('noname_authorization')) await gainAuthorization(); + + const tags = await getRepoTags({ + username: owner, + repository: repo + }); + + for (const tag of tags) { + const tagName = tag.name; + if (tagName !== 'v1998') return tagName; + } + + throw new Error('No valid tags found in the repository'); +} + +/** + * Fetches trees from a GitHub repository within specified directories. + * @param {string[]} directories - The list of directories to fetch the trees from. + * @param {string} version - The version or branch to fetch the trees from. + * @param {string} owner - The owner of the GitHub repository. + * @param {string} repo - The name of the GitHub repository. + * @returns {Promise<{ + * path: string; + * mode: string; + * type: "blob" | "tree"; + * sha: string; + * size: number; + * url: string; + * }[][]>} A promise that resolves with trees from the specified directories. + * @throws {Error} Will throw an error if unable to fetch the repository tree from GitHub. + */ +export async function getTreesFromGithub(directories, version, owner = 'libccy', repo = 'noname') { + if (!localStorage.getItem('noname_authorization')) await gainAuthorization(); + + const treesResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${version}?recursive=1`, { + headers: defaultHeaders + }); + + if (!treesResponse.ok) throw new Error(`Failed to fetch the GitHub repository tree: HTTP status ${treesResponse.status}`); + /** + * @type {{ + * sha: string; + * url: string; + * tree: { + * path: string; + * mode: string; + * type: "blob" | "tree"; + * sha: string; + * size: number; + * url: string; + * }[]; + * truncated: boolean; + * }} + */ + const trees = await treesResponse.json(); + const tree = trees.tree; + return directories.map(directory => tree.filter(({ type, path }) => type === 'blob' && path.startsWith(directory))); +} diff --git a/noname/ui/create/menu/pages/otherMenu.js b/noname/ui/create/menu/pages/otherMenu.js index bba7d2ca2..c601b417f 100644 --- a/noname/ui/create/menu/pages/otherMenu.js +++ b/noname/ui/create/menu/pages/otherMenu.js @@ -1,11 +1,5 @@ import { menuContainer, - popupContainer, - updateActive, - setUpdateActive, - updateActiveCard, - setUpdateActiveCard, - menux, menuxpages, menuUpdates, openMenu, @@ -22,11 +16,12 @@ import { checkVersion, getRepoTags, getRepoTagDescription, - getRepoFilesList, flattenRepositoryFiles, request, createProgress, gainAuthorization, + getLatestVersionFromGitHub, + getTreesFromGithub } from "../../../../library/update.js"; export const otherMenu = function (/** @type { boolean | undefined } */ connectMenu) { @@ -338,7 +333,7 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM .then(() => { cp.exec( `start /b ${__dirname}\\noname-server.exe -platform=electron`, - () => {} + () => { } ); function loadURL() { let myAbortController = @@ -410,42 +405,41 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM } checkAssetButton.innerHTML = "正在检查更新"; checkAssetButton.disabled = true; + function refresh() { checkAssetButton.innerHTML = "检查素材更新"; checkAssetButton.disabled = false; } - const assetDirs = []; - if (lib.config.asset_font) { - assetDirs.push("font"); - } - if (lib.config.asset_audio) { - assetDirs.push("audio"); - } - if (lib.config.asset_image) { - assetDirs.push("image"); - } - const files = await Promise.all(assetDirs.map((dir) => flattenRepositoryFiles(dir))); - assetDirs.forEach((value, index) => { + + const assetDirectories = []; + if (lib.config.asset_font) assetDirectories.push('font'); + if (lib.config.asset_audio) assetDirectories.push('audio'); + if (lib.config.asset_image) assetDirectories.push('image'); + const version = await getLatestVersionFromGitHub(); + const files = await getTreesFromGithub(assetDirectories, version); + + assetDirectories.forEach((assetDirectory, index) => { const arr = files[index]; const size = arr.reduce((previous, current) => { return previous + current.size; }, 0); - game.saveConfig(`asset_${value}_size`, parseSize(size)); + game.saveConfig(`asset_${assetDirectory}_size`, parseSize(size)); }); + /** - * @param { any[] } arr - * @param { Function } predicate + * @template T + * @param { T[] } arr + * @param { (value: T) => Promise } predicate */ const asyncFilter = async (arr, predicate) => { - // @ts-ignore const results = await Promise.all(arr.map(predicate)); - // @ts-ignore return arr.filter((_v, index) => results[index]); }; - // @ts-ignore + const result = await asyncFilter(files.flat(), async (v) => { return v.size != (await game.promises.readFile(v.path)).length; }).then((arr) => arr.map((v) => v.path)); + console.log("需要更新的文件有:", result); game.print("需要更新的文件有:", result); const finish = async () => { @@ -465,71 +459,57 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM * @type {progress} */ let unZipProgress; - request( - "noname.unitedrhythmized.club/api", - (receivedBytes, total, filename) => { - if (typeof filename == "string") { - progress.setFileName(filename); - } - let received = 0, - max = 0; - if (total) { - max = +(total / (1024 * 1024)).toFixed(1); - } else { - max = 1000; - } - received = +(receivedBytes / (1024 * 1024)).toFixed(1); - if (received > max) max = received; - progress.setProgressMax(max); - progress.setProgressValue(received); - }, - { - method: "post", - body: JSON.stringify({ - fileList: result.concat("game/asset.js"), - }), + request('api.unitedrhythmized.club/noname', (receivedBytes, total, filename) => { + if (typeof filename == 'string') { + progress.setFileName(filename); } - ) - .then(async (blob) => { - progress.remove(); - const zip = await get.promises.zip(); - zip.load(await blob.arrayBuffer()); - const entries = Object.entries(zip.files); - let root; - const hiddenFileFlags = [".", "_"]; - unZipProgress = createProgress( - "正在解压" + progress.getFileName(), - entries.length - ); - let i = 0; - for (const [key, value] of entries) { - unZipProgress.setProgressValue(i++); - const fileName = - typeof root == "string" && key.startsWith(root) - ? key.replace(root, "") - : key; - if (hiddenFileFlags.includes(fileName[0])) continue; - if (value.dir) { - await game.promises.createDir(fileName); - continue; - } - unZipProgress.setFileName(fileName); - const [path, name] = [ - fileName.split("/").slice(0, -1).join("/"), - fileName.split("/").slice(-1).join("/"), - ]; - game.print(`${fileName}(${i}/${entries.length})`); - await game.promises.writeFile(value.asArrayBuffer(), path, name); - } - unZipProgress.remove(); - await finish(); + let received = 0, max = 0; + if (total) { + max = +(total / (1024 * 1024)).toFixed(1) + } else { + max = 1000; + } + received = +(receivedBytes / (1024 * 1024)).toFixed(1); + if (received > max) max = received; + progress.setProgressMax(max); + progress.setProgressValue(received); + }, { + method: 'POST', + body: JSON.stringify({ + action: 'downloadAssets', + version, + fileList: result.concat('game/asset.js') }) - .catch((e) => { - if (progress.parentNode) progress.remove(); - if (unZipProgress && unZipProgress.parentNode) unZipProgress.remove(); - refresh(); - throw e; - }); + }).then(async blob => { + progress.remove(); + const zip = await get.promises.zip(); + zip.load(await blob.arrayBuffer()); + const entries = Object.entries(zip.files); + let root; + const hiddenFileFlags = ['.', '_']; + unZipProgress = createProgress('正在解压' + progress.getFileName(), entries.length); + let i = 0; + for (const [key, value] of entries) { + unZipProgress.setProgressValue(i++); + const fileName = typeof root == 'string' && key.startsWith(root) ? key.replace(root, '') : key; + if (hiddenFileFlags.includes(fileName[0])) continue; + if (value.dir) { + await game.promises.createDir(fileName); + continue; + } + unZipProgress.setFileName(fileName); + const [path, name] = [fileName.split('/').slice(0, -1).join('/'), fileName.split('/').slice(-1).join('/')]; + game.print(`${fileName}(${i}/${entries.length})`); + await game.promises.writeFile(value.asArrayBuffer(), path, name); + } + unZipProgress.remove(); + await finish(); + }).catch(e => { + if (progress.parentNode) progress.remove(); + if (unZipProgress && unZipProgress.parentNode) unZipProgress.remove(); + refresh(); + throw e; + }); } else { await finish(); } @@ -1666,3 +1646,4 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM if (!active.link) active._initLink(); rightPane.appendChild(active.link); }; + From d664165c264e973b664614856a4b94a5a6601e16 Mon Sep 17 00:00:00 2001 From: Tipx-L <138244655+Tipx-L@users.noreply.github.com> Date: Tue, 16 Apr 2024 00:51:44 -0700 Subject: [PATCH 2/2] Prettify. --- noname/game/promises.js | 4 + noname/library/update.js | 34 +++--- noname/ui/create/menu/pages/otherMenu.js | 125 +++++++++++++---------- 3 files changed, 95 insertions(+), 68 deletions(-) diff --git a/noname/game/promises.js b/noname/game/promises.js index 57c9e7e97..6659b8187 100644 --- a/noname/game/promises.js +++ b/noname/game/promises.js @@ -65,6 +65,10 @@ export class GamePromises { game.download(url, folder, resolve, reject, dev, onprogress); }); } + /** + * @param {string} filename + * @returns {Promise} + */ readFile(filename) { return new Promise((resolve, reject) => { // @ts-ignore diff --git a/noname/library/update.js b/noname/library/update.js index 511abdc08..ebc22697f 100644 --- a/noname/library/update.js +++ b/noname/library/update.js @@ -386,7 +386,9 @@ export async function request(url, onProgress, options = {}) { try { // @ts-ignore filename = response.headers.get("Content-Disposition").split(";")[1].split("=")[1]; - } catch { /* empty */ } + } catch { + /* empty */ + } let receivedBytes = 0; let chunks = []; @@ -527,20 +529,20 @@ export function createProgress(title, max, fileName, value) { * or rejects with an error if the operation fails. * @throws {Error} Will throw an error if the fetch operation fails or if no valid tags are found. */ -export async function getLatestVersionFromGitHub(owner = 'libccy', repo = 'noname') { - if (!localStorage.getItem('noname_authorization')) await gainAuthorization(); +export async function getLatestVersionFromGitHub(owner = "libccy", repo = "noname") { + if (!localStorage.getItem("noname_authorization")) await gainAuthorization(); const tags = await getRepoTags({ username: owner, - repository: repo + repository: repo, }); for (const tag of tags) { const tagName = tag.name; - if (tagName !== 'v1998') return tagName; + if (tagName !== "v1998") return tagName; } - throw new Error('No valid tags found in the repository'); + throw new Error("No valid tags found in the repository"); } /** @@ -559,14 +561,18 @@ export async function getLatestVersionFromGitHub(owner = 'libccy', repo = 'nonam * }[][]>} A promise that resolves with trees from the specified directories. * @throws {Error} Will throw an error if unable to fetch the repository tree from GitHub. */ -export async function getTreesFromGithub(directories, version, owner = 'libccy', repo = 'noname') { - if (!localStorage.getItem('noname_authorization')) await gainAuthorization(); +export async function getTreesFromGithub(directories, version, owner = "libccy", repo = "noname") { + if (!localStorage.getItem("noname_authorization")) await gainAuthorization(); - const treesResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${version}?recursive=1`, { - headers: defaultHeaders - }); + const treesResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/trees/${version}?recursive=1`, + { + headers: defaultHeaders, + } + ); - if (!treesResponse.ok) throw new Error(`Failed to fetch the GitHub repository tree: HTTP status ${treesResponse.status}`); + if (!treesResponse.ok) + throw new Error(`Failed to fetch the GitHub repository tree: HTTP status ${treesResponse.status}`); /** * @type {{ * sha: string; @@ -584,5 +590,7 @@ export async function getTreesFromGithub(directories, version, owner = 'libccy', */ const trees = await treesResponse.json(); const tree = trees.tree; - return directories.map(directory => tree.filter(({ type, path }) => type === 'blob' && path.startsWith(directory))); + return directories.map((directory) => + tree.filter(({ type, path }) => type === "blob" && path.startsWith(directory)) + ); } diff --git a/noname/ui/create/menu/pages/otherMenu.js b/noname/ui/create/menu/pages/otherMenu.js index c601b417f..07e96dbd2 100644 --- a/noname/ui/create/menu/pages/otherMenu.js +++ b/noname/ui/create/menu/pages/otherMenu.js @@ -21,7 +21,7 @@ import { createProgress, gainAuthorization, getLatestVersionFromGitHub, - getTreesFromGithub + getTreesFromGithub, } from "../../../../library/update.js"; export const otherMenu = function (/** @type { boolean | undefined } */ connectMenu) { @@ -333,7 +333,7 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM .then(() => { cp.exec( `start /b ${__dirname}\\noname-server.exe -platform=electron`, - () => { } + () => {} ); function loadURL() { let myAbortController = @@ -412,9 +412,9 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM } const assetDirectories = []; - if (lib.config.asset_font) assetDirectories.push('font'); - if (lib.config.asset_audio) assetDirectories.push('audio'); - if (lib.config.asset_image) assetDirectories.push('image'); + if (lib.config.asset_font) assetDirectories.push("font"); + if (lib.config.asset_audio) assetDirectories.push("audio"); + if (lib.config.asset_image) assetDirectories.push("image"); const version = await getLatestVersionFromGitHub(); const files = await getTreesFromGithub(assetDirectories, version); @@ -459,57 +459,73 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM * @type {progress} */ let unZipProgress; - request('api.unitedrhythmized.club/noname', (receivedBytes, total, filename) => { - if (typeof filename == 'string') { - progress.setFileName(filename); - } - let received = 0, max = 0; - if (total) { - max = +(total / (1024 * 1024)).toFixed(1) - } else { - max = 1000; - } - received = +(receivedBytes / (1024 * 1024)).toFixed(1); - if (received > max) max = received; - progress.setProgressMax(max); - progress.setProgressValue(received); - }, { - method: 'POST', - body: JSON.stringify({ - action: 'downloadAssets', - version, - fileList: result.concat('game/asset.js') - }) - }).then(async blob => { - progress.remove(); - const zip = await get.promises.zip(); - zip.load(await blob.arrayBuffer()); - const entries = Object.entries(zip.files); - let root; - const hiddenFileFlags = ['.', '_']; - unZipProgress = createProgress('正在解压' + progress.getFileName(), entries.length); - let i = 0; - for (const [key, value] of entries) { - unZipProgress.setProgressValue(i++); - const fileName = typeof root == 'string' && key.startsWith(root) ? key.replace(root, '') : key; - if (hiddenFileFlags.includes(fileName[0])) continue; - if (value.dir) { - await game.promises.createDir(fileName); - continue; + request( + "api.unitedrhythmized.club/noname", + (receivedBytes, total, filename) => { + if (typeof filename == "string") { + progress.setFileName(filename); } - unZipProgress.setFileName(fileName); - const [path, name] = [fileName.split('/').slice(0, -1).join('/'), fileName.split('/').slice(-1).join('/')]; - game.print(`${fileName}(${i}/${entries.length})`); - await game.promises.writeFile(value.asArrayBuffer(), path, name); + let received = 0, + max = 0; + if (total) { + max = +(total / (1024 * 1024)).toFixed(1); + } else { + max = 1000; + } + received = +(receivedBytes / (1024 * 1024)).toFixed(1); + if (received > max) max = received; + progress.setProgressMax(max); + progress.setProgressValue(received); + }, + { + method: "POST", + body: JSON.stringify({ + action: "downloadAssets", + version, + fileList: result.concat("game/asset.js"), + }), } - unZipProgress.remove(); - await finish(); - }).catch(e => { - if (progress.parentNode) progress.remove(); - if (unZipProgress && unZipProgress.parentNode) unZipProgress.remove(); - refresh(); - throw e; - }); + ) + .then(async (blob) => { + progress.remove(); + const zip = await get.promises.zip(); + zip.load(await blob.arrayBuffer()); + const entries = Object.entries(zip.files); + let root; + const hiddenFileFlags = [".", "_"]; + unZipProgress = createProgress( + "正在解压" + progress.getFileName(), + entries.length + ); + let i = 0; + for (const [key, value] of entries) { + unZipProgress.setProgressValue(i++); + const fileName = + typeof root == "string" && key.startsWith(root) + ? key.replace(root, "") + : key; + if (hiddenFileFlags.includes(fileName[0])) continue; + if (value.dir) { + await game.promises.createDir(fileName); + continue; + } + unZipProgress.setFileName(fileName); + const [path, name] = [ + fileName.split("/").slice(0, -1).join("/"), + fileName.split("/").slice(-1).join("/"), + ]; + game.print(`${fileName}(${i}/${entries.length})`); + await game.promises.writeFile(value.asArrayBuffer(), path, name); + } + unZipProgress.remove(); + await finish(); + }) + .catch((e) => { + if (progress.parentNode) progress.remove(); + if (unZipProgress && unZipProgress.parentNode) unZipProgress.remove(); + refresh(); + throw e; + }); } else { await finish(); } @@ -1646,4 +1662,3 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM if (!active.link) active._initLink(); rightPane.appendChild(active.link); }; -