aboutsummaryrefslogtreecommitdiffstats
path: root/games/Games.qml
diff options
context:
space:
mode:
Diffstat (limited to 'games/Games.qml')
-rw-r--r--games/Games.qml89
1 files changed, 83 insertions, 6 deletions
diff --git a/games/Games.qml b/games/Games.qml
index 2d22460..ef5576e 100644
--- a/games/Games.qml
+++ b/games/Games.qml
@@ -21,6 +21,10 @@ Scope {
property string monitor: "DP-1"
property var items: []
property string query: ""
+ // Down switches the strip to the Steam store, searched as you type.
+ property bool store: false
+ property var results: []
+ property bool searching: false
// Horizontal shear of the strip's tiles, as a fraction of their height.
readonly property real slant: -0.25
@@ -30,6 +34,7 @@ Scope {
Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0]
readonly property var shown: {
+ if (root.store) return root.results;
const q = root.query.toLowerCase();
return root.items.filter(g => g.name.toLowerCase().includes(q));
}
@@ -46,6 +51,11 @@ Scope {
function launch(g) {
if (!g) return;
+ if (root.store) {
+ Quickshell.execDetached(["steam", `steam://store/${g.appid}`]);
+ root.open = false;
+ return;
+ }
// -silent keeps the Steam window down whether or not Steam is running.
// Ids are strings: a shortcut's game id does not fit a JS number.
Quickshell.execDetached(["steam", "-silent",
@@ -58,10 +68,58 @@ Scope {
// read again.
onOpenChanged: if (open) {
root.query = "";
+ root.store = false;
+ root.results = [];
list.running = true;
refresh.running = true;
}
+ // Local art is a path, store art a URL.
+ function src(p) { return p ? (p.startsWith("/") ? `file://${p}` : p) : ""; }
+
+ onQueryChanged: if (store) search.restart()
+ onStoreChanged: if (store) search.restart()
+
+ Timer {
+ id: search
+ interval: 350
+ onTriggered: root.storeSearch(root.query.trim())
+ }
+
+ // The store's own search endpoint needs no key, but answers nothing
+ // without a country, which also sets the prices. The locale is en_US
+ // here, so it cannot be derived from that.
+ // ponytail: fixed to Italy; a setting when another country matters.
+ readonly property string country: "it"
+
+ function storeSearch(term) {
+ if (!term) { root.results = []; root.searching = false; return; }
+ root.searching = true;
+ const xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState !== XMLHttpRequest.DONE || term !== root.query.trim()) return;
+ root.searching = false;
+ let found = [];
+ try { found = JSON.parse(xhr.responseText).items ?? []; } catch (e) {}
+ const mine = {};
+ for (const g of root.items) if (g.appid) mine[g.appid] = g;
+ const cdn = "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/";
+ root.results = found.map(r => {
+ const a = String(r.id);
+ // Newer apps keep their art under hashed paths, so these
+ // can 404; the capsule search returns always exists.
+ // installed: drawn like a library game, not greyed.
+ return { name: r.name, id: a, appid: a, mine: mine[a],
+ installed: true, last: 0, price: r.price,
+ cover: `${cdn}${a}/library_600x900.jpg`,
+ hero: `${cdn}${a}/library_hero.jpg`, capsule: r.tiny_image };
+ });
+ };
+ xhr.open("GET", `https://store.steampowered.com/api/storesearch/?l=english&cc=${root.country}&term=`
+ + encodeURIComponent(term));
+ xhr.send();
+ }
+
Process {
id: list
command: ["/usr/bin/python3", `${Quickshell.shellDir}/games.py`]
@@ -116,9 +174,16 @@ Scope {
Item {
anchors { top: parent.top; left: parent.left; right: parent.right; bottom: strip.top }
+ // A store result's capsule, under its hero in case that 404s.
+ Image {
+ anchors.fill: parent
+ source: root.src(win.game?.capsule)
+ fillMode: Image.PreserveAspectCrop
+ asynchronous: true
+ }
Image {
anchors.fill: parent
- source: win.game ? (win.game.hero || win.game.cover ? `file://${win.game.hero || win.game.cover}` : "") : ""
+ source: win.game ? root.src(win.game.hero || win.game.cover) : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
}
@@ -136,7 +201,11 @@ Scope {
spacing: 8
Text {
- text: win.game?.name ?? (root.items.length ? "No match" : "")
+ text: win.game?.name
+ ?? (!root.store ? (root.items.length ? "No match · Down searches the store" : "")
+ : root.searching ? "Searching the store…"
+ : root.query.trim() ? "Nothing on the store"
+ : "Type to search the Steam store")
color: Theme.text
font.family: Theme.fontFamily
font.pixelSize: 56
@@ -145,10 +214,15 @@ Scope {
Text {
visible: !!win.game
text: !win.game ? ""
+ : root.store ? [win.game.mine ? (win.game.mine.installed ? "Installed" : "Owned")
+ : win.game.price ? `${(win.game.price.final / 100).toFixed(2)} ${win.game.price.currency}`
+ : "Free or unpriced",
+ "Enter opens the store page"].join(" · ")
: !win.game.installed ? "Not installed · Enter installs"
: win.game.last ? `Last played ${new Date(win.game.last * 1000).toLocaleDateString(Qt.locale(), "d MMMM yyyy")}`
: "Never played"
- color: win.game?.installed ? Theme.subtext : Theme.peach
+ color: root.store && win.game?.mine ? Theme.green
+ : win.game?.installed ? Theme.subtext : Theme.peach
font.family: Theme.fontFamily
font.pixelSize: 20
}
@@ -166,7 +240,9 @@ Scope {
font.pixelSize: 28
onTextEdited: root.query = text
- Keys.onEscapePressed: root.open = false
+ Keys.onEscapePressed: { if (root.store) root.store = false; else root.open = false; }
+ Keys.onDownPressed: root.store = true
+ Keys.onUpPressed: root.store = false
Keys.onLeftPressed: strip.decrementCurrentIndex()
Keys.onRightPressed: strip.incrementCurrentIndex()
Keys.onReturnPressed: root.launch(win.game)
@@ -218,6 +294,7 @@ Scope {
Rectangle { anchors.fill: parent; color: Theme.surface }
Image {
+ id: cover
x: -tile.height * Math.abs(root.slant) / 2
width: parent.width + tile.height * Math.abs(root.slant)
height: parent.height
@@ -225,7 +302,7 @@ Scope {
matrix: Qt.matrix4x4(1, -root.slant, 0, root.slant * tile.height / 2,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
- source: tile.modelData.cover ? `file://${tile.modelData.cover}` : ""
+ source: root.src(tile.modelData.cover)
sourceSize.width: 480
fillMode: Image.PreserveAspectCrop
asynchronous: true
@@ -236,7 +313,7 @@ Scope {
// No cover: the initial stands in for it.
Text {
anchors.centerIn: parent
- visible: !tile.modelData.cover
+ visible: cover.status !== Image.Ready
text: tile.modelData.name.charAt(0)
color: Theme.text
font.family: Theme.fontFamily