install-history/qml/pages/MainPage.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