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
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
|
|
|