mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
begin implementing app store in qml
- Added `anyhow` and `ipatool` dependencies to `Cargo.toml`. - Created a new `apps.rs` module to manage application state and user authentication. - Refactored image loading logic to handle HEIC files and generate thumbnails using the new bridge functions. - Enhanced utility functions for managing QMap state in the application.
This commit is contained in:
+7
-5
@@ -55,7 +55,8 @@ find_package(PkgConfig REQUIRED)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia MultimediaWidgets Network QuickControls2 SerialPort Positioning Location QuickWidgets)
|
||||
# DBUS
|
||||
if (UNIX AND NOT APPLE)
|
||||
find_package(Qt6 REQUIRED COMPONENTS DBus)
|
||||
find_package(Qt6 REQUIRED COMPONENTS DBus)
|
||||
pkg_check_modules(DBUS REQUIRED IMPORTED_TARGET dbus-1)
|
||||
endif()
|
||||
find_package(SQLite3 REQUIRED)
|
||||
# Add QTermWidget
|
||||
@@ -135,7 +136,7 @@ cxx_qt_import_crate(
|
||||
MANIFEST_PATH src/rust/Cargo.toml
|
||||
CRATES idescriptor_rust_codebase
|
||||
# LOCKED
|
||||
QT_MODULES Qt::Core Qt::Gui Qt::Qml Qt::QuickControls2
|
||||
QT_MODULES Qt::Core Qt::Gui Qt::Qml Qt::QuickControls2 Qt::Widgets
|
||||
)
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
@@ -186,10 +187,11 @@ endif()
|
||||
file(GLOB PROJECT_SOURCES
|
||||
# src/*.h
|
||||
# src/*.cpp
|
||||
# src/core/helpers/*.cpp
|
||||
# src/core/services/*.cpp
|
||||
src/core/helpers/*.cpp
|
||||
src/core/services/*.cpp
|
||||
# src/base/*.cpp
|
||||
# src/base/*.h
|
||||
src/networkdeviceprovider.h
|
||||
src/main.cpp
|
||||
src/constants.h
|
||||
# src/thumbnailmodel.h
|
||||
@@ -306,7 +308,7 @@ target_link_libraries(iDescriptor PRIVATE
|
||||
)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_libraries(iDescriptor PRIVATE Qt6::DBus)
|
||||
target_link_libraries(iDescriptor PRIVATE Qt6::DBus PkgConfig::DBUS)
|
||||
endif()
|
||||
|
||||
if(ENABLE_RECOVERY_DEVICE_SUPPORT)
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
<file>src/qml/DeviceGallery.qml</file>
|
||||
<file>src/qml/AlbumContents.qml</file>
|
||||
<file>src/qml/PreviewWindow.qml</file>
|
||||
<file>src/qml/SidebarTabButton.qml</file>
|
||||
<file>src/qml/AppsTab.qml</file>
|
||||
<file>src/qml/LoginDialog.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
+1
-118
@@ -400,121 +400,4 @@ inline QJsonObject getVersionedConfig(const QJsonObject &rootObj)
|
||||
}
|
||||
}
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
struct XmlPlistDict {
|
||||
pugi::xml_node current_node;
|
||||
|
||||
XmlPlistDict() = default;
|
||||
explicit XmlPlistDict(pugi::xml_node n) : current_node(n) {}
|
||||
|
||||
bool valid() const { return current_node; }
|
||||
|
||||
bool isDict() const
|
||||
{
|
||||
return current_node && std::strcmp(current_node.name(), "dict") == 0;
|
||||
}
|
||||
|
||||
bool isArray() const
|
||||
{
|
||||
return current_node && std::strcmp(current_node.name(), "array") == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
// helper: for dict lookups
|
||||
pugi::xml_node findValueNode(const char *key) const
|
||||
{
|
||||
if (!isDict())
|
||||
return {};
|
||||
|
||||
for (pugi::xml_node child = current_node.first_child(); child;
|
||||
child = child.next_sibling()) {
|
||||
if (std::strcmp(child.name(), "key") == 0 &&
|
||||
std::strcmp(child.text().as_string(), key) == 0) {
|
||||
return child.next_sibling(); // the value node
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
public:
|
||||
// dict key access
|
||||
XmlPlistDict operator[](const char *key) const
|
||||
{
|
||||
return XmlPlistDict(findValueNode(key));
|
||||
}
|
||||
|
||||
XmlPlistDict operator[](const std::string &key) const
|
||||
{
|
||||
return (*this)[key.c_str()];
|
||||
}
|
||||
|
||||
XmlPlistDict operator[](const QString &key) const
|
||||
{
|
||||
return (*this)[key.toUtf8().constData()];
|
||||
}
|
||||
|
||||
// array index access
|
||||
XmlPlistDict operator[](int index) const
|
||||
{
|
||||
if (!isArray() || index < 0)
|
||||
return XmlPlistDict();
|
||||
|
||||
int i = 0;
|
||||
for (pugi::xml_node child = current_node.first_child(); child;
|
||||
child = child.next_sibling()) {
|
||||
if (!child.name() || !*child.name())
|
||||
continue; // skip text/whitespace
|
||||
if (i == index)
|
||||
return XmlPlistDict(child);
|
||||
++i;
|
||||
}
|
||||
return XmlPlistDict();
|
||||
}
|
||||
|
||||
// getters on current node (like PlistNavigator)
|
||||
bool getBool(bool def = false) const
|
||||
{
|
||||
if (!current_node)
|
||||
return def;
|
||||
|
||||
const char *name = current_node.name();
|
||||
if (!std::strcmp(name, "true"))
|
||||
return true;
|
||||
if (!std::strcmp(name, "false"))
|
||||
return false;
|
||||
|
||||
std::string s = current_node.text().as_string();
|
||||
if (s == "true" || s == "1")
|
||||
return true;
|
||||
if (s == "false" || s == "0")
|
||||
return false;
|
||||
return def;
|
||||
}
|
||||
|
||||
uint64_t getUInt(uint64_t def = 0) const
|
||||
{
|
||||
if (!current_node)
|
||||
return def;
|
||||
std::string s = current_node.text().as_string();
|
||||
if (s.empty())
|
||||
return def;
|
||||
try {
|
||||
return std::stoull(s);
|
||||
} catch (...) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
std::string getString(const std::string &def = std::string()) const
|
||||
{
|
||||
if (!current_node)
|
||||
return def;
|
||||
return current_node.text().as_string();
|
||||
}
|
||||
|
||||
pugi::xml_node getNode() const { return current_node; }
|
||||
};
|
||||
|
||||
void parseOldDeviceBattery(XmlPlistDict &ioreg, DeviceInfo &d);
|
||||
void parseDeviceBattery(XmlPlistDict &ioreg, DeviceInfo &d);
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
#ifdef WIN32
|
||||
#include "platform/windows/win_common.h"
|
||||
#endif
|
||||
#include "networkdeviceprovider.h"
|
||||
// #include "thumbnailmodel.h"
|
||||
#include "thumbnailprovider.h"
|
||||
#include <QQmlApplicationEngine>
|
||||
@@ -138,6 +139,8 @@ int main(int argc, char *argv[])
|
||||
engine.addImageProvider("thumb", ThumbnailProvider::sharedInstance());
|
||||
engine.rootContext()->setContextProperty(
|
||||
"ThumbnailProvider", ThumbnailProvider::sharedInstance());
|
||||
engine.rootContext()->setContextProperty(
|
||||
"NetworkDeviceProvider", NetworkDeviceProvider::sharedInstance());
|
||||
engine.load(url);
|
||||
|
||||
return a.exec();
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import com.kdab.cxx_qt.demo 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property Apps apps: Apps {}
|
||||
readonly property string sponsorsUrl: "https://raw.githubusercontent.com/iDescriptor/iDescriptor/refs/heads/main/sponsors.json"
|
||||
|
||||
property bool loading: true
|
||||
property string error: ""
|
||||
property string email: ""
|
||||
property bool isLoggedIn: email.length > 0
|
||||
|
||||
ListModel { id: appModel }
|
||||
|
||||
function clearApps() { appModel.clear(); }
|
||||
|
||||
function addApp(obj) { appModel.append(obj); }
|
||||
|
||||
function pickLastVersionKey(obj) {
|
||||
var keys = Object.keys(obj || {});
|
||||
if (!keys.length) return "";
|
||||
keys.sort();
|
||||
// FIXME: use semantic version matching instead of last key.
|
||||
return keys[keys.length - 1];
|
||||
}
|
||||
|
||||
function addSponsors(tierObj, label, color) {
|
||||
if (!tierObj || !tierObj.members) return;
|
||||
for (var i = 0; i < tierObj.members.length; ++i) {
|
||||
var m = tierObj.members[i];
|
||||
addApp({
|
||||
name: m.name || "",
|
||||
bundleId: m.bundleId || "",
|
||||
description: m.description || "",
|
||||
logoUrl: m.logo || "",
|
||||
websiteUrl: m.url || "",
|
||||
useBundleIdForIcon: m.useBundleIdForIcon !== false,
|
||||
sponsorLabel: label,
|
||||
sponsorColor: color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addDefaultApps() {
|
||||
addApp({ name: "Instagram", bundleId: "com.burbn.instagram", description: "Photo & Video sharing social network", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "Spotify", bundleId: "com.spotify.client", description: "Music streaming and podcast platform", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "YouTube", bundleId: "com.google.ios.youtube", description: "Video sharing and streaming platform", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "X", bundleId: "com.atebits.Tweetie2", description: "Social media and microblogging", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "TikTok", bundleId: "com.zhiliaoapp.musically", description: "Short-form video hosting service", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "Twitch", bundleId: "tv.twitch", description: "Live streaming platform", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "Telegram", bundleId: "ph.telegra.Telegraph", description: "Cloud-based instant messaging", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
addApp({ name: "Reddit", bundleId: "com.reddit.Reddit", description: "Social news aggregation platform", logoUrl: "", websiteUrl: "", useBundleIdForIcon: true, sponsorLabel: "", sponsorColor: "" });
|
||||
}
|
||||
|
||||
function fetchSponsors() {
|
||||
loading = true;
|
||||
error = "";
|
||||
clearApps();
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", sponsorsUrl);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var rootObj = JSON.parse(xhr.responseText);
|
||||
var key = pickLastVersionKey(rootObj);
|
||||
var versioned = key ? rootObj[key] : null;
|
||||
var sponsors = versioned && versioned.sponsors ? versioned.sponsors : null;
|
||||
|
||||
if (sponsors) {
|
||||
addSponsors(sponsors.platinum, "Platinum", "#E5E4E2");
|
||||
addSponsors(sponsors.gold, "Gold", "#D4AF37");
|
||||
addSponsors(sponsors.silver, "Silver", "#C0C0C0");
|
||||
addSponsors(sponsors.bronze, "Bronze", "#CD7F32");
|
||||
}
|
||||
} catch (e) {
|
||||
error = "Failed to parse sponsors JSON.";
|
||||
}
|
||||
} else {
|
||||
error = "Failed to fetch sponsors.";
|
||||
}
|
||||
|
||||
addDefaultApps();
|
||||
loading = false;
|
||||
};
|
||||
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 if required.
|
||||
apps.init();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: apps
|
||||
function onStateChanged() {
|
||||
var s = apps.state;
|
||||
if (!s) return;
|
||||
if (s.error) error = s.error;
|
||||
email = s.email || "";
|
||||
if (s.init) fetchSponsors();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
spacing: 12
|
||||
|
||||
TextField {
|
||||
Layout.preferredWidth: 240
|
||||
enabled: false
|
||||
placeholderText: isLoggedIn ? "Search for apps..." : "Sign in to search"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: isLoggedIn ? ("Signed in as " + email) : "Not signed in"
|
||||
color: "#666"
|
||||
}
|
||||
|
||||
Button {
|
||||
text: isLoggedIn ? "Sign Out" : "Sign In"
|
||||
// enabled:
|
||||
onClicked : {
|
||||
const comp = Qt.createComponent("qrc:/src/qml/LoginDialog.qml")
|
||||
|
||||
// if (comp.status === Component.ready) {
|
||||
const win = comp.createObject(root,{
|
||||
apps: root.apps
|
||||
})
|
||||
win.open()
|
||||
// } else {
|
||||
// console.error("Component failed to load:", comp.errorString())
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
cellWidth: Math.max(320, parent.width / 3)
|
||||
cellHeight: 140
|
||||
model: appModel
|
||||
interactive: true
|
||||
|
||||
delegate: Rectangle {
|
||||
width: grid.cellWidth - 20
|
||||
height: grid.cellHeight - 20
|
||||
radius: 8
|
||||
border.color: "#ddd"
|
||||
color: "transparent"
|
||||
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 10
|
||||
|
||||
Image {
|
||||
width: 48
|
||||
height: 48
|
||||
source: iconSource
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: model.name
|
||||
font.pixelSize: 16
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: model.description
|
||||
color: "#666"
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
text: "Install"
|
||||
color: "#007AFF"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: (model.websiteUrl && model.websiteUrl.length) ? "Website" : "Download IPA"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: wire click handling and install flow later.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: loading
|
||||
visible: loading
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 12
|
||||
text: error
|
||||
color: "#c00"
|
||||
visible: error.length > 0 && !loading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -6,17 +6,19 @@ Item {
|
||||
id: root
|
||||
property var info: ({})
|
||||
property var udid: ""
|
||||
required property int currentSection
|
||||
|
||||
|
||||
DeviceInfo {
|
||||
anchors.fill: parent
|
||||
visible : currentSection == 0
|
||||
info: root.info
|
||||
}
|
||||
|
||||
DeviceGallery {
|
||||
visible : true
|
||||
visible : currentSection == 1
|
||||
anchors.fill: parent
|
||||
udid: root.udid
|
||||
// info: root.info
|
||||
}
|
||||
|
||||
DeviceInfo {
|
||||
anchors.fill: parent
|
||||
visible : false
|
||||
info: root.info
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.kdab.cxx_qt.demo 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property Query query : Query {}
|
||||
property bool loading: true
|
||||
required property var udid
|
||||
@@ -56,6 +57,7 @@ Item {
|
||||
}
|
||||
|
||||
function onAlbumsChanged() {
|
||||
console.log(JSON.stringify(query.albums))
|
||||
albumModel.clear()
|
||||
|
||||
const keys = Object.keys(query.albums)
|
||||
@@ -81,8 +83,6 @@ Item {
|
||||
target : ThumbnailProvider
|
||||
|
||||
function onThumbnailReady(path, data, rowHint) {
|
||||
console.log(path, rowHint, "!!!!!!!!! thumb ready")
|
||||
|
||||
const item = albumModel.get(rowHint)
|
||||
if (item && item.filePath == path) {
|
||||
albumModel.setProperty(rowHint, "thumbVersion", item.thumbVersion + 1)
|
||||
@@ -111,12 +111,12 @@ Item {
|
||||
|
||||
GridView {
|
||||
id: gallery
|
||||
// anchors.fill: parent // Remove this line
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: albumId ? false : query.albums
|
||||
interactive: false
|
||||
|
||||
interactive: true
|
||||
// FIXME: only available in Qt 6.9
|
||||
acceptedButtons : Qt.NoButton
|
||||
cellWidth: 250
|
||||
cellHeight: 250
|
||||
model: albumModel
|
||||
@@ -226,7 +226,7 @@ Item {
|
||||
query : root.query
|
||||
udid : root.udid
|
||||
albumId: root.albumId
|
||||
Layout.fillWidth: true // Add this line
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
Item {
|
||||
property var info: ({})
|
||||
required property var info
|
||||
// property string udid: ""
|
||||
|
||||
function v(key, fallback) {
|
||||
|
||||
+57
-7
@@ -6,6 +6,9 @@ import com.kdab.cxx_qt.demo 1.0
|
||||
Item {
|
||||
id: root
|
||||
property ListModel devices: ListModel {}
|
||||
property string currentDeviceUdid : ""
|
||||
// default info section
|
||||
property int currentSection : 0
|
||||
|
||||
property bool showWelcomePage : true
|
||||
readonly property Core core: Core {}
|
||||
@@ -20,18 +23,65 @@ Item {
|
||||
function onDevice_event(eventType, udid, info) {
|
||||
console.log("Device event:", eventType, udid, JSON.stringify(info))
|
||||
if (eventType === 1) {
|
||||
root.showWelcomePage = false;
|
||||
devices.append({ udid: udid, info: info })
|
||||
root.showWelcomePage = false
|
||||
root.currentDeviceUdid = udid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: devices
|
||||
delegate: Device {
|
||||
udid: model.udid
|
||||
anchors.fill: parent
|
||||
info: model.info
|
||||
// Connections {
|
||||
// target : NetworkDeviceProvider
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
// Layout.fillWidth : true
|
||||
// Layout.fillHeight : true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth : true
|
||||
Layout.fillHeight : true
|
||||
Layout.preferredWidth: 220
|
||||
Repeater {
|
||||
model: devices
|
||||
delegate: Item {
|
||||
SidebarTabButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 220
|
||||
Layout.preferredHeight: 40
|
||||
// udid: model.udid
|
||||
|
||||
// anchors.fill: parent
|
||||
// info: model.info
|
||||
onSectionChanged: {
|
||||
console.log("sectionIndex", sectionIndex)
|
||||
// if (root.currentDeviceUdid !== udid)
|
||||
// root.currentDeviceUdid = udid
|
||||
if (root.currentSection !== sectionIndex)
|
||||
root.currentSection = sectionIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Repeater {
|
||||
model: devices
|
||||
delegate:Item {
|
||||
Layout.fillWidth : true
|
||||
Layout.fillHeight : true
|
||||
visible : model.udid === root.currentDeviceUdid
|
||||
Device {
|
||||
udid: model.udid
|
||||
anchors.fill: parent
|
||||
info: model.info
|
||||
currentSection: root.currentSection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import com.kdab.cxx_qt.demo 1.0
|
||||
|
||||
Dialog {
|
||||
id: dialog
|
||||
readonly property Apps apps: Apps {}
|
||||
title: "Login to App Store - iDescriptor"
|
||||
modal: true
|
||||
standardButtons: Dialog.NoButton
|
||||
anchors.centerIn: parent
|
||||
|
||||
Connections {
|
||||
target : apps
|
||||
function onStateChanged() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
property bool loading: false
|
||||
property string errorText: ""
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 12
|
||||
// padding: 16
|
||||
|
||||
Label { text: "Email:"; font.pixelSize: 14 }
|
||||
TextField {
|
||||
id: emailField
|
||||
placeholderText: "Enter your email"
|
||||
enabled: !dialog.loading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label { text: "Password:"; font.pixelSize: 14 }
|
||||
TextField {
|
||||
id: passwordField
|
||||
placeholderText: "Enter your password"
|
||||
echoMode: TextInput.Password
|
||||
enabled: !dialog.loading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "You shouldn't be using your main account here and don't worry, your credentials won't be stored or shared anywhere. This App is open-source."
|
||||
font.pixelSize: 10
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
running: dialog.loading
|
||||
visible: dialog.loading
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
text: dialog.errorText
|
||||
color: "#c00"
|
||||
visible: dialog.errorText.length > 0
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
text: "Cancel"
|
||||
enabled: !dialog.loading
|
||||
onClicked: dialog.reject()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Sign In"
|
||||
enabled: !dialog.loading
|
||||
onClicked: {
|
||||
if (!emailField.text || !passwordField.text) {
|
||||
dialog.errorText = "Email and password cannot be empty."
|
||||
return
|
||||
}
|
||||
dialog.loading = true
|
||||
dialog.errorText = ""
|
||||
apps.login
|
||||
// FIXME: implement
|
||||
apps.login(emailField.text, passwordField.text);
|
||||
|
||||
dialog.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import com.kdab.cxx_qt.demo 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id:root
|
||||
signal sectionChanged(int sectionIndex)
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
contentItem : Text {
|
||||
text: "Info"
|
||||
color: "Red"
|
||||
}
|
||||
background : Rectangle {
|
||||
color : "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
root.sectionChanged(0)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
contentItem : Text {
|
||||
text: "Gallery"
|
||||
color: "Red"
|
||||
}
|
||||
background : Rectangle {
|
||||
color : "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
root.sectionChanged(1)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
contentItem : Text {
|
||||
text: "Apps"
|
||||
color: "Red"
|
||||
}
|
||||
background : Rectangle {
|
||||
color : "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
root.sectionChanged(2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
contentItem : Text {
|
||||
text: "Files"
|
||||
color: "Red"
|
||||
}
|
||||
background : Rectangle {
|
||||
color : "transparent"
|
||||
}
|
||||
onClicked: {
|
||||
root.sectionChanged(3)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,4 +24,9 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
AppsTab {
|
||||
anchors.fill: parent
|
||||
visible : currentIndex == 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Generated
+1241
-170
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,8 @@ serde_json = "1.0.149"
|
||||
rusqlite = { version = "0.39.0", features = ["bundled"] }
|
||||
filetime = "0.2.27"
|
||||
priority-queue = "2.7.0"
|
||||
anyhow = "1.0.102"
|
||||
ipatool = { path = "/home/uncore/Desktop/proj/ipatool-rs", default-features = false }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -16,12 +16,15 @@ fn main() {
|
||||
"src/query_sqlite.rs",
|
||||
"src/image_loader.rs",
|
||||
"src/bridge.rs",
|
||||
"src/apps.rs",
|
||||
])
|
||||
.include_dir("include");
|
||||
|
||||
let builder = unsafe {
|
||||
builder.cc_builder(|cc| {
|
||||
cc.file("src/thumbnail.cc");
|
||||
cc.file("src/heic_to_image.cc");
|
||||
cc.file("src/qinput_get_text.cc");
|
||||
})
|
||||
};
|
||||
builder.build();
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
#include "rust/cxx.h"
|
||||
#include <QImage>
|
||||
|
||||
QImage heic_to_image(rust::Slice<const uint8_t> buf);
|
||||
|
||||
class AfcReader;
|
||||
|
||||
QImage generate_thumbnail_with_reader(const AfcReader &reader,
|
||||
int32_t file_size, int32_t requested_w,
|
||||
int32_t requested_h);
|
||||
int32_t requested_h);
|
||||
|
||||
QString qinput_get_text(bool ok);
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
#include "rust/cxx.h"
|
||||
#include <QImage>
|
||||
|
||||
QImage load_heic(rust::Vec<uint8_t> data);
|
||||
@@ -0,0 +1,107 @@
|
||||
use crate::RUNTIME;
|
||||
|
||||
use crate::qmap_insert;
|
||||
use cxx_qt::Threading;
|
||||
use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QString, QVariant};
|
||||
use ipatool::Account;
|
||||
use ipatool::IpaTool;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[cxx_qt::bridge]
|
||||
mod qobject {
|
||||
unsafe extern "C++" {
|
||||
include!("cxx-qt-lib/qlist.h");
|
||||
include!("cxx-qt-lib/qmap.h");
|
||||
include!("cxx-qt-lib/qstring.h");
|
||||
|
||||
type QString = cxx_qt_lib::QString;
|
||||
type QMap_QString_QVariant = cxx_qt_lib::QMap<cxx_qt_lib::QMapPair_QString_QVariant>;
|
||||
}
|
||||
|
||||
extern "RustQt" {
|
||||
#[qobject]
|
||||
#[qml_element]
|
||||
#[qproperty(QMap_QString_QVariant, state)]
|
||||
type Apps = super::RApps;
|
||||
|
||||
#[qinvokable]
|
||||
fn init(self: Pin<&mut Apps>);
|
||||
|
||||
#[qinvokable]
|
||||
fn sign_in(self: Pin<&mut Apps>, email: &QString, password: &QString);
|
||||
}
|
||||
impl cxx_qt::Threading for Apps {}
|
||||
}
|
||||
pub struct RApps {
|
||||
state: QMap<QMapPair_QString_QVariant>,
|
||||
}
|
||||
|
||||
impl Default for RApps {
|
||||
fn default() -> Self {
|
||||
let mut state = QMap::<QMapPair_QString_QVariant>::default();
|
||||
qmap_insert!(state, "init", false);
|
||||
qmap_insert!(state, "error", QString::default());
|
||||
qmap_insert!(state, "email", QString::default());
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl qobject::Apps {
|
||||
fn init(self: Pin<&mut Self>) {
|
||||
let q_thread = self.qt_thread();
|
||||
RUNTIME.spawn(async move {
|
||||
let res: anyhow::Result<Option<ipatool::Account>> = async {
|
||||
let tool = IpaTool::new_default().await?;
|
||||
Ok(tool.account_info().await?)
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(maybe_acc) => {
|
||||
let acc = maybe_acc.unwrap_or_default();
|
||||
println!("email :{}", acc.email);
|
||||
|
||||
let mut state = QMap::<QMapPair_QString_QVariant>::default();
|
||||
qmap_insert!(state, "init", true);
|
||||
qmap_insert!(state, "error", QString::default());
|
||||
qmap_insert!(state, "email", QString::from(acc.email));
|
||||
|
||||
q_thread.queue(|t| t.set_state(state)).ok();
|
||||
}
|
||||
Err(err) => {
|
||||
let mut state = QMap::<QMapPair_QString_QVariant>::default();
|
||||
qmap_insert!(state, "init", true);
|
||||
qmap_insert!(state, "error", QString::from(format!("{}", err)));
|
||||
qmap_insert!(state, "email", QString::default());
|
||||
|
||||
q_thread.queue(|t| t.set_state(state)).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn sign_in(self: Pin<&mut Self>, email: &QString, password: &QString) {
|
||||
// FIXME: implement
|
||||
// RUNTIME.spawn(async move {
|
||||
// let res: anyhow::Result<()> = async {
|
||||
// let tool = IpaTool::new_default().await?;
|
||||
|
||||
// let auth_code_cb: Box<dyn Fn() -> ipatool::Result<String> + Send + Sync> =
|
||||
// Box::new(|| {
|
||||
// });
|
||||
// tool.login(
|
||||
// &email.to_string(),
|
||||
// &password.to_string(),
|
||||
// Some(auth_code_cb),
|
||||
// None,
|
||||
// )
|
||||
// .await;
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
// .await;
|
||||
|
||||
// Ok(())
|
||||
// });
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ impl AfcReader {
|
||||
|
||||
#[cxx_qt::bridge]
|
||||
pub mod bridge {
|
||||
|
||||
extern "Rust" {
|
||||
type AfcReader;
|
||||
|
||||
@@ -75,14 +76,16 @@ pub mod bridge {
|
||||
}
|
||||
|
||||
unsafe extern "C++" {
|
||||
include!("thumbnail.h");
|
||||
include!("bridge.h");
|
||||
|
||||
include!("cxx-qt-lib/qimage.h");
|
||||
include!("cxx-qt-lib/qbytearray.h");
|
||||
// include!("cxx-qt-lib/qmap.h");
|
||||
// include!("cxx-qt-lib/qstring.h");
|
||||
include!("cxx-qt-lib/qstring.h");
|
||||
|
||||
type QImage = cxx_qt_lib::QImage;
|
||||
type QByteArray = cxx_qt_lib::QByteArray;
|
||||
type QString = cxx_qt_lib::QString;
|
||||
|
||||
fn generate_thumbnail_with_reader(
|
||||
reader: &AfcReader,
|
||||
@@ -90,5 +93,9 @@ pub mod bridge {
|
||||
requested_w: i32,
|
||||
requested_h: i32,
|
||||
) -> QImage;
|
||||
|
||||
fn heic_to_image(data: &[u8]) -> QImage;
|
||||
|
||||
fn qinput_get_text(ok: bool) -> QString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <QImage>
|
||||
#include <libheif/heif.h>
|
||||
|
||||
QImage load_heic(rust::Vec<uint8_t> &data)
|
||||
QImage heic_to_image(rust::Slice<const uint8_t> buf)
|
||||
{
|
||||
heif_context *ctx = heif_context_alloc();
|
||||
if (!ctx) {
|
||||
@@ -31,7 +31,7 @@ QImage load_heic(rust::Vec<uint8_t> &data)
|
||||
}
|
||||
|
||||
heif_error err =
|
||||
heif_context_read_from_memory(ctx, data.data(), data.size(), nullptr);
|
||||
heif_context_read_from_memory(ctx, buf.data(), buf.size(), nullptr);
|
||||
if (err.code != heif_error_Ok) {
|
||||
// qWarning() << "Failed to read HEIC from memory:" << err.message;
|
||||
heif_context_free(ctx);
|
||||
@@ -139,70 +139,81 @@ fn ensure_worker_started() {
|
||||
RUNTIME.spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
let afc_arc = {
|
||||
let maybe_device = APP_DEVICE_STATE
|
||||
.lock()
|
||||
.await
|
||||
.get(key.udid.as_str())
|
||||
.cloned();
|
||||
let res: anyhow::Result<()> = async {
|
||||
let afc_arc = {
|
||||
let maybe_device = APP_DEVICE_STATE
|
||||
.lock()
|
||||
.await
|
||||
.get(key.udid.as_str())
|
||||
.cloned();
|
||||
|
||||
let device = match maybe_device {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
// eprintln!(
|
||||
// "image_loader::read_file_via_afc: device {udid} not found"
|
||||
// );
|
||||
return;
|
||||
}
|
||||
let device = match maybe_device {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
// eprintln!(
|
||||
// "image_loader::read_file_via_afc: device {udid} not found"
|
||||
// );
|
||||
anyhow::bail!("No device");
|
||||
}
|
||||
};
|
||||
|
||||
device.afc.clone()
|
||||
};
|
||||
|
||||
device.afc.clone()
|
||||
};
|
||||
|
||||
let mut afc = afc_arc.lock().await;
|
||||
|
||||
let info = afc.get_file_info(&key.path).await;
|
||||
|
||||
let size = match info {
|
||||
Ok(i) => i.size,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
drop(afc);
|
||||
|
||||
let mut img = QImage::default();
|
||||
if is_video_file(&key.path) {
|
||||
// FIXME: can we do something better here ?
|
||||
let reader =
|
||||
crate::bridge::AfcReader::new(key.udid.clone(), key.path.clone());
|
||||
|
||||
let reader_for_block = reader;
|
||||
let size_for_block = size as i32;
|
||||
img = tokio::task::spawn_blocking(move || {
|
||||
crate::bridge::bridge::generate_thumbnail_with_reader(
|
||||
&reader_for_block,
|
||||
size_for_block,
|
||||
// FIXME: sizes aren't respected
|
||||
320,
|
||||
240,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
let mut afc = afc_arc.lock().await;
|
||||
img = file_to_image(&mut afc, key.path).await;
|
||||
}
|
||||
|
||||
let row = payload.row;
|
||||
let path_for_qt = payload.path_for_qt;
|
||||
let qt_thread = payload.qt_thread;
|
||||
let info = afc.get_file_info(&key.path).await;
|
||||
|
||||
if let Err(e) = qt_thread.queue(move |mut backend_qobj| {
|
||||
backend_qobj.thumbnail_ready(path_for_qt, img, row);
|
||||
}) {
|
||||
eprintln!("image_loader: failed to queue thumbnail_ready: {e}");
|
||||
let size = match info {
|
||||
Ok(i) => i.size,
|
||||
Err(_) => anyhow::bail!("File has no size ?"),
|
||||
};
|
||||
|
||||
drop(afc);
|
||||
|
||||
let mut img = QImage::default();
|
||||
if is_video_file(&key.path) {
|
||||
// FIXME: can we do something better here ?
|
||||
let reader =
|
||||
crate::bridge::AfcReader::new(key.udid.clone(), key.path.clone());
|
||||
|
||||
let reader_for_block = reader;
|
||||
let size_for_block = size as i32;
|
||||
img = tokio::task::spawn_blocking(move || {
|
||||
crate::bridge::bridge::generate_thumbnail_with_reader(
|
||||
&reader_for_block,
|
||||
size_for_block,
|
||||
// FIXME: sizes aren't respected
|
||||
320,
|
||||
240,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
let mut afc = afc_arc.lock().await;
|
||||
if key.path.to_ascii_lowercase().ends_with(".heic") {
|
||||
let mut fd = afc.open(key.path, AfcFopenMode::RdOnly).await?;
|
||||
let buf = fd.read_entire().await?;
|
||||
img = crate::bridge::bridge::heic_to_image(&buf);
|
||||
} else {
|
||||
img = file_to_image(&mut afc, key.path).await;
|
||||
}
|
||||
}
|
||||
|
||||
let row = payload.row;
|
||||
let path_for_qt = payload.path_for_qt;
|
||||
let qt_thread = payload.qt_thread;
|
||||
|
||||
if let Err(e) = qt_thread.queue(move |mut backend_qobj| {
|
||||
backend_qobj.thumbnail_ready(path_for_qt, img, row);
|
||||
}) {
|
||||
eprintln!("image_loader: failed to queue thumbnail_ready: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,6 +40,8 @@ mod utils;
|
||||
mod query_sqlite;
|
||||
mod image_loader;
|
||||
mod bridge;
|
||||
mod apps;
|
||||
|
||||
|
||||
const POSSIBLE_ROOT: &str = "../../../../";
|
||||
const APP_LABEL: &str = "iDescriptor";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <QInputDialog>
|
||||
#include <QString>
|
||||
|
||||
QString qinput_get_text(bool ok)
|
||||
{
|
||||
return QInputDialog::getText(
|
||||
nullptr, "SSH Root Password",
|
||||
"Enter the root password: \n(leave empty if you "
|
||||
"want to use the default)",
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "thumbnail.h"
|
||||
#include "bridge.h"
|
||||
#include "rust/cxx.h"
|
||||
#include <QImage>
|
||||
extern "C" {
|
||||
@@ -244,6 +244,7 @@ QImage generate_thumbnail_with_reader(const AfcReader &reader,
|
||||
}
|
||||
|
||||
result = imgCopy;
|
||||
// FIXME: scale
|
||||
// Scale to requested size
|
||||
/*
|
||||
TODO: scaling might become optional
|
||||
|
||||
@@ -169,3 +169,20 @@ pub fn create_album_info(
|
||||
json!({"album_id" : album_id, "item_count" : item_count,"file_path" : format!("{}/{}",asset_dir,asset_file_name)})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// pub fn emit<T>(thread: cxx_qt::CxxQtThread<T>) {}
|
||||
use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QVariant};
|
||||
|
||||
pub fn qmap_insert<T>(map: &mut QMap<QMapPair_QString_QVariant>, key: &str, value: &T)
|
||||
where
|
||||
QVariant: for<'a> From<&'a T>,
|
||||
{
|
||||
map.insert(QString::from(key), QVariant::from(value));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! qmap_insert {
|
||||
($map:expr, $key:expr , $value:expr) => {
|
||||
$crate::utils::qmap_insert(&mut $map, $key, &$value)
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user