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.
 
 
 

220 lines
8.7 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
Page {
id: statsPage
allowedOrientations: Orientation.All
states: [
State { name: "pkgs"
PropertyChanges { target: menu1
text: qsTr("View repositories")
}
PropertyChanges { target: sheader
text: qsTr("Most active packages")
}
PropertyChanges { target: statsView
model: packageModel
viewRepos: false
}
}
]
// does not exist on older SFOS versions, see https://codeberg.org/nephros/install-history/issues/14
/*
BusyLabel {
running: packageModel.loading || repoModel.loading
text: qsTr("Crunching Numbers…")
}
*/
PullDownMenu { id: pdp
flickable: flick
MenuItem { id: menu1; text: qsTr("View packages")
onClicked: statsPage.state = (statsPage.state == "") ? "pkgs" : ""
}
}
ListModel { id: packageModel
property bool loading: true
property int max: 0
property int sum: 0
}
ListModel { id: repoModel
property bool loading: true
property int max: 0
property int sum: 0
}
onStatusChanged: {
if ( status === PageStatus.Active) {
if( repoModel.count <= 0 ) { fillModel(repoModel, "install", "repoName", 10)}
if( packageModel.count <= 0 ) { fillModel(packageModel, "install", "appName", 10)}
}
}
SilicaFlickable { id: flick
anchors.fill: parent
contentHeight: mainColumn.height
Column{ id: mainColumn
width: parent.width
spacing: Theme.paddingSmall
PageHeader { id: header ; title: qsTr("Install History") ; description: qsTr("Statistics")}
Label {
width: parent.width - Theme.horizontalPageMargin
color: Theme.secondaryHighlightColor
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Percentages are relative to all recorded installation events. Bars are relative to the item with the highest event count.")
horizontalAlignment: Text.AlignLeft
wrapMode: Text.WordWrap
}
SectionHeader { id: sheader ; text: qsTr("Most active Repositories") }
ColumnView { id: statsView
//width: parent.width - Theme.itemSizeSmall
width: parent.width - Theme.horizontalPageMargin
anchors.horizontalCenter: parent.horizontalCenter
itemHeight: Theme.itemSizeMedium
property int max: model.max
property int sum: model.sum
property bool viewRepos: true
model: viewRepos ? repoModel : packageModel
onViewReposChanged: model = viewRepos ? repoModel : packageModel
delegate: SilicaItem { id: statsDelegate
height: Theme.itemSizeMedium
width: parent.width
property bool viewRepos: statsView.viewRepos
property double factor: ( statsView.max - installations ) / statsView.max
property double perc: ( installations / statsView.sum * 100).toFixed(2)
property double cutoff: viewRepos ? 1.0 : 0.1
property color barColor: viewRepos ? Theme.highlightBackgroundColor : Theme.secondaryHighlightColor
/*
Rectangle {
anchors.fill: parent
anchors.centerIn: parent
radius: Theme.paddingSmall
color: Theme.rgba(Theme.secondaryColor, 0.05)
}
*/
Row {
width: parent.width
Label { text: ((name === "__OTHERS") ? qsTr("others", "things that don't fit in a category") : name) + ": "; color: Theme.secondaryColor }
Label { text: qsTr("%L1", "number of events").arg(installations) }
}
//Label { anchors.right: parent.right; text: qsTr("(%L1%)", "percentage in parentheses, best translate as '(%L1%)'" ).arg(perc)}
Label { anchors.right: parent.right; text: qsTr("%L1%", "percentage" ).arg(perc)}
Rectangle{ id: bar
height: Theme.paddingLarge * 1.5
anchors.verticalCenter: parent.verticalCenter
color: parent.perc > parent.cutoff ? parent.barColor : Theme.secondaryColor
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.5; color: bar.color }
GradientStop { position: 1.0; color: "transparent" }
}
width: !visible ? 0 : Math.max(Theme.paddingSmall, (name === "__OTHERS") ? parent.width * perc /100 : parent.width - parent.width * parent.factor)
Behavior on width { NumberAnimation{ from: 0; duration: 750 } }
}
}
}
}
}
/*
* transform the history data model into a sorted model containing count and name
*
* ex: fillModel(myListModel, "install", "appName", 10)
*
* ListModel myListModel: the model to fill
* string action: the key in the original model to count
* string modelProperty: the key in the original model to set as name
* int cutoff: number of actions to sum up as "others"
*
*/
function fillModel(listModel, action, modelProperty, cutoff) {
console.debug("Filling statistics model");
console.time("Filling model took");
const sum = 0;
const cutoffCount = 0;
if (cutoff === undefined) { cutoff = 10; }
// get the source values from historymodel
var arr = [];
for (var i = 0; i < appHistoryModel.count; i++) {
if (appHistoryModel.get(i).action == action)
arr.push(appHistoryModel.get(i)[modelProperty]);
}
// we get an obj of objs which we can not sort
const model = {};
model = statsPage.countUnique(arr);
// so push it into something sortable
const sortable = [];
for (var key in model) { sortable.push([key, model[key]]); }
// sort descending ;)
sortable.sort(function(a, b) { return - ( a[1] - b[1]); });
// gather values
for (var i=0; i < sortable.length ; i++) {
var e = sortable[i]; // e is a tuple
if (e[0] === "") continue; // uninstall events have empty repo names
sum += e[1] // count, TODO: move to array.reduce()
if ( e[1] > cutoff) {
listModel.append({ "installations": e[1], "name": e[0]});
} else {
cutoffCount++;
}
}
// special string to use later for translations
listModel.append({ "installations": cutoffCount, "name": "__OTHERS"});
listModel.sum = sum;
listModel.max = sortable[0][1]; // should be the highest value, right?
listModel.loading = false;
console.timeEnd("Filling model took");
}
/*
// https://www.tutorialspoint.com/unique-sort-removing-duplicates-and-sorting-an-array-in-javascript
function uniqSort(arr){
const map = {};
const res = [];
for (i = 0; i < arr.length; i++) {
if (!map[arr[i]]) {
map[arr[i]] = true;
res.push(arr[i]);
}
}
return res.sort(function(a,b) { return a - b } );
}
*/
// https://www.tutorialspoint.com/counting-unique-elements-in-an-array-in-javascript
function countUnique(arr) {
const counts = {};
for (var i = 0; i < arr.length; i++) {
counts[arr[i]] = 1 + (counts[arr[i]] || 0);
};
return counts;
}
}
// vim: expandtab ts=4 st=4 sw=4 filetype=javascript