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