mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
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:
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,8 @@ Button {
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth : 0
|
||||
// Layout.fillHeight: true
|
||||
background: Rectangle {
|
||||
color : "transparent"
|
||||
}
|
||||
|
||||
+2
-3
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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("/")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+12
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "rust/cxx.h"
|
||||
#include <QImage>
|
||||
|
||||
QImage load_heic(rust::Vec<uint8_t> data);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {}
|
||||
|
||||
// ")
|
||||
|
||||
// }
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user