diff options
| author | JSON Derulo <136133082+xXJSONDeruloXx@users.noreply.github.com> | 2025-05-06 01:09:53 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-06 01:09:53 -0400 |
| commit | 358a0933031a1e862dc46194f75d1ec6d0ec26fc (patch) | |
| tree | e7788d357c7242a45ae9ccaee272d1ce3c3ce6ad | |
| parent | c226e87f77375ec5682834aaf9049a0076f3e9c2 (diff) | |
| parent | 431fb640d17faeef8d2a0a455392c39528205e11 (diff) | |
| download | decky-bazzite-buddy-358a0933031a1e862dc46194f75d1ec6d0ec26fc.tar.gz decky-bazzite-buddy-358a0933031a1e862dc46194f75d1ec6d0ec26fc.zip | |
Merge pull request #4 from victor-borges/main
| -rw-r--r-- | .editorconfig | 12 | ||||
| -rw-r--r-- | decky.pyi | 184 | ||||
| -rw-r--r-- | main.py | 9 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/FetchReleases.ts | 46 | ||||
| -rw-r--r-- | src/PartnerEventStorePatch.tsx | 352 | ||||
| -rwxr-xr-x | src/index.tsx | 49 | ||||
| -rw-r--r-- | tsconfig.json | 2 |
8 files changed, 487 insertions, 169 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a1c77bf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +max_line_length = 120 + +[{*.ts,*.tsx}] +indent_size = 2 + +[{*.py,*.pyi}] +indent_size = 4 diff --git a/decky.pyi b/decky.pyi new file mode 100644 index 0000000..066f085 --- /dev/null +++ b/decky.pyi @@ -0,0 +1,184 @@ +""" +This module exposes various constants and helpers useful for decky plugins. + +* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`. +* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`. +* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`. + +Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended. + +Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`. + +A logging facility `logger` is available which writes to the recommended location. +""" + +__version__ = '1.0.0' + +import logging + +from typing import Any + +""" +Constants +""" + +HOME: str +""" +The home directory of the effective user running the process. +Environment variable: `HOME`. +If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in. +e.g.: `/home/deck` +""" + +USER: str +""" +The effective username running the process. +Environment variable: `USER`. +It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in. +e.g.: `deck` +""" + +DECKY_VERSION: str +""" +The version of the decky loader. +Environment variable: `DECKY_VERSION`. +e.g.: `v2.5.0-pre1` +""" + +DECKY_USER: str +""" +The user whose home decky resides in. +Environment variable: `DECKY_USER`. +e.g.: `deck` +""" + +DECKY_USER_HOME: str +""" +The home of the user where decky resides in. +Environment variable: `DECKY_USER_HOME`. +e.g.: `/home/deck` +""" + +DECKY_HOME: str +""" +The root of the decky folder. +Environment variable: `DECKY_HOME`. +e.g.: `/home/deck/homebrew` +""" + +DECKY_PLUGIN_SETTINGS_DIR: str +""" +The recommended path in which to store configuration files (created automatically). +Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`. +e.g.: `/home/deck/homebrew/settings/decky-plugin-template` +""" + +DECKY_PLUGIN_RUNTIME_DIR: str +""" +The recommended path in which to store runtime data (created automatically). +Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`. +e.g.: `/home/deck/homebrew/data/decky-plugin-template` +""" + +DECKY_PLUGIN_LOG_DIR: str +""" +The recommended path in which to store persistent logs (created automatically). +Environment variable: `DECKY_PLUGIN_LOG_DIR`. +e.g.: `/home/deck/homebrew/logs/decky-plugin-template` +""" + +DECKY_PLUGIN_DIR: str +""" +The root of the plugin's directory. +Environment variable: `DECKY_PLUGIN_DIR`. +e.g.: `/home/deck/homebrew/plugins/decky-plugin-template` +""" + +DECKY_PLUGIN_NAME: str +""" +The name of the plugin as specified in the 'plugin.json'. +Environment variable: `DECKY_PLUGIN_NAME`. +e.g.: `Example Plugin` +""" + +DECKY_PLUGIN_VERSION: str +""" +The version of the plugin as specified in the 'package.json'. +Environment variable: `DECKY_PLUGIN_VERSION`. +e.g.: `0.0.1` +""" + +DECKY_PLUGIN_AUTHOR: str +""" +The author of the plugin as specified in the 'plugin.json'. +Environment variable: `DECKY_PLUGIN_AUTHOR`. +e.g.: `John Doe` +""" + +DECKY_PLUGIN_LOG: str +""" +The path to the plugin's main logfile. +Environment variable: `DECKY_PLUGIN_LOG`. +e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log` +""" + +""" +Migration helpers +""" + + +def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]: + """ + Migrate files and directories to a new location and remove old locations. + Specified files will be migrated to `target_dir`. + Specified directories will have their contents recursively migrated to `target_dir`. + + Returns the mapping of old -> new location. + """ + + +def migrate_settings(*files_or_directories: str) -> dict[str, str]: + """ + Migrate files and directories relating to plugin settings to the recommended location and remove old locations. + Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`. + Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`. + + Returns the mapping of old -> new location. + """ + + +def migrate_runtime(*files_or_directories: str) -> dict[str, str]: + """ + Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations + Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`. + Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`. + + Returns the mapping of old -> new location. + """ + + +def migrate_logs(*files_or_directories: str) -> dict[str, str]: + """ + Migrate files and directories relating to plugin logs to the recommended location and remove old locations. + Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`. + Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`. + + Returns the mapping of old -> new location. + """ + + +""" +Logging +""" + +logger: logging.Logger +"""The main plugin logger writing to `DECKY_PLUGIN_LOG`.""" + +""" +Event handling +""" +async def emit(event: str, *args: Any) -> None: + """ + Send an event to the frontend. + """ + @@ -0,0 +1,9 @@ +class Plugin: + # noinspection PyMethodMayBeStatic + async def get_bazzite_branch(self) -> str | None: + try: + file = open("/etc/bazzite/image_branch") + branch = file.read() + return branch + finally: + return "stable" diff --git a/package.json b/package.json index e631f15..91742f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bazzite-buddy", - "version": "1.1.0", + "version": "1.2.0", "description": "A plugin to easily view Bazzite changelogs within game mode, primarily for handhelds.", "type": "module", "scripts": { diff --git a/src/FetchReleases.ts b/src/FetchReleases.ts new file mode 100644 index 0000000..9928d37 --- /dev/null +++ b/src/FetchReleases.ts @@ -0,0 +1,46 @@ +import {callable} from "@decky/api"; + +const getBazziteBranch = callable<[], string>("get_bazzite_branch"); + +export async function isBazziteBranchTesting() { + const branch = await getBazziteBranch(); + return branch === "testing"; +} + +export async function* fetchReleases(signal?: AbortSignal) { + const testing = await isBazziteBranchTesting(); + let currentPage = 1; + let done = false; + + while (!done) { + let response: Response; + let responseJson: any; + + try { + response = await fetch( + `https://api.github.com/repos/ublue-os/bazzite/releases?page=${currentPage++}&per_page=10`, + { signal }); + + if (response.ok) { + responseJson = await response.json(); + } else { + responseJson = []; + } + } catch { + responseJson = []; + } + + if (!Array.isArray(responseJson) || responseJson.length == 0) { + done = true; + } else { + responseJson.sort((a, b) => (new Date(b.created_at)).getTime() - (new Date(a.created_at)).getTime()); + + for (let release of responseJson) { + if (release && ((testing && release.prerelease) || (!testing && !release.prerelease))) + yield release; + } + } + } + + return undefined; +} diff --git a/src/PartnerEventStorePatch.tsx b/src/PartnerEventStorePatch.tsx index 2307283..f7df4b3 100644 --- a/src/PartnerEventStorePatch.tsx +++ b/src/PartnerEventStorePatch.tsx @@ -1,11 +1,12 @@ -import { findModuleExport } from "@decky/ui"; -import { afterPatch } from "decky-frontend-lib"; +import {findModuleExport, Patch} from "@decky/ui"; +import {replacePatch} from "decky-frontend-lib"; import remarkHtml from "remark-html" import remarkParse from "remark-parse" import remarkGfm from "remark-gfm" import {unified} from "unified" import html2bbcode from "./html2bbcode"; import {Mutex} from 'async-mutex'; +import {fetchReleases, isBazziteBranchTesting} from "./FetchReleases"; const PartnerEventStore = findModuleExport( (e) => e?.prototype?.InternalLoadAdjacentPartnerEvents @@ -31,163 +32,230 @@ const SteamID = findModuleExport( const steamClanSteamID = "103582791470414830"; const steamClanID = "40893422"; const steamOSAppId = 1675200; -const githubReleasesURI = "https://api.github.com/repos/ublue-os/bazzite/releases"; +let generator: AsyncGenerator<any, undefined, unknown>; +const mutex = new Mutex(); +const cachedGithubReleases: { gid: string, release: any }[] = []; enum SteamEventType { - SmallUpdate = 12, + // SmallUpdate = 12, Update = 13, - BigUpdate = 14, + // BigUpdate = 14, } -const mutex = new Mutex(); -let releases: any[]; -let channel: string; +type SteamTags = { + require_tags?: string[] +} -export function patchPartnerEventStore() { - return afterPatch( +enum SteamOSChannel { + Stable = "stablechannel", + Beta = "betachannel", + // Preview = "previewchannel", +} + +export function patchPartnerEventStore(): Patch[] { + const loadAdjacentPartnerEventsPatch = replacePatch( PartnerEventStore.prototype, - "InternalLoadAdjacentPartnerEvents", - async function(args, ret) { - let [, , , appId, , , c, ] = args; + "LoadAdjacentPartnerEvents", + async function (args) { + let [gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token] = args; + const module = this; if (appId !== steamOSAppId) { - return ret; + return module.InternalLoadAdjacentPartnerEvents(gidEvent, void 0, gidAnnouncement, appId, countBefore, countAfter, tags, token); } - ret = await Promise.resolve(ret); + return mutex.runExclusive(async () => { + return LoadBazziteReleasesAsPartnerEvents(module, gidEvent?.GID || gidEvent?.AnnouncementGID, tags, countBefore, countAfter); + }); + } + ); - if (!Array.isArray(ret)) { - return ret; + const loadAdjacentPartnerEventsByAnnouncementPatch = replacePatch( + PartnerEventStore.prototype, + "LoadAdjacentPartnerEventsByAnnouncement", + async function (args) { + let [gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token] = args; + const module = this; + + if (appId !== steamOSAppId) { + return module.InternalLoadAdjacentPartnerEvents(void 0, gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token); } - ret.length = 0; - - await mutex.runExclusive(async () => { - if (releases && releases.length > 0) - return; - - let response: Response; - let responseJson: any; - - try { - response = await fetch(githubReleasesURI); - - if (!response.ok) - return; - - responseJson = await response.json(); - } - catch { - responseJson = []; - } - - if (!Array.isArray(responseJson) || responseJson.length == 0) { - return; - } - - responseJson.sort((a, b) => (new Date(b.created_at)).getTime() - (new Date(a.created_at)).getTime()); - - if (c?.require_tags && c?.require_tags?.includes("stablechannel")) { - releases = responseJson.filter(r => !r.prerelease); - channel = "stablechannel"; - } else if (c?.require_tags && (c?.require_tags?.includes("betachannel") || c?.require_tags?.includes("previewchannel"))) { - releases = responseJson.filter(r => r.prerelease); - channel = "betachannel"; - } else { - releases = responseJson; - channel = "stablechannel"; - } + return mutex.runExclusive(async () => { + return LoadBazziteReleasesAsPartnerEvents(module, gidEvent?.GID || gidEvent?.AnnouncementGID, tags, countBefore, countAfter); }); - - if (!releases || releases.length == 0) - return ret; - - for (const release of releases) { - const releaseCreatedAt = Math.floor((new Date(release.created_at)).getTime() / 1000); - - const html = await unified() - .use(remarkParse) - .use(remarkGfm) - .use(remarkHtml) - .process(release.body); - - const converter = new (html2bbcode.HTML2BBCode)(); - const bbcode = converter.feed(html.value); - - // @ts-ignore - const event = { - "gid": String(release.id), - "clan_steamid": steamClanSteamID, - "event_name": release.name, - "event_type": SteamEventType.Update, - "appid": steamOSAppId, - "server_address": "", - "server_password": "", - "rtime32_start_time": releaseCreatedAt, // only used for certain event_type, not used for updates, but anyway - "rtime32_end_time": releaseCreatedAt, // only used for certain event_type, not used for updates, but anyway - "comment_count": 0, - "creator_steamid": "0", - "last_update_steamid": "0", - "event_notes": "see announcement body", - "jsondata": "{\n\t\"localized_subtitle\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_summary\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_title_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_capsule_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"bSaleEnabled\": false,\n\t\"sale_show_creator\": false,\n\t\"sale_sections\": [\n\n\t]\n\t,\n\t\"sale_browsemore_text\": \"\",\n\t\"sale_browsemore_url\": \"\",\n\t\"sale_browsemore_color\": \"\",\n\t\"sale_browsemore_bgcolor\": \"\",\n\t\"localized_sale_header\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_overlay\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_product_banner\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_product_mobile_banner\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_logo\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"sale_font\": \"\",\n\t\"sale_background_color\": \"\",\n\t\"sale_header_offset\": 150,\n\t\"referenced_appids\": [\n\n\t]\n\t,\n\t\"bBroadcastEnabled\": false,\n\t\"broadcastChatSetting\": \"hide\",\n\t\"default_broadcast_title\": \"#Broadcast_default_title_dev\",\n\t\"localized_broadcast_title\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_broadcast_left_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_broadcast_right_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"broadcast_whitelist\": [\n\n\t]\n\t,\n\t\"bScheduleEnabled\": false,\n\t\"scheduleEntries\": [\n\n\t]\n\t,\n\t\"valve_access_log\": [\n\t\t{\n\t\t\t\"strSteamID\": \"76561197979253178\",\n\t\t\t\"rtUpdated\": 1741648377\n\t\t},\n\t\t{\n\t\t\t\"strSteamID\": \"76561198840299494\",\n\t\t\t\"rtUpdated\": 1741740761\n\t\t}\n\t]\n\t,\n\t\"clone_from_event_gid\": \"519706073503894980\",\n\t\"clone_from_sale_enabled\": false,\n\t\"automatically_push_updated_source\": true\n}", - "announcement_body": { - "gid": String(release.id), - "clanid": steamClanID, - "posterid": "0", - "headline": `Bazzite ${release.name}`, - "posttime": releaseCreatedAt, - "updatetime": releaseCreatedAt, - "body": bbcode.toString(), - "commentcount": 0, - "tags": [ - "patchnotes", - channel, - ], - "language": 0, - "hidden": 0, - "forum_topic_id": "0", - "event_gid": "0", - "voteupcount": 0, - "votedowncount": 0, - "ban_check_result": 0, - "banned": 0 - }, - "published": 1, - "hidden": 0, - "rtime32_visibility_start": 0, - "rtime32_visibility_end": 0, - "broadcaster_accountid": 0, - "follower_count": 0, - "ignore_count": 0, - "forum_topic_id": "0", - "rtime32_last_modified": releaseCreatedAt, - "news_post_gid": "0", - "rtime_mod_reviewed": 0, - "featured_app_tagid": 0, - "referenced_appids": [], - "build_id": 0, - "build_branch": "", - "unlisted": 0, - "votes_up": 0, - "votes_down": 0, - "comment_type": "ForumTopic", // haven't found a way to hide likes and comments - "gidfeature": "0", - "gidfeature2": "0" - }; - - // @ts-ignore - if (!this.m_mapExistingEvents.has(event.gid)) { - let steamId = new SteamID(event.clan_steamid); - - // @ts-ignore - this.InsertEventModelFromClanEventData(steamId, event) - } - - // @ts-ignore - ret.push(this.m_mapExistingEvents.get(event.gid)); + } + ); + + const loadAdjacentPartnerEventsByEventPatch = replacePatch( + PartnerEventStore.prototype, + "LoadAdjacentPartnerEventsByEvent", + async function (args) { + let [gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token] = args; + const module = this; + + if (appId !== steamOSAppId) { + const clanId = gidAnnouncement || gidEvent.clanSteamID; + + return gidEvent.bOldAnnouncement + ? module.InternalLoadAdjacentPartnerEvents(void 0, gidEvent.AnnouncementGID, clanId, appId, countBefore, countAfter, tags, token) + : module.InternalLoadAdjacentPartnerEvents(gidEvent.GID, gidEvent.AnnouncementGID, clanId, appId, countBefore, countAfter, tags, token); } - return ret; + return mutex.runExclusive(async () => { + return LoadBazziteReleasesAsPartnerEvents(module, gidEvent?.GID || gidEvent?.AnnouncementGID, tags, countBefore, countAfter); + }); } ); + + return [ + loadAdjacentPartnerEventsPatch, + loadAdjacentPartnerEventsByAnnouncementPatch, + loadAdjacentPartnerEventsByEventPatch + ] +} + +async function LoadBazziteReleasesAsPartnerEvents(module: any, gid: any, tags: SteamTags, countBefore: number, countAfter: number) { + const ret: any[] = []; + + // InternalLoadAdjacentPartnerEvents minified code, gets announcement from cache if it exists + if (module.m_mapAdjacentAnnouncementGIDs.has(gid)) { + // noinspection JSPrimitiveTypeWrapperUsage + let e = module.m_mapAdjacentAnnouncementGIDs.get(gid) + , r = new Array; + // noinspection CommaExpressionJS + if (e.forEach(((e: any) => { + if (module.m_mapAnnouncementBodyToEvent.has(e)) { + let t = module.m_mapAnnouncementBodyToEvent.get(e); + ret.push(module.m_mapExistingEvents.get(t)) + } else + r.push(e) + } + )), + r.length > 0) { + (await module.LoadBatchPartnerEventsByEventGIDsOrAnnouncementGIDs(null, r, tags)).forEach(((e: any) => ret.push(e))) + } + } + + if (cachedGithubReleases.length === 0) { + await fetchMoreReleases(countAfter); + } + + const releaseIndex = gid ? cachedGithubReleases.findIndex((e: any) => e.gid === gid) : -1; + let releases: { gid: any, release: any }[]; + + if (releaseIndex === -1) { + releases = cachedGithubReleases.slice(0, countAfter); + } else { + if (releaseIndex + countAfter + 1 > cachedGithubReleases.length) { + const toFetch = releaseIndex + countAfter + 1 - cachedGithubReleases.length; + await fetchMoreReleases(toFetch); + } + + releases = cachedGithubReleases.slice(Math.max(releaseIndex - countBefore + 1, 0), releaseIndex + countAfter + 1); + } + + for (const {release} of releases) { + const releasePublishedAt = Math.floor((new Date(release.published_at)).getTime() / 1000); + + const html = await unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkHtml) + .process(release.body); + + const converter = new (html2bbcode.HTML2BBCode)(); + const bbcode = converter.feed(html.value); + + // haven't found a way to hide likes and comments yet + const event = { + "gid": String(release.id), + "clan_steamid": steamClanSteamID, + "event_name": release.name, + "event_type": SteamEventType.Update, + "appid": steamOSAppId, + "server_address": "", + "server_password": "", + // start and end time are only used for certain event_type, not used for updates, but fill it anyway + "rtime32_start_time": releasePublishedAt, + "rtime32_end_time": releasePublishedAt, + "comment_count": 0, + "creator_steamid": "0", + "last_update_steamid": "0", + "event_notes": "see announcement body", + "jsondata": "", + "announcement_body": { + "gid": String(release.id), + "clanid": steamClanID, + "posterid": "0", + "headline": `Bazzite ${release.tag_name}`, + "posttime": releasePublishedAt, + "updatetime": releasePublishedAt, + "body": bbcode.toString(), + "commentcount": 0, + "tags": [ + "patchnotes", + (await isBazziteBranchTesting()) ? SteamOSChannel.Beta : SteamOSChannel.Stable, + ], + "language": 0, + "hidden": 0, + "forum_topic_id": "0", + "event_gid": "0", + "voteupcount": 0, + "votedowncount": 0, + "ban_check_result": 0, + "banned": 0 + }, + "published": 1, + "hidden": 0, + "rtime32_visibility_start": 0, + "rtime32_visibility_end": 0, + "broadcaster_accountid": 0, + "follower_count": 0, + "ignore_count": 0, + "forum_topic_id": "0", + "rtime32_last_modified": releasePublishedAt, + "news_post_gid": "0", + "rtime_mod_reviewed": 0, + "featured_app_tagid": 0, + "referenced_appids": [], + "build_id": 0, + "build_branch": "", + "unlisted": 0, + "votes_up": 0, + "votes_down": 0, + "comment_type": "ForumTopic", + "gidfeature": "0", + "gidfeature2": "0" + }; + + // InternalLoadAdjacentPartnerEvents minified code, maps announcement and add it to cache + if (!module.m_mapExistingEvents.has(event.gid)) { + let steamId = new SteamID(event.clan_steamid); + module.InsertEventModelFromClanEventData(steamId, event) + } + + ret.push(module.m_mapExistingEvents.get(event.gid)); + } + + return ret; +} + +async function fetchMoreReleases(count: number) { + const releases = []; + + if (!generator && cachedGithubReleases.length === 0) + generator = fetchReleases(); + + let iterator; + + do { + iterator = await generator.next(); + const release = iterator.value; + releases.push(release); + } while (releases.length < count && !iterator.done) + + for (const release of releases) { + cachedGithubReleases.push({gid: String(release.id), release}); + } } diff --git a/src/index.tsx b/src/index.tsx index 28ae219..ed3327f 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,12 +1,13 @@ -import { useEffect, useState } from "react"; -import { FaClipboardList } from "react-icons/fa"; -import { definePlugin } from "decky-frontend-lib"; +import {useEffect, useState} from "react"; +import {FaClipboardList} from "react-icons/fa"; import remarkHtml from "remark-html" import remarkParse from "remark-parse" import remarkGfm from "remark-gfm" -import { unified } from "unified" -import { patchPartnerEventStore } from "./PartnerEventStorePatch"; +import {unified} from "unified" +import {patchPartnerEventStore} from "./PartnerEventStorePatch"; import {staticClasses} from "@decky/ui"; +import {definePlugin} from "@decky/api" +import {fetchReleases} from "./FetchReleases"; function Content() { const [changelogHtml, setChangelogHtml] = useState<string | null>(null); @@ -14,25 +15,20 @@ function Content() { const [isRefreshing, setIsRefreshing] = useState<boolean>(false); const fetchChangelog = async (signal?: AbortSignal) => { - const url = "https://api.github.com/repos/ublue-os/bazzite/releases/latest"; try { - const response = await fetch(url, { - headers: { - Accept: "application/vnd.github.v3+json", - }, - signal, - }); + const generator = fetchReleases(signal); + const iterator = await generator.next(); - if (!response.ok) { - throw new Error(`Failed to fetch: ${response.statusText}`); + if (!iterator || iterator.done) { + setError("An unknown error occurred while fetching the changelog."); + return; } - const data = await response.json(); const html = await unified() - .use(remarkParse) - .use(remarkGfm) - .use(remarkHtml) - .process(data.body) + .use(remarkParse) + .use(remarkGfm) + .use(remarkHtml) + .process(iterator.value.body); setChangelogHtml(html.value as string); setError(null); @@ -84,7 +80,7 @@ function Content() { }} > <h2>Bazzite Release Notes</h2> - <div style={{ display: "flex", gap: "10px", marginBottom: "15px" }}> + <div style={{display: "flex", gap: "10px", marginBottom: "15px"}}> <button style={{ padding: "10px 20px", @@ -121,7 +117,7 @@ function Content() { </button> </div> {error ? ( - <p style={{ color: "red" }} aria-live="polite"> + <p style={{color: "red"}} aria-live="polite"> {error} </p> ) : changelogHtml ? ( @@ -181,16 +177,19 @@ function Content() { ); } +// noinspection JSUnusedGlobalSymbols export default definePlugin(() => { - const patch = patchPartnerEventStore(); + const patches = patchPartnerEventStore(); return { name: "Bazzite Changelog Viewer", title: <div className={staticClasses.Title}>Bazzite Buddy</div>, - icon: <FaClipboardList />, - content: <Content />, + icon: <FaClipboardList/>, + content: <Content/>, onDismount() { - patch.unpatch(); + patches.forEach(patch => { + patch.unpatch(); + }); }, }; }); diff --git a/tsconfig.json b/tsconfig.json index c7da3dd..69bd8f0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "noUnusedParameters": true, "esModuleInterop": true, "noImplicitReturns": true, - "noImplicitThis": true, + "noImplicitThis": false, "noImplicitAny": true, "strict": true, "allowSyntheticDefaultImports": true, |
