399 lines
21 KiB
QML
399 lines
21 KiB
QML
/*
|
|
*
|
|
* Copyright 2022,2023 Peter G. (nephros) <sailfish@nephros.org>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
import QtQuick 2.6
|
|
import Sailfish.Silica 1.0
|
|
// for LauncherIcon
|
|
import Sailfish.Lipstick 1.0
|
|
// for LauncherModel, LauncherItem and friends
|
|
import org.nemomobile.lipstick 0.1
|
|
|
|
Page {
|
|
id: page
|
|
|
|
allowedOrientations: Orientation.All
|
|
|
|
property bool unclutter: false
|
|
property bool showLocal: true
|
|
property string historyfile: "/var/log/zypp/history"
|
|
|
|
property int progress: 0
|
|
|
|
Connections {
|
|
onStatusChanged: {
|
|
if (pageStack.busy) {
|
|
pageStack.busyChanged.connect(pushStatsPage)
|
|
} else {
|
|
pushStatsPage()
|
|
}
|
|
}
|
|
}
|
|
|
|
function pushStatsPage() {
|
|
if ( status === PageStatus.Active && pageStack.nextPage() === null && !pageStack.busy ) {
|
|
pageStack.pushAttached(statsPage);
|
|
pageStack.busyChanged.disconnect(pushStatsPage);
|
|
}
|
|
}
|
|
|
|
Component { id: statsPage; StatsPage{ } }
|
|
|
|
DockedPanel { id: detailInfo
|
|
z: 10 // this fixes transparency and focus problems
|
|
|
|
LauncherItem{ id: appInfo; filePath: "/usr/share/applications/" + detailInfo.appName + ".desktop" }
|
|
property string displayName: appInfo.isValid ? appInfo.title : ""
|
|
|
|
property var date
|
|
property var dateTime
|
|
property string appName
|
|
property string version
|
|
property string repo
|
|
property bool install
|
|
|
|
dock: Dock.Bottom
|
|
modal: true
|
|
animationDuration : 250
|
|
height: content.height
|
|
width: parent.width
|
|
Rectangle {
|
|
clip: true
|
|
anchors.fill: parent
|
|
anchors.centerIn: parent
|
|
radius: Theme.paddingSmall
|
|
//color: Theme.rgba(Theme.highlightDimmerColor, Theme.opacityOverlay)
|
|
//color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityOverlay)
|
|
//color: Theme.rgba(Theme.highlightDimmerFromColor(Theme.secondaryHighlightColor, Theme.colorScheme), Theme.opacityOverlay)
|
|
gradient: Gradient {
|
|
GradientStop { position: 0; color: Theme.rgba(Theme.highlightDimmerFromColor(Theme.secondaryHighlightColor, Theme.colorScheme), Theme.opacityOverlay) }
|
|
GradientStop { position: 2; color: Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityOverlay) }
|
|
}
|
|
|
|
}
|
|
Separator { anchors.verticalCenter: parent.top; anchors.horizontalCenter: parent.horizontalCenter;
|
|
width: parent.width ; height: Theme.paddingSmall
|
|
color: Theme.primaryColor;
|
|
horizontalAlignment: Qt.AlignHCenter
|
|
}
|
|
Row { id: content
|
|
anchors.centerIn: parent
|
|
width: parent.width
|
|
spacing: Theme.paddingMedium
|
|
padding: Theme.paddingLarge
|
|
LauncherIcon { id: icon
|
|
anchors.verticalCenter: detailCol.verticalCenter
|
|
width: size
|
|
icon: appInfo.isValid ? appInfo.iconId : "icon-m-file-rpm"
|
|
opacity: 1.0
|
|
size: Theme.iconSizeLarge
|
|
fillMode: Image.PreserveAspectFit
|
|
cache: true
|
|
smooth: false
|
|
asynchronous: true
|
|
}
|
|
Column { id: detailCol
|
|
width: parent.width - icon.width
|
|
Repeater { model: detailModel; delegate: Component { DetailItem{ width: detailCol.width; alignment: Qt.AlignLeft; visible: lbl.length > 0; label: lbl; value: val } } }
|
|
}
|
|
}
|
|
ListModel{ id: detailModel }
|
|
onExpandedChanged: updateModel()
|
|
function updateModel() {
|
|
detailModel.clear();
|
|
if (appInfo.isValid) {
|
|
detailModel.append( { "lbl": qsTr("Application"), "val": detailInfo.displayName });
|
|
}
|
|
detailModel.append( { "lbl": qsTr("Package"), "val": detailInfo.appName });
|
|
detailModel.append( { "lbl": qsTr("Version"), "val": detailInfo.version });
|
|
detailModel.append( { "lbl": detailInfo.install ? qsTr("Installed") : qsTr("Removed"),
|
|
"val": Format.formatDate(detailInfo.dateTime, Formatter.DateMedium)
|
|
+ " " + Format.formatDate(detailInfo.dateTime, Formatter.TimeValue)
|
|
});
|
|
detailModel.append( { "lbl": qsTr("Repository"), "val": detailInfo.repo ? detailInfo.repo : qsTr("n/a") } );
|
|
if (appInfo.isValid) {
|
|
detailModel.append( { "lbl": qsTr("Executes"), "val": appInfo.exec });
|
|
}
|
|
/*
|
|
if (appInfo.isValid) {
|
|
detailModel.append( { "lbl": qsTr("Sandboxed"), "val": detailInfo.appInfo.isSandboxed.toString() });
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
PullDownMenu { id: pdp
|
|
flickable: flick
|
|
MenuItem { text: qsTr("Refresh");
|
|
// refresh on delayedclick otherwise the bounce animation freezes while we reload
|
|
onDelayedClick: { updateHistory(page.historyfile,showLocal); }
|
|
}
|
|
MenuItem { text: showLocal ? qsTr("Hide %1", "show/hide local menu option").arg(qsTr("Local Installs", "menu option parameter")) : qsTr("Show %1", "show/hide local menu option").arg(qsTr("Local Installs", "menu option parameter")) ;
|
|
// refresh on delayedclick otherwise the bounce animation freezes while we reload
|
|
onClicked: { showLocal = !showLocal }
|
|
onDelayedClick: { updateHistory(page.historyfile,showLocal); }
|
|
}
|
|
MenuItem { text: page.unclutter ? qsTr("Verbose Display", "menu option") : qsTr("Reduced Display", "menu option") ;
|
|
onClicked: page.unclutter = !page.unclutter
|
|
}
|
|
MenuItem { text: dateSearch.active ? qsTr("Hide search") : qsTr("Search by Date") ;
|
|
onClicked: {
|
|
dateSearch.active = !dateSearch.active
|
|
nameSearch.active = false
|
|
}
|
|
}
|
|
MenuItem { text: nameSearch.active ? qsTr("Hide search") : qsTr("Search by Name") ;
|
|
onClicked: {
|
|
nameSearch.active = !nameSearch.active
|
|
dateSearch.active = false
|
|
}
|
|
}
|
|
}
|
|
|
|
SilicaFlickable { id: flick
|
|
anchors.fill: parent
|
|
PageHeader { id: header ; title: qsTr("Install History") ; description: (appHistoryModel.count > 0) ? qsTr("%Ln event(s)", "very, very unlikely to have only one, still, plurals please!", appHistoryModel.count) : qsTr("Loading…")
|
|
}
|
|
Column {
|
|
id: searchBar
|
|
anchors.top: header.bottom
|
|
width: parent.width
|
|
SearchField { id: dateSearch
|
|
active: false
|
|
width: parent.width - Theme.horizontalPageMargin
|
|
readOnly: true
|
|
placeholderText: view.jumpDate != null ? view.jumpDate.toISOString().substr(0,10) : qsTr("Date")
|
|
onClicked: {
|
|
var dialog = pageStack.push(datePicker)
|
|
dialog.accepted.connect( function() { view.jumpDate = dialog.date; })
|
|
}
|
|
Separator { anchors.verticalCenter: parent.bottom; width: parent.width; color: Theme.primaryColor;}
|
|
}
|
|
SearchField { id: nameSearch
|
|
active: false
|
|
width: parent.width - Theme.horizontalPageMargin
|
|
placeholderText: qsTr("Name")
|
|
inputMethodHints: Qt.ImhNoAutoUppercase
|
|
EnterKey.enabled: text.length > 3
|
|
EnterKey.iconSource: "image://theme/icon-m-enter-next"
|
|
EnterKey.onClicked: view.findNames(text)
|
|
onVisibleChanged: { focus = visible }
|
|
Separator { anchors.verticalCenter: parent.bottom; width: parent.width; color: Theme.primaryColor;}
|
|
}
|
|
}
|
|
|
|
SilicaListView { id: view
|
|
anchors.top: searchBar.bottom
|
|
height: parent.height - (header.height + searchBar.height)
|
|
width: parent.width - Theme.horizontalPageMargin
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
cacheBuffer: page.height * 2
|
|
populate: Transition { NumberAnimation { properties: "y"; from: 100; duration: 600 } }
|
|
clip: true
|
|
spacing: Theme.paddingMedium
|
|
model: appHistoryModel
|
|
section {
|
|
labelPositioning: page.unclutter ? ViewSection.CurrentLabelAtStart : ViewSection.InlineLabels
|
|
property: "date"
|
|
delegate: SectionHeader {
|
|
text: Format.formatDate(section, Formatter.TimepointSectionRelative)
|
|
font.capitalization: Font.Capitalize
|
|
}
|
|
}
|
|
|
|
property var jumpDate: null
|
|
onJumpDateChanged: if (jumpDate != null) { view.jumpTo(jumpDate) }
|
|
/*
|
|
* TODO: make this smarter:
|
|
*
|
|
* maybe have a separate object containing a search index with only the first occurrence of each date
|
|
* filled on first use and re-used
|
|
*/
|
|
function jumpTo(jumpDate) {
|
|
console.time("Jump search took: ");
|
|
var pos = findIndex(appHistoryModel, function(item) { return item.date.match("^" + jumpDate.toISOString().substr(0,10)) });
|
|
if (pos) {
|
|
positionViewAtIndex(pos, ListView.Beginning);
|
|
currentIndex = pos;
|
|
return;
|
|
} else {
|
|
var searchDate = jumpDate;
|
|
for (i=Number(jumpDate.toISOString().substr(-2)); i > 0; i--) {
|
|
searchDate = searchDate - 864e5; // remove 86,400,000 milliseconds == one day
|
|
pos = find(appHistoryModel, function(item) { return item.date.match("^" + jumpDate.toISOString().substr(0,10)) });
|
|
if (pos) {
|
|
positionViewAtIndex(pos, ListView.Beginning);
|
|
currentIndex = pos;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
console.timeEnd("Jump search took: ");
|
|
}
|
|
|
|
function findNames(name) {
|
|
console.time("Name search took: ");
|
|
var pos = findIndex(appHistoryModel, function(item) { return item.appName.match(name) });
|
|
positionViewAtIndex(pos, ListView.Beginning);
|
|
currentIndex = pos;
|
|
console.timeEnd("Name search took: ");
|
|
}
|
|
/* find something, return index */
|
|
function findIndex(model, criteria) {
|
|
for(var i = ((currentIndex > 0) ? currentIndex+1 : 0); i < model.count; ++i) if (criteria(model.get(i))) return i
|
|
return -1
|
|
}
|
|
|
|
/* find something, return object */
|
|
function find(model, criteria) {
|
|
for(var i = 0; i < model.count; ++i) if (criteria(model.get(i))) return model.get(i)
|
|
return null
|
|
}
|
|
|
|
highlight: highlightBar
|
|
currentIndex: -1
|
|
delegate: Component { id: historyItem
|
|
ListItem { id: li
|
|
width: ListView.view.width
|
|
contentHeight: packagerow.height
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
property bool install: (action == "install")
|
|
property bool unclutter: page.unclutter
|
|
menu: Component { ContextMenu {
|
|
//MenuItem { enabled: false; text: qsTr("Search in %1").arg(qsTr("Jolla Store")) ; onClicked: Qt.openUrlExternally("" + appName) }
|
|
MenuItem { enabled: repoType == "openrepos" ; text: qsTr("Search on %1").arg("OpenRepos.") ; onClicked: Qt.openUrlExternally("https://openrepos.net/search/node/" + appName) }
|
|
MenuItem { enabled: repoType == "chum" ; text: qsTr("Search on %1").arg("SailfishOS:Chum") ; onClicked: Qt.openUrlExternally("https://build.sailfishos.org/package/show/sailfishos:chum/" + appName) }
|
|
}
|
|
}
|
|
onClicked: {
|
|
detailInfo.appName = appName;
|
|
detailInfo.version = version;
|
|
detailInfo.repo = repo;
|
|
detailInfo.date = date;
|
|
detailInfo.dateTime = dateTime;
|
|
detailInfo.install = install;
|
|
detailInfo.show();
|
|
}
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.centerIn: parent
|
|
radius: Theme.paddingSmall
|
|
color: install ? Theme.rgba(Theme.secondaryColor, 0.05) : Theme.rgba(Theme.overlayBackgroundColor, Theme.opacityFaint)
|
|
}
|
|
Icon { id: plusicon;
|
|
anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter
|
|
height: unclutter ? Theme.iconSizeSmall : Theme.iconSizeMedium
|
|
source: install ? "image://theme/icon-m-add?" + Theme.highlightFromColor(Theme.presenceColor(Theme.PresenceAvailable), Theme.colorScheme) : "image://theme/icon-m-remove?" + Theme.highlightFromColor(Theme.presenceColor(Theme.PresenceAway), Theme.colorScheme)
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
Row { id: packagerow
|
|
anchors.verticalCenter: plusicon.verticalCenter
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.left: plusicon.right
|
|
anchors.margins: Theme.paddingSmall
|
|
width: parent.width - plusicon.width
|
|
Column { id: col
|
|
width: parent.width
|
|
Row { spacing: Theme.paddingSmall
|
|
visible: li.unclutter
|
|
// name, version: uncluttered
|
|
Label{ visible: li.unclutter;
|
|
font.pixelSize: Theme.fontSizeSmall; color: Theme.secondaryColor;
|
|
horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight
|
|
width: Math.min(implicitWidth, col.width * 2/3);
|
|
maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideMiddle;
|
|
text: page.isLandscape ?
|
|
Format.formatDate(dateTime, Formatter.DateMedium) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
: Format.formatDate(dateTime, Formatter.DateMediumWithoutYear) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
}
|
|
Label{ text: appName ; maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideMiddle; horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight; color: install ? Theme.highlightColor : Theme.primaryColor }
|
|
Label{ visible: page.isLandscape; text: version ; maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideLeft; horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight; color: install ? Theme.secondaryHighlightColor : Theme.secondaryColor}
|
|
Label{ visible: ( install && page.isLandscape)
|
|
text: repo; color: Theme.highlightBackgroundColor; font.pixelSize: Theme.fontSizeSmall}
|
|
}
|
|
Row { spacing: Theme.paddingSmall
|
|
visible: !li.unclutter
|
|
// name, version: default
|
|
Label{ visible: li.unclutter;
|
|
font.pixelSize: Theme.fontSizeSmall; color: Theme.secondaryColor;
|
|
horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight
|
|
width: Math.min(implicitWidth, col.width * 2/3);
|
|
maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideMiddle;
|
|
//text: Format.formatDate(date, Formatter.DateMedium) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
text: page.isLandscape ?
|
|
Format.formatDate(dateTime, Formatter.DateMedium) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
: Format.formatDate(dateTime, Formatter.DateMediumWithoutYear) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
}
|
|
Label{ text: appName ; maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideMiddle; horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight; color: install ? Theme.highlightColor : Theme.primaryColor }
|
|
Label{ visible: !li.unclutter; text: version ; maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideLeft; horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight; color: install ? Theme.secondaryHighlightColor : Theme.secondaryColor}
|
|
}
|
|
Row { spacing: Theme.paddingSmall
|
|
visible: !li.unclutter
|
|
// date
|
|
Label{ font.pixelSize: Theme.fontSizeSmall; color: Theme.secondaryColor;
|
|
horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight
|
|
width: Math.min(implicitWidth, col.width * 2/3);
|
|
maximumLineCount: 1; truncationMode: TruncationMode.Elide; elide: Text.ElideMiddle;
|
|
//text: Format.formatDate(date, Formatter.DateMedium) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
//text: Format.formatDate(date, Formatter.DateMediumWithoutYear) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
text: Format.formatDate(dateTime, Formatter.DateMediumWithoutYear) + " " + Format.formatDate(dateTime, Formatter.TimeValue)
|
|
}
|
|
// repo
|
|
Label{ visible: ( install && !li.unclutter )
|
|
text: repoName ; horizontalAlignment: install ? Text.AlignLeft : Text.AlignRight; color: Theme.highlightBackgroundColor; font.pixelSize: Theme.fontSizeSmall}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VerticalScrollDecorator {}
|
|
}
|
|
}
|
|
|
|
Component{ id: datePicker; DatePickerDialog {} }
|
|
|
|
Component { id: highlightBar
|
|
Rectangle {
|
|
color: Theme.rgba(Theme.highlightBackgroundColor, Theme.opacityFaint);
|
|
border.color: Theme.highlightBackgroundColor
|
|
radius: Theme.paddingSmall
|
|
}
|
|
}
|
|
|
|
ListModel { id: appHistoryModel }
|
|
|
|
// handle data mangling in a worker script:
|
|
Component.onCompleted: getHistory(page.historyfile, showLocal)
|
|
|
|
function getHistory(fileName, filter) {
|
|
if (!worker.ready) { console.warn("worker not ready!") }
|
|
worker.sendMessage({ action: "getHistory", parameter: { model: appHistoryModel, fn: fileName, f: filter}})
|
|
}
|
|
|
|
function updateHistory(fileName, filter) {
|
|
appHistoryModel.clear();
|
|
getHistory(fileName, filter);
|
|
}
|
|
|
|
WorkerScript { id: worker; source: "../js/worker.js"
|
|
onMessage: function(m) {
|
|
if (m.event === "progress") page.progress = m.percent
|
|
}
|
|
}
|
|
}
|
|
// vim: expandtab ts=4 st=4 sw=4 filetype=javascript
|