feat(apps): implement search functionality and create AppItem component

This commit is contained in:
uncor3
2026-06-08 20:50:28 +00:00
parent 18d15d5113
commit 0b7dbf8151
2 changed files with 184 additions and 113 deletions
+68 -113
View File
@@ -1,10 +1,13 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import QtQuick.Controls.impl
import "./app-store/"
import "./base"
Item {
StateView {
id: root
anchors.fill: parent
@@ -16,6 +19,10 @@ Item {
property string email: ""
property bool isLoggedIn: email.length > 0
property string searchTerm: ""
property bool searchError: false
ListModel { id : searchResultsModel }
ListModel { id: appModel }
function clearApps() { appModel.clear(); }
@@ -95,24 +102,6 @@ Item {
xhr.send();
}
function fetchAppIconFromApple(bundleId, cb) {
if (!bundleId) { cb(""); return; }
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://itunes.apple.com/lookup?bundleId=" + encodeURIComponent(bundleId));
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status !== 200) { cb(""); return; }
try {
var obj = JSON.parse(xhr.responseText);
var results = obj && obj.results ? obj.results : [];
var iconUrl = results.length ? (results[0].artworkUrl100 || "") : "";
cb(iconUrl);
} catch (e) {
cb("");
}
};
xhr.send();
}
Component.onCompleted: {
// FIXME: show keychain/cred dialog.
@@ -128,9 +117,42 @@ Item {
email = s.email || "";
if (s.init) fetchSponsors();
}
function onSearch_ready(searchTerm, success, res) {
if (!success) {
console.error("Search failed for term:", searchTerm);
searchError = true;
return;
}
searchResultsModel.clear();
const parsedRes = JSON.parse(res);
if (!parsedRes || !parsedRes.results || !Array.isArray(parsedRes.results)) {
console.error("Invalid search results format: 'results' array not found or not an array.", res);
searchError = true;
return;
}
searchError = false;
for (var i = 0; i < parsedRes.results.length; ++i) {
const item = parsedRes.results[i];
searchResultsModel.append({
id : item.id || 0,
bundleId: item.bundle_id || "",
name: item.name || "",
price: item.price || 0,
description: "",
logoUrl: "",
useBundleIdForIcon: true,
sponsorLabel: "",
sponsorColor: ""
});
}
}
}
ColumnLayout {
contentItem : ColumnLayout {
anchors.fill: parent
spacing: 12
@@ -140,9 +162,19 @@ Item {
spacing: 12
TextField {
id: searchField
Layout.preferredWidth: 240
enabled: true
placeholderText: isLoggedIn ? "Search for apps..." : "Sign in to search"
onTextChanged: {
if (!apps) return;
searchTerm = searchField.text
searchResultsModel.clear();
// if (searchTerm.length === 0) return;
apps.search(searchTerm)
}
}
Item { Layout.fillWidth: true }
@@ -182,103 +214,26 @@ Item {
GridView {
id: grid
anchors.fill: parent
anchors.margins: 16
cellWidth: Math.max(250, width / 3)
cellHeight: 140
model: appModel
delegate: Rectangle {
id: rec
width: grid.cellWidth - 20
height: grid.cellHeight - 20
radius: 8
border.color: "#ddd"
color: "transparent"
delegate: Item {
width: grid.cellWidth - 12
height: grid.cellHeight - 12
property string iconSource: ""
Component.onCompleted: {
if (model.logoUrl && !model.useBundleIdForIcon) {
iconSource = model.logoUrl;
} else if (model.bundleId) {
fetchAppIconFromApple(model.bundleId, function(url) { iconSource = url; });
}
}
RowLayout {
AppItem {
anchors.fill: parent
anchors.margins: 12
spacing: 10
IconLoader {
iconSource: rec.iconSource
}
ColumnLayout {
Layout.fillWidth: true
Layout.minimumWidth: 0
spacing: 6
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: model.name
font.pixelSize: 16
wrapMode: Text.NoWrap
elide: Text.ElideRight
Layout.fillWidth: true
Layout.minimumWidth: 0
}
Rectangle {
visible: model.sponsorLabel && model.sponsorLabel.length > 0
color: model.sponsorColor
radius: 4
height: 16
Label {
anchors.centerIn: parent
text: model.sponsorLabel
font.pixelSize: 10
color: "#333"
padding: 4
}
}
}
Label {
text: model.description
color: "#666"
font.pixelSize: 12
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.minimumWidth: 0
maximumLineCount: 3
elide: Text.ElideRight
}
}
ColumnLayout {
spacing: 6
// FIXME: wire up click handling
Layout.alignment: Qt.AlignCenter
Layout.minimumWidth: implicitWidth
Button {
text: "Install"
font.pixelSize: 12
font.bold: true
}
Button {
text: (model.websiteUrl && model.websiteUrl.length) ? "Website" : "Download IPA"
font.pixelSize: 12
font.bold: true
}
}
name: model.name
bundleId: model.bundleId
description: model.description
logoUrl: model.logoUrl
websiteUrl: model.websiteUrl
useBundleIdForIcon: model.useBundleIdForIcon
sponsorLabel: model.sponsorLabel
sponsorColor: model.sponsorColor
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt5Compat.GraphicalEffects
import QtQuick.Controls.impl
import ".."
Rectangle {
required property var name;
required property var bundleId;
required property var description;
required property var logoUrl;
required property var websiteUrl;
required property var useBundleIdForIcon;
required property var sponsorLabel;
required property var sponsorColor;
id: root
width: grid.cellWidth - 20
height: grid.cellHeight - 20
radius: 8
border.color: "#ddd"
color: "transparent"
property string iconSource: ""
Component.onCompleted: {
console.log("Name:", root.name)
if (root.logoUrl && !root.useBundleIdForIcon) {
iconSource = root.logoUrl;
} else if (root.bundleId) {
Helpers.fetchAppIconFromApple(root.bundleId, function(url) { iconSource = url; });
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
IconLoader {
iconSource: root.iconSource
}
ColumnLayout {
Layout.fillWidth: true
Layout.minimumWidth: 0
spacing: 6
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: root.name
font.pixelSize: 16
wrapMode: Text.NoWrap
elide: Text.ElideRight
Layout.fillWidth: true
Layout.minimumWidth: 0
}
Rectangle {
visible: root.sponsorLabel && root.sponsorLabel.length > 0
color: root.sponsorColor
radius: 4
height: 16
Label {
anchors.centerIn: parent
text: root.sponsorLabel
font.pixelSize: 10
color: "#333"
padding: 4
}
}
}
Label {
text: root.description
color: "#666"
font.pixelSize: 12
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.minimumWidth: 0
maximumLineCount: 3
elide: Text.ElideRight
}
}
ColumnLayout {
spacing: 6
// FIXME: wire up click handling
Layout.alignment: Qt.AlignCenter
Layout.minimumWidth: implicitWidth
Button {
text: "Install"
font.pixelSize: 12
font.bold: true
background: Rectangle {
color: "#0076FF"
radius: 4
}
}
Button {
// FIXME: move this logic to another qml file
text: (root.websiteUrl && root.websiteUrl.length) ? "Website" : "Get IPA"
font.pixelSize: 12
font.bold: true
}
}
}
}