|
| 1 | +const Gio = imports.gi.Gio; |
| 2 | +const GLib = imports.gi.GLib; |
| 3 | +const GdkPixbuf = imports.gi.GdkPixbuf; |
| 4 | +const St = imports.gi.St; |
| 5 | +const Util = imports.misc.util; |
| 6 | +const { ImageHelper } = require("./helpers/image.helper"); |
| 7 | + |
| 8 | +// AppIDs for games/tools to be filtered out from the list |
| 9 | +const FILTERED_APP_IDS = [ |
| 10 | + "1628350", // Proton Experimental |
| 11 | + "1493710", // Steam Linux Runtime 3.0 (sniper) |
| 12 | + "1391110", // Steam Linux Runtime 2.0 (soldier) |
| 13 | +]; |
| 14 | + |
| 15 | +class SteamHelper { |
| 16 | + // Helper to read the paths of the Steam library folders |
| 17 | + static extractLibraryPaths(vdfString) { |
| 18 | + const paths = []; |
| 19 | + const regex = /"path"\s*"(.*?)"/g; |
| 20 | + let match; |
| 21 | + while ((match = regex.exec(vdfString)) !== null) { |
| 22 | + if (!match[0].includes("debian-installation")) { |
| 23 | + paths.push(match[1]); |
| 24 | + } |
| 25 | + } |
| 26 | + return paths; |
| 27 | + } |
| 28 | + |
| 29 | + // Helper to extract game info from an appmanifest file |
| 30 | + static async extractGameInfo(filePath) { |
| 31 | + const file = Gio.file_new_for_path(filePath); |
| 32 | + const [, contentBytes] = await new Promise(resolve => |
| 33 | + file.load_contents_async(null, (obj, res) => resolve(obj.load_contents_finish(res))) |
| 34 | + ); |
| 35 | + const content = new TextDecoder("utf-8").decode(contentBytes); |
| 36 | + |
| 37 | + const nameMatch = /"name"\s*"(.*?)"/.exec(content); |
| 38 | + const appidMatch = /"appid"\s*"(.*?)"/.exec(content); |
| 39 | + const lastPlayedMatch = /"LastPlayed"\s*"(.*?)"/.exec(content); |
| 40 | + |
| 41 | + if (nameMatch && appidMatch && lastPlayedMatch) { |
| 42 | + return { |
| 43 | + name: nameMatch[1], |
| 44 | + appid: appidMatch[1], |
| 45 | + lastPlayed: lastPlayedMatch[1], |
| 46 | + }; |
| 47 | + } |
| 48 | + return null; |
| 49 | + } |
| 50 | + |
| 51 | + // Helper to get all installed games |
| 52 | + static async getGames(steamInstallType) { |
| 53 | + // Get Steam library paths |
| 54 | + let libraryfoldersFilePath = GLib.get_home_dir() + "/.steam/steam/steamapps/libraryfolders.vdf"; |
| 55 | + if (steamInstallType === "flatpak") { |
| 56 | + libraryfoldersFilePath = GLib.get_home_dir() + "/.var/app/com.valvesoftware.Steam/data/Steam/steamapps/libraryfolders.vdf"; |
| 57 | + } |
| 58 | + |
| 59 | + const libraryfoldersFile = Gio.file_new_for_path(libraryfoldersFilePath); |
| 60 | + if (!libraryfoldersFile.query_exists(null)) { |
| 61 | + throw new Error(`Steam library file not found at: ${libraryfoldersFilePath}`); |
| 62 | + } |
| 63 | + |
| 64 | + const [success, libraryfoldersFileContentBytes] = await new Promise(resolve => |
| 65 | + libraryfoldersFile.load_contents_async(null, (obj, res) => resolve(obj.load_contents_finish(res))) |
| 66 | + ); |
| 67 | + const libraryfoldersFileContent = new TextDecoder("utf-8").decode(libraryfoldersFileContentBytes); |
| 68 | + const libraryPaths = this.extractLibraryPaths(libraryfoldersFileContent); |
| 69 | + |
| 70 | + // Find all appmanifest files in the library paths |
| 71 | + const appmanifestPaths = []; |
| 72 | + for (const path of libraryPaths) { |
| 73 | + const steamAppsPath = GLib.build_filenamev([path, "steamapps"]); |
| 74 | + const out = await new Promise(resolve => Util.spawn_async(["find", steamAppsPath, "-name", "*.acf"], stdout => resolve(stdout))); |
| 75 | + if (out) { |
| 76 | + const paths = out |
| 77 | + .trim() |
| 78 | + .split("\n") |
| 79 | + .filter(p => p); |
| 80 | + appmanifestPaths.push(...paths); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + // Extract game info from each appmanifest file |
| 85 | + const gamePromises = []; |
| 86 | + for (const path of appmanifestPaths) { |
| 87 | + if (path) { |
| 88 | + gamePromises.push(this.extractGameInfo(path)); |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + const games = await Promise.all(gamePromises); |
| 93 | + const filteredGames = games.filter(game => game !== null && game.lastPlayed && !FILTERED_APP_IDS.includes(game.appid)); |
| 94 | + |
| 95 | + // Filter and sort the games by last played date (newest first) |
| 96 | + const sortedGames = filteredGames.sort((a, b) => parseInt(b.lastPlayed, 10) - parseInt(a.lastPlayed, 10)); |
| 97 | + |
| 98 | + return sortedGames; |
| 99 | + } |
| 100 | + |
| 101 | + // Helper to load a game's header image from the Steam appcache |
| 102 | + static getGameHeaderImage(appid, requestedWidth, requestedHeight) { |
| 103 | + const appCachePath = GLib.get_home_dir() + "/.steam/steam/appcache/librarycache/"; |
| 104 | + const commonImageNames = ["header.jpg", "library_header.jpg", "library_hero.jpg"]; |
| 105 | + |
| 106 | + let imagePath = null; |
| 107 | + for (const name of commonImageNames) { |
| 108 | + const potentialPath = GLib.build_filenamev([appCachePath, appid, name]); |
| 109 | + if (GLib.file_test(potentialPath, GLib.FileTest.EXISTS)) { |
| 110 | + imagePath = potentialPath; |
| 111 | + break; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + let pixBuf = null; |
| 116 | + if (imagePath) { |
| 117 | + try { |
| 118 | + pixBuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imagePath, requestedWidth, requestedHeight); |
| 119 | + } catch (e) { |
| 120 | + global.logError(`Error loading image ${imagePath}: ${e}`); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + if (pixBuf) { |
| 125 | + const imageActor = ImageHelper.createActorFromPixbuf(pixBuf); |
| 126 | + |
| 127 | + const clickableBin = new St.Bin({ |
| 128 | + reactive: true, |
| 129 | + width: pixBuf.get_width(), |
| 130 | + height: pixBuf.get_height(), |
| 131 | + }); |
| 132 | + clickableBin.set_child(imageActor); |
| 133 | + return clickableBin; |
| 134 | + } |
| 135 | + |
| 136 | + global.logError(`Could not load an image for appid ${appid}`); |
| 137 | + return new St.Label({ text: "Error" }); |
| 138 | + } |
| 139 | + |
| 140 | + static getSteamCommand(steamInstallType) { |
| 141 | + return steamInstallType === "flatpak" ? "flatpak run com.valvesoftware.Steam" : "/usr/games/steam"; |
| 142 | + } |
| 143 | + |
| 144 | + static runGame(appid, steamInstallType) { |
| 145 | + const cmd = this.getSteamCommand(steamInstallType); |
| 146 | + GLib.spawn_command_line_async(`${cmd} steam://rungameid/${appid}`); |
| 147 | + } |
| 148 | + |
| 149 | + static openStorePage(appid, steamInstallType) { |
| 150 | + const cmd = this.getSteamCommand(steamInstallType); |
| 151 | + GLib.spawn_command_line_async(`${cmd} steam://store/${appid}`); |
| 152 | + } |
| 153 | +} |
0 commit comments