mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
feat(InstalledApps): implement InstalledApps
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ".."
|
||||
|
||||
Button {
|
||||
id: root
|
||||
|
||||
required property var device
|
||||
required property string appName
|
||||
required property string bundleId
|
||||
property string version: ""
|
||||
required property string iconSource
|
||||
property bool selected: false
|
||||
|
||||
signal appClicked(string bundleId)
|
||||
|
||||
width: ListView.view ? ListView.view.width : implicitWidth
|
||||
height: 60
|
||||
padding: 0
|
||||
hoverEnabled: true
|
||||
enabled: true
|
||||
onClicked: root.appClicked(root.bundleId)
|
||||
|
||||
Component.onCompleted: {
|
||||
device.sb_client.fetch_app_icon(root.bundleId)
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 10
|
||||
color: root.selected ? palette.highlight : (root.hovered ? Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.1) : Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.06))
|
||||
border.width: 1
|
||||
border.color: root.selected ? Qt.lighter(palette.highlight, 1.15) : Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.12)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.topMargin: 8
|
||||
anchors.bottomMargin: 8
|
||||
spacing: 10
|
||||
|
||||
// Rectangle {
|
||||
// Layout.preferredWidth: 32
|
||||
// Layout.preferredHeight: 32
|
||||
// radius: 7
|
||||
// color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.08)
|
||||
// clip: true
|
||||
|
||||
// IconLoader {
|
||||
// id: iconImg
|
||||
// iconSource: root.iconSource
|
||||
// // visible: root.iconSource.length > 0 && status === Image.Ready
|
||||
// }
|
||||
|
||||
// // Text {
|
||||
// // anchors.centerIn: parent
|
||||
// // text: root.appName.length > 0 ? root.appName.charAt(0).toUpperCase() : qsTr("?")
|
||||
// // color: root.selected ? palette.highlightedText : palette.text
|
||||
// // font.bold: true
|
||||
// // font.pixelSize: 14
|
||||
// // visible: root.iconSource.length === 0 || iconImg.status !== Image.Ready
|
||||
// // }
|
||||
// }
|
||||
|
||||
IconLoader {
|
||||
id: iconImg
|
||||
iconSource: root.iconSource
|
||||
// visible: root.iconSource.length > 0 && status === Image.Ready
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.appName
|
||||
color: root.selected ? palette.highlightedText : palette.text
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.version
|
||||
color: root.selected ? Qt.rgba(palette.highlightedText.r, palette.highlightedText.g, palette.highlightedText.b, 0.75) : Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.62)
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: root.version.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
import ".."
|
||||
import "../base"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string udid
|
||||
required property var device
|
||||
property bool loaded: false
|
||||
property bool loading: true
|
||||
property bool fileSharingOnly: true
|
||||
property string errorMessage: ""
|
||||
property string searchText: ""
|
||||
property string selectedBundleId: ""
|
||||
property var houseArrestAfcClient: null
|
||||
property bool tabsDisabled: false
|
||||
property var iconForBundleId: ({})
|
||||
property var appTabForBundleId: ({})
|
||||
|
||||
function init() {
|
||||
if (loaded)
|
||||
return ;
|
||||
|
||||
loaded = true;
|
||||
refresh();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
root.loading = true;
|
||||
root.errorMessage = "";
|
||||
root.selectedBundleId = "";
|
||||
root.houseArrestAfcClient = null;
|
||||
allAppsModel.clear();
|
||||
filteredAppsModel.clear();
|
||||
updateViewState();
|
||||
if (!root.device || !root.device.service_manager) {
|
||||
root.loading = false;
|
||||
root.errorMessage = qsTr("Failed to retrieve installed apps.");
|
||||
updateViewState();
|
||||
return ;
|
||||
}
|
||||
root.device.service_manager.fetch_installed_apps();
|
||||
}
|
||||
|
||||
function updateViewState() {
|
||||
if (root.loading) {
|
||||
stateView.viewState = StateView.State.Loading;
|
||||
return ;
|
||||
}
|
||||
if (root.errorMessage.length > 0) {
|
||||
stateView.errorText = root.errorMessage;
|
||||
stateView.viewState = StateView.State.Error;
|
||||
return ;
|
||||
}
|
||||
stateView.viewState = StateView.State.Content;
|
||||
}
|
||||
|
||||
function appDisplayName(app) {
|
||||
var name = app.displayName && app.displayName.length > 0 ? app.displayName : app.bundleId;
|
||||
if (app.appType === "System")
|
||||
name += qsTr(" (System)");
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function appendApp(appJson) {
|
||||
var app = null;
|
||||
try {
|
||||
app = JSON.parse(appJson);
|
||||
} catch (e) {
|
||||
console.log("InstalledApps: failed to parse app JSON:", e);
|
||||
return ;
|
||||
}
|
||||
var bundleId = app.bundle_id || "";
|
||||
/*
|
||||
Always fails to load Fitness app container
|
||||
even though file sharing is enabled
|
||||
*/
|
||||
if (bundleId.length === 0 || bundleId === "com.apple.Fitness")
|
||||
return ;
|
||||
|
||||
allAppsModel.append({
|
||||
"appName": appDisplayName({
|
||||
"displayName": app.CFBundleDisplayName || "",
|
||||
"bundleId": bundleId,
|
||||
"appType": app.app_type || ""
|
||||
}),
|
||||
"displayName": app.CFBundleDisplayName || bundleId,
|
||||
"bundleId": bundleId,
|
||||
"version": app.CFBundleShortVersionString || "",
|
||||
"appType": app.app_type || "",
|
||||
"fileSharingEnabled": !!app.UIFileSharingEnabled,
|
||||
"iconSource": ""
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: use a proper filtering model
|
||||
function rebuildFilteredApps() {
|
||||
filteredAppsModel.clear();
|
||||
var needle = root.searchText.toLowerCase();
|
||||
for (var i = 0; i < allAppsModel.count; i++) {
|
||||
var app = allAppsModel.get(i);
|
||||
if (root.fileSharingOnly && !app.fileSharingEnabled)
|
||||
continue;
|
||||
|
||||
if (needle.length > 0) {
|
||||
var name = (app.appName || "").toLowerCase();
|
||||
var bundleId = (app.bundleId || "").toLowerCase();
|
||||
if (name.indexOf(needle) === -1 && bundleId.indexOf(needle) === -1)
|
||||
continue;
|
||||
|
||||
}
|
||||
filteredAppsModel.append({
|
||||
"appName": app.appName,
|
||||
"displayName": app.displayName,
|
||||
"bundleId": app.bundleId,
|
||||
"version": app.version,
|
||||
"appType": app.appType,
|
||||
"fileSharingEnabled": app.fileSharingEnabled,
|
||||
"iconSource" : app.iconSource,
|
||||
"selected": root.selectedBundleId === app.bundleId
|
||||
});
|
||||
}
|
||||
if (filteredAppsModel.count > 0 && !root.selectedBundleId)
|
||||
selectApp(filteredAppsModel.get(0).bundleId);
|
||||
}
|
||||
|
||||
function selectApp(bundleId) {
|
||||
if (!bundleId || bundleId.length === 0 || root.tabsDisabled)
|
||||
return ;
|
||||
|
||||
root.selectedBundleId = bundleId;
|
||||
root.tabsDisabled = true;
|
||||
root.houseArrestAfcClient = null;
|
||||
if (typeof serviceFactory === "undefined" || !serviceFactory) {
|
||||
root.tabsDisabled = false;
|
||||
root.errorMessage = qsTr("serviceFactory is not available in QML scope.");
|
||||
updateViewState();
|
||||
return ;
|
||||
}
|
||||
var client = serviceFactory.create_hause_arrest_afc_client(root.udid, bundleId);
|
||||
root.houseArrestAfcClient = client;
|
||||
root.tabsDisabled = false;
|
||||
if (client === null)
|
||||
// FIXME: expose a success/error signal for house arrest session creation if this factory can fail asynchronously.
|
||||
containerMessage.text = qsTr("No data available for this app.");
|
||||
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
Component.onCompleted: {
|
||||
updateViewState();
|
||||
loadTimer.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: loadTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: root.init()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onInstalled_apps_retrieved(success, apps) {
|
||||
allAppsModel.clear();
|
||||
filteredAppsModel.clear();
|
||||
if (!success) {
|
||||
root.loading = false;
|
||||
root.errorMessage = qsTr("Failed to retrieve installed apps.");
|
||||
updateViewState();
|
||||
return ;
|
||||
}
|
||||
for (var key in apps) appendApp(apps[key])
|
||||
root.loading = false;
|
||||
if (allAppsModel.count === 0)
|
||||
// FIXME: return success as well
|
||||
root.errorMessage = qsTr("No apps found or failed to retrieve apps.");
|
||||
else
|
||||
root.errorMessage = "";
|
||||
updateViewState();
|
||||
rebuildFilteredApps();
|
||||
}
|
||||
|
||||
target: root.device && root.device.service_manager ? root.device.service_manager : null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target : root.device.sb_client
|
||||
|
||||
function onApp_icon_loaded(bundleId, iconData) {
|
||||
var tab = root.appTabForBundleId[bundleId];
|
||||
if (tab) {
|
||||
tab.iconSource = iconData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: allAppsModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredAppsModel
|
||||
}
|
||||
|
||||
StateView {
|
||||
id: stateView
|
||||
autoSwitchContent: false
|
||||
anchors.fill: parent
|
||||
viewState: StateView.State.Loading
|
||||
retryable: true
|
||||
onRetryRequested: root.refresh()
|
||||
|
||||
contentItem: RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 400
|
||||
Layout.minimumWidth: 100
|
||||
Layout.maximumWidth: 500
|
||||
Layout.fillHeight: true
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.12)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 60
|
||||
Layout.leftMargin: 5
|
||||
Layout.rightMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
spacing: 8
|
||||
|
||||
TextField {
|
||||
id: searchEdit
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Search apps...")
|
||||
text: root.searchText
|
||||
onTextChanged: {
|
||||
root.searchText = text;
|
||||
rebuildFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: fileSharingCheck
|
||||
|
||||
checked: root.fileSharingOnly
|
||||
text: qsTr("File Sharing")
|
||||
font.pixelSize: 10
|
||||
onToggled: {
|
||||
root.fileSharingOnly = checked;
|
||||
rebuildFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: appList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 10
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: filteredAppsModel
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: appList.width
|
||||
height: 60
|
||||
|
||||
AppTab {
|
||||
id : tab
|
||||
Component.onCompleted: {
|
||||
root.appTabForBundleId[model.bundleId] = tab;
|
||||
}
|
||||
device: root.device
|
||||
width: appList.width
|
||||
appName: model.appName
|
||||
bundleId: model.bundleId
|
||||
version: model.version
|
||||
selected: root.selectedBundleId === model.bundleId
|
||||
enabled: !root.tabsDisabled
|
||||
iconSource: model.iconSource
|
||||
onAppClicked: (bundleId) => {
|
||||
return root.selectApp(bundleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
FileExplorer {
|
||||
id: explorer
|
||||
|
||||
anchors.fill: parent
|
||||
afcClient: root.houseArrestAfcClient
|
||||
rootPath: "/Documents"
|
||||
favEnabled: false
|
||||
visible: !!root.houseArrestAfcClient
|
||||
}
|
||||
|
||||
Text {
|
||||
id: containerMessage
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width * 0.8, 420)
|
||||
text: root.selectedBundleId.length > 0 ? qsTr("No data available for this app.") : qsTr("Select an app to browse its documents.")
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.72)
|
||||
visible: !root.houseArrestAfcClient
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user