refactor(image-pipeline): move image loading logic to rust

- Implement Gallery in QML
- Read gallery from `sqlitedb`
- Implement `ThumbnailProvider` to handle thumbnail requests and caching
- Move `FluentUI` related QML code to `windows` folder
This commit is contained in:
uncor3
2026-04-30 05:45:39 +00:00
parent e989212264
commit e76ed0fd71
29 changed files with 2372 additions and 191 deletions
+3
View File
@@ -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
)
+7 -1
View File
@@ -1,11 +1,17 @@
<RCC>
<qresource prefix="/">
<file>src/qml/Main.qml</file>
<file>src/qml/Index.qml</file>
<!-- <file>src/qml/Index.qml</file> -->
<file>src/qml/Tabs.qml</file>
<file>src/qml/HowToConnect.qml</file>
<file>src/qml/Welcome.qml</file>
<file>src/qml/TabButton.qml</file>
<file>src/qml/DeviceTab.qml</file>
<file>src/qml/Device.qml</file>
<file>src/qml/DeviceImage.qml</file>
<file>src/qml/DeviceInfo.qml</file>
<file>src/qml/DeviceGallery.qml</file>
<file>src/qml/AlbumContents.qml</file>
<file>src/qml/PreviewWindow.qml</file>
</qresource>
</RCC>
+6 -2
View File
@@ -31,7 +31,8 @@
#ifdef WIN32
#include "platform/windows/win_common.h"
#endif
// #include "thumbnailmodel.h"
#include "thumbnailprovider.h"
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickWindow>
@@ -132,8 +133,11 @@ int main(int argc, char *argv[])
engine.addImportPath("C:/Qt/6.8.3/mingw_64/qml");
#endif
Constants constants;
// qmlRegisterType<ThumbnailModel>("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();
+207
View File
@@ -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)
}
}
}
}
}
+22 -56
View File
@@ -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
}
}
+233
View File
@@ -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
}
}
}
+135
View File
@@ -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")
}
}
}
+112
View File
@@ -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: "WiFi 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 }
}
}
}
}
+46
View File
@@ -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
}
}
+47 -42
View File
@@ -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
}
}
+35
View File
@@ -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
}
}
+2 -1
View File
@@ -15,7 +15,8 @@ Button {
}
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth : 0
// Layout.fillHeight: true
background: Rectangle {
color : "transparent"
}
+2 -3
View File
@@ -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
+3 -3
View File
@@ -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 }
+50
View File
@@ -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("/")
}
}
}
+12
View File
@@ -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"
+2 -1
View File
@@ -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"] }
+22 -4
View File
@@ -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();
}
.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();
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include "rust/cxx.h"
#include <QImage>
QImage load_heic(rust::Vec<uint8_t> data);
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "rust/cxx.h"
#include <QImage>
class AfcReader;
QImage generate_thumbnail_with_reader(const AfcReader &reader,
int32_t file_size, int32_t requested_w,
int32_t requested_h);
+94
View File
@@ -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<u8> {
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<u8>;
}
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;
}
}
+354
View File
@@ -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<Arc<Semaphore>> = Lazy::new(|| Arc::new(Semaphore::new(10)));
static SCHEDULER: Lazy<Arc<Scheduler>> = 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<qobject::ImageBackend>,
}
struct QueueState {
pq: PriorityQueue<JobKey, (u32, Reverse<u64>)>,
payloads: HashMap<JobKey, JobPayload>,
}
struct Scheduler {
state: Mutex<QueueState>,
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<u8> {
// 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<u8> {
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);
}
}
+126 -77
View File
@@ -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<cxx_qt_lib::QMapPair_QString_QVariant>;
@@ -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::<QMapPair_QString_QVariant>::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::<QMapPair_QString_QVariant>::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::<QMapPair_QString_QVariant>::default());
})
.ok();
}
@@ -524,7 +529,7 @@ async fn emit_connected(qt_thread: cxx_qt::CxxQtThread<Core>, 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<Core>,
) -> Result<(String, String), IdeviceError> {
) -> Result<(String, QMap<QMapPair_QString_QVariant>), IdeviceError> {
let provider_name = type_name::<T>();
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<QMap<QMapPair_QString_QVariant>, IdeviceError> {
let mut info = QMap::<QMapPair_QString_QVariant>::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<Core>,
@@ -798,7 +847,7 @@ async fn spawn_heartbeat_task(
core_qobj.device_event(
EV_DISCONNECTED,
&QString::from(udid_for_event),
&QString::from(""),
&QMap::<QMapPair_QString_QVariant>::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::<QMapPair_QString_QVariant>::default(),
);
});
break;
+85
View File
@@ -0,0 +1,85 @@
/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "rust/cxx.h"
// #include <QDebug>
#include <QImage>
#include <libheif/heif.h>
QImage load_heic(rust::Vec<uint8_t> &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;
}
+387
View File
@@ -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<QString>;
type QByteArray = cxx_qt_lib::QByteArray;
type QMap_QString_QVariant = cxx_qt_lib::QMap<cxx_qt_lib::QMapPair_QString_QVariant>;
}
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<QMapPair_QString_QVariant>,
error: QString,
connection: Option<Arc<Mutex<Connection>>>,
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<dyn std::error::Error + Send + Sync>> = (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::<QMapPair_QString_QVariant>::default();
let res: Result<(), Box<dyn std::error::Error + Send + Sync>> = (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<QList<QString>, Box<dyn std::error::Error + Send + Sync>> = (async {
let con = con_arc.lock().await;
let mut list: QList<QString> = 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 = {}
// ")
// }
+274
View File
@@ -0,0 +1,274 @@
#include "thumbnail.h"
#include "rust/cxx.h"
#include <QImage>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/display.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#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<StreamContext *>(opaque);
if (ctx->currentPos >= ctx->fileSize) {
return AVERROR_EOF;
}
int toRead = std::min<int>(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<int>(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<StreamContext *>(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<unsigned char *>(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<int32_t *>(sd->data));
}
// Convert frame to RGB24
SwsContext *swsCtx =
sws_getContext(frame->width, frame->height,
static_cast<AVPixelFormat>(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;
}
+13 -1
View File
@@ -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<u8>) -> Result<u64, rusqlite::Erro
unsafe {
let db_ptr = rusqlite::ffi::sqlite3_deserialize(
conn.handle(),
b"main\0".as_ptr() as *const i8,
b"main\0".as_ptr() as *const std::os::raw::c_char,
db_bytes.as_mut_ptr(),
db_bytes.len() as i64,
db_bytes.len() as i64,
@@ -157,3 +158,14 @@ pub async fn ensure_public_staging(afc: &mut AfcClient) -> 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()
}
+79
View File
@@ -0,0 +1,79 @@
#include "idescriptor_rust_codebase/src/image_loader.cxxqt.h"
#include <QCache>
#include <QQuickImageProvider>
#include <QUrlQuery>
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<QString, QImage> m_cache;
ImageBackend m_imageLoader;
};