summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJSON Derulo <136133082+xXJSONDeruloXx@users.noreply.github.com>2025-05-06 01:09:53 -0400
committerGitHub <noreply@github.com>2025-05-06 01:09:53 -0400
commit358a0933031a1e862dc46194f75d1ec6d0ec26fc (patch)
treee7788d357c7242a45ae9ccaee272d1ce3c3ce6ad
parentc226e87f77375ec5682834aaf9049a0076f3e9c2 (diff)
parent431fb640d17faeef8d2a0a455392c39528205e11 (diff)
downloaddecky-bazzite-buddy-358a0933031a1e862dc46194f75d1ec6d0ec26fc.tar.gz
decky-bazzite-buddy-358a0933031a1e862dc46194f75d1ec6d0ec26fc.zip
Merge pull request #4 from victor-borges/main
-rw-r--r--.editorconfig12
-rw-r--r--decky.pyi184
-rw-r--r--main.py9
-rw-r--r--package.json2
-rw-r--r--src/FetchReleases.ts46
-rw-r--r--src/PartnerEventStorePatch.tsx352
-rwxr-xr-xsrc/index.tsx49
-rw-r--r--tsconfig.json2
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.
+ """
+
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..a1690fb
--- /dev/null
+++ b/main.py
@@ -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,