diff --git a/CMakeLists.txt b/CMakeLists.txt
index 372189c..9852a09 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -192,6 +192,9 @@ file(GLOB PROJECT_SOURCES
# src/base/*.h
src/main.cpp
src/constants.h
+# src/thumbnailmodel.h
+# src/thumbnailmodel.cpp
+src/thumbnailprovider.h
resources.qrc
resources.ui.qrc
)
diff --git a/resources.ui.qrc b/resources.ui.qrc
index 618fc86..6a015df 100644
--- a/resources.ui.qrc
+++ b/resources.ui.qrc
@@ -1,11 +1,17 @@
src/qml/Main.qml
- src/qml/Index.qml
+
src/qml/Tabs.qml
src/qml/HowToConnect.qml
src/qml/Welcome.qml
src/qml/TabButton.qml
+ src/qml/DeviceTab.qml
src/qml/Device.qml
+ src/qml/DeviceImage.qml
+ src/qml/DeviceInfo.qml
+ src/qml/DeviceGallery.qml
+ src/qml/AlbumContents.qml
+ src/qml/PreviewWindow.qml
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index feeeb2e..c5cd090 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -31,7 +31,8 @@
#ifdef WIN32
#include "platform/windows/win_common.h"
#endif
-
+// #include "thumbnailmodel.h"
+#include "thumbnailprovider.h"
#include
#include
#include
@@ -132,8 +133,11 @@ int main(int argc, char *argv[])
engine.addImportPath("C:/Qt/6.8.3/mingw_64/qml");
#endif
Constants constants;
-
+ // qmlRegisterType("iDescriptor", 1, 0, "ThumbnailModel");
engine.rootContext()->setContextProperty("CONSTANTS", &constants);
+ engine.addImageProvider("thumb", ThumbnailProvider::sharedInstance());
+ engine.rootContext()->setContextProperty(
+ "ThumbnailProvider", ThumbnailProvider::sharedInstance());
engine.load(url);
return a.exec();
diff --git a/src/qml/AlbumContents.qml b/src/qml/AlbumContents.qml
new file mode 100644
index 0000000..d4f7527
--- /dev/null
+++ b/src/qml/AlbumContents.qml
@@ -0,0 +1,207 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+// import iDescriptor 1.0
+import com.kdab.cxx_qt.demo 1.0
+
+
+Item {
+ id: root
+ required property Query query
+ property bool loading: true
+ required property var udid
+ required property var albumId
+
+
+ onAlbumIdChanged: {
+ console.log("loading album contents")
+ query.query_album(albumId)
+ }
+
+ ListModel {
+ id: albumContentsModel
+ }
+
+ function selectItemsInRect(rect, append) {
+ for (let i = 0; i < gallery.count; i++) {
+ const item = gallery.itemAtIndex(i)
+ if (!item) continue
+
+ const itemRect = {
+ x: item.x,
+ y: item.y - gallery.contentY,
+ w: item.width,
+ h: item.height
+ }
+
+ const intersects =
+ itemRect.x < rect.x + rect.width &&
+ itemRect.x + itemRect.w > rect.x &&
+ itemRect.y < rect.y + rect.height &&
+ itemRect.y + itemRect.h > rect.y
+
+ if (intersects) {
+ albumContentsModel.setProperty(i, "selected", true)
+ } else if (!append) {
+ albumContentsModel.setProperty(i, "selected", false)
+ }
+ }
+ }
+
+ Connections {
+ target: query
+
+ function onAlbum_queried(id, items) {
+ if (id !== albumId || !items) return
+ albumContentsModel.clear()
+
+ for (const item of items) {
+ console.log("item",item)
+ albumContentsModel.append({
+ fileName : "wtf",
+ filePath : item,
+ thumbVersion : 0,
+ selected : false
+ })
+ }
+
+ }
+ }
+
+ Connections {
+ target : ThumbnailProvider
+
+ function onThumbnailReady(path, data, rowHint) {
+ console.log(path, rowHint, "!!!!!!!!! album contents thumb ready")
+
+ const item = albumContentsModel.get(rowHint)
+ if (item && item.filePath == path) {
+ albumContentsModel.setProperty(rowHint, "thumbVersion", item.thumbVersion + 1)
+ }
+ }
+ }
+
+ BusyIndicator {
+ running: !query.albums
+ anchors.centerIn: parent
+ }
+
+
+ GridView {
+ id: gallery
+ anchors.fill: parent
+ interactive: true
+
+ cellWidth: 250
+ cellHeight: 250
+ model: albumContentsModel
+
+ delegate: ItemDelegate {
+ width: 250
+ height: 250
+ highlighted: selected
+
+ MouseArea {
+ anchors.fill: parent
+ onDoubleClicked: {
+ const comp = Qt.createComponent("PreviewWindow.qml")
+
+ if (comp.status === Component.Ready) {
+ const win = comp.createObject(null,{
+ filePath,
+ udid : root.udid
+ })
+ if (win !== null) {
+ win.show()
+ } else {
+ console.error("createObject failed:", comp.errorString())
+ }
+
+ } else if (comp.status === Component.Error) {
+ console.error("Component failed to load:", comp.errorString())
+ }
+
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: selected ? "#4FC3F7" : "transparent"
+ opacity : 0.3
+ z : 1
+ }
+
+ Image {
+ cache: false
+ anchors.fill: parent
+ //FIXME:use encodeuricomp
+ source: "image://thumb/" + filePath + "?udid=" + root.udid + "&index=" + index + "&v=" + thumbVersion
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Text {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ text: fileName
+ font.pixelSize: 10
+ color: "white"
+ elide: Text.ElideMiddle
+ }
+ }
+
+ //rubber band
+ Item {
+ anchors.fill: parent
+
+ Rectangle {
+ id: selectionRect
+ color: "transparent"
+ border.color: "blue"
+ border.width: 1
+ visible: false
+
+ opacity: 0.3
+ Rectangle { anchors.fill: parent; color: "blue"; opacity: 0.2 }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ property point startPos
+
+ propagateComposedEvents: true
+
+ onPressed: (mouse) => {
+ // mouse.accepted = false
+ startPos = Qt.point(mouse.x, mouse.y)
+ selectionRect.x = startPos.x
+ selectionRect.y = startPos.y
+ selectionRect.width = 0
+ selectionRect.height = 0
+ selectionRect.visible = true
+ }
+
+ onPositionChanged: {
+ selectionRect.x = Math.min(mouse.x, startPos.x)
+ selectionRect.y = Math.min(mouse.y, startPos.y)
+ selectionRect.width = Math.abs(mouse.x - startPos.x)
+ selectionRect.height = Math.abs(mouse.y - startPos.y)
+ }
+
+ onReleased: {
+ selectionRect.visible = false
+
+ const append = mouse.modifiers & Qt.ControlModifier
+
+ selectItemsInRect({
+ x: selectionRect.x,
+ y: selectionRect.y,
+ width: selectionRect.width,
+ height: selectionRect.height
+ }, append)
+ }
+ }
+ }
+ }
+}
diff --git a/src/qml/Device.qml b/src/qml/Device.qml
index bffb5d2..8682fa1 100644
--- a/src/qml/Device.qml
+++ b/src/qml/Device.qml
@@ -1,56 +1,22 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-import "."
-import com.kdab.cxx_qt.demo 1.0
-
-Item {
- id: root
- property ListModel devices: ListModel {}
-
- property bool showWelcomePage : true
- readonly property Core core: Core {}
-
- Component.onCompleted: {
- root.core.init()
- }
-
- Connections {
- target: root.core
-
- function onDevice_event(eventType, udid, info) {
- console.log("Device event:", eventType, udid, info)
- root.showWelcomePage = false;
- if (eventType === 1) {
- // Use append to add items to the ListModel
- devices.append({ udid: udid, info: info })
- }
- }
- }
-
- Repeater {
- model: devices
- delegate: Label {
- text: model.info
- font.pixelSize: 16
- padding: 10
- Layout.fillWidth: true
- //MouseArea {
- // anchors.fill: parent
- // onClicked: {
- // root.currentIndex = index + 1
- // root.showWelcomePage = false
- // }
- //}
- }
- }
-
-
- Welcome {
- id: welcomePage
- visible : showWelcomePage
- Layout.fillWidth: true
- Layout.fillHeight: true
- }
-
-}
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+Item {
+ id: root
+ property var info: ({})
+ property var udid: ""
+ DeviceGallery {
+ visible : true
+ anchors.fill: parent
+ udid: root.udid
+ // info: root.info
+ }
+
+ DeviceInfo {
+ anchors.fill: parent
+ visible : false
+ info: root.info
+ }
+
+}
\ No newline at end of file
diff --git a/src/qml/DeviceGallery.qml b/src/qml/DeviceGallery.qml
new file mode 100644
index 0000000..b910d6b
--- /dev/null
+++ b/src/qml/DeviceGallery.qml
@@ -0,0 +1,233 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+// import iDescriptor 1.0
+import com.kdab.cxx_qt.demo 1.0
+
+
+Item {
+ id: root
+ readonly property Query query : Query {}
+ property bool loading: true
+ required property var udid
+ property int albumId
+ property bool isMainPage : root.albumId == -1 ? false : root.albumId == -2 ? false : !root.albumId
+
+
+ Component.onCompleted: {
+ query.init(root.udid);
+ }
+
+ ListModel {
+ id: albumModel
+ }
+
+ function selectItemsInRect(rect, append) {
+ for (let i = 0; i < gallery.count; i++) {
+ const item = gallery.itemAtIndex(i)
+ if (!item) continue
+
+ const itemRect = {
+ x: item.x,
+ y: item.y - gallery.contentY,
+ w: item.width,
+ h: item.height
+ }
+
+ const intersects =
+ itemRect.x < rect.x + rect.width &&
+ itemRect.x + itemRect.w > rect.x &&
+ itemRect.y < rect.y + rect.height &&
+ itemRect.y + itemRect.h > rect.y
+
+ if (intersects) {
+ albumModel.setProperty(i, "selected", true)
+ } else if (!append) {
+ albumModel.setProperty(i, "selected", false)
+ }
+ }
+ }
+
+ Connections {
+ target: query
+
+ function onIs_initChanged() {
+ query.read_albums()
+ }
+
+ function onAlbumsChanged() {
+ albumModel.clear()
+
+ const keys = Object.keys(query.albums)
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i]
+ const jsonStr = query.albums[key]
+ const obj = JSON.parse(jsonStr)
+
+ albumModel.append({
+ albumId : obj.album_id ? obj.album_id : -1,
+ fileName: key,
+ filePath: obj.file_path,
+ dateTime: new Date(),
+ fileType: 0,
+ selected: false,
+ thumbVersion: 0
+ })
+ }
+ }
+ }
+
+ Connections {
+ 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)
+ }
+ }
+ }
+
+ BusyIndicator {
+ running: !query.albums
+ anchors.centerIn: parent
+ }
+
+
+ ColumnLayout {
+
+ anchors.fill : parent
+
+ Button {
+ text: isMainPage ? "BACK" : "BACK TO MAIN"
+ enabled : root.albumId != 0
+ onClicked : {
+ root.albumId = 0
+ }
+ }
+
+
+ GridView {
+ id: gallery
+ // anchors.fill: parent // Remove this line
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: albumId ? false : query.albums
+ interactive: false
+
+ cellWidth: 250
+ cellHeight: 250
+ model: albumModel
+
+ delegate: ItemDelegate {
+ // required property int index
+ // required property string filePath
+ // required property int albumId
+
+ width: 250
+ height: 250
+ highlighted: selected
+
+ MouseArea {
+ anchors.fill: parent
+ onDoubleClicked: {
+ console.log("delegate double-click", index, albumId)
+ root.albumId = albumId
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: selected ? "#4FC3F7" : "transparent"
+ opacity : 0.3
+ z : 1
+ }
+
+ Image {
+ cache: false
+ anchors.fill: parent
+ //FIXME:use encodeuricomp
+ source: "image://thumb/" + filePath + "?udid=" + root.udid + "&index=" + index + "&v=" + thumbVersion
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Text {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ text: fileName + albumId
+ font.pixelSize: 10
+ color: "white"
+ elide: Text.ElideMiddle
+ }
+ }
+
+ //rubber band
+ Item {
+ anchors.fill: parent
+
+ Rectangle {
+ id: selectionRect
+ color: "transparent"
+ border.color: "blue"
+ border.width: 1
+ visible: false
+
+ // Optional: semi-transparent fill like standard rubber bands
+ opacity: 0.3
+ Rectangle { anchors.fill: parent; color: "blue"; opacity: 0.2 }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ property point startPos
+
+ propagateComposedEvents: true
+
+ onPressed: (mouse) => {
+ // mouse.accepted = false
+ startPos = Qt.point(mouse.x, mouse.y)
+ selectionRect.x = startPos.x
+ selectionRect.y = startPos.y
+ selectionRect.width = 0
+ selectionRect.height = 0
+ selectionRect.visible = true
+ }
+
+ onPositionChanged: {
+ selectionRect.x = Math.min(mouse.x, startPos.x)
+ selectionRect.y = Math.min(mouse.y, startPos.y)
+ selectionRect.width = Math.abs(mouse.x - startPos.x)
+ selectionRect.height = Math.abs(mouse.y - startPos.y)
+ }
+
+ onReleased: {
+ selectionRect.visible = false
+
+ const append = mouse.modifiers & Qt.ControlModifier
+
+ selectItemsInRect({
+ x: selectionRect.x,
+ y: selectionRect.y,
+ width: selectionRect.width,
+ height: selectionRect.height
+ }, append)
+ }
+ }
+ }
+ }
+
+
+ AlbumContents {
+ visible : !isMainPage
+ query : root.query
+ udid : root.udid
+ albumId: root.albumId
+ Layout.fillWidth: true // Add this line
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/src/qml/DeviceImage.qml b/src/qml/DeviceImage.qml
new file mode 100644
index 0000000..4924bf8
--- /dev/null
+++ b/src/qml/DeviceImage.qml
@@ -0,0 +1,135 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import Qt5Compat.GraphicalEffects
+
+Item {
+ id: root
+ // width: 500
+ // height: 500
+
+ implicitWidth: 500
+ implicitHeight: 500
+
+ property string mockupName: "16"
+
+ readonly property int iosVersion: 26
+
+ readonly property int cornerRadiusPx: 35
+
+ readonly property bool isUnknown: mockupName === "unknown"
+ readonly property bool useRoundedCorners: (mockupName === "x" || mockupName === "15" || mockupName === "16")
+
+ function mockupSourceForName(name) {
+ if (name === "iPad" || name === "unknown")
+ return "qrc:/resources/ipad-mockups/ipad.png"
+ return "qrc:/resources/iphone-mockups/iphone-" + name + ".png"
+ }
+
+ function wallpaperSourceForIos(version) {
+ // FIXME: hardcoded
+ return "qrc:/resources/ios-wallpapers/iphone-ios26.png"
+ }
+
+ function screenRectForMockup(name, srcW, srcH) {
+ if (name === "3") return Qt.rect(145, 72, 209, 310)
+ if (name === "4") return Qt.rect(414, 181, 380, 548)
+ if (name === "5") return Qt.rect(27, 106, 304, 537)
+ if (name === "6") return Qt.rect(68, 348, 1279, 2270)
+ if (name === "x") return Qt.rect(245, 200, 2389, 5303)
+ if (name === "15") return Qt.rect(15, 10, 337, 730)
+ if (name === "16") return Qt.rect(17, 16, 333, 730)
+ if (name === "iPad") return Qt.rect(30, 30, 480, 690)
+ if (name === "unknown") return Qt.rect(33, 36, 471, 680)
+
+ return Qt.rect(srcW * 0.12, srcH * 0.08, srcW * 0.76, srcH * 0.84)
+ }
+
+ Image {
+ id: mockup
+ z: 10
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ source: root.mockupSourceForName(root.mockupName)
+ }
+
+ readonly property real paintedLeft: mockup.x + (mockup.width - mockup.paintedWidth) / 2
+ readonly property real paintedTop: mockup.y + (mockup.height - mockup.paintedHeight) / 2
+
+ readonly property real srcW: (mockup.sourceSize.width > 0 ? mockup.sourceSize.width : mockup.implicitWidth)
+ readonly property real srcH: (mockup.sourceSize.height > 0 ? mockup.sourceSize.height : mockup.implicitHeight)
+
+ readonly property real scaleX: (srcW > 0 ? mockup.paintedWidth / srcW : 1.0)
+ readonly property real scaleY: (srcH > 0 ? mockup.paintedHeight / srcH : 1.0)
+
+ readonly property rect screenRectSrc: screenRectForMockup(root.mockupName, srcW, srcH)
+ readonly property rect screenRectPainted: Qt.rect(
+ paintedLeft + screenRectSrc.x * scaleX,
+ paintedTop + screenRectSrc.y * scaleY,
+ screenRectSrc.width * scaleX,
+ screenRectSrc.height * scaleY
+ )
+
+ Item {
+ id: screenLayer
+ z: 5
+ x: root.screenRectPainted.x
+ y: root.screenRectPainted.y
+ width: root.screenRectPainted.width
+ height: root.screenRectPainted.height
+
+ Image {
+ id: wallpaper
+ anchors.fill: parent
+ source: root.wallpaperSourceForIos(root.iosVersion)
+ fillMode: Image.Stretch
+ smooth: true
+ visible: !root.useRoundedCorners
+ }
+
+ Rectangle {
+ id: roundedMask
+ anchors.fill: parent
+ radius: root.cornerRadiusPx * Math.min(root.scaleX, root.scaleY)
+ color: "white"
+ visible: false
+ }
+
+ OpacityMask {
+ id: roundedMaskedWallpaper
+ anchors.fill: parent
+ source: wallpaper
+ maskSource: roundedMask
+ visible: root.useRoundedCorners
+ }
+
+ // question mark for unknown devices
+ Text {
+ anchors.centerIn: parent
+ visible: root.isUnknown
+ text: "?"
+ color: "white"
+ font.bold: true
+ font.pixelSize: Math.max(12, parent.width / 3)
+ style: Text.Outline
+ styleColor: "#96000000"
+ }
+
+ // time
+ Text {
+ id: timeText
+ anchors.centerIn: parent
+ text: Qt.formatTime(new Date(), "hh:mm")
+ color: "white"
+ font.weight: Font.Light
+ font.pixelSize: Math.max(10, parent.width / 5)
+ }
+
+ Timer {
+ interval: 60000
+ running: true
+ repeat: true
+ onTriggered: timeText.text = Qt.formatTime(new Date(), "hh:mm")
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/qml/DeviceInfo.qml b/src/qml/DeviceInfo.qml
new file mode 100644
index 0000000..793e228
--- /dev/null
+++ b/src/qml/DeviceInfo.qml
@@ -0,0 +1,112 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+Item {
+ property var info: ({})
+ // property string udid: ""
+
+ function v(key, fallback) {
+ if (!info) return fallback
+ const val = info[key]
+ if (val === undefined || val === null || val === "") return fallback
+ return val
+ }
+
+ RowLayout {
+ spacing: 12
+
+ DeviceImage { }
+
+ ColumnLayout {
+ spacing: 10
+ Layout.fillWidth: true
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 10
+
+ Label {
+ text: v("DeviceClass", "TODO")
+ font.bold: true
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+
+ Label {
+ // FIXME: hardcoded
+ text: "5W/USB"
+ color: "#666"
+ }
+ }
+
+ GridLayout {
+ id: grid
+ columns: 4
+ columnSpacing: 14
+ rowSpacing: 8
+ Layout.fillWidth: true
+
+ // Row 0
+ Label { text: "iOS Version:"; font.bold: true }
+ Label { text: v("ProductVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Device Name:"; font.bold: true }
+ Label { text: v("DeviceName", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 1
+ Label { text: "Activation State:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Device Class:"; font.bold: true }
+ Label { text: v("DeviceClass", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 2
+ Label { text: "Jailbroken:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Model Number:"; font.bold: true }
+ Label { text: v("ModelNumber", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 3
+ Label { text: "CPU Architecture:"; font.bold: true }
+ Label { text: v("CPUArchitecture", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Build Version:"; font.bold: true }
+ Label { text: v("BuildVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 4
+ Label { text: "Hardware Model:"; font.bold: true }
+ Label { text: v("HardwareModel", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Region:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 5
+ Label { text: "Hardware Platform:"; font.bold: true }
+ Label { text: v("HardwarePlatform", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Firmware Version:"; font.bold: true }
+ Label { text: v("FirmwareVersion", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 6
+ Label { text: "Bluetooth Address:"; font.bold: true }
+ Label { text: v("BluetoothAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Wi‑Fi Address:"; font.bold: true }
+ Label { text: v("WiFiAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 7
+ Label { text: "Ethernet Address:"; font.bold: true }
+ Label { text: v("EthernetAddress", "TODO"); elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Battery Health:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 8
+ Label { text: "Production Device:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "Serial Number:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+
+ // Row 9
+ Label { text: "IMEI:"; font.bold: true }
+ Label { text: "TODO"; elide: Text.ElideRight; Layout.fillWidth: true }
+ Label { text: "UDID:"; font.bold: true }
+ Label { text: v("UniqueDeviceID", "TODO"); elide: Text.ElideMiddle; Layout.fillWidth: true }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/qml/DeviceTab.qml b/src/qml/DeviceTab.qml
new file mode 100644
index 0000000..cd9eb2d
--- /dev/null
+++ b/src/qml/DeviceTab.qml
@@ -0,0 +1,46 @@
+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
+ property ListModel devices: ListModel {}
+
+ property bool showWelcomePage : true
+ readonly property Core core: Core {}
+
+ Component.onCompleted: {
+ root.core.init()
+ }
+
+ Connections {
+ target: root.core
+
+ 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 })
+ }
+ }
+ }
+
+ Repeater {
+ model: devices
+ delegate: Device {
+ udid: model.udid
+ anchors.fill: parent
+ info: model.info
+ }
+ }
+
+
+ Welcome {
+ id: welcomePage
+ visible : showWelcomePage
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+}
diff --git a/src/qml/Main.qml b/src/qml/Main.qml
index 183a69a..3ac9ed9 100644
--- a/src/qml/Main.qml
+++ b/src/qml/Main.qml
@@ -1,49 +1,54 @@
import QtQuick 2.15
-import QtQuick.Window 2.15
-// import QtQuick.Controls 2.15
+import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
-import FluentUI 1.0
+import "."
-FluLauncher {
- id: app
- // Connections{
- // target: FluTheme
- // function onDarkModeChanged(){
- // SettingsHelper.saveDarkMode(FluTheme.darkMode)
- // }
- // }
- // Connections{
- // target: FluApp
- // function onUseSystemAppBarChanged(){
- // SettingsHelper.saveUseSystemAppBar(FluApp.useSystemAppBar)
- // }
- // }
- // Connections{
- // target: TranslateHelper
- // function onCurrentChanged(){
- // SettingsHelper.saveLanguage(TranslateHelper.current)
- // }
- // }
- Component.onCompleted: {
- // Network.openLog = false
- // Network.setInterceptor(function(param){
- // param.addHeader("Token","000000000000000000000")
- // })
- FluApp.init(app,Qt.locale())
- // FluApp.windowIcon = "qrc:/example/res/image/favicon.ico"
- // FluApp.useSystemAppBar = SettingsHelper.getUseSystemAppBar()
- FluApp.useSystemAppBar = false
- // FluTheme.darkMode = SettingsHelper.getDarkMode()
- FluTheme.darkMode = false
- FluTheme.animationEnabled = true
- FluRouter.routes = {
- "/":"qrc:/src/qml/Index.qml",
+ApplicationWindow {
+
+ id:window
+ title: "iDescriptor"
+ width: 1000
+ height: 668
+ minimumWidth: 668
+ minimumHeight: 320
+ visible: true
+ property int currentIndex: 0
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 10
+ spacing: 0
+ TabButton {
+ text: qsTr("iDevice")
+ onClicked: currentIndex = 0
+ active: currentIndex == 0
+ }
+
+ TabButton {
+ text: qsTr("Apps")
+ onClicked: currentIndex = 1
+ active: currentIndex == 1
+ }
+ TabButton {
+ text: qsTr("Toolbox")
+ onClicked: currentIndex = 2
+ active: currentIndex == 2
+ }
+ TabButton {
+ text: qsTr("Jailbroken")
+ onClicked: currentIndex = 3
+ active: currentIndex == 3
+ }
}
- var args = Qt.application.arguments
- if(args.length>=2 && args[1].startsWith("-crashed=")){
- FluRouter.navigate("/crash",{crashFilePath:args[1].replace("-crashed=","")})
- }else{
- FluRouter.navigate("/")
+
+
+ Tabs {
+ currentIndex: window.currentIndex
+ Layout.fillWidth : true
+ Layout.fillHeight : true
}
}
diff --git a/src/qml/PreviewWindow.qml b/src/qml/PreviewWindow.qml
new file mode 100644
index 0000000..163c382
--- /dev/null
+++ b/src/qml/PreviewWindow.qml
@@ -0,0 +1,35 @@
+import QtQuick 2.15
+import QtQuick.Window 2.15
+
+Window {
+ required property string filePath
+ property int thumbVersion: 0
+ required property string udid
+ property int row : 999999
+ id: root
+ visible: true
+ width: Screen.width
+ height: Screen.height
+ visibility: Window.FullScreen
+
+
+
+ Connections {
+ target : ThumbnailProvider
+
+ function onThumbnailReady(path, data, rowHint) {
+ if (path == root.filePath && rowHint == root.row) {
+ root.thumbVersion++
+ }
+ }
+ }
+
+ Image {
+ cache: false
+ anchors.fill: parent
+ //FIXME:use encodeuricomp
+ source: "image://thumb/" + filePath + "?udid=" + root.udid + "&index=" + row + "&v=" + thumbVersion
+ fillMode: Image.PreserveAspectFit
+ }
+
+}
\ No newline at end of file
diff --git a/src/qml/TabButton.qml b/src/qml/TabButton.qml
index aa11211..bbbc7f3 100644
--- a/src/qml/TabButton.qml
+++ b/src/qml/TabButton.qml
@@ -15,7 +15,8 @@ Button {
}
Layout.fillWidth: true
- Layout.fillHeight: true
+ Layout.preferredWidth : 0
+ // Layout.fillHeight: true
background: Rectangle {
color : "transparent"
}
diff --git a/src/qml/Tabs.qml b/src/qml/Tabs.qml
index 61d4360..09552fa 100644
--- a/src/qml/Tabs.qml
+++ b/src/qml/Tabs.qml
@@ -1,17 +1,16 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
-import "."
import com.kdab.cxx_qt.demo 1.0
Item {
id: root
- anchors.fill: parent
property int currentIndex : 0
- Device {
+ DeviceTab {
id : device
+ anchors.fill: parent
visible : currentIndex == 0
opacity: root.currentIndex === 0 ? 1 : 0
property real slideY: root.currentIndex === 0 ? 0 : 20
diff --git a/src/qml/Welcome.qml b/src/qml/Welcome.qml
index e73df3b..249a84d 100644
--- a/src/qml/Welcome.qml
+++ b/src/qml/Welcome.qml
@@ -27,7 +27,7 @@ Item {
font.pixelSize: 28
font.weight: Font.DemiBold
wrapMode: Text.WordWrap
- color: palette.text
+ color: "white"
}
Item { Layout.preferredHeight: 6 }
@@ -40,7 +40,7 @@ Item {
font.pixelSize: 10
font.weight: Font.Normal
wrapMode: Text.WordWrap
- color: palette.text
+ // color: palette.text
}
Item { Layout.preferredHeight: 12 }
@@ -114,7 +114,7 @@ Item {
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 14
wrapMode: Text.WordWrap
- color: palette.text
+ // color: palette.text
}
Item { Layout.preferredHeight: 10 }
diff --git a/src/qml/Index.qml b/src/qml/wIndows/Index.qml
similarity index 100%
rename from src/qml/Index.qml
rename to src/qml/wIndows/Index.qml
diff --git a/src/qml/wIndows/Main.qml b/src/qml/wIndows/Main.qml
new file mode 100644
index 0000000..d537726
--- /dev/null
+++ b/src/qml/wIndows/Main.qml
@@ -0,0 +1,50 @@
+import QtQuick 2.15
+import QtQuick.Window 2.15
+// import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import FluentUI 1.0
+
+FluLauncher {
+ id: app
+ // Connections{
+ // target: FluTheme
+ // function onDarkModeChanged(){
+ // SettingsHelper.saveDarkMode(FluTheme.darkMode)
+ // }
+ // }
+ // Connections{
+ // target: FluApp
+ // function onUseSystemAppBarChanged(){
+ // SettingsHelper.saveUseSystemAppBar(FluApp.useSystemAppBar)
+ // }
+ // }
+ // Connections{
+ // target: TranslateHelper
+ // function onCurrentChanged(){
+ // SettingsHelper.saveLanguage(TranslateHelper.current)
+ // }
+ // }
+ Component.onCompleted: {
+ // Network.openLog = false
+ // Network.setInterceptor(function(param){
+ // param.addHeader("Token","000000000000000000000")
+ // })
+ FluApp.init(app,Qt.locale())
+ // FluApp.windowIcon = "qrc:/example/res/image/favicon.ico"
+ // FluApp.useSystemAppBar = SettingsHelper.getUseSystemAppBar()
+ FluApp.useSystemAppBar = false
+ // FluTheme.darkMode = SettingsHelper.getDarkMode()
+ FluTheme.darkMode = false
+ FluTheme.animationEnabled = true
+ FluRouter.routes = {
+ "/":"qrc:/src/qml/windows/Index.qml",
+ }
+ var args = Qt.application.arguments
+ if(args.length>=2 && args[1].startsWith("-crashed=")){
+ FluRouter.navigate("/crash",{crashFilePath:args[1].replace("-crashed=","")})
+ }else{
+ FluRouter.navigate("/")
+ }
+ }
+
+}
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index dc9b3ce..316fdc3 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -1235,6 +1235,7 @@ dependencies = [
"once_cell",
"plist",
"plist-macro",
+ "priority-queue",
"regex",
"rusqlite",
"serde_json",
@@ -1800,6 +1801,17 @@ dependencies = [
"syn",
]
+[[package]]
+name = "priority-queue"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96"
+dependencies = [
+ "equivalent",
+ "indexmap",
+ "serde",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.106"
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index e7ad3b6..e34f049 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -24,7 +24,8 @@ urlencoding = "2.1.3"
serde_json = "1.0.149"
rusqlite = { version = "0.39.0", features = ["bundled"] }
filetime = "0.2.27"
+priority-queue = "2.7.0"
[build-dependencies]
-cxx-qt-build = { version = "0.8.1", features = ["link_qt_object_files"] }
+cxx-qt-build = { version = "0.8.1", features = ["link_qt_object_files"] }
\ No newline at end of file
diff --git a/src/rust/build.rs b/src/rust/build.rs
index 7b9ba57..036ecef 100644
--- a/src/rust/build.rs
+++ b/src/rust/build.rs
@@ -1,10 +1,28 @@
use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() {
- CxxQtBuilder::new_qml_module(
+ let builder = CxxQtBuilder::new_qml_module(
QmlModule::new("com.kdab.cxx_qt.demo").qml_file("../qml/Tabs.qml"),
)
.qt_module("Qml")
- .files(["src/lib.rs","src/afc_services.rs","src/afc2_services.rs","src/service_manager.rs","src/screenshot.rs","src/hause_arrest.rs","src/io_manager.rs"])
- .build();
-}
\ No newline at end of file
+ .files([
+ "src/lib.rs",
+ "src/afc_services.rs",
+ "src/afc2_services.rs",
+ "src/service_manager.rs",
+ "src/screenshot.rs",
+ "src/hause_arrest.rs",
+ "src/io_manager.rs",
+ "src/query_sqlite.rs",
+ "src/image_loader.rs",
+ "src/bridge.rs",
+ ])
+ .include_dir("include");
+
+ let builder = unsafe {
+ builder.cc_builder(|cc| {
+ cc.file("src/thumbnail.cc");
+ })
+ };
+ builder.build();
+}
diff --git a/src/rust/include/heic.h b/src/rust/include/heic.h
new file mode 100644
index 0000000..429bafa
--- /dev/null
+++ b/src/rust/include/heic.h
@@ -0,0 +1,5 @@
+#pragma once
+#include "rust/cxx.h"
+#include
+
+QImage load_heic(rust::Vec data);
\ No newline at end of file
diff --git a/src/rust/include/thumbnail.h b/src/rust/include/thumbnail.h
new file mode 100644
index 0000000..7a3efb4
--- /dev/null
+++ b/src/rust/include/thumbnail.h
@@ -0,0 +1,9 @@
+#pragma once
+#include "rust/cxx.h"
+#include
+
+class AfcReader;
+
+QImage generate_thumbnail_with_reader(const AfcReader &reader,
+ int32_t file_size, int32_t requested_w,
+ int32_t requested_h);
\ No newline at end of file
diff --git a/src/rust/src/bridge.rs b/src/rust/src/bridge.rs
new file mode 100644
index 0000000..593fd57
--- /dev/null
+++ b/src/rust/src/bridge.rs
@@ -0,0 +1,94 @@
+use crate::{APP_DEVICE_STATE, run_sync};
+use idevice::afc::AfcClient;
+use idevice::afc::opcode::AfcFopenMode;
+use std::io::SeekFrom;
+use tokio::io::{AsyncReadExt, AsyncSeekExt};
+
+pub struct AfcReader {
+ udid: String,
+ path: String,
+}
+
+impl AfcReader {
+ pub fn new(udid: String, path: String) -> Self {
+ Self { udid, path }
+ }
+ pub fn read_at(&self, offset: i64, size: i32) -> Vec {
+ if size <= 0 || offset < 0 {
+ return Vec::new();
+ }
+
+ let udid = self.udid.clone();
+ let path = self.path.clone();
+ // FIXME: is run_sync safe in this context?
+ run_sync(async move {
+ let maybe_device = APP_DEVICE_STATE.lock().await.get(udid.as_str()).cloned();
+
+ let device = match maybe_device {
+ Some(d) => d,
+ None => {
+ eprintln!("read_at: device {} not found", udid);
+ return Vec::new();
+ }
+ };
+
+ let mut afc = device.afc.lock().await;
+
+ let mut fd = match afc.open(path.clone(), AfcFopenMode::RdOnly).await {
+ Ok(f) => f,
+ Err(e) => {
+ eprintln!("read_at: open({}) failed: {}", path, e);
+ return Vec::new();
+ }
+ };
+
+ if offset > 0 {
+ if let Err(e) = fd.seek(SeekFrom::Start(offset as u64)).await {
+ eprintln!("read_at: seek({}, {}) failed: {}", path, offset, e);
+ let _ = fd.close().await;
+ return Vec::new();
+ }
+ }
+
+ let mut buf = vec![0u8; size as usize];
+ let n = match fd.read(&mut buf).await {
+ Ok(n) => n,
+ Err(e) => {
+ eprintln!("read_at: read({}, {}) failed: {}", path, offset, e);
+ let _ = fd.close().await;
+ return Vec::new();
+ }
+ };
+ buf.truncate(n);
+ let _ = fd.close().await;
+ buf
+ })
+ }
+}
+
+#[cxx_qt::bridge]
+pub mod bridge {
+ extern "Rust" {
+ type AfcReader;
+
+ fn read_at(self: &AfcReader, offset: i64, size: i32) -> Vec;
+ }
+
+ unsafe extern "C++" {
+ include!("thumbnail.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");
+
+ type QImage = cxx_qt_lib::QImage;
+ type QByteArray = cxx_qt_lib::QByteArray;
+
+ fn generate_thumbnail_with_reader(
+ reader: &AfcReader,
+ file_size: i32,
+ requested_w: i32,
+ requested_h: i32,
+ ) -> QImage;
+ }
+}
diff --git a/src/rust/src/image_loader.rs b/src/rust/src/image_loader.rs
new file mode 100644
index 0000000..088fcf1
--- /dev/null
+++ b/src/rust/src/image_loader.rs
@@ -0,0 +1,354 @@
+use cxx_qt::Threading;
+use cxx_qt_lib::{QByteArray, QImage, QString};
+use idevice::afc::{self, AfcClient};
+
+use crate::{APP_DEVICE_STATE, RUNTIME};
+use core::ffi;
+use idevice::afc::opcode::AfcFopenMode;
+use once_cell::sync::Lazy;
+use priority_queue::PriorityQueue;
+use std::cmp::Reverse;
+use std::collections::HashMap;
+use std::sync::{
+ Arc, Mutex,
+ atomic::{AtomicBool, AtomicU64, Ordering},
+};
+use tokio::{
+ io::AsyncReadExt,
+ sync::{Notify, Semaphore},
+};
+
+#[cxx_qt::bridge]
+mod qobject {
+ unsafe extern "C++" {
+ include!("cxx-qt-lib/qstring.h");
+ include!("cxx-qt-lib/qbytearray.h");
+ include!("cxx-qt-lib/qimage.h");
+
+ type QImage = cxx_qt_lib::QImage;
+ type QString = cxx_qt_lib::QString;
+ type QByteArray = cxx_qt_lib::QByteArray;
+ }
+
+ extern "RustQt" {
+ #[qobject]
+ type ImageBackend = super::ImageRustBackend;
+
+ #[qinvokable]
+ fn request_thumbnail(
+ self: Pin<&mut ImageBackend>,
+ udid: &QString,
+ file_path: &QString,
+ row: u32,
+ );
+
+ #[qsignal]
+ fn thumbnail_ready(self: Pin<&mut ImageBackend>, file_path: QString, img: QImage, row: u32);
+ }
+
+ impl cxx_qt::Threading for ImageBackend {}
+}
+
+static POOL_SEM: Lazy> = Lazy::new(|| Arc::new(Semaphore::new(10)));
+static SCHEDULER: Lazy> = Lazy::new(|| Arc::new(Scheduler::new()));
+static WORKER_STARTED: AtomicBool = AtomicBool::new(false);
+static NEXT_SEQ: AtomicU64 = AtomicU64::new(0);
+
+#[derive(Default)]
+pub struct ImageRustBackend;
+
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
+struct JobKey {
+ udid: String,
+ path: String,
+}
+
+struct JobPayload {
+ row: u32,
+ path_for_qt: QString,
+ qt_thread: cxx_qt::CxxQtThread,
+}
+
+struct QueueState {
+ pq: PriorityQueue)>,
+ payloads: HashMap,
+}
+
+struct Scheduler {
+ state: Mutex,
+ notify: Notify,
+}
+
+impl Scheduler {
+ fn new() -> Self {
+ Self {
+ state: Mutex::new(QueueState {
+ pq: PriorityQueue::new(),
+ payloads: HashMap::new(),
+ }),
+ notify: Notify::new(),
+ }
+ }
+
+ fn enqueue(&self, key: JobKey, payload: JobPayload, row: u32) {
+ let seq = NEXT_SEQ.fetch_add(1, Ordering::Relaxed);
+ let priority = (row, Reverse(seq));
+
+ {
+ let mut guard = self.state.lock().expect("scheduler mutex poisoned");
+ guard.payloads.insert(key.clone(), payload);
+
+ if guard.pq.get_priority(&key).is_some() {
+ guard.pq.change_priority(&key, priority);
+ } else {
+ guard.pq.push(key, priority);
+ }
+ }
+
+ self.notify.notify_one();
+ }
+
+ fn pop_next(&self) -> Option<(JobKey, JobPayload)> {
+ let mut guard = self.state.lock().expect("scheduler mutex poisoned");
+ let (key, _) = guard.pq.pop()?;
+ let payload = guard.payloads.remove(&key)?;
+ Some((key, payload))
+ }
+}
+
+fn ensure_worker_started() {
+ if WORKER_STARTED
+ .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok()
+ {
+ RUNTIME.spawn(async {
+ loop {
+ let Some((key, payload)) = SCHEDULER.pop_next() else {
+ SCHEDULER.notify.notified().await;
+ continue;
+ };
+
+ let permit = match POOL_SEM.clone().acquire_owned().await {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("image_loader: semaphore acquire failed: {e}");
+ continue;
+ }
+ };
+
+ 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 device = match maybe_device {
+ Some(d) => d,
+ None => {
+ // eprintln!(
+ // "image_loader::read_file_via_afc: device {udid} not found"
+ // );
+ return;
+ }
+ };
+
+ 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;
+
+ 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}");
+ }
+ });
+ }
+ });
+ }
+}
+//FIXME:move to utils
+fn is_video_file(path: &str) -> bool {
+ let ext = path
+ .rsplit_once('.')
+ .map(|(_, e)| e.to_ascii_lowercase())
+ .unwrap_or_default();
+
+ matches!(
+ ext.as_str(),
+ "mp4"
+ | "mov"
+ | "m4v"
+ | "avi"
+ | "mkv"
+ | "webm"
+ | "flv"
+ | "wmv"
+ | "3gp"
+ | "mpeg"
+ | "mpg"
+ | "ts"
+ | "mts"
+ | "m2ts"
+ )
+}
+
+// async fn read_file_via_afc(udid: String, path: String) -> Vec {
+// let mut buf = Vec::new();
+// let mut chunk = vec![0u8; 8192];
+
+// loop {
+// let n = match fd.read(&mut chunk).await {
+// Ok(n) => n,
+// Err(e) => {
+// eprintln!("image_loader::read_file_via_afc: failed to read {path}: {e}");
+// buf.clear();
+// break;
+// }
+// };
+
+// if n == 0 {
+// break;
+// }
+
+// buf.extend_from_slice(&chunk[..n]);
+// }
+
+// buf
+// }
+
+// FIXME: move or remove
+async fn file_to_buffer(afc: &mut AfcClient, path: String) -> Vec {
+ let mut buf = Vec::new();
+
+ let mut fd = match afc.open(path, AfcFopenMode::RdOnly).await {
+ Ok(f) => f,
+ Err(e) => {
+ // eprintln!("file_to_buffer: failed to open {path}: {e}");
+ return buf;
+ }
+ };
+
+ let mut chunk = vec![0u8; 8192];
+
+ loop {
+ let n = match fd.read(&mut chunk).await {
+ Ok(n) => n,
+ Err(e) => {
+ // eprintln!("file_to_buffer: failed to read {path}: {e}");
+ buf.clear();
+ break;
+ }
+ };
+ if n == 0 {
+ break;
+ }
+ buf.extend_from_slice(&chunk[..n]);
+ }
+ fd.close().await.ok();
+ buf
+}
+
+//FIXME: move
+async fn file_to_image(afc: &mut AfcClient, path: String) -> QImage {
+ let mut buf = Vec::new();
+
+ let mut fd = match afc.open(path, AfcFopenMode::RdOnly).await {
+ Ok(f) => f,
+ Err(e) => {
+ // eprintln!("file_to_buffer: failed to open {path}: {e}");
+ return QImage::default();
+ }
+ };
+
+ let mut chunk = vec![0u8; 8192];
+
+ loop {
+ let n = match fd.read(&mut chunk).await {
+ Ok(n) => n,
+ Err(e) => {
+ // eprintln!("file_to_buffer: failed to read {path}: {e}");
+ buf.clear();
+ break;
+ }
+ };
+ if n == 0 {
+ break;
+ }
+ buf.extend_from_slice(&chunk[..n]);
+ }
+ fd.close().await.ok();
+
+ match QImage::from_data(&buf, None) {
+ Some(img) => img,
+ None => QImage::default(),
+ }
+}
+
+impl qobject::ImageBackend {
+ fn request_thumbnail(
+ self: ::std::pin::Pin<&mut Self>,
+ udid: &QString,
+ file_path: &QString,
+ row: u32,
+ ) {
+ ensure_worker_started();
+
+ let udid_string = udid.to_string();
+ let path_string = file_path.to_string();
+
+ let key = JobKey {
+ udid: udid_string,
+ path: path_string,
+ };
+
+ let payload = JobPayload {
+ row,
+ path_for_qt: file_path.clone(),
+ qt_thread: self.qt_thread(),
+ };
+
+ SCHEDULER.enqueue(key, payload, row);
+ }
+}
diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs
index c9d8839..9bdb91f 100644
--- a/src/rust/src/lib.rs
+++ b/src/rust/src/lib.rs
@@ -9,6 +9,8 @@ use idevice::{
provider::TcpProvider,
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdListenEvent},
};
+use idevice::afc::opcode::AfcFopenMode;
+
use std::{any::type_name, sync::Arc};
use std::{collections::HashMap, net::IpAddr};
use tokio::sync::Mutex;
@@ -26,7 +28,7 @@ use cxx_qt_lib::{QMap, QMapPair_QString_QVariant, QString, QVariant};
use crate::qobject::Core;
use once_cell::sync::Lazy;
-use plist::Value;
+use plist::{Dictionary, Value};
mod afc;
mod afc2_services;
mod afc_services;
@@ -35,6 +37,9 @@ mod io_manager;
mod screenshot;
mod service_manager;
mod utils;
+mod query_sqlite;
+mod image_loader;
+mod bridge;
const POSSIBLE_ROOT: &str = "../../../../";
const APP_LABEL: &str = "iDescriptor";
@@ -82,9 +87,9 @@ where
#[cxx_qt::bridge]
mod qobject {
unsafe extern "C++" {
- include!("cxx-qt-lib/qstring.h");
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;
@@ -113,7 +118,7 @@ mod qobject {
fn remove_device(self: Pin<&mut Core>, udid: &QString);
#[qsignal]
- fn device_event(self: Pin<&mut Core>, event_type: u32, udid: &QString, info: &QString);
+ fn device_event(self: Pin<&mut Core>, event_type: u32, udid: &QString, info: &QMap_QString_QVariant);
#[qsignal]
fn init_failed(self: Pin<&mut Core>, mac_address: &QString);
@@ -207,7 +212,7 @@ impl qobject::Core {
core_qobj.device_event(
EV_DISCONNECTED,
&QString::from(udid),
- &QString::from(""),
+ &QMap::::default(),
);
})
.ok();
@@ -296,7 +301,7 @@ impl qobject::Core {
core_qobj.device_event(
EV_CONNECTED,
&QString::from(udid),
- &QString::from(info),
+ &info,
);
})
.ok();
@@ -426,7 +431,7 @@ async fn handle_pairing(
core_qobj.device_event(
EV_PAIRING_PENDING,
&QString::from(udid_for_event),
- &QString::from(""),
+ &QMap::::default(),
);
})
.ok();
@@ -495,7 +500,7 @@ fn emit_pairing_failed(
) {
qt_thread
.queue(move |core_qobj| {
- core_qobj.device_event(EV_FAIL, &QString::from(udid), &QString::from(""));
+ core_qobj.device_event(EV_FAIL, &QString::from(udid), &QMap::::default());
})
.ok();
}
@@ -524,7 +529,7 @@ async fn emit_connected(qt_thread: cxx_qt::CxxQtThread, udid: String) {
core_qobj.device_event(
EV_CONNECTED,
&QString::from(udid_for_event),
- &QString::from(info_for_event),
+ &info_for_event,
);
})
.ok();
@@ -567,7 +572,7 @@ async fn init_idescriptor_device<
>(
provider: T,
qt_thread: cxx_qt::CxxQtThread,
-) -> Result<(String, String), IdeviceError> {
+) -> Result<(String, QMap), IdeviceError> {
let provider_name = type_name::();
let is_wireless = provider_name == "idevice::provider::TcpProvider";
@@ -575,7 +580,6 @@ async fn init_idescriptor_device<
let mut lc = LockdownClient::connect(&provider).await?;
lc.start_session(&pf).await?;
- eprintln!("init_idescriptor_device: Attempting to get default values from Lockdown.");
let mut def_vals = match lc.get_value(None, None).await {
Ok(v) => v,
Err(e) => {
@@ -587,11 +591,11 @@ async fn init_idescriptor_device<
// FIXME: we may need our own error types here
// but InternalError should be fine for now
- let udid = def_vals
- .as_dictionary()
- .ok_or_else(|| {
- IdeviceError::InternalError("Lockdown root is not a dictionary".to_string())
- })?
+ let def_vals_dict = def_vals.as_dictionary_mut().ok_or_else(|| {
+ IdeviceError::InternalError("Lockdown root is not a dictionary".to_string())
+ })?;
+
+ let udid = def_vals_dict
.get("UniqueDeviceID")
.and_then(|v| v.as_string())
.ok_or_else(|| {
@@ -603,6 +607,7 @@ async fn init_idescriptor_device<
eprintln!("init_idescriptor_device: UDID is empty.");
return Err(IdeviceError::InvalidHostID);
}
+
let mut hb = None;
if is_wireless {
@@ -617,8 +622,6 @@ async fn init_idescriptor_device<
eprintln!("init_idescriptor_device: Connected to HeartbeatClient.");
}
- let disk_vals = lc.get_value(None, Some("com.apple.disk_usage")).await?;
-
eprintln!("init_idescriptor_device: Attempting to connect to AFC client.");
let mut afc_client = AfcClient::connect(&provider).await?;
@@ -637,53 +640,15 @@ async fn init_idescriptor_device<
}
};
- eprintln!("init_idescriptor_device: Attempting to get AFC device info.");
- let afc_info = match afc_client.get_device_info().await {
- Ok(i) => i,
- Err(e) => {
- eprintln!("get_device_info failed: {e:?}");
- return Err(e);
- }
- };
- eprintln!("init_idescriptor_device: AFC device info obtained.");
+ let info = collect_info(
+ def_vals_dict,
+ &mut afc_client,
+ &mut lc,
+ &mut diag_relay,
+ is_wireless,
+ )
+ .await?;
- if let (Value::Dictionary(d_target), Value::Dictionary(d_source)) = (&mut def_vals, disk_vals) {
- d_target.extend(d_source);
-
- let mut afc_info_dict = plist::Dictionary::new();
- afc_info_dict.insert("Model".into(), Value::String(afc_info.model));
- afc_info_dict.insert(
- "TotalBytes".into(),
- Value::Integer((afc_info.total_bytes as u64).into()),
- );
- afc_info_dict.insert(
- "FreeBytes".into(),
- Value::Integer((afc_info.free_bytes as u64).into()),
- );
- afc_info_dict.insert(
- "BlockSize".into(),
- Value::Integer((afc_info.block_size as u64).into()),
- );
-
- d_target.insert("AFC_INFO".into(), Value::Dictionary(afc_info_dict));
- d_target.insert(
- "Jailbroken".into(),
- Value::Boolean(utils::detect_jailbroken(&mut afc_client).await),
- );
-
- if let Some(battery_info) = utils::get_battery_info(&mut diag_relay).await {
- d_target.insert("DIAG_INFO".into(), Value::Dictionary(battery_info));
- }
-
- d_target.insert(
- "ConnectionType".into(),
- Value::String(if is_wireless {
- "Wireless".into()
- } else {
- "USB".into()
- }),
- );
- }
eprintln!("init_idescriptor_device: Storing device services.");
let device_services = DeviceServices {
@@ -738,23 +703,107 @@ async fn init_idescriptor_device<
}
}
- let mut buf = Vec::new();
- if def_vals.to_writer_xml(&mut buf).is_err() {
- eprintln!("init_idescriptor_device: Failed to serialize default values to XML.");
- return Err(IdeviceError::InternalError(
- "Failed to serialize default values to XML".to_string(),
- ));
- }
-
- let info = String::from_utf8(buf).map_err(|_| {
- IdeviceError::InternalError("Failed to convert default values XML to UTF-8".to_string())
- })?;
-
eprintln!("init_idescriptor_device: Device has been initialized.");
Ok((udid, info))
}
+async fn collect_info(
+ mut def_vals_dict: &mut Dictionary,
+ mut afc: &mut AfcClient,
+ mut lc: &mut LockdownClient,
+ mut diag_relay: &mut DiagnosticsRelayClient,
+ is_wireless: bool,
+) -> Result, IdeviceError> {
+ let mut info = QMap::::default();
+
+ eprintln!("init_idescriptor_device: Attempting to get default values from Lockdown.");
+
+ let disk_vals = lc.get_value(None, Some("com.apple.disk_usage")).await?;
+
+ eprintln!("init_idescriptor_device: Attempting to get AFC device info.");
+ let afc_info = match afc.get_device_info().await {
+ Ok(i) => i,
+ Err(e) => {
+ eprintln!("get_device_info failed: {e:?}");
+ return Err(e);
+ }
+ };
+ eprintln!("init_idescriptor_device: AFC device info obtained.");
+
+ if let (d_target, Value::Dictionary(d_source)) = (&mut def_vals_dict, disk_vals) {
+ d_target.extend(d_source);
+
+ let mut afc_info_dict = plist::Dictionary::new();
+ afc_info_dict.insert("Model".into(), Value::String(afc_info.model));
+ afc_info_dict.insert(
+ "TotalBytes".into(),
+ Value::Integer((afc_info.total_bytes as u64).into()),
+ );
+ afc_info_dict.insert(
+ "FreeBytes".into(),
+ Value::Integer((afc_info.free_bytes as u64).into()),
+ );
+ afc_info_dict.insert(
+ "BlockSize".into(),
+ Value::Integer((afc_info.block_size as u64).into()),
+ );
+
+ d_target.insert("AFC_INFO".into(), Value::Dictionary(afc_info_dict));
+ d_target.insert(
+ "Jailbroken".into(),
+ Value::Boolean(utils::detect_jailbroken(&mut afc).await),
+ );
+
+ if let Some(battery_info) = utils::get_battery_info(&mut diag_relay).await {
+ d_target.insert("DIAG_INFO".into(), Value::Dictionary(battery_info));
+ }
+
+ d_target.insert(
+ "ConnectionType".into(),
+ Value::String(if is_wireless {
+ "Wireless".into()
+ } else {
+ "USB".into()
+ }),
+ );
+ }
+
+ let keys_to_insert_string = [
+ "DeviceName",
+ "DeviceClass",
+ "DeviceColor",
+ "ModelNumber",
+ "CPUArchitecture",
+ "BuildVersion",
+ "HardwareModel",
+ "HardwarePlatform",
+ "EthernetAddress",
+ "BluetoothAddress",
+ "FirmwareVersion",
+ "ProductVersion",
+ "WiFiAddress",
+ "UniqueDeviceID",
+ ];
+
+ let mut insert_string = |key: &str| {
+ info.insert(
+ QString::from(key),
+ QVariant::from(&QString::from(
+ def_vals_dict
+ .get(key)
+ .and_then(|v| v.as_string())
+ .unwrap_or_else(|| ""),
+ )),
+ );
+ };
+
+ for key in keys_to_insert_string.iter() {
+ insert_string(key);
+ }
+
+ Ok(info)
+}
async fn spawn_heartbeat_task(
mut hb_client: heartbeat::HeartbeatClient,
qt_thread: cxx_qt::CxxQtThread,
@@ -798,7 +847,7 @@ async fn spawn_heartbeat_task(
core_qobj.device_event(
EV_DISCONNECTED,
&QString::from(udid_for_event),
- &QString::from(""),
+ &QMap::::default(),
);
});
break;
@@ -831,7 +880,7 @@ async fn spawn_heartbeat_task(
core_qobj.device_event(
EV_DISCONNECTED,
&QString::from(udid_for_event),
- &QString::from(""),
+ &QMap::::default(),
);
});
break;
diff --git a/src/rust/src/load_heic.cc b/src/rust/src/load_heic.cc
new file mode 100644
index 0000000..f543a64
--- /dev/null
+++ b/src/rust/src/load_heic.cc
@@ -0,0 +1,85 @@
+/*
+ * iDescriptor: A free and open-source idevice management tool.
+ *
+ * Copyright (C) 2025 Uncore
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+#include "rust/cxx.h"
+// #include
+#include
+#include
+
+QImage load_heic(rust::Vec &data)
+{
+ heif_context *ctx = heif_context_alloc();
+ if (!ctx) {
+ // qWarning() << "Failed to allocate heif_context";
+ return QImage();
+ }
+
+ heif_error err =
+ heif_context_read_from_memory(ctx, data.data(), data.size(), nullptr);
+ if (err.code != heif_error_Ok) {
+ // qWarning() << "Failed to read HEIC from memory:" << err.message;
+ heif_context_free(ctx);
+ return QImage();
+ }
+
+ heif_image_handle *handle;
+ err = heif_context_get_primary_image_handle(ctx, &handle);
+ if (err.code != heif_error_Ok) {
+ // qWarning() << "Failed to get primary image handle:" << err.message;
+ heif_context_free(ctx);
+ return QImage();
+ }
+
+ heif_image *img;
+ err = heif_decode_image(handle, &img, heif_colorspace_RGB,
+ heif_chroma_interleaved_RGB, nullptr);
+ if (err.code != heif_error_Ok) {
+ // qWarning() << "Failed to decode HEIC image:" << err.message;
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ return QImage();
+ }
+
+ int width = heif_image_get_width(img, heif_channel_interleaved);
+ int height = heif_image_get_height(img, heif_channel_interleaved);
+ int stride;
+ /*
+ FIXME: use heif_image_get_plane_readonly2 in future, on ubuntu 24 it's not
+ available yet
+ */
+ const uint8_t *data =
+ heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride);
+
+ if (!data) {
+ // qWarning() << "Failed to get image plane data";
+ heif_image_release(img);
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ return QImage();
+ }
+
+ QImage qimg(data, width, height, stride, QImage::Format_RGB888);
+ QImage copy =
+ qimg.copy(); // Deep copy since the original data will be freed
+ heif_image_release(img);
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+
+ return copy;
+}
diff --git a/src/rust/src/query_sqlite.rs b/src/rust/src/query_sqlite.rs
new file mode 100644
index 0000000..3ad694c
--- /dev/null
+++ b/src/rust/src/query_sqlite.rs
@@ -0,0 +1,387 @@
+use cxx_qt::{Constructor, CxxQtType, Threading};
+use cxx_qt_lib::{QByteArray, QList, QMap, QMapPair_QString_QVariant, QString, QVariant};
+
+use crate::POSSIBLE_ROOT;
+use crate::{APP_DEVICE_STATE, RUNTIME, afc, run_sync};
+use cxx_qt_lib::{QHash, QHashPair_QString_QVariant};
+use idevice::afc::{AfcClient, opcode::AfcFopenMode};
+use idevice::{
+ IdeviceError, IdeviceService, diagnostics_relay::DiagnosticsRelayClient,
+ house_arrest::HouseArrestClient, installation_proxy::InstallationProxyClient,
+ provider::IdeviceProvider,
+};
+use once_cell::sync::Lazy;
+use plist::Dictionary as PlistDictionary;
+use plist_macro::plist;
+use regex::Regex;
+use rusqlite::{Connection, Rows};
+use serde_json::json;
+use std::default;
+use std::fmt::format;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::{io::SeekFrom, pin::Pin};
+use tokio::io::{AsyncReadExt, AsyncSeekExt};
+use tokio::net::TcpListener;
+use tokio::sync::Mutex;
+
+use tokio::sync::oneshot;
+
+#[cxx_qt::bridge]
+mod qobject {
+ unsafe extern "C++" {
+ include!("cxx-qt-lib/qstring.h");
+ include!("cxx-qt-lib/qlist.h");
+ include!("cxx-qt-lib/qbytearray.h");
+ include!("cxx-qt-lib/qmap.h");
+ include!("cxx-qt-lib/qvariant.h");
+
+ type QString = cxx_qt_lib::QString;
+ type QList_QString = cxx_qt_lib::QList;
+ type QByteArray = cxx_qt_lib::QByteArray;
+ type QMap_QString_QVariant = cxx_qt_lib::QMap;
+ }
+
+ extern "RustQt" {
+ #[qobject]
+ #[qml_element]
+ #[qproperty(QMap_QString_QVariant, albums)]
+ #[qproperty(QString, error)]
+ #[qproperty(bool, is_init)]
+ #[qproperty(bool, is_err)]
+ type Query = super::RQuery;
+
+ #[qinvokable]
+ fn init(self: Pin<&mut Self>, udid: &QString);
+
+ #[qinvokable]
+ fn read_albums(self: Pin<&mut Self>);
+
+ #[qinvokable]
+ fn query_album(self: Pin<&mut Self>, id: i32);
+
+ #[qsignal]
+ fn album_queried(self: Pin<&mut Self>, id: i32, items: QList_QString);
+
+ }
+ impl cxx_qt::Threading for Query {}
+}
+
+#[derive(Default)]
+pub struct RQuery {
+ udid: QString,
+ albums: QMap,
+ error: QString,
+ connection: Option>>,
+ is_init: bool,
+ is_err: bool,
+}
+
+impl qobject::Query {
+ fn init(mut self: Pin<&mut Self>, udid: &QString) {
+ let udid_clone = udid.clone();
+ let qt_thread = self.qt_thread();
+
+ RUNTIME.spawn(async move {
+ let res: Result<(), Box> = (async {
+ let mut gallery_db_bytes = {
+ let afc_arc = {
+ let device = APP_DEVICE_STATE
+ .lock()
+ .await
+ .get(&udid_clone.to_string())
+ .cloned();
+ match device {
+ Some(d) => d.afc.clone(),
+ None => return Err("Device not found".into()),
+ }
+ };
+
+ let mut afc = afc_arc.lock().await;
+ let mut fd = afc
+ .open("/PhotoData/Photos.sqlite", AfcFopenMode::RdOnly)
+ .await?;
+ fd.read_entire().await?
+ };
+
+ let conn: Connection = Connection::open_in_memory()?;
+
+ // HACK: WAL -> legacy mode patch
+ if gallery_db_bytes.len() > 20 && gallery_db_bytes[18] == 0x02 {
+ gallery_db_bytes[18] = 0x01;
+ gallery_db_bytes[19] = 0x01;
+ }
+
+ unsafe {
+ let db_ptr = rusqlite::ffi::sqlite3_deserialize(
+ conn.handle(),
+ b"main\0".as_ptr() as *const std::os::raw::c_char,
+ gallery_db_bytes.as_mut_ptr(),
+ gallery_db_bytes.len() as i64,
+ gallery_db_bytes.len() as i64,
+ rusqlite::ffi::SQLITE_DESERIALIZE_READONLY as u32,
+ );
+ if db_ptr != rusqlite::ffi::SQLITE_OK {
+ return Err("Failed to deserialize SQLite database".into());
+ }
+ };
+
+ //FIXME:need to drop vec somewhere safe
+ /*
+ std::mem::forget is needed because vec is dropped but
+ sqlite still needs, we need to manually drop the vec
+ */
+ std::mem::forget(gallery_db_bytes);
+
+ qt_thread
+ .queue(|mut s| {
+ s.as_mut().rust_mut().connection = Some(Arc::new(Mutex::new(conn)));
+ })
+ .ok();
+
+ Ok(())
+ })
+ .await;
+ match res {
+ Ok(_) => {
+ qt_thread.queue(move |s| s.set_is_init(true)).ok();
+ qt_thread.queue(move |s| s.set_is_err(false)).ok();
+ }
+ Err(e) => {
+ // eprintln!("Failed to read sqlite db");
+ qt_thread.queue(move |s| s.set_is_init(false)).ok();
+ qt_thread.queue(move |s| s.set_is_err(true)).ok();
+ }
+ };
+ });
+ }
+
+ fn read_albums(mut self: Pin<&mut Self>) {
+ let q_thread = self.qt_thread();
+ let con_arc = match &self.connection {
+ Some(c) => c.clone(),
+ None => {
+ println!("WTF NO CONN");
+ return;
+ }
+ };
+
+ RUNTIME.spawn(async move {
+ let mut albums = QMap::::default();
+ let res: Result<(), Box> = (async {
+ //recents album
+
+ let conn = con_arc.lock().await;
+ let mut recents_stmt = conn.prepare(
+ "
+ SELECT
+ ZASSET.ZFILENAME as 'FNAME',
+ ZASSET.ZDIRECTORY as 'DIR',
+ (SELECT COUNT(*) FROM ZASSET) as 'COUNT'
+ FROM ZASSET
+ ORDER BY ZASSET.Z_PK DESC
+ LIMIT 1
+ ",
+ )?;
+
+ let (fname, fdir, count) = recents_stmt.query_row([], |r| {
+ let fname: String = r.get(0)?;
+ let fdir: String = r.get(1)?;
+ let count: i32 = r.get(2)?;
+ Ok((fname, fdir, count))
+ })?;
+
+ let recents_album_data =
+ json!({ "item_count" : count, "file_path" : format!("{}/{}",fdir,fname)})
+ .to_string();
+
+ albums.insert(
+ QString::from("Recents"),
+ QVariant::from(&QString::from(recents_album_data)),
+ );
+
+ //favs
+ let mut favs_stmt = conn.prepare(
+ "
+ SELECT
+ ZASSET.ZFILENAME,
+ ZASSET.ZDIRECTORY,
+ (SELECT COUNT(*) FROM ZASSET WHERE ZASSET.ZFAVORITE = 1)
+ FROM ZASSET
+ WHERE ZASSET.ZFAVORITE = 1
+ ORDER BY ZASSET.Z_PK DESC
+ LIMIT 1
+ ",
+ )?;
+
+ let (fname, fdir, count) = favs_stmt.query_row([], |r| {
+ let fname: String = r.get(0)?;
+ let fdir: String = r.get(1)?;
+ let count: i32 = r.get(2)?;
+ Ok((fname, fdir, count))
+ })?;
+
+ let favs_album_data =
+ json!({"item_count" : count, "file_path" : format!("{}/{}",fdir,fname) })
+ .to_string();
+
+ albums.insert(
+ QString::from("Favorites"),
+ QVariant::from(&QString::from(favs_album_data)),
+ );
+
+ // IOS 26
+ // let mut stmt = conn.prepare(
+ // "SELECT
+ // ZGENERICALBUM.Z_PK,
+ // ZGENERICALBUM.ZTITLE,
+ // ZGENERICALBUM.ZCACHEDCOUNT,
+ // ZASSET.ZDIRECTORY,
+ // ZASSET.ZFILENAME
+ // FROM ZGENERICALBUM
+ // LEFT JOIN ZASSET ON ZGENERICALBUM.Z_ENT = ZASSET.Z_PK
+ // WHERE ZGENERICALBUM.Z_ENT IS NOT NULL
+ // AND ZGENERICALBUM.ZTITLE IS NOT NULL
+ // AND ZGENERICALBUM.ZCACHEDCOUNT IS NOT 0
+ // ",
+ // )?;
+
+ let mut stmt = conn.prepare(
+ "SELECT
+ ZGENERICALBUM.Z_PK,
+ ZGENERICALBUM.ZTITLE,
+ ZGENERICALBUM.ZCACHEDCOUNT,
+ ZASSET.ZDIRECTORY,
+ ZASSET.ZFILENAME
+ FROM ZGENERICALBUM
+ LEFT JOIN ZASSET ON ZGENERICALBUM.ZKEYASSET = ZASSET.Z_PK
+ WHERE ZGENERICALBUM.ZKEYASSET IS NOT NULL
+ ",
+ )?;
+
+ let rows_iter = stmt.query_map([], |row| {
+ let album_id: i32 = row.get(0)?;
+ let title: String = row.get(1)?;
+ let item_count: i32 = row.get(2)?;
+ let asset_dir: String = row.get(3)?;
+ let asset_file_name: String = row.get(4)?;
+ Ok((album_id, title, item_count, asset_dir, asset_file_name))
+ })?;
+
+ for row_res in rows_iter {
+ let (album_id, title, item_count, asset_dir, asset_file_name) = row_res?;
+
+ let album_data = crate::utils::create_album_info(
+ album_id,
+ item_count,
+ asset_dir,
+ asset_file_name,
+ );
+
+ albums.insert(
+ QString::from(title),
+ QVariant::from(&QString::from(album_data)),
+ );
+ }
+
+ Ok(())
+ })
+ .await;
+
+ if let Err(e) = res {
+ q_thread
+ .queue(move |q_self| {
+ q_self.set_error(QString::from(e.to_string()));
+ })
+ .ok();
+
+ q_thread
+ .queue(move |q_self| {
+ q_self.set_albums(albums);
+ })
+ .ok();
+ } else {
+ q_thread
+ .queue(move |q_self| {
+ q_self.set_error(QString::default());
+ })
+ .ok();
+
+ q_thread
+ .queue(move |q_self| {
+ q_self.set_albums(albums);
+ })
+ .ok();
+ }
+ });
+ }
+
+ fn query_album(self: Pin<&mut Self>, id: i32) {
+ let con_arc = match &self.connection {
+ Some(c) => c.clone(),
+ None => return,
+ };
+ let q_thread = self.qt_thread();
+
+ RUNTIME.spawn(async move {
+ let res: Result, Box> = (async {
+ let con = con_arc.lock().await;
+ let mut list: QList = QList::default();
+ let mut stmt = con.prepare(&format!(
+ "
+ SELECT
+ ZASSET.ZDIRECTORY,
+ ZASSET.ZFILENAME
+ FROM ZASSET WHERE ZASSET.Z_OPT = {}
+ ",
+ id
+ ))?;
+
+ let row_iter = stmt.query_map([], |r| {
+ let fdir: String = r.get(0)?;
+ let fname: String = r.get(1)?;
+ Ok((fdir, fname))
+ })?;
+
+ for item in row_iter {
+ let (fdir, fname) = item?;
+ let full_path = format!("{}/{}", fdir, fname);
+ list.append(QString::from(full_path));
+ }
+ Ok((list))
+ })
+ .await;
+
+ match res {
+ Ok(list) => {
+ q_thread
+ .queue(move |q| {
+ q.album_queried(id, list);
+ })
+ .ok();
+ }
+ Err(_) => {
+ println!("Error querying album")
+ }
+ }
+ });
+ }
+
+ fn query_favs(self: Pin<&mut Self>) {}
+
+ fn query_recents(self: Pin<&mut Self>) {}
+}
+
+// fn get_album_query_sql(ios_ver : i32, id : i32) {
+// let assets_table = crate::utils::get_sqlite_assets_name(ios_ver);
+
+// format!("
+// SELECT
+// {}.ZDIRECTORY,
+// {}.ZFILENAME
+// FROM {} WHERE
+// {}.Z_OPT = {}
+
+// ")
+
+// }
diff --git a/src/rust/src/thumbnail.cc b/src/rust/src/thumbnail.cc
new file mode 100644
index 0000000..bd911f3
--- /dev/null
+++ b/src/rust/src/thumbnail.cc
@@ -0,0 +1,274 @@
+#include "thumbnail.h"
+#include "rust/cxx.h"
+#include
+extern "C" {
+#include
+#include
+#include
+#include
+#include
+}
+
+#include "idescriptor_rust_codebase/src/bridge.cxxqt.h"
+
+QImage generate_thumbnail_with_reader(const AfcReader &reader,
+ int32_t file_size, int32_t requested_w,
+ int32_t requested_h)
+{
+ QImage result;
+
+ AVFormatContext *formatCtx = avformat_alloc_context();
+ if (!formatCtx) {
+ // qWarning() << "Failed to allocate format context";
+ return result;
+ }
+
+ struct StreamContext {
+ const AfcReader *reader;
+ int32_t fileSize;
+ int currentPos;
+ };
+
+ auto *streamCtx = new StreamContext{&reader, file_size, 0};
+
+ auto readPacket = [](void *opaque, uint8_t *buf, int bufSize) -> int {
+ auto *ctx = static_cast(opaque);
+
+ if (ctx->currentPos >= ctx->fileSize) {
+ return AVERROR_EOF;
+ }
+
+ int toRead = std::min(bufSize, ctx->fileSize - ctx->currentPos);
+ auto data = ctx->reader->read_at(ctx->currentPos, toRead);
+
+ if (data.empty()) {
+ return (toRead == 0) ? AVERROR_EOF : AVERROR(EIO);
+ }
+
+ const int n = std::min(data.size(), toRead);
+ memcpy(buf, data.data(), n);
+ ctx->currentPos += n;
+ return n;
+ };
+
+ auto seekPacket = [](void *opaque, int64_t offset, int whence) -> int64_t {
+ auto *ctx = static_cast(opaque);
+
+ if (whence == AVSEEK_SIZE) {
+ return ctx->fileSize;
+ }
+
+ int newPos = 0;
+ switch (whence) {
+ case SEEK_SET:
+ newPos = offset;
+ break;
+ case SEEK_CUR:
+ newPos = ctx->currentPos + offset;
+ break;
+ case SEEK_END:
+ newPos = ctx->fileSize + offset;
+ break;
+ default:
+ return -1;
+ }
+
+ if (newPos < 0 || newPos > ctx->fileSize) {
+ return -1;
+ }
+
+ ctx->currentPos = newPos;
+ return newPos;
+ };
+
+ const int avioBufferSize = 32768;
+ unsigned char *avioBuffer =
+ static_cast(av_malloc(avioBufferSize));
+ if (!avioBuffer) {
+ delete streamCtx;
+ avformat_free_context(formatCtx);
+ return {};
+ }
+
+ AVIOContext *avioCtx =
+ avio_alloc_context(avioBuffer, avioBufferSize, 0, streamCtx, readPacket,
+ nullptr, seekPacket);
+ if (!avioCtx) {
+ av_free(avioBuffer);
+ delete streamCtx;
+ avformat_free_context(formatCtx);
+ return {};
+ }
+
+ formatCtx->pb = avioCtx;
+ formatCtx->flags |= AVFMT_FLAG_CUSTOM_IO;
+
+ // Open input
+ if (avformat_open_input(&formatCtx, nullptr, nullptr, nullptr) < 0) {
+ // qWarning() << "Failed to open video format";
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ avformat_free_context(formatCtx);
+ return {};
+ }
+
+ // Find stream info
+ if (avformat_find_stream_info(formatCtx, nullptr) < 0) {
+ // qWarning() << "Failed to find stream info";
+ avformat_close_input(&formatCtx);
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ return {};
+ }
+
+ // Find video stream
+ int videoStreamIndex = -1;
+ const AVCodec *codec = nullptr;
+ AVCodecParameters *codecParams = nullptr;
+
+ for (unsigned int i = 0; i < formatCtx->nb_streams; i++) {
+ if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
+ videoStreamIndex = i;
+ codecParams = formatCtx->streams[i]->codecpar;
+ codec = avcodec_find_decoder(codecParams->codec_id);
+ break;
+ }
+ }
+
+ if (videoStreamIndex == -1 || !codec) {
+ // qWarning() << "No video stream found";
+ avformat_close_input(&formatCtx);
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ return {};
+ }
+
+ // Allocate codec context
+ AVCodecContext *codecCtx = avcodec_alloc_context3(codec);
+ if (!codecCtx) {
+ avformat_close_input(&formatCtx);
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ return {};
+ }
+
+ if (avcodec_parameters_to_context(codecCtx, codecParams) < 0) {
+ avcodec_free_context(&codecCtx);
+ avformat_close_input(&formatCtx);
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ return {};
+ }
+
+ // Open codec
+ if (avcodec_open2(codecCtx, codec, nullptr) < 0) {
+ avcodec_free_context(&codecCtx);
+ avformat_close_input(&formatCtx);
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ return {};
+ }
+
+ // Allocate frame
+ AVFrame *frame = av_frame_alloc();
+ AVPacket *packet = av_packet_alloc();
+
+ if (!frame || !packet) {
+ if (frame)
+ av_frame_free(&frame);
+ if (packet)
+ av_packet_free(&packet);
+ avcodec_free_context(&codecCtx);
+ avformat_close_input(&formatCtx);
+ av_free(avioCtx->buffer);
+ avio_context_free(&avioCtx);
+ return {};
+ }
+
+ // Read frames until we get a valid one
+ bool frameDecoded = false;
+ while (av_read_frame(formatCtx, packet) >= 0) {
+ if (packet->stream_index == videoStreamIndex) {
+ if (avcodec_send_packet(codecCtx, packet) >= 0) {
+ if (avcodec_receive_frame(codecCtx, frame) >= 0) {
+ frameDecoded = true;
+ av_packet_unref(packet);
+ break;
+ }
+ }
+ }
+ av_packet_unref(packet);
+ }
+
+ if (frameDecoded) {
+ // Get rotation from display matrix
+ double rotation = 0.0;
+ if (AVFrameSideData *sd =
+ av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX)) {
+ rotation =
+ -av_display_rotation_get(reinterpret_cast(sd->data));
+ }
+
+ // Convert frame to RGB24
+ SwsContext *swsCtx =
+ sws_getContext(frame->width, frame->height,
+ static_cast(frame->format),
+ frame->width, frame->height, AV_PIX_FMT_RGB24,
+ SWS_BILINEAR, nullptr, nullptr, nullptr);
+
+ if (swsCtx) {
+ AVFrame *rgbFrame = av_frame_alloc();
+ if (rgbFrame) {
+ rgbFrame->format = AV_PIX_FMT_RGB24;
+ rgbFrame->width = frame->width;
+ rgbFrame->height = frame->height;
+
+ if (av_frame_get_buffer(rgbFrame, 0) >= 0) {
+ sws_scale(swsCtx, frame->data, frame->linesize, 0,
+ frame->height, rgbFrame->data,
+ rgbFrame->linesize);
+
+ // Convert to QImage
+ QImage img(rgbFrame->data[0], rgbFrame->width,
+ rgbFrame->height, rgbFrame->linesize[0],
+ QImage::Format_RGB888);
+
+ // Create a deep copy since AVFrame will be freed
+ QImage imgCopy = img.copy();
+
+ // Apply rotation
+ if (rotation != 0.0) {
+ QTransform transform;
+ transform.rotate(rotation);
+ imgCopy = imgCopy.transformed(transform);
+ }
+
+ result = imgCopy;
+ // Scale to requested size
+ /*
+ TODO: scaling might become optional
+ if we ever needed the raw frame,
+ might need to abstract the main logic to get the
+ frame and handle scaling separately
+ */
+ // result =
+ // imgCopy.scaled(requestedSize,
+ // Qt::KeepAspectRatio,
+ // Qt::SmoothTransformation);
+ }
+
+ av_frame_free(&rgbFrame);
+ }
+
+ sws_freeContext(swsCtx);
+ }
+ }
+
+ // Cleanup
+ av_frame_free(&frame);
+ av_packet_free(&packet);
+ avcodec_free_context(&codecCtx);
+ avformat_close_input(&formatCtx);
+
+ return result;
+}
\ No newline at end of file
diff --git a/src/rust/src/utils.rs b/src/rust/src/utils.rs
index 558a02c..f781c70 100644
--- a/src/rust/src/utils.rs
+++ b/src/rust/src/utils.rs
@@ -8,6 +8,7 @@ use idevice::{
use plist::Dictionary as PlistDictionary;
use plist_macro::plist;
use rusqlite::Connection;
+use serde_json::json;
use std::path::PathBuf;
pub const PUBLIC_STAGING: &str = "PublicStaging";
@@ -98,7 +99,7 @@ pub fn query_gallery_usage(db_bytes: &mut Vec) -> Result Result<(), IdeviceErr
Err(_) => afc.mk_dir(PUBLIC_STAGING).await,
}
}
+
+// converts album info to json
+pub fn create_album_info(
+ album_id: i32,
+ item_count: i32,
+ asset_dir: String,
+ asset_file_name: String,
+) -> String {
+ json!({"album_id" : album_id, "item_count" : item_count,"file_path" : format!("{}/{}",asset_dir,asset_file_name)})
+ .to_string()
+}
diff --git a/src/thumbnailprovider.h b/src/thumbnailprovider.h
new file mode 100644
index 0000000..3af1d51
--- /dev/null
+++ b/src/thumbnailprovider.h
@@ -0,0 +1,79 @@
+#include "idescriptor_rust_codebase/src/image_loader.cxxqt.h"
+#include
+#include
+#include
+
+class ThumbnailProvider : public QQuickImageProvider
+{
+ Q_OBJECT
+public:
+ ThumbnailProvider() : QQuickImageProvider(Image)
+ {
+ // 350 MB
+ m_cache.setMaxCost(350 * 1024 * 1024);
+ connect(&m_imageLoader, &ImageBackend::thumbnail_ready, this,
+ [this](const QString &path, const QImage &img,
+ unsigned int rowHint) {
+ insert(path, img);
+ qDebug() << "thumb ready in provider" << path << "Row"
+ << rowHint;
+ emit thumbnailReady(path, img, rowHint);
+ });
+ }
+
+ static ThumbnailProvider *sharedInstance()
+ {
+ static auto *instance = new ThumbnailProvider();
+ return instance;
+ }
+
+ QImage requestImage(const QString &id, QSize *size,
+ const QSize &requestedSize) override
+ {
+ const QUrl url(QStringLiteral("image://thumb/") + id);
+ const QString path = url.path().mid(1); // strip leading '/'
+ const QUrlQuery query(url);
+ const QString udid = query.queryItemValue("udid");
+ const qint32 index = query.queryItemValue("index").toInt();
+
+ // FIXME: dont use path for key
+ if (m_cache.contains(path)) {
+ qDebug() << "Serving from provider cache";
+ const QImage img = *m_cache.object(path);
+ if (size)
+ *size = img.size();
+ return img;
+ }
+
+ qDebug() << "path" << path << "udid" << udid;
+
+ m_imageLoader.request_thumbnail(udid, path, index);
+
+ const QString resPath = QStringLiteral(
+ ":/resources/icons/MaterialSymbolsLightImageOutlineSharp.png");
+ qDebug() << "ThumbnailProvider: requestImage id=" << id
+ << " requestedSize=" << requestedSize;
+
+ QImage placeholder(resPath);
+
+ if (size)
+ *size = placeholder.size();
+ return placeholder;
+ }
+
+ void insert(const QString &id, const QImage &img)
+ {
+ QImage *heapImg = new QImage(img);
+
+ int cacheCost =
+ heapImg->width() * heapImg->height() * heapImg->depth() / 8;
+ m_cache.insert(id, heapImg, cacheCost);
+ }
+signals:
+ void thumbnailReady(const QString &path, const QImage &data,
+ unsigned int rowHint);
+
+private:
+ QCache m_cache;
+ ImageBackend m_imageLoader;
+};
\ No newline at end of file