A package installation history tool for Sailfish OS. Binary packages can be found at https://build.sailfishos.org/project/show/sailfishos:chum
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

536 lines
27 KiB

/*
*
* Copyright 2022 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"
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);
}
}
/*
onStatusChanged: {
if ( status === PageStatus.Active && !pageStack.busy && pageStack.nextPage() === null ) { pageStack.pushAttached(statsPage) }
}
*/
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
//onClicked: { appHistoryModel.clear(); view.forceLayout(); }
onClicked: { appHistoryModel.clear(); view.forceLayout(); view.model = null; }
onDelayedClick: { getHistory(page.historyfile,showLocal); view.model = appHistoryModel }
}
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; appHistoryModel.clear(); view.forceLayout(); view.model = null; }
onDelayedClick: { getHistory(page.historyfile,showLocal); view.model = appHistoryModel }
}
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 {}
}
}
ListModel { id: appHistoryModel; Component.onCompleted: { getHistory(page.historyfile,showLocal); } }
Component{ id: datePicker; DatePickerDialog {} }
Component { id: highlightBar
Rectangle {
color: Theme.rgba(Theme.highlightBackgroundColor, Theme.opacityFaint);
border.color: Theme.highlightBackgroundColor
radius: Theme.paddingSmall
}
}
/* ----- functions ----- */
/* load zypper history log file */
function getHistory(fn,filter) {
console.assert((typeof fn !== "undefined"), "Called without filename");
console.info("Loading history from " + fn);
console.time("File request took");
const query = Qt.resolvedUrl("file://" + fn);
var r = new XMLHttpRequest()
r.open('GET', query);
r.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
r.send();
r.onreadystatechange = function(event) {
if (r.readyState == XMLHttpRequest.DONE) {
fillModel(r.response.split("\n").reverse(),filter);
console.timeEnd("File request took");
}
}
}
/* helper to reduce payload */
function filterData(data) {
// lines have the format:
// 20xx-mm-dd HH:MM:SS|install
// 20xx-mm-dd HH:MM:SS|remove
// ..................^17
// everything else is not interesting
const re = /^20.{17}(\|install|\|remove).*$/;
return (re.test(data)) ? true : false;
}
function filterLocal(data) {
const re = /\|PK_TMP_DIR\|/ // pkcon
const re = /\|_tmpRPMcache_\|/ // zypper
return (!re.test(data)) ? true : false;
}
/* populate ListModel from payload */
function fillModel(a,filter) {
console.time("List loaded in");
console.time("Filtering took");
const data = a.filter(filterData);
if (!filter)
const data = data.filter(filterLocal);
console.timeEnd("Filtering took");
console.debug("Filtered Lines: " + data.length + "/" + a.length);
// performance: compile regexp and define and loop variables
// outside the loop
// also, looks oldschool to declare all vars at the top.
const isOpenrepos = /^openrepos-/
//const jollarepos = /(adaptation0 adaptation1 aliendalvik apps customer-jolla jolla sailfish-eas store xt9)/
const line;
const dt;
const d;
const t;
const date;
const dateTime;
const repo;
const repotype = "other";
const reponame = null;
const element = {};
data.forEach(function(r){
line = r.split("|");
// zypper stores local time at the time of the event.
// see https://codeberg.org/nephros/install-history/issues/16
dt = line[0].split(" ");
d = dt[0].split("-"); t = dt[1].split(":");
// months are counted from 0-11, so do a d[1] - 1
date = new Date( d[0], d[1] - 1, d[2], 0, 0, 0, 0).toISOString();
dateTime = new Date( d[0], d[1] - 1, d[2], t[0], t[1], t[2], 0);
/* assign pretty names:
*
grep install /var/log/zypp/history |grep -v PK_TMP_DIR | awk -F\| '{print $7}'| sort -u | grep -v openrepos
adaptation0
adaptation1
aliendalvik
customer-jolla
harbour-storeman-obs
mentaljam-obs
sailfish-eas
sailfishos-chum
sailfishos-chum-testing
store
xt9
*/
repo = line[6];
reponame = repo;
if ( (repo === "PK_TMP_DIR") || (repo === "_tmpRPMcache_") ) {
repo=qsTr("local", "short name for the 'local' repo");
reponame=qsTr("Local Installs", "name for the 'local' repo");
repotype="local";
} else if (repo === "store") {
reponame=qsTr("Jolla Store", "name for the jolla store repo");
repotype="jolla";
} else if (repo === "jolla") {
reponame=qsTr("Jolla System", "name for the jolla jolla repo");
repotype="jolla";
} else if (repo === "aliendalvik") {
reponame=qsTr("Android App Support", "name for the android support repo");
repotype="jolla";
} else if (repo === "apps") {
reponame=qsTr("Jolla Applications", "name for the jolla apps repo");
repotype="jolla";
} else if (repo === "sailfish-eas") {
reponame=qsTr("Exchange Feature", "name for the jolla feature repo");
repotype="jolla";
} else if (repo === "xt9") {
reponame=qsTr("XT9 Feature", "name for the jolla feature repo");
repotype="jolla";
} else if (repo === "customer-jolla") {
reponame=qsTr("Jolla Feature", "name for the jolla customer repo");
repotype="jolla";
} else if (repo === "adaptation0") {
reponame=qsTr("Device Adaptation", "name for the jolla adaptation0 repo");
repotype="jolla";
} else if (repo === "adaptation1") {
reponame=qsTr("Device Adaptation", "name for the jolla adaptation1 repo");
repotype="jolla";
} else if ( (repo === "mentaljam-obs") || (repo === "harbour-storeman-obs") ) {
reponame=qsTr("Storeman", "name for the storeman repo");
} else if (repo === "sailfishos-chum") {
reponame=qsTr("SailfishOS:Chum", "name for the chum repo");
repotype="chum";
} else if (repo === "sailfishos-chum-testing") {
reponame=qsTr("SailfishOS:Chum Testing", "name for the chum testing repo");
repotype="chum";
} else if (repo === "nubecula-mls-offline-repo") {
reponame=qsTr("MLS", "name for the Nubecula Offline MLS repo");
} else if ( isOpenrepos.test(repo) ) {
reponame= qsTr("OpenRepos: %1", "prefix for a openrepos repo").arg(repo.replace("openrepos-", ""));
repotype="openrepos";
}
element = {
"date": date,
"dateTime": dateTime,
"action": line[1].trim(),
"appName": line[2],
"version": line[3],
"repo": repo,
"repoName": reponame,
"repoType": repotype,
}
appHistoryModel.append(element);
}); // foreach func end
console.timeEnd("List loaded in");
}
}
// vim: expandtab ts=4 st=4 sw=4 filetype=javascript