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:
uncor3
2026-05-05 06:22:27 +00:00
parent b05416dd68
commit 9af7f8a9f4
25 changed files with 2024 additions and 383 deletions
+7 -5
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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);
}
+3
View File
@@ -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();
+297
View File
@@ -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
View File
@@ -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
}
}
+6 -6
View File
@@ -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
}
}
+1 -1
View File
@@ -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
View File
@@ -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
}
}
}
}
+94
View File
@@ -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
}
}
}
}
}
+71
View File
@@ -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)
}
}
}
+5
View File
@@ -24,4 +24,9 @@ Item {
}
}
AppsTab {
anchors.fill: parent
visible : currentIndex == 1
}
}
+1241 -170
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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]
+3
View File
@@ -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);
-5
View File
@@ -1,5 +0,0 @@
#pragma once
#include "rust/cxx.h"
#include <QImage>
QImage load_heic(rust::Vec<uint8_t> data);
+107
View File
@@ -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(())
// });
}
}
+9 -2
View File
@@ -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);
+68 -57
View File
@@ -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;
});
}
});
+2
View File
@@ -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";
+11
View File
@@ -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);
}
+2 -1
View File
@@ -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
+17
View File
@@ -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)
};
}