feat: bury offline mode, ship antergos-layan-theme

remember when i thought an offline installer was a good idea?
a pre-built squashfs that still needed internet to install DEs?
"offline" in name only. it was a lie. a beautiful, naive lie.

so i killed it. ripped out the mode picker, the connectivity check,
the whole AskMode() circus. calamares-next now just shows a notice
saying "yeah that wasnt actually offline" and launches directly
into online mode. no more pretending.

also fixed the QML nav button because having a greyed out "Next"
on the last page is terrible UX. now it says "Done" and quits.
youre welcome.

and since we nuked the offline mode, might as well make the
online mode look fucking spectacular. enter antergos-layan-theme:

- kvantum theme because your apps deserve better than qt dreck
- sddm theme (AntergosDark-Layan) for a sexy login screen
- every plasma plasmoid from the rice (panel colorizer, netspeed,
  shutdown switch, applets, separators, music toolbar, the works)
- yakuake arc-dark skin
- antergos logos in /usr/share/logos and /usr/share/pixmaps
- konsole profile, desktop appletsrc, all that jazz
- kde_settings.conf now points to AntergosDark-Layan sddm theme

also replaced the old antergos-grub-theme with the Layan grub
theme because the old one was a git-sourced abomination. 75 distro
icons, terminal box, selection widgets, antergos wallpaper.
ships to /usr/share/grub/themes/antergos/ so calamares finds it.

oh and a dozen audit fixes:
- kscreenlockerrc wallpaper path fixed
- kdialogrc no longer says some other distro
- 7 stale knsregistry files deleted (dev machine was leaking)
- systemd-inhibit guarded with command -v (dinit doesnt have it)
- plasma restart uses killall not systemctl --user
- "XeroLinux Toolkit" -> "Antergos Toolkit" in appletsrc
- KDE_SESSION_VERSION=5 -> =6 (its 2026)
- mirrorlist script says Artix not Arch
- SDDM Messages.sh removed from build artifacts
This commit is contained in:
2026-07-16 21:57:36 +02:00
parent 716ff74560
commit 82ff06c4da
1302 changed files with 308189 additions and 235 deletions
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,46 @@
# Plasma News Widget
## Overview
The Plasma News Widget is a lightweight plasmoid that fetches and displays news articles from the internet using RSS feeds. While designed primarily for news, it supports any valid RSS feed.
## Features
- Fetches news articles from specified RSS feeds.
- View the full article by clicking the Read More button
- Easily switch between news sources and navigate between articles
## Project Structure
```
plasma-news-widget
├── contents
│ ├── config
│ │ ├── config.qml # Configuration settings
│ │ ├── main.xml # Main configuration file
│ ├── ui
│ │ ├── configGeneral.qml # General settings UI
│ │ ├── main.qml # Main UI layout
│ ├── utils
│ │ ├── newsFetcher.js # Handles fetching and processing RSS feeds
├── LICENSE # License information
├── metadata.json # Widget metadata
└── README.md # Project documentation
```
## Setup Instructions
1. **Clone the repository:**
```
git clone https://github.com/yourusername/plasma-news-widget.git
cd plasma-news-widget
```
Requires: Plasma >= 6.0
2. **Testing:**
Refer to: https://develop.kde.org/docs/plasma/widget/testing/
3. **Installing:**
You can install it easily from the KDE Widgets store, or alternative you can execute `kpackagetool6 -i .` in the root directory of the applet.
## Contributing
Contributions are welcome! Please feel free to submit a pull request or open an issue for any suggestions or improvements.
## License
This project is licensed under the GNU General Public License version 2. See LICENSE for details
@@ -0,0 +1,10 @@
import QtQuick 2.1
import org.kde.plasma.configuration 2.0
ConfigModel {
ConfigCategory {
name: i18nc("@title", "General")
icon: "configure"
source: "configGeneral.qml"
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
<kcfgfile name=""/>
<group name="General">
<entry name="feeds" type="String">
<default>
https://rss.nytimes.com/services/xml/rss/nyt/Technology.xml
https://feeds.bbci.co.uk/news/technology/rss.xml
</default>
</entry>
</group>
</kcfg>
@@ -0,0 +1,24 @@
import QtQuick 2.0
import QtQuick.Controls 2.5 as QQC2
import org.kde.kirigami 2.4 as Kirigami
import QtQuick.Layouts 1.1 as QtLayouts
Kirigami.FormLayout {
id: configPage
// use string list
property alias cfg_feeds: feeds.text
QQC2.Label {
QtLayouts.Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignBottom
text: i18n("List of feeds")
}
QQC2.TextArea {
id: feeds
placeholderText: i18n("Enter a list of feeds, separated by new lines")
text: cfg_feeds // Load stored feeds as newline-separated text
}
}
@@ -0,0 +1,272 @@
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.2
import "../utils/newsFetcher.js" as NewsFetcher
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.kquickcontrolsaddons 2.0
PlasmoidItem {
id: mainWindow
readonly property int implicitWidth: Kirigami.Units.gridUnit * 40
readonly property int implicitHeight: Kirigami.Units.gridUnit * 18.5
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground
Plasmoid.icon: "news"
width: implicitWidth
height: implicitHeight
readonly property int minimumWidth: Kirigami.Units.gridUnit * 8
readonly property int minimumHeight: Kirigami.Units.gridUnit * 8
property var feedList: []
property string errors: ""
property var newsItems: [];
property int currentIndex: 0;
function fetchFeeds() {
var feedUrls = plasmoid.configuration.feeds.split("\n"); // Read feed URLs from configuration
NewsFetcher.fetchAllFeeds(feedUrls, function(allFeeds, errorMessage) {
errors = errorMessage;
feedList = allFeeds.sort(function(a, b) { // sort title alphabetically
return a.title.localeCompare(b.title);
});
if(feedList.length > 0) {
fetchCurrentFeed(currentIndex);
}
});
}
// Function to load a specific feed's news items
function fetchCurrentFeed(index) {
if (index < feedList.length) {
// sort by date - oldest first
newsItems = feedList[index].items.sort(function(a, b) {
return new Date(a.pubDate) - new Date(b.pubDate);
});
currentIndex = newsItems.length - 1;
}
}
Component.onCompleted: fetchFeeds();
Plasmoid.onUserConfiguringChanged: fetchFeeds();
Row { // Tab Container
id: tabContainer
width: parent.width
height: Math.max(feedTabs.implicitHeight, refreshButton.implicitHeight)
anchors.top: parent.top
spacing: 5
Flickable {
id: tabBarFlickable
width: parent.width - refreshButton.width - 10
height: feedTabs.implicitHeight
clip: true
contentWidth: feedTabs.width
interactive: feedTabs.width > width
TabBar {
id: feedTabs
width: implicitWidth
height: implicitHeight
Repeater {
model: feedList
TabButton {
text: feedList[model.index].title
onClicked: fetchCurrentFeed(model.index)
}
}
}
}
// Refresh Button
PlasmaComponents.Button {
id: refreshButton
anchors.right: parent.right
icon.name: "view-refresh"
height: implicitHeight
onClicked: fetchFeeds()
z: 5
}
}
Rectangle { // Error Display
id: errorDisplay
visible: errors !== ""
width: parent.width
height: parent.height
// slgihtly red background
color: "#FFCCCC"
border.color: "#990000"
border.width: 2
radius: 8
z: -1
Column {
anchors.centerIn: parent
spacing: 10
width: parent.width
Kirigami.Icon {
source: "data-error"
width: 50
height: 50
anchors.horizontalCenter: parent.horizontalCenter
}
PlasmaComponents.Label {
text: errors
color: "#990000"
font.bold: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
width: parent.width
}
}
}
Column { // News Container
anchors.fill: parent
anchors.topMargin: feedTabs.height
visible: errors === ""
spacing: 10
Row {
anchors.fill: parent
anchors.topMargin: feedTabs.height / 3
spacing: 10
// News Image
Image {
source: newsItems.length > 0 && newsItems[currentIndex].imageUrl !== ""
? newsItems[currentIndex].imageUrl : ""
width: parent.width * 0.32
fillMode: Image.PreserveAspectFit
height: parent.height * 0.8
visible: newsItems.length > 0 && newsItems[currentIndex].imageUrl !== ""
}
// News Text Content
Column {
id: newsContent
width: newsItems.length > 0 && newsItems[currentIndex].imageUrl !== ""
? parent.width * 0.68 : parent.width + 10
height: parent.height
spacing: 5
PlasmaComponents.Label {
id: newsTitle
text: newsItems.length > 0 ? newsItems[currentIndex].title : ""
font.bold: true
font.pointSize: 15.4
wrapMode: Text.Wrap
width: parent.width - 20
}
ScrollView {
id: newsScrollView
width: newsContent.width
// Set height to the smaller of content height or available space
height: Math.min(contentLabel.implicitHeight, parent.height - newsTitle.height - pubDateLabel.implicitHeight - navigationRow.height - 20)
visible: newsItems.length > 0 && newsItems[currentIndex].description !== ""
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
PlasmaComponents.Label {
id: contentLabel
text: newsItems.length > 0 ? newsItems[currentIndex].description : ""
wrapMode: Text.Wrap
font.pointSize: 12
width: newsScrollView.width - 20
}
}
PlasmaComponents.Label {
id: pubDateLabel
text: newsItems.length > 0 ? newsItems[currentIndex].pubDate : ""
font.italic: true
font.pointSize: 10
color: "gray"
}
}
}
Row { // Navigation Buttons
id: navigationRow
width: parent.width
spacing: 10
height: parent.height * 0.1
anchors.bottom: parent.bottom
visible: errors === ""
PlasmaComponents.Button {
text: "Read More"
enabled: newsItems.length > 0
onClicked: {
if (newsItems.length > 0) {
Qt.openUrlExternally(newsItems[currentIndex].link);
}
}
anchors.left: parent.left
}
Item {
Layout.fillWidth: true // Fills space between buttons
}
Row {
spacing: 10
anchors.right: parent.right
PlasmaComponents.Button {
icon.name: "arrow-left-double"
enabled: currentIndex > 0
onClicked: {
if (currentIndex > 0) {
currentIndex = 0;
}
}
}
PlasmaComponents.Button {
icon.name: "arrow-left"
enabled: currentIndex > 0
onClicked: {
if (currentIndex > 0) {
currentIndex--;
}
}
}
PlasmaComponents.Button {
icon.name: "arrow-right"
enabled: currentIndex < newsItems.length - 1
onClicked: {
if (currentIndex < newsItems.length - 1) {
currentIndex++;
}
}
}
PlasmaComponents.Button {
icon.name: "arrow-right-double"
enabled: currentIndex < newsItems.length - 1
onClicked: {
if (currentIndex < newsItems.length - 1) {
currentIndex = newsItems.length - 1;
}
}
}
}
}
}
}
@@ -0,0 +1,220 @@
function getAttributeValue(node, attributeName) {
if (node.attributes) {
for (let i = 0; i < node.attributes.length; i++) {
if (node.attributes[i].name === attributeName) {
return node.attributes[i].value;
}
}
}
return "";
}
function getTextContent(node) {
return node && node.firstChild ? node.firstChild.nodeValue : "";
}
// Removes inline tags that could break layout but don't have content
function cleanDescription(html) {
if (!html) return "";
var allowedTags = ["b", "strong", "i", "em", "u", "a", "p", "br", "ul", "ol", "li", "span", "strike", "s", "del", "sub", "sup", "code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6"];
// Remove HTML comments
html = html.replace(/<!--[\s\S]*?-->/g, "");
// Regex to remove disallowed tags but keep content
html = html.replace(/<\/?([a-zA-Z0-9]+)(?:\s[^>]*)?>/g, function (match, tagName) {
tagName = tagName.toLowerCase();
return allowedTags.includes(tagName) ? match : "";
});
return html.trim();
}
function fetchNews(rssUrl, callback) {
var xhr = new XMLHttpRequest();
xhr.responseType = "document";
console.log("Fetching news from " + rssUrl);
xhr.open("GET", rssUrl);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var xmlDoc = xhr.responseXML;
if (!xmlDoc || !xmlDoc.documentElement) {
callback([], "Failed to parse XML document");
return;
}
var newsArray = [];
var feedTitle = "Unknown Feed";
var root = xmlDoc.documentElement;
if (root.nodeName === "rss") {
// --- RSS Feed ---
var channel = null;
var rootChildren = root.childNodes;
for (let i = 0; i < rootChildren.length; i++) {
if (rootChildren[i].nodeName === "channel") {
channel = rootChildren[i];
break;
}
}
if (!channel) {
callback([], "No <channel> element found in RSS feed");
return;
}
var titleNode = channel.firstChild;
while (titleNode) {
if (titleNode.nodeName === "title") {
feedTitle = getTextContent(titleNode);
break;
}
titleNode = titleNode.nextSibling;
}
var itemNode = channel.firstChild;
while (itemNode) {
if (itemNode.nodeName === "item") {
newsArray.push(parseRssItem(itemNode));
}
itemNode = itemNode.nextSibling;
}
} else if (root.nodeName === "feed") {
// --- Atom Feed ---
var titleNode = root.firstChild;
while (titleNode) {
if (titleNode.nodeName === "title") {
feedTitle = getTextContent(titleNode);
break;
}
titleNode = titleNode.nextSibling;
}
var entryNode = root.firstChild;
while (entryNode) {
if (entryNode.nodeName === "entry") {
newsArray.push(parseAtomEntry(entryNode));
}
entryNode = entryNode.nextSibling;
}
} else {
callback([], "Unsupported feed format: " + root.nodeName);
return;
}
if (newsArray.length === 0) {
callback([], "No news articles found in the feed.");
} else {
callback({ url: rssUrl, title: feedTitle, items: newsArray }, "");
}
} else {
callback([], "Failed to load RSS feed (HTTP Status " + xhr.status + ")");
}
}
};
xhr.send();
}
function parseRssItem(itemNode) {
var title = "No title";
var link = "#";
var description = "No description";
var pubDate = "No date";
var imageUrl = "";
var children = itemNode.childNodes;
for (let i = 0; i < children.length; i++) {
var child = children[i];
switch (child.nodeName) {
case "title":
title = getTextContent(child);
break;
case "link":
if (link === "#") link = getTextContent(child);
break;
case "description":
description = cleanDescription(getTextContent(child));
break;
case "pubDate":
pubDate = getTextContent(child);
break;
case "thumbnail":
case "enclosure":
case "content":
imageUrl = getAttributeValue(child, "url");
break;
case "group":
var mediaChild = child.firstChild;
while (mediaChild) {
if (mediaChild.nodeName === "content") {
imageUrl = getAttributeValue(mediaChild, "url");
break;
}
mediaChild = mediaChild.nextSibling;
}
break;
}
}
return { title, link, description, pubDate, imageUrl };
}
function parseAtomEntry(entryNode) {
var title = "No title";
var link = "#";
var description = "No description";
var pubDate = "No date";
var imageUrl = "";
var children = entryNode.childNodes;
for (let i = 0; i < children.length; i++) {
var child = children[i];
switch (child.nodeName) {
case "title":
title = getTextContent(child);
break;
case "link":
var rel = getAttributeValue(child, "rel");
if (rel !== "self" && getAttributeValue(child, "href")) {
link = getAttributeValue(child, "href");
}
break;
case "summary":
case "content":
description = cleanDescription(getTextContent(child));
break;
case "updated":
case "published":
pubDate = getTextContent(child);
break;
case "thumbnail":
case "content":
imageUrl = getAttributeValue(child, "url");
break;
}
}
return { title, link, description, pubDate, imageUrl };
}
function fetchAllFeeds(feedUrls, callback) {
var allFeeds = [];
var errors = [];
var remaining = feedUrls.length;
feedUrls.forEach(rssUrl => {
fetchNews(rssUrl, (feedData, errorMessage) => {
if (errorMessage) errors.push(`Error fetching ${rssUrl}: ${errorMessage}`);
else allFeeds.push(feedData);
if (--remaining === 0) callback(allFeeds, errors.length ? errors.join("\n") : "");
});
});
}
@@ -0,0 +1,25 @@
{
"KPlugin": {
"Authors": [
{
"Email": "info@fenriris.net",
"Name": "Fenriris"
}
],
"Category": "Internet",
"Description": "View news articles from the Internet",
"EnabledByDefault": true,
"Icon": "news-subscribe",
"Id": "com.SrS2225a.feeds",
"License": "GPL",
"Name": "News",
"KPackageStructure": "Plasma/Applet",
"Version": "1.2",
"Website": "https://fenriris.net/"
},
"X-Plasma-API": "declarativeappletscript",
"X-Plasma-MainScript": "ui/main.qml",
"X-Plasma-StandAloneApp": true,
"X-Plasma-API-Minimum-Version": "6.0",
"KPackageStructure": "Plasma/Applet"
}
@@ -0,0 +1,33 @@
import org.kde.plasma.configuration
ConfigModel {
ConfigCategory {
name: i18n("General")
icon: "preferences-desktop"
source: "configuration/General.qml"
}
ConfigCategory {
name: i18n("Upgrade & Management")
icon: "preferences-system-startup"
source: "configuration/Upgrade.qml"
}
ConfigCategory {
name: i18n("Appearance")
icon: "preferences-desktop-display-color"
source: "configuration/Appearance.qml"
}
ConfigCategory {
name: i18n("Rules")
icon: "preferences-system-windows-behavior"
source: "configuration/Rules.qml"
}
ConfigCategory {
name: i18n("Support me")
icon: "donate"
source: "configuration/Support.qml"
}
}
@@ -0,0 +1,116 @@
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
<kcfgfile name="apdatifier"/>
<group name="General">
<entry name="checkMode" type="string"><default>interval</default></entry>
<entry name="intervalMinutes" type="int"><default>120</default></entry>
<entry name="dailyHour" type="int"><default>20</default></entry>
<entry name="dailyMinute" type="int"><default>30</default></entry>
<entry name="weeklyDay" type="int"><default>0</default></entry>
<entry name="weeklyHour" type="int"><default>20</default></entry>
<entry name="weeklyMinute" type="int"><default>30</default></entry>
<entry name="arch" type="bool"><default>true</default></entry>
<entry name="aur" type="bool"><default>false</default></entry>
<entry name="flatpak" type="bool"><default>false</default></entry>
<entry name="widgets" type="bool"><default>false</default></entry>
<entry name="fwupd" type="bool"><default>false</default></entry>
<entry name="feedsEnabled" type="bool"><default>false</default></entry>
<entry name="feedsFetchCount" type="int"><default>3</default></entry>
<entry name="feeds" type="string"><default>https://archlinux.org/feeds/news/</default></entry>
<entry name="middleAction" type="string"><default>upgradeSystem</default></entry>
<entry name="rightAction" type="string"><default></default></entry>
<entry name="scrollUpAction" type="string"><default>switchScheduler</default></entry>
<entry name="scrollDownAction" type="string"><default>management</default></entry>
<entry name="notifyUpdates" type="bool"><default>true</default></entry>
<entry name="notifyUpdatesAction" type="bool"><default>false</default></entry>
<entry name="notifyEveryBump" type="bool"><default>true</default></entry>
<entry name="notifyNews" type="bool"><default>true</default></entry>
<entry name="notifyNewsAction" type="bool"><default>false</default></entry>
<entry name="notifyErrors" type="bool"><default>true</default></entry>
<entry name="notifySound" type="bool"><default>true</default></entry>
<entry name="notifyPersistent" type="bool"><default>false</default></entry>
<entry name="checkConn" type="bool"><default>false</default></entry>
<entry name="respectMeteredConn" type="bool"><default>false</default></entry>
</group>
<group name="Upgrade">
<entry name="wrapper" type="string"><default></default></entry>
<entry name="archFlags" type="string"><default></default></entry>
<entry name="sudoBin" type="string"><default>sudo</default></entry>
<entry name="rebootSystem" type="bool"><default>false</default></entry>
<entry name="mirrors" type="string"><default>false</default></entry>
<entry name="mirrorsAge" type="int"><default>7</default></entry>
<entry name="mirrorCount" type="int"><default>5</default></entry>
<entry name="mirrorProgress" type="bool"><default>false</default></entry>
<entry name="dynamicUrl" type="string"><default>null</default></entry>
<entry name="flatpakRemoveUnused" type="bool"><default>false</default></entry>
<entry name="flatpakFlags" type="string"><default></default></entry>
<entry name="widgetConfirmation" type="bool"><default>true</default></entry>
<entry name="restartShell" type="bool"><default>true</default></entry>
<entry name="restartCommand" type="string"><default>killall plasmashell</default></entry>
<entry name="trayEnabledByDefault" type="bool"><default>false</default></entry>
<entry name="preExec" type="string"><default></default></entry>
<entry name="postExec" type="string"><default></default></entry>
<entry name="terminal" type="string"><default></default></entry>
<entry name="idleInhibit" type="bool"><default>false</default></entry>
<entry name="tmuxSession" type="bool"><default>false</default></entry>
<entry name="termFont" type="bool"><default>false</default></entry>
<entry name="mngHideFlatpak" type="bool"><default>false</default></entry>
<entry name="mngHideHeaders" type="bool"><default>false</default></entry>
<entry name="mngHideHelp" type="bool"><default>false</default></entry>
<entry name="mngHideNums" type="bool"><default>false</default></entry>
<entry name="fzfOptions" type="string"><default></default></entry>
</group>
<group name="Appearance">
<entry name="hideIconPolicy" type="int"><default>0</default></entry>
<entry name="selectedIcon" type="string"><default>apdatifier-plasmoid</default></entry>
<entry name="busyIndicator" type="string"><default>spinner</default></entry>
<entry name="counterMode" type="string"><default>side</default></entry>
<entry name="counterBadgePosition" type="string"><default>topRight</default></entry>
<entry name="counterSidePosition" type="string"><default>left</default></entry>
<entry name="counterGapsInner" type="int"><default>0</default></entry>
<entry name="counterGapsOuter" type="int"><default>0</default></entry>
<entry name="counterOffsetX" type="int"><default>0</default></entry>
<entry name="counterOffsetY" type="int"><default>0</default></entry>
<entry name="counterColor" type="string"><default></default></entry>
<entry name="counterRadius" type="int"><default>3</default></entry>
<entry name="counterOpacity" type="int"><default>8</default></entry>
<entry name="counterFontFamily" type="string"><default></default></entry>
<entry name="counterFontSize" type="int"><default>4</default></entry>
<entry name="counterFontBold" type="bool"><default>true</default></entry>
<entry name="pauseBadgePosition" type="string"><default>topLeft</default></entry>
<entry name="updatedBadgePosition" type="string"><default>topRight</default></entry>
<entry name="badgeOffsetX" type="int"><default>0</default></entry>
<entry name="badgeOffsetY" type="int"><default>0</default></entry>
<entry name="ownIconsUI" type="bool"><default>true</default></entry>
<entry name="autoHideScrollBar" type="bool"><default>true</default></entry>
<entry name="defaultTab" type="int"><default>0</default></entry>
<entry name="switchDefaultTab" type="bool"><default>false</default></entry>
<entry name="sorting" type="bool"><default>true</default></entry>
<entry name="showStatusText" type="bool"><default>true</default></entry>
<entry name="showToolBar" type="bool"><default>true</default></entry>
<entry name="searchButton" type="bool"><default>true</default></entry>
<entry name="intervalButton" type="bool"><default>true</default></entry>
<entry name="sortButton" type="bool"><default>true</default></entry>
<entry name="viewButton" type="bool"><default>true</default></entry>
<entry name="managementButton" type="bool"><default>true</default></entry>
<entry name="upgradeButton" type="bool"><default>true</default></entry>
<entry name="checkButton" type="bool"><default>true</default></entry>
<entry name="pinButton" type="bool"><default>true</default></entry>
<entry name="settingsButton" type="bool"><default>true</default></entry>
<entry name="compactInfo" type="string"><default>repository</default></entry>
<entry name="extendedShowRepository" type="bool"><default>true</default></entry>
<entry name="extendedShowInstallReason" type="bool"><default>true</default></entry>
<entry name="highlightVersionDiff" type="bool"><default>false</default></entry>
<entry name="colorizeInstallReason" type="bool"><default>false</default></entry>
<entry name="tabBarVisible" type="bool"><default>true</default></entry>
<entry name="tabBarTexts" type="bool"><default>true</default></entry>
</group>
<group name="Miscellaneous">
<entry name="version" type="string"><default>v0</default></entry>
<entry name="configMsg" type="bool"><default>true</default></entry>
<entry name="dbPath" type="string"><default>${TMPDIR:-/tmp}/checkup-db-$(id -u)</default></entry>
</group>
</kcfg>
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
scriptDir="$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")"
appletDir="${scriptDir%/contents/*}"
configDir="${APDATIFIER_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/apdatifier}"
cacheDir="${APDATIFIER_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/apdatifier}"
localDir="${XDG_LOCAL_HOME:-$HOME/.local/share}"
iconsDir="$localDir/icons/breeze/status/22"
notifDir="$localDir/knotifications6"
config="$configDir/config.conf"
notif="apdatifier.notifyrc"
icons=(apdatifier-plasmoid.svg
apdatifier-packages.svg
apdatifier-package.svg
apdatifier-flatpak.svg)
test -d "$configDir" || mkdir -p "$configDir"
test -f "$config" || touch "$config"
test -d "$cacheDir" || mkdir -p "$cacheDir"
# Migrate cache files from old location
[[ -f "$configDir/updates.json" && ! -f "$cacheDir/updates.json" ]] && mv "$configDir/updates.json" "$cacheDir/updates.json"
[[ -f "$configDir/news.json" && ! -f "$cacheDir/news.json" ]] && mv "$configDir/news.json" "$cacheDir/news.json"
for dir in $iconsDir $notifDir; do
test -d "$dir" || mkdir -p "$dir"
done
for icon in "${icons[@]}"; do
test -f "$iconsDir/$icon" || cp "$appletDir/contents/ui/assets/icons/$icon" "$iconsDir"
done
test -d "$notifDir" && cat > "$notifDir/$notif" << EOF
[Global]
IconName=apdatifier-plasmoid
Comment=Apdatifier
[Event/updates]
Name=New updates
Comment=Event when updates notification enabled without sound
Action=Popup
[Event/updatesSound]
Name=New updates (with sound)
Comment=Event when updates notification enabled with sound
Action=Popup|Sound
Sound=service-login
[Event/error]
Name=Error
Comment=Event when error notification enabled without sound
Action=Popup
[Event/errorSound]
Name=Error (with sound)
Comment=Event when errors notification enabled with sound
Action=Popup|Sound
Sound=dialog-error-serious
[Event/news]
Name=News
Comment=Event when news notification without sound
Action=Popup
[Event/newsSound]
Name=News (with sound)
Comment=Event when news notification with sound
Action=Popup|Sound
Sound=dialog-information
EOF
@@ -0,0 +1,606 @@
#!/usr/bin/env bash
source "$(dirname "$0")/utils"
[ "$1" ] && selected="$1" || selected=0
if [[ $aur != true || -z $wrapper ]]; then
wrapper="pacman"
wrapper_sudo="$sudoBin $wrapper"
else
wrapper="${wrapper##*/}"
wrapper_sudo=$wrapper
fi
if [[ $termFont = "true" ]]; then
ICO_MNG_01="󱝩"
ICO_MNG_02="󱝕"
ICO_MNG_03="󱝭"
ICO_MNG_04="󱝧"
ICO_MNG_05="󱝥"
ICO_MNG_06="󱝝"
ICO_MNG_07=""
ICO_MNG_08="󱘴"
ICO_MNG_09="󰌠"
ICO_MNG_10="󱝏"
ICO_MNG_11="󰏖"
ICO_MNG_12="󱧕"
ICO_MNG_13="󱝑"
fi
fzf_settings="--preview-window=right:70%,border-none \
--height=100% \
--margin=0 \
--padding=0 \
--layout=reverse \
--style minimal \
--scrollbar \
--color dark \
--multi \
--track \
--exact \
--cycle \
--no-separator \
--info=right \
--marker=•"
[[ -n "$fzfOptions" ]] && fzf_settings+=" $fzfOptions"
show_help=false
showMenu() {
menu_type=$1
opts=()
case $menu_type in
main)
opts+=("$ICO_MNG_01 $MNG_BROWSE")
opts+=("$ICO_MNG_02 $MNG_INST")
opts+=("$ICO_MNG_03 $MNG_EXPL")
opts+=("$ICO_MNG_04 $MNG_ORPH")
opts+=("$ICO_MNG_05 $MNG_DOWNGR")
opts+=("$ICO_MNG_06 $MNG_CACHE")
opts+=("$ICO_MNG_05 $MNG_REBUILD")
opts+=("$ICO_MNG_08 $MNG_MIRROR")
opts+=("$ICO_MNG_10 $MNG_RECENT")
redraw
case $selected in
0) [[ $mngHideFlatpak = true ]] && fzfExec Slq || showMenu browse;;
1) [[ $mngHideFlatpak = true ]] && fzfExec Qq || showMenu installed;;
2) showMenu explicit;;
3) showMenu orphans;;
4) downgrade;;
5) showMenu cache;;
6) showMenu rebuild;;
7) tui_exit; exec "$scriptDir/mirrorlist" menu "$selected";;
8) recentlyInstalled;;
9) showMenu main;;
esac
;;
browse)
selected=0
opts+=("$ICO_MNG_11 pacman")
opts+=("$ICO_MNG_11 flathub")
redraw
case $selected in
0) fzfExec Slq;;
1) flatpakFlathub;;
2) selected=0; showMenu main;;
esac
;;
installed)
selected=0
opts+=("$ICO_MNG_11 pacman")
opts+=("$ICO_MNG_11 flatpak")
redraw
case $selected in
0) fzfExec Qq;;
1) flatpakInstalled;;
2) selected=1; showMenu main;;
esac
;;
explicit)
selected=0
opts+=("$ICO_MNG_03 $MNG_EXPL_L")
opts+=("$ICO_MNG_04 $MNG_EXPL_U")
redraw
case $selected in
0) fzfExec Qqe;;
1) fzfExec Qqet;;
2) selected=2; showMenu main;;
esac
;;
orphans)
selected=0
opts+=("$ICO_MNG_06 $MNG_ORPH_L")
opts+=("$ICO_MNG_07 $MNG_ORPH_R")
redraw
case $selected in
0) fzfExec Qqtd;;
1) uninstallOrphans;;
2) selected=3; showMenu main;;
esac
;;
cache)
selected=0
opts+=("$ICO_MNG_07 $MNG_CACHE_A")
opts+=("$ICO_MNG_06 $MNG_CACHE_U")
redraw
case $selected in
0) cleanCache Scc;;
1) cleanCache Sc;;
2) selected=5; showMenu main;;
esac
;;
rebuild)
selected=0
opts+=("$ICO_MNG_05 $MNG_REBUILD_A")
opts+=("$ICO_MNG_09 $MNG_REBUILD_P")
redraw
case $selected in
0) fzfExec Qqm;;
1) rebuildPython;;
2) selected=6; showMenu main;;
esac
;;
esac
}
draw_menu() {
printf '\e[H'
local min_height=$((${#opts[@]} + 4))
if ((LINES < min_height)); then
printf '\e[2J'
echo "Terminal too small (need ${min_height} lines)"
return
fi
if [[ $mngHideHeaders == false ]]; then
local header
case $menu_type in
main) header="$ICO_MNG_13 $MNG_HEADER";;
browse) header="$ICO_MNG_12 $MNG_BROWSE_H";;
installed) header="$ICO_MNG_11 $MNG_INST_H";;
explicit) header="$ICO_MNG_03 $MNG_EXPL_H";;
orphans) header="$ICO_MNG_04 $MNG_ORPH_H";;
cache) header="$ICO_MNG_06 $MNG_CACHE_H";;
rebuild) header="$ICO_MNG_05 $MNG_REBUILD_H";;
esac
echo -e "$(colorize gray bold $header)\n"
fi
for i in "${!opts[@]}"; do
local number=$((i + 1))
if [[ $mngHideNums == false ]]; then
# if [[ $termFont = "true" ]]; then
# case $number in
# 1) number=󰲡;;
# 2) number=󰲣;;
# 3) number=󰲥;;
# 4) number=󰲧;;
# 5) number=󰲩;;
# 6) number=󰲫;;
# 7) number=󰲭;;
# 8) number=󰲯;;
# 9) number=󰲱;;
# esac
# number="$(colorize gray $number) "
# else
number="$(colorize gray $number.) "
# fi
else
number=""
fi
if [[ $i -eq $selected ]]; then
echo "$number$(colorize green bold ${opts[$i]})"
else
echo "$number${opts[$i]# }"
fi
done
if [[ $mngHideHelp == false ]]; then
tput cup $((LINES - 2)) 0
local footer
if [[ $show_help == true ]]; then
footer='→ l Enter 1-9 · select ↑ k · up ↓ j · down ← h · back q · quit'
else
footer='? · help'
fi
printf "$(colorize gray '%s')%*s\n" "$footer" "$((COLUMNS - ${#footer}))" ""
fi
}
redraw() {
clear_screen
draw_menu
handleInput
}
handleInput() {
while true; do
read -rsN1 "${read_flags[@]}" input
[[ -z $input ]] && { draw_menu; continue; }
if [[ $input == $'\e' ]]; then
read -rsN2 -t 0.02 rest || rest=""
input+="$rest"
fi
case "$input" in
k|$'\e[A') ((selected--)) ;;
j|$'\e[B') ((selected++)) ;;
l|$'\e[C'|$'\n'|$'\r') break ;;
h|$'\e[D')
if [[ $menu_type != main ]]; then
selected=${#opts[@]}
break
fi
;;
$'\e')
if [[ $menu_type != main ]]; then
selected=${#opts[@]}
break
else
exit
fi
;;
[1-9]) (( input>0 && input<=${#opts[@]} )) && selected=$((input-1)) && break ;;
'?') [[ $show_help == true ]] && show_help=false || show_help=true ;;
q|Q) exit ;;
esac
(( selected < 0 )) && selected=$((${#opts[@]} - 1))
(( selected >= ${#opts[@]} )) && selected=0
draw_menu
done
}
fzfExec() {
tui_exit
dependency pacman fzf || return
argList="-$1"
case $1 in
Slq) argPreview="-Si"; argAction="-Syu";;
Qqm) argPreview="-Qi"; argAction="-S --rebuild";;
*) argPreview="-Qil"; argAction="-Rsn";;
esac
if [[ $aur == true && $1 == "Slq" && $wrapper == "paru" ]]; then
packages=$( ( pacman $argList; curl -fsSL https://aur.archlinux.org/packages.gz | gzip -dc ) | LC_ALL=C sort -u | fzf $fzf_settings --preview "$wrapper $argPreview {}" )
else
packages=$($wrapper $argList | LC_ALL=C sort -u | fzf $fzf_settings --preview "$wrapper $argPreview {}")
fi
if [[ -n "$packages" ]]; then
packages=$(echo "$packages" | oneLine)
printExec "$wrapper_sudo $argAction $packages"
$wrapper_sudo $argAction $packages
pressReturn
fi
tui_restore
}
uninstallOrphans() {
tui_exit
dependency pacman || return
orphans=$($wrapper -Qqtd | oneLine)
if [[ -n "$orphans" ]]; then
printExec "$wrapper_sudo -Rsn $orphans"
printImportant "$MNG_WARN"; echo
$wrapper_sudo -Rsn $orphans
else
printDone "$MNG_DONE"
fi
pressReturn
tui_restore
}
downgrade() {
tui_exit
dependency pacman fzf || return
pacman_cache_dir="$(pacman-conf CacheDir)"
wrapper_cache_dir="$HOME/.cache/$wrapper"
files=$(find "$pacman_cache_dir" "$wrapper_cache_dir" -type f -name "*.pkg.tar.zst" -printf "%f\n" 2>/dev/null | fzf $fzf_settings)
if [[ -n "$files" ]]; then
file_paths=()
for file in $files; do
if [ -f "${pacman_cache_dir%/}/${file}" ]; then
file_paths+=("${pacman_cache_dir%/}/${file}")
else
path=$(find "$wrapper_cache_dir" -type f -name "$file" 2>/dev/null)
[[ -n "$path" ]] && file_paths+=("$path")
fi
done
if [ ${#file_paths[@]} -gt 0 ]; then
cache="${file_paths[*]}"
printExec "$wrapper_sudo -U $cache"
$wrapper_sudo -U $cache
fi
pressReturn
fi
tui_restore
}
cleanCache() {
tui_exit
dependency pacman || return
pacman_cache_dir="$(pacman-conf CacheDir | head -n1)"
mapfile -t dirs < <(find "$pacman_cache_dir" -mindepth 1 -maxdepth 1 -type d)
if ((${#dirs[@]})); then
printImportant "$MNG_CACHE_DIR"
printf ' %q\n' "${dirs[@]}"
echo
while true; do
printQuestion "$MNG_CACHE_DIR_RM"; read -r answer
case "$answer" in
[Yy]*)
[ -w "$pacman_cache_dir" ] && prefix="" || prefix="$sudoBin "
cmd="${prefix}rm -rf"
for dir in "${dirs[@]}"; do
printf -v escaped '%q' "$dir"
cmd+=" $escaped"
done
echo
printExec "$cmd"
if [ -n "$prefix" ]; then
"$sudoBin" rm -rf "${dirs[@]}"
else
rm -rf "${dirs[@]}"
fi
break
;;
[Nn]*|"") break ;;
*) ;;
esac
done
echo
fi
printExec "$wrapper_sudo -$1"
$wrapper_sudo -$1
pressReturn
tui_restore
}
rebuildPython() {
tui_exit
if [[ $wrapper = "pacman" ]]; then
found=0
for h in yay paru pikaur; do
if command -v "$h" >/dev/null 2>&1; then
wrapper_sudo="$h"
found=1
break
fi
done
if [ "$found" -eq 0 ]; then
printError "$CMD_ERR: yay/paru/pikaur"
pressReturn
tui_restore
return
fi
fi
rebuild_dir=$(find /usr/lib -maxdepth 1 -type d -name "python*.*" | sort -V | tail -1)
rebuild_packages=$( { pacman -Qqo "$rebuild_dir" | pacman -Qqm - | oneLine; } 2>/dev/null)
if [[ -z "$rebuild_packages" ]]; then
printDone "$MNG_DONE"
else
printExec "$wrapper_sudo -S --rebuild" "$rebuild_packages"
while true; do
printQuestion "$MNG_RESUME"; read -r answer
case "$answer" in
[Yy]*) echo; break;;
[Nn]*|"") echo; pressReturn;;
*) ;;
esac
done
pacman -Qqo "$rebuild_dir" | pacman -Qqm - | $wrapper -S --rebuild -
fi
pressReturn
tui_restore
}
recentlyInstalled() {
tui_exit
echo "$MNG_RECENT_SCAN"
local log="/var/log/pacman.log"
if [[ ! -f "$log" ]]; then
printError "Pacman log file not found."
pressReturn
tui_restore
return
fi
local explicit_pkgs orphan_pkgs installed_pkgs
installed_pkgs=$(pacman -Qq)
explicit_pkgs=$(pacman -Qqe)
orphan_pkgs=$(pacman -Qqtd 2>/dev/null)
declare -A pkg_date
while IFS= read -r line; do
[[ $line =~ ^\[([0-9-]+)T([0-9:]+)[^\]]*\]\ \[ALPM\]\ installed\ ([^\ ]+) ]] || continue
pkg_date["${BASH_REMATCH[3]}"]="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
done < "$log"
local output=""
for pkg in "${!pkg_date[@]}"; do
echo "$installed_pkgs" | grep -qx "$pkg" || continue
local status status_color
if echo "$orphan_pkgs" | grep -qx "$pkg"; then
status="$MNG_RECENT_ORPHAN"; status_color="31"
elif echo "$explicit_pkgs" | grep -qx "$pkg"; then
status="$MNG_RECENT_EXPLICIT"; status_color="32"
else
status="$MNG_RECENT_DEPEND"; status_color="33"
fi
output+="${pkg_date[$pkg]}|${pkg}|${status}|${status_color}\n"
done
printf '\e[H\e[2J\e[3J'
{
printf "%s\t%s\t%s\n" "$(colorize gray bold DATE)" "$(colorize gray bold PACKAGE)" "$(colorize gray bold TYPE)"
echo -e "$output" | sort -t'|' -k1 | tail -100 | while IFS='|' read -r date pkg status color; do
[[ -z "$pkg" ]] && continue
printf "\e[90m%s\e[0m\t%s\t\e[%sm%s\e[0m\n" "$date" "$pkg" "$color" "$status"
done
} | column -t -s$'\t'
pressReturn
tui_restore
}
flatpakInstalled() {
tui_exit
dependency flatpak fzf || return
local apps=$(
flatpak list --app --columns=application,name \
| sort -t$'\t' -k2,2f -k1,1 \
| fzf $fzf_settings -d$'\t' --with-nth=2 --preview 'flatpak info {1}'
)
if [[ -n "$apps" ]]; then
local ids=$(echo "$apps" | cut -f1 | oneLine)
printExec "flatpak uninstall $ids"
flatpak uninstall $ids
if [ "$flatpakRemoveUnused" = true ]; then
echo
printExec "flatpak uninstall --unused $flatpakFlags"
flatpak uninstall --unused $flatpakFlags
fi
pressReturn
fi
tui_restore
}
flatpakFlathub() {
tui_exit
dependency flatpak fzf curl jq || return
local apps=$(curl -s https://flathub.org/api/v2/appstream | jq -r '.[]' | fzf $fzf_settings --preview "flatpak remote-info flathub {}")
if [[ -n "$apps" ]]; then
apps=$(echo "$apps" | oneLine)
printExec "flatpak install $apps"
flatpak install $apps
pressReturn
fi
tui_restore
}
pressReturn() {
printf '\e[?25l'
echo -e "\n$(colorize blue bold $ICO_RETURN $MNG_RETURN)"
IFS= read -rs
}
tui_exit() {
reset_terminal
clear_screen
}
tui_restore() {
clear_screen
setup_terminal
showMenu $menu_type
}
dependency() {
for cmd in "$@"; do
if ! command -v "$cmd" >/dev/null; then
printError "${CMD_ERR} ${cmd}"
pressReturn
tui_restore
return 1
fi
done
}
get_term_size() {
read -r LINES COLUMNS < <(stty size)
}
setup_terminal() {
printf '\e[?1049h\e[?7l\e[?25l\e[2J\e[H'
stty -echo
}
reset_terminal() {
printf '\e[?7h\e[?25h\e[2J\e[;r\e[?1049l'
stty echo
}
clear_screen() {
printf '\e[H\e[2J'
}
main() {
((BASH_VERSINFO[0] > 3)) && read_flags=(-t 0.05)
trap '' SIGINT
trap 'reset_terminal' EXIT
trap 'get_term_size; clear_screen; draw_menu' WINCH
get_term_size
setup_terminal
showMenu main
}
main
# \e[?1049h Alternative screen buffer
# \e[?1049l Restore main screen buffer
# \e[?7l Disable line wrapping
# \e[?7h Enable line wrapping
# \e[?25l Hide cursor
# \e[?25h Show cursor
# \e[2J Clear terminal
# \e[H Move cursor to top-left
# \e[;r Set the scroll region to its default value
# stty -echo Hide user input
# stty echo Show user input
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
UPGRADE_PROGRESS = i18n("Upgrade in progress")
UPGRADE_FULL_ARCH = i18n("Full system upgrade")
UPGRADE_FULL_FLATPAK = i18n("Flatpak Upgrade")
UPGRADE_FULL_WIDGETS = i18n("Plasma Widgets Upgrade")
UPGRADE_PACKAGE = i18n("Upgrade")
UPGRADE_EXECTIME = i18n("Total execution time:")
UPGRADE_CRITICAL = i18n("Critical package(s) updated, reboot may be required:")
UPGRADE_REBOOT = i18n("Do you want to reboot now?")
UPGRADE_REBOOTING = i18n("The system is going to reboot now...")
UPGRADE_ENTER = i18n("Press Enter to close")
UPGRADE_SKIPPED = i18n("Skipped...")
UPGRADE_COMPLETE = i18n("Upgrade complete, close the terminal")
MIRRORS_FETCH = i18n("Fetching the latest filtered mirror list")
MIRRORS_RANK = i18n("Ranking mirrors by their connection and opening speed")
MIRRORS_ERR = i18n("Check your mirrorlist generator settings...")
MIRRORS_UPD = i18n("was updated with the following servers:")
MIRRORS_SUDO = i18n("To write to the mirrorlist file, sudo privileges are required")
MIRROR_TIME = i18n("Your current mirrorlist:")
MIRROR_REFRESH = i18n("Refresh mirror list?")
WIDGETS_WARN = i18n("For some widgets you may need to Log Out or restart plasmashell after upgrade")
WIDGETS_CHECK = i18n("Checking widgets for updates")
WIDGETS_FETCH = i18n("Fetching data from the API")
WIDGETS_LINK = i18n("Getting the download link")
WIDGETS_DOWNLOADING = i18n("Downloading package")
WIDGETS_PROCEED = i18n("Proceed with upgrade?")
WIDGETS_RESTART = i18n("Restart plasmashell now?")
WIDGETS_NODESC = i18n("No description")
WIDGETS_ERR = i18n("Unable check widgets: ")
WIDGETS_ERR_API = i18n("Too many API requests in the last 15 minutes from your IP address, please try again later")
WIDGETS_ERR_API_FAIL = i18n("Failed to retrieve data from the API")
WIDGETS_ERR_UNKNOWN = i18n("Unkwnown error")
WIDGETS_ERR_NO_JSON = i18n("File metadata.json not found")
WIDGETS_ERR_JSON = i18n("Errors in metadata.json file")
WIDGETS_ERR_EXT = i18n("Unsupported file format")
WIDGETS_ERR_NOFILES = i18n("No files for download")
WIDGETS_ERR_NOTAGGED = i18n("No file tagged with version")
WIDGETS_ERR_TAGGED = i18n("Multiple files are tagged with version")
WIDGETS_ERR_PACKAGE_FAIL = i18n("Failed to download the package")
MNG_HEADER = i18n("Management")
MNG_BROWSE = i18n("Browse available")
MNG_BROWSE_H = i18n("Search all available packages and select what to install");
MNG_INST = i18n("Manage installed")
MNG_INST_H = i18n("View installed packages and select what to remove")
MNG_EXPL = i18n("Explicit installs")
MNG_EXPL_H = i18n("Show packages you installed directly");
MNG_EXPL_L = i18n("All explicit installs")
MNG_EXPL_U = i18n("Explicit installs not required by anything")
MNG_ORPH = i18n("Orphaned dependencies")
MNG_ORPH_H = i18n("Dependencies that are no longer needed by anything installed");
MNG_ORPH_L = i18n("List orphans")
MNG_ORPH_R = i18n("Remove orphans")
MNG_DOWNGR = i18n("Downgrade from cache")
MNG_CACHE = i18n("Clean cache")
MNG_CACHE_H = i18n("Remove cached files to free disk space");
MNG_CACHE_A = i18n("Remove all cache")
MNG_CACHE_U = i18n("Remove unused cache")
MNG_CACHE_DIR = i18n("Found directories in the pacman cache:")
MNG_CACHE_DIR_RM = i18n("Remove these directories?")
MNG_REBUILD = i18n("Rebuild packages")
MNG_REBUILD_H = i18n("Select packages that need reinstall with rebuild")
MNG_REBUILD_A = i18n("Rebuild AUR packages")
MNG_REBUILD_P = i18n("Detect Python packages to rebuild")
MNG_MIRROR = i18n("Refresh mirrors")
MNG_WARN = i18n("Review dependency warnings before removing")
MNG_RESUME = i18n("Resume?")
MNG_RETURN = i18n("Press Enter to return menu")
MNG_RECENT = i18n("Recent installs")
MNG_RECENT_SCAN = i18n("Scanning pacman log...")
MNG_RECENT_EXPLICIT = i18n("Explicitly installed")
MNG_RECENT_DEPEND = i18n("Installed as a dependency")
MNG_RECENT_ORPHAN = i18n("Orphaned dependency")
MNG_EXEC = i18n("Executed:")
MNG_DONE = i18n("Nothing to do")
CMD_ERR = i18n("Required installed")
NEWS_ERR = i18n("Cannot fetch news ")
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
source "$(dirname "$0")/utils"
[[ "$1" == "menu" ]] && MENU=1 || MENU=0
selected=$2
returnMenu() {
printf '\e[?25l'
echo -e "\n$(colorize blue bold $ICO_RETURN $MNG_RETURN)"
IFS= read -rs
exec "$scriptDir/management" "$selected"
}
rank_mirrors() {
while IFS= read -r line <&3; do
[[ "$line" =~ ^[[:space:]]*#?[[:space:]]*Server[[:space:]]*=[[:space:]]*(.+) ]] || continue
local server="${BASH_REMATCH[1]}"
server="${server%%#*}"
server="${server%"${server##*[![:space:]]}"}"
local domain="$server"
if [[ "$server" =~ ^([^/]+//[^/]+) ]]; then
domain="${BASH_REMATCH[1]}"
fi
local url="${server%/}"
url="${url//'$arch'/$(uname -m)}"
url="${url//'$repo'/core}"
url="${url}/core.db"
local response=$(curl -s -m 9 -w "%{time_total} %{http_code}" "$url" -o /dev/null 2>/dev/null)
local exit_code=$?
local time status
if [[ $exit_code -ne 0 ]]; then
status="Unreachable"
else
time="${response%% *}"
local code="${response##* }"
[[ -z "$code" || $code -lt 200 || $code -gt 299 ]] && status="Unreachable"
fi
if [[ -n "$status" ]]; then
if [ "$mirrorProgress" = true ]; then
echo "$(colorize red bold $status) $domain" >&2
fi
continue
fi
if [ "$mirrorProgress" = true ]; then
echo "$(colorize gray bold "$time" sec) $domain" >&2
fi
echo "$time $server" >> "$rankmirrors_time"
done 3< "$rankmirrors_mirrorlist"
echo "## Artix Linux repository mirrorlist" > "$rankmirrors_mirrorlist"
echo "## Generated by Antergos Apdatifier on $(date '+%Y-%m-%d %H:%M:%S')" >> "$rankmirrors_mirrorlist"
sort -n -o "$rankmirrors_time" "$rankmirrors_time"
local i=0
while IFS=' ' read -r time server; do
[[ -n "$mirrorCount" && "$mirrorCount" -gt 0 && $i -ge "$mirrorCount" ]] && break
echo "Server = $server" >> "$rankmirrors_mirrorlist"
((i++))
done < "$rankmirrors_time"
}
validate() {
! grep -qE '^[[:space:]]*#?[[:space:]]*Server[[:space:]]*=[[:space:]]*' "$1" && return 1
return 0
}
mirrorfile="/etc/pacman.d/mirrorlist"
OUTDATED=false
[[ ! -f "$mirrorfile" ]] && {
NORM_TIME="(no file)"
OUTDATED=true
} || {
FILE_TIME=$(date -r "$mirrorfile" +%s)
NORM_TIME=$(date -d @"$FILE_TIME" +"%d %b %H:%M:%S")
CURR_TIME=$(date +%s)
[[ "$mirrors" == "age" && $MENU -eq 0 ]] && {
(( CURR_TIME - FILE_TIME >= mirrorsAge * 86400 )) && OUTDATED=true || exit
}
}
[[ "$mirrors" != "force" || $MENU -eq 1 ]] && {
printImportant "$MIRROR_TIME" "$NORM_TIME"
while true; do
printQuestion "$MIRROR_REFRESH"; read -r answer
case "$answer" in
[Yy]*) echo; break;;
[Nn]*|"") (( MENU )) && { returnMenu; } || { echo; exit; };;
*) ;;
esac
done
}
rankmirrors_mirrorlist="$(mktemp)"
rankmirrors_time="$(mktemp)"
trap 'rm -f "$rankmirrors_mirrorlist" "$rankmirrors_time"' INT TERM EXIT
curl -s -m 10 -o "$rankmirrors_mirrorlist" "$dynamicUrl" 2>/dev/null & printWhile $! "$MIRRORS_FETCH"
if ! validate "$rankmirrors_mirrorlist"; then
printError "$MIRRORS_ERR"
printError "No server entries found."
(( MENU )) && returnMenu || exit
fi
if [ "$mirrorProgress" = true ]; then
echo -e "$(colorize blue bold "$ICO_MIRRORS $MIRRORS_RANK")"
rank_mirrors
else
rank_mirrors & printWhile $! "$MIRRORS_RANK"
fi
if ! validate "$rankmirrors_mirrorlist"; then
(( MENU )) && returnMenu || exit
fi
${sudoBin} -n true 2>/dev/null || { echo; printImportant "$MIRRORS_SUDO"; }
cat "$rankmirrors_mirrorlist" | ${sudoBin} tee "$mirrorfile" > /dev/null
if [ $? -eq 0 ]; then
printDone "$mirrorfile $MIRRORS_UPD"
echo -e "$y$(tail -n +3 "$mirrorfile" | sed 's/Server = //g')$c\n"
(( MENU )) && returnMenu
else
printError "$MIRRORS_SUDO"
(( MENU )) && returnMenu || exit
fi
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
source "$(dirname "$0")/utils"
declare -A termArg=( ["gnome-terminal"]="--" ["ptyxis"]="--" ["terminator"]="-x" )
name="${1^}"
sess="Apdatifier"
term="$(basename "$terminal")"
termArg="${termArg[$term]:-"-e"}"
scr="$(dirname "$0")/$1"
arg="${@:2}"
_attach="tmux attach-session -t $sess \; select-window -t $name"
_script="$scr $arg"
[[ $tmuxSession = "true" && -x $(command -v tmux) ]] && _TMUX=1
run_terminal() {
"$term" "$termArg" bash -c "$1"
}
run_yakuake() {
qdbusCMD sessions runCommandInTerminal "$session" "bash -c '$1'"
}
create_tmux_session() {
! tmux has-session -t "$sess" 2>/dev/null && {
tmux new-session -d -s "$sess" -n "$name" "bash -c '$scr $arg'"
} || {
! tmux list-windows -t "$sess" | grep -wq "$name" && {
tmux new-window -t "$sess:" -n "$name" "bash -c '$scr $arg'"
}
}
}
if [[ $term = "yakuake" ]]; then
session=$(qdbusCMD sessions addSession)
visible=$(qdbusCMD MainWindow_1 org.qtproject.Qt.QWidget.visible)
qdbusCMD tabs org.kde.yakuake.setTabTitle "$session" "$name"
if (( _TMUX )); then
create_tmux_session
run_yakuake "$_attach"
else
run_yakuake "tput sc; clear; $_script"
fi
[[ $visible = "false" ]] && qdbusCMD window org.kde.yakuake.toggleWindowState
elif (( _TMUX )); then
create_tmux_session
run_terminal "$_attach"
else
run_terminal "$_script"
fi
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
source "$(dirname "$0")/utils"
trap 'echo -e "\n\n$(colorize red bold $ICO_WARN "CTRL+C. $UPGRADE_SKIPPED")"' SIGINT
run() {
[[ -z $2 ]] && printExec "$1"
if [ "$idleInhibit" = true ] && command -v systemd-inhibit >/dev/null 2>&1; then
read -ra cmd <<< "$1"
systemd-inhibit --what=idle:sleep:shutdown --who="Apdatifier" --why="$UPGRADE_PROGRESS" "${cmd[@]}"
else
eval ${1}
fi
}
printClose() {
tput civis
printMsg "$UPGRADE_ENTER"
while ! read -t 30 -r; do
if command -v gdbus >/dev/null; then
gdbus call --session \
--dest org.kde.plasmashell \
--object-path /org/kde/osdService \
--method org.kde.osdService.showText \
apdatifier-plasmoid "$UPGRADE_COMPLETE" >/dev/null 2>&1
fi
done
tput cnorm
[[ $(basename "$terminal") = "yakuake" ]] && qdbusCMD sessions removeSession "$(qdbusCMD sessions activeSessionId)"
}
bin="$sudoBin pacman"; [ "$aur" = true ] && bin=$wrapper
fullSystemUpgrade() {
startTime=$(date +%s)
critical_updated=()
critical_installed=()
critical_packages=(
"amd-ucode"
"intel-ucode"
"cryptsetup"
"linux"
"linux-hardened"
"linux-lts"
"linux-zen"
"linux-rt"
"linux-rt-lts"
"linux-firmware*"
"linux-cachyos*"
"linux-cacule*"
"nvidia"
"nvidia-dkms"
"nvidia-*xx-dkms"
"nvidia-*xx"
"nvidia-*lts-dkms"
"nvidia*-lts"
"mkinitcpio*"
"booster*"
"mesa"
"systemd*"
"wayland"
"virtualbox-guest-utils"
"virtualbox-host-dkms"
"virtualbox-host-modules-arch"
"egl-wayland"
"xf86-video-*"
"xorg-server*"
"xorg-fonts*"
"winesync-dkms"
)
if [ "$arch" = true ]; then
printMsg "$UPGRADE_FULL_ARCH"
echo
[ "$mirrors" != "false" ] && "$scriptDir"/mirrorlist
if [ -n "$preExec" ]; then
run "$preExec"
printf '\n\033[32m'; printf '%.0s:' {1..48}; printf '\033[0m\n\n'
fi
if [ "$rebootSystem" = true ]; then
for package in "${critical_packages[@]}"; do
matches=$(pacman -Qq | grep -E "^${package//\*/.*}$")
[ -n "$matches" ] && critical_installed+=($matches)
done
declare -A beforeVersions
for pkg in "${critical_installed[@]}"; do
beforeVersions[$pkg]=$(pacman -Q $pkg | awk '{print $2}')
done
fi
test -f "$configDir/env.sh" && source "$configDir/env.sh"
ignorePkgs="$@"
run "$bin -Syu $archFlags $ignorePkgs"
for pkg in "${critical_installed[@]}"; do
afterVersion=$(pacman -Q $pkg | awk '{print $2}')
if [ "${beforeVersions[$pkg]}" != "$afterVersion" ]; then
critical_updated+=("$pkg")
fi
done
fi
if [ "$flatpak" = true ]; then
printMsg "$UPGRADE_FULL_FLATPAK"
echo
run "flatpak update $flatpakFlags"
if [ "$flatpakRemoveUnused" = true ]; then
echo
run "flatpak uninstall --unused $flatpakFlags"
fi
fi
if [ "$widgets" = true ]; then
printMsg "$UPGRADE_FULL_WIDGETS"
echo
run "$scriptDir/widgets upgradeAll" noPrint
fi
if [ "$fwupd" = true ]; then
printMsg "fwupdmgr"
echo
run "$sudoBin fwupdmgr update"
fi
if [ -n "$postExec" ]; then
printf '\n\033[32m'; printf '%.0s:' {1..48}; printf '\033[0m\n\n'
run "$postExec"
fi
trap ' ' SIGINT
endTime=$(date +%s)
runTime=$((endTime-startTime))
echo; echo
printImportant "$UPGRADE_EXECTIME" "$(printf "%02dh:%02dm:%02ds" $((runTime / 3600)) $(( (runTime % 3600) / 60 )) $((runTime % 60)))"
if [ ${#critical_updated[@]} -gt 0 ]; then
echo
printImportant "$UPGRADE_CRITICAL" "${critical_updated[*]}"
while true; do
printQuestion "$UPGRADE_REBOOT"; read -r answer
case "$answer" in
[Yy]*) sys_reboot; exit;;
[Nn]*|"") break;;
*) ;;
esac
done
fi
printClose
}
sys_reboot() {
tput civis
printMsg "$UPGRADE_REBOOTING"
read -s -t 2
qdbus6 org.kde.Shutdown /Shutdown logoutAndReboot
}
flatpak_package() {
printMsg "$UPGRADE_PACKAGE: $2"
echo
printExec "flatpak update" "$1 $flatpakFlags"
flatpak update $1 $flatpakFlags
printClose
}
widget_package() {
printMsg "$UPGRADE_PACKAGE: $2"
echo
"$scriptDir"/widgets upgrade "$1" "$2"
printClose
}
case "$1" in
"full") shift; fullSystemUpgrade $@ ;;
"flatpak") shift; flatpak_package $1 $2;;
"widget") shift; widget_package $1 $2;;
*) exit;;
esac
@@ -0,0 +1,524 @@
#!/usr/bin/env bash
source "$(dirname "$0")/vars"
cleanup() {
[ -d "$tempDir" ] && rm -rf "$tempDir"
}
makeTempDir() {
cleanup
mkdir "$tempDir"
}
colorize() {
local red="\033[31m"
local green="\033[32m"
local blue="\033[34m"
local yellow="\033[33m"
local white="\033[37m"
local gray="\e[90m"
local clear="\033[0m"
local bold="\033[1m"
local color="$1"; shift
local style=""
if [[ "$1" == "bold" ]]; then
style="${!1}"
shift
fi
echo -ne "${!color}${style}$*${clear}"
}
printDone() {
echo -e "$(colorize green bold "$ICO_DONE $1")"
}
printError() {
echo -e "$(colorize red bold "$ICO_ERR $1")"
}
printImportant() {
echo -e "$(colorize yellow bold "$ICO_WARN $1") $2"
}
printQuestion() {
echo -en "$(colorize yellow bold "$ICO_QUESTION $1") [y/N]: "
}
printExec() {
echo -e "$(colorize blue bold "$ICO_EXEC $MNG_EXEC") $(colorize white $1) \n"
}
printMsg() {
local text="$1"
local padding=$(( (48 - ${#text} - 2) / 2 ))
local l=$(printf ":%.0s" {1..48})
local s=$(printf ":%.0s" $(seq 1 $padding))
local p=${s}$( (( ${#text} % 2 )) && echo ":" )
echo
echo -e "$(colorize green bold $l)"
echo -e "$(colorize green bold $s) $(colorize white bold $text) $(colorize green bold $p)"
echo -e "$(colorize green bold $l)"
}
printClose() {
tput civis
printMsg "$UPGRADE_ENTER"
read -r
tput cnorm
[[ $(basename "$terminal") = "yakuake" ]] && qdbusCMD sessions removeSession "$(qdbusCMD sessions activeSessionId)"
}
printWhile() {
tput sc
pid=$1
spinner $pid "$2"
wait $pid
exitCode=$?
tput rc
tput ed
[[ $exitCode -eq 0 ]] && printDone "$2" || printError "$2"
if [[ "$2" = "$WIDGETS_FETCH" ]]; then
case $exitCode in
1) printError "$WIDGETS_ERR_API_FAIL" ;;
2) printError "$WIDGETS_ERR_API" ;;
3) printError "$WIDGETS_ERR_UNKNOWN" ;;
esac
elif [[ "$2" = "$WIDGETS_LINK" ]]; then
case $exitCode in
1) printError "$WIDGETS_ERR_NOFILES. $UPGRADE_SKIPPED" ;;
2) printError "$WIDGETS_ERR_NOTAGGED $latestVer. $UPGRADE_SKIPPED" ;;
3) printError "$WIDGETS_ERR_TAGGED $latestVer. $UPGRADE_SKIPPED" ;;
esac
elif [[ "$2" = "$WIDGETS_DOWNLOADING" ]]; then
case $exitCode in
1) printError "$WIDGETS_ERR_PACKAGE_FAIL" ;;
2) printError "$WIDGETS_ERR_EXT" ;;
3) printError "$WIDGETS_ERR_NO_JSON" ;;
4) printError "$WIDGETS_ERR_JSON" ;;
esac
fi
}
qdbusCMD() {
qdbus6 org.kde.yakuake /yakuake/$1 "${@:2}"
}
dependencies() {
for cmd in ${1}; do
if ! command -v "$cmd" >/dev/null; then
printError "${CMD_ERR} ${cmd}"
[ $2 ] && returnMenu || exit
fi
done
}
oneLine() {
tr '\n' ' ' | sed 's/ $//'
}
spinner() {
local spin="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
while kill -0 $1 2>/dev/null; do
i=$(( (i+1) %10 ))
printf "\r$(colorize red ${spin:$i:1}) $(colorize blue bold "$2...")"
sleep .2
done
}
clearVer() {
local ver="${1}"
ver="${ver//[^0-9.]/.}"
while [[ "$ver" == *".."* ]]; do
ver="${ver//../.}"
done
ver="${ver#.}"
ver="${ver%.}"
echo "${ver}"
}
compareVer() {
[[ $1 == $2 ]] && return 0
local IFS=.; local i ver1=($1) ver2=($2)
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do ver1[i]=0; done
for ((i=0; i<${#ver1[@]}; i++)); do
[[ -z ${ver2[i]} ]] && ver2[i]=0
((10#${ver1[i]} > 10#${ver2[i]})) && return 1
((10#${ver1[i]} < 10#${ver2[i]})) && return 2
done
return 0
}
getId() {
[[ $1 =~ ^[0-9]+$ ]] && f=2 || f=1
grep "$1" "$(dirname "$0")/widgets-id" | cut -d' ' -f$f
}
restartPlasmashell() {
if [[ "$restartShell" = true ]]; then
sleep 1
while true; do
printQuestion "$WIDGETS_RESTART"; read -r answer
case "$answer" in
[Yy]*) break;;
[Nn]*|"") exit;;
*) ;;
esac
done
eval ${restartCommand}
else
printImportant "$WIDGETS_WARN"
fi
}
combineFiles() {
sleep 1
[ -f "$1" ] && rm "$1"
for cache in $(find "$cacheDir" -name "updates.json_*" 2>/dev/null | sort); do
cat "$cache" >> "$1"; rm "$cache"
done
}
getIcons() {
while read -r pkg file; do
[[ "$processed" == *"$pkg"* ]] && continue
icon=$(awk -F= '/^Icon=/ {print $2; exit}' "$file" 2>/dev/null || true) && [ -n "$icon" ] || continue
processed="$processed $pkg"
echo "$pkg $icon"
done < <(pacman -Ql "$@" | grep '/usr/share/applications/.*\.desktop$')
}
updateJson() {
cat <<< "$2" > "$1.tmp" && mv "$1.tmp" "$1"
}
rss() {
command -v jq >/dev/null || { echo "${CMD_ERR} jq" >&2; return 127; }
# Wait for DNS to be ready (up to 30s) — avoids failures at boot/resume
local host=$(echo "$1" | sed -e 's|^[^/]*//||' -e 's|/.*$||')
for i in $(seq 1 30); do
getent hosts "$host" >/dev/null 2>&1 && break
sleep 1
done
makeTempDir
trap cleanup EXIT
local newsFile="$cacheDir/news.json"
local errors=""
if [[ ! -s "$newsFile" ]] \
|| ! jq -e '.' "$newsFile" >/dev/null 2>&1 \
|| ! jq -e 'all(.[]; .timestamp | numbers)' "$newsFile" >/dev/null 2>&1; then
echo '[]' > "$newsFile"
fi
# Build per-URL output file list for parallel download
local -a urls=("$@")
local -a rssFiles=()
local -a curlArgs=()
for url in "${urls[@]}"; do
local rssFile=$(mktemp "$tempDir/XXXXXX.xml")
rssFiles+=("$rssFile")
curlArgs+=(-o "$rssFile" --url "$url")
done
# Fetch all feeds simultaneously
curl -s --parallel --parallel-immediate \
--connect-timeout 5 --retry 3 --retry-delay 1 --retry-all-errors --max-time 15 \
"${curlArgs[@]}" 2>/dev/null
# Process each downloaded file
for i in "${!urls[@]}"; do
local url="${urls[$i]}"
local rssFile="${rssFiles[$i]}"
# Strip default XML namespace to avoid XPath 1.0 namespace issues
local cleanFile=$(mktemp "$tempDir/XXXXXX.xml")
sed 's/ xmlns="[^"]*"//g' "$rssFile" > "$cleanFile"
xmllint --noout "$cleanFile" 2>/dev/null || {
errors+="$url "
continue
}
# Detect RSS 2.0 vs Atom 1.0 by root element name
local root=$(xmllint --xpath "name(/*)" "$cleanFile" 2>/dev/null)
# Count available items in feed (capped at feedsFetchCount)
local count title
if [[ "$root" == "feed" ]]; then
count=$(xmllint --xpath "count(/feed/entry)" "$cleanFile" 2>/dev/null || echo 0)
title=$(xmllint --xpath "string(/feed/title)" "$cleanFile")
else
count=$(xmllint --xpath "count(/rss/channel/item)" "$cleanFile" 2>/dev/null || echo 0)
title=$(xmllint --xpath "string(/rss/channel/title)" "$cleanFile")
fi
[[ -z "$title" ]] && { errors+="$url "; continue; }
local max=${feedsFetchCount:-3}
(( count > max )) && count=$max
local n
for (( n=1; n<=count; n++ )); do
local article timestamp link
if [[ "$root" == "feed" ]]; then
article=$(xmllint --xpath "string(/feed/entry[$n]/title)" "$cleanFile")
timestamp=$(date -d "$(xmllint --xpath "string(/feed/entry[$n]/updated)" "$cleanFile")" +%s 2>/dev/null)
link=$(xmllint --xpath "string(/feed/entry[$n]/link/@href)" "$cleanFile")
else
article=$(xmllint --xpath "string(/rss/channel/item[$n]/title)" "$cleanFile")
timestamp=$(date -d "$(xmllint --xpath "string(/rss/channel/item[$n]/pubDate)" "$cleanFile")" +%s 2>/dev/null)
link=$(xmllint --xpath "string(/rss/channel/item[$n]/link)" "$cleanFile")
fi
[[ -z "$article" || -z "$timestamp" || -z "$link" ]] && continue
if ! jq -e --arg l "$link" '.[] | select(.link == $l)' "$newsFile" > /dev/null; then
local item=$(jq -n --arg t "$title" --arg a "$article" --arg l "$link" --argjson ts "$timestamp" \
'{title: $t, article: $a, timestamp: $ts, link: $l, removed: false}')
updateJson "$newsFile" "$(jq --argjson item "$item" '[$item] + .' "$newsFile")"
fi
done
# Prune oldest entries for this feed down to feedsFetchCount
while (( $(jq --arg t "$title" '[.[] | select(.title == $t)] | length' "$newsFile") > max )); do
index=$(jq --arg t "$title" 'to_entries | reverse | map(select(.value.title == $t)) | .[0].key' "$newsFile")
updateJson "$newsFile" "$(jq --argjson index "$index" 'del(.[$index])' "$newsFile")"
done
done
updateJson "$newsFile" "$(jq 'sort_by(.timestamp) | reverse' "$newsFile")"
jq . $newsFile
if [[ -n "$errors" ]]; then
errors="$NEWS_ERR: ${errors// /, }"
errors="${errors%, }"
echo "$errors" >&2
exit 1
fi
}
sync_dbs() {
! type -p fakeroot >/dev/null && {
echo "fakeroot not found" >&2
exit 127
}
! mkdir -p "$dbPath" && {
echo "Failed to create directory $dbPath" >&2
exit 1
}
db_conf="$(pacman-conf DBPath)"
db_conf="${db_conf%/}"
! ln -sf "${db_conf}/local" "$dbPath" &> /dev/null && {
echo "Failed to create symlink to $db_conf/local" >&2
exit 1
}
test -f "$dbPath/db.lck" && rm -f "$dbPath/db.lck"
! fakeroot -- pacman -Sy --disable-sandbox-filesystem --dbpath "$dbPath" --logfile /dev/null 2>/dev/null && {
echo "Failed to synchronize pacman databases" >&2
exit 1
}
}
checkCurrentVersions() {
local list
if [ "$arch" = true ]; then
list+=$(pacman -Q | awk '{print "{\"NM\": \"" $1 "\", \"VO\": \"" $2 "\"},"}')
fi
if [ "$flatpak" = true ]; then
list+=$(flatpak list --app --columns=name,version,active | awk -F'\t' '{print "{\"NM\": \"" $1 "\", \"VO\": \"" $2 $3 "\"},"}')
fi
if [ "$widgets" = true ]; then
list+=$("$scriptDir"/widgets list)
fi
if [ "$fwupd" = true ]; then
if command -v jq >/dev/null 2>&1; then
list+=$(fwupdmgr get-devices --json | jq -cr '.Devices | map({NM: ((.Vendor + " " + .Name)), VO: (.Version // "")}) | .[] | @json + ","')
fi
fi
echo "[${list%,}]"
}
checkSetup() {
if (( systemWide )); then
printError "Operation not permitted - the widget is installed system-wide"
printClose
exit
fi
}
install_devel() {
checkSetup
printMsg "Downloading devel"
echo
required="git jq"
for cmd in ${required}; do command -v "$cmd" >/dev/null || { printError "Required installed ${cmd}"; printClose; exit; }; done;
commit=$(curl -s https://api.github.com/repos/exequtic/apdatifier/commits/main | jq -r '.sha' | cut -c1-7)
if [[ "github commit: $commit" = "$(jq -r '.KPlugin.Description' "$appletDir/metadata.json")" ]]; then
printDone "Up to date"
printClose
exit
fi
savedir=$(pwd)
clonedir=$(mktemp -d)
cd "$clonedir"
{ git clone -n --depth=10 --filter=tree:0 -b main https://github.com/exequtic/apdatifier 2>/dev/null
} & printWhile $! "Clone main branch"
[[ $exitCode -ne 0 ]] && { printClose; exit; }
cd apdatifier
{ git sparse-checkout set --no-cone package 2>/dev/null && git checkout 2>/dev/null
} & printWhile $! "Checkout package directory"
[[ $exitCode -ne 0 ]] && { printClose; exit; }
if command -v less &>/dev/null; then
echo
printImportant "Last 10 commits:"
git log --oneline
sleep 2
fi
echo
printMsg "Upgrading Apdatifier"
echo
cd package || exit 1
updateJson metadata.json "$(jq --arg new_value "github commit: $commit" '.KPlugin.Description = $new_value' metadata.json)"
[[ $trayEnabledByDefault == "true" ]] && updateJson $metadata "$(jq '.KPlugin.EnabledByDefault = true' $metadata)"
while true; do
printQuestion "Do you want build translations?"; read -r answer
case "$answer" in
[Yy]*) cd translate; bash build; cd ..; break;;
[Nn]*|"") break;;
*) ;;
esac
done
echo
tar --exclude=./apdatifier.tar -cf apdatifier.tar .
kpackagetool6 -t Plasma/Applet -u apdatifier.tar 2>/dev/null
echo
cd "$savedir"
[ ! -d "$clonedir" ] || rm -rf "$clonedir"
restartShell=true
restartPlasmashell
printClose
exit
}
install_stable() {
checkSetup
if [[ "$(jq -r '.KPlugin.Description' "$appletDir/metadata.json")" = "Arch Update Notifier" ]]; then
while true; do
printImportant "It looks like you already have the stable version."
printQuestion "Do you still want to proceed?"; read -r answer
case "$answer" in
[Yy]*) break;;
[Nn]*|"") printClose; exit;;
*) ;;
esac
done
fi
$scriptDir/upgrade widget "2135796" "apdatifier"
}
uninstall() {
checkSetup
iconsDir="$localDir/icons/breeze/status/22"
notifDir="$localDir/knotifications6"
notif="apdatifier.notifyrc"
icons=(apdatifier-plasmoid.svg
apdatifier-packages.svg
apdatifier-package.svg
apdatifier-flatpak.svg)
printMsg "Uninstall Apdatifier"
echo
while true; do
printQuestion "Continue?"; read -r answer
case "$answer" in
[Yy]*) break;;
[Nn]*|"") printClose; exit;;
*) ;;
esac
done
echo
showCommand() {
echo -e "$(colorize yellow bold "$ICO_EXEC Command:") $1"
}
printImportant "Remove icons"
for icon in "${icons[@]}"; do
showCommand "rm $iconsDir/$icon"
test -f "$iconsDir/$icon" && rm "$iconsDir/$icon"
done
showCommand "rmdir -p --ignore-fail-on-non-empty $iconsDir"
test -d "$iconsDir" && rmdir -p --ignore-fail-on-non-empty "$iconsDir"
sleep 1; echo
printImportant "Remove notification"
showCommand "rm $notifDir/$notif"
test -f "$notifDir/$notif" && rm "$notifDir/$notif"
showCommand "rmdir -p --ignore-fail-on-non-empty $notifDir"
test -d "$notifDir" && rmdir -p --ignore-fail-on-non-empty "$notifDir"
sleep 1; echo
printImportant "Remove configuration"
showCommand "rm -rf $configDir"
test -d "$configDir" && rm -rf "$configDir"
sleep 1; echo
printImportant "Uninstall plasmoid"
showCommand "kpackagetool6 -t Plasma/Applet -r com.github.exequtic.apdatifier"
kpackagetool6 -t Plasma/Applet -r com.github.exequtic.apdatifier 2>/dev/null
sleep 1; echo
echo "Bye :("; sleep 1
printClose
exit
}
case "$1" in
"syncdb") sync_dbs ;;
"rss") shift; rss "$@" ;;
"combineFiles") shift; combineFiles $1 ;;
"currentVersions") checkCurrentVersions ;;
"getIcons") shift; getIcons "$@" ;;
"installDev") install_devel ;;
"installStable") install_stable ;;
"uninstall") uninstall ;;
esac
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
scriptDir="$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")"
appletDir="${scriptDir%/contents/*}"
[[ "$appletDir" == /home/*/.local* ]]; systemWide=$?
configDir="${APDATIFIER_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/apdatifier}"
cacheDir="${APDATIFIER_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/apdatifier}"
localDir="${XDG_LOCAL_HOME:-$HOME/.local/share}"
tempDir="${TMPDIR:-/tmp}/apdatifier"
config="$configDir/config.conf"
export TEXTDOMAINDIR="$scriptDir/../../locale"
export TEXTDOMAIN="plasma_applet_com.github.exequtic.apdatifier"
source "$config"
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
key="${line%%=*}"
key="${key%% *}"
value="${line#*=}"
text=$(echo "$value" | grep -oP '(?<=\().*(?=\))' | sed 's/"//g')
[[ -n "$key" && -n "$text" ]] && eval "$key=\"\$(gettext \"$text\")\""
done < "$scriptDir/messages"
if [[ $termFont = "true" ]]; then
ICO_ERR=""
ICO_DONE=""
ICO_WARN="󱇎"
ICO_QUESTION=""
ICO_EXEC="󰅱"
ICO_MIRRORS="󰓅"
ICO_RETURN="󰬨"
ICO_SELECT="󰄾"
else
ICO_ERR="✘"
ICO_DONE="✔"
ICO_WARN="::"
ICO_QUESTION="::"
ICO_EXEC="::"
ICO_MIRRORS="::"
ICO_RETURN="<<"
ICO_SELECT=">"
fi
@@ -0,0 +1,365 @@
#!/usr/bin/env bash
source "$(dirname "$0")/utils"
fetch_api_data() {
echo '<?xml version="1.0" encoding="UTF-8"?><data>' > "$XML"
local ps=100 url="https://api.kde-look.org/ocs/v1/content/data"
local q="categories=705x715x719x720&sort=new&pagesize=$ps"
local p0=$(mktemp "$tempDir/XXXXXX.xml")
curl -s -o "$p0" --connect-timeout 5 --retry 2 --max-time 60 --request GET --url "$url?$q&page=0"
! xmllint --noout "$p0" 2>/dev/null && { echo '</data>' >> "$XML"; return 1; }
case $(xmllint --xpath "string(//ocs/meta/statuscode)" "$p0") in
""|200) echo '</data>' >> "$XML"; return 2;;
[!100]*) echo '</data>' >> "$XML"; return 3;;
esac
local total=$(xmllint --xpath "string(//ocs/meta/totalitems)" "$p0")
xmllint --xpath "//content[@details='summary']" "$p0" 2>/dev/null >> "$XML"
[[ $total =~ ^[0-9]+$ ]] || total=0
(( total <= ps )) && { echo '</data>' >> "$XML"; return 0; }
local pages=$(( (total - 1) / ps ))
local -a files=() args=()
for ((p=1; p<=pages; p++)); do
f=$(mktemp "$tempDir/XXXXXX.xml"); files+=("$f")
args+=(-o "$f" --url "$url?$q&page=$p")
done
curl -s --parallel --parallel-immediate --connect-timeout 5 --retry 2 --max-time 60 "${args[@]}"
for f in "${files[@]}"; do
xmllint --noout "$f" 2>/dev/null || { echo '</data>' >> "$XML"; return 2; }
case $(xmllint --xpath "string(//ocs/meta/statuscode)" "$f") in
100) xmllint --xpath "//content[@details='summary']" "$f" 2>/dev/null >> "$XML";;
*) echo '</data>' >> "$XML"; return 2;;
esac
done
echo '</data>' >> "$XML"
}
get_packages() {
packages=($(find \
"$localDir/kwin/effects" \
"$localDir/kwin/scripts" \
"$localDir/plasma/plasmoids" \
"$localDir/plasma/wallpapers" \
-mindepth 1 -maxdepth 1 -type d 2>/dev/null))
[ ${#packages[@]} -eq 0 ] && return 1
return 0
}
get_package_type() {
case $(xmllint --xpath "string(//id[text()='$contentId']/../typeid)" "$XML") in
"715") echo "Plasma/Wallpaper";;
"719") echo "KWin/Effect";;
"720") echo "KWin/Script";;
*) echo "Plasma/Applet";;
esac
}
get_package_info() {
local id=$(basename "$package")
local metadata="$package/metadata.json"
[[ -s "$metadata" ]] || return 1
jq . "$metadata" >/dev/null 2>&1 || return 2
local originName=$(jq -r '.KPlugin.Name' "$metadata")
name=$(echo "$originName" | sed 's/ /-/g; s/.*/\L&/')
local quotedOriginName='"'$(echo "$originName" | sed 's/"/\\"/g')'"'
contentId=$(xmllint --xpath "string(//name[text()=$quotedOriginName]/../id)" "$XML")
if [[ -z "$contentId" ]]; then
knsregistry=("plasmoids" "kwinscripts" "kwineffect" "wallpaperplugin")
for kns in "${knsregistry[@]}"; do
kns="$HOME/.local/share/knewstuff3/$kns.knsregistry"
[[ -s "$kns" ]] && contentId=$(xmllint --xpath "string(//installedfile[contains(text(), '/$id')]/../id)" "$kns")
[[ -n "$contentId" ]] && break
done
fi
[[ -z "$contentId" ]] && contentId="$(getId "$id" | head -n 1)"
[[ -z "$contentId" ]] && return 3
currentVer=$(clearVer "$(jq -r '.KPlugin.Version' "$metadata")")
latestVer=$(clearVer "$(xmllint --xpath "string(//id[text()='$contentId']/../version)" $XML)")
[ -z "$currentVer" ] || [ -z "$latestVer" ] && return 4
compareVer "$currentVer" "$latestVer"
[[ $? != 2 ]] && return 5
description=$(jq -r '.KPlugin.Description' "$metadata" | sed 's/"/\\"/g' | tr -d '\n')
[ -z "$description" ] || [ "$description" = "null" ] && description="$WIDGETS_NODESC"
author=$(jq -r '.KPlugin.Authors[].Name' "$metadata" | paste -sd "," - | sed 's/,/, /g')
[ -z "$author" ] || [ "$author" = "null" ] && author="?"
icon=$(jq -r '.KPlugin.Icon' "$metadata")
local fallbackIcon="start-here-kde-plasma-symbolic"
if [ -z "$icon" ]; then
icon=$fallbackIcon
else
local iconTheme=$(kreadconfig6 --file kdeglobals --group Icons --key Theme)
local themeDir1="/usr/share/icons/$iconTheme/"
local themeDir2="$HOME/.local/share/icons/$iconTheme/"
local themeDir3="$HOME/.icons/$iconTheme/"
! find -L "$themeDir1" "$themeDir2" "$themeDir3" \
-type f -name "$icon.svg" -print -quit 2>/dev/null | grep -q . \
&& icon=$fallbackIcon
fi
url="https://store.kde.org/p/$contentId"
repo="kde-store"
type=$(get_package_type)
if ! jq -e ".KPackageStructure == \"$type\"" "$metadata" >/dev/null 2>&1; then
updateJson "$metadata" "$(jq ". + { \"KPackageStructure\": \"$type\" }" "$metadata")"
fi
return 0
}
get_download_link() {
local signed=()
local files=0
while read -r download_version; do
((files++))
if [[ "$latestVer" == "$(clearVer "$(xmllint --xpath "string(//id[text()='$contentId']/../$download_version)" "$XML")")" ]]; then
signed+=("${download_version#download_version}")
fi
done < <(xmllint --xpath "//content[id='$contentId']/*" "$XML" | grep -o 'download_version[0-9]\+' | uniq)
if [[ $files -eq 1 || ${#signed[@]} -eq 1 ]]; then
echo $(xmllint --xpath "string(//id[text()='$contentId']/../downloadlink${signed[0]:-1})" "$XML") > "$tempDir/link"
return 0
else
[[ $files -eq 0 ]] && return 1
[[ ${#signed[@]} -eq 0 ]] && return 2
(( ${#signed[@]} > 1 )) && return 3
fi
}
make_updates_list() {
local tmp="$tempDir/update.list.lines.json"
: > "$tmp"
for package in "${packages[@]}"; do
get_package_info
[[ $? -ne 0 ]] && continue
jq -cn \
--arg NM "$name" \
--arg RE "$repo" \
--arg TP "$type" \
--arg CN "$contentId" \
--arg IN "$icon" \
--arg DE "$description" \
--arg AU "$author" \
--arg VO "$currentVer" \
--arg VN "$latestVer" \
--arg LN "$url" \
'{NM:$NM,RE:$RE,TP:$TP,CN:$CN,IN:$IN,DE:$DE,AU:$AU,VO:$VO,VN:$VN,LN:$LN}' >> "$tmp"
done
jq -s '.' "$tmp" > "$updatesList"
}
check_packages_updates() {
command -v jq >/dev/null || {
echo "$WIDGETS_ERR $CMD_ERR jq" >&2
exit 127
}
declare -a packages
! get_packages && {
echo "[]"
exit
}
makeTempDir
XML="$tempDir/api.kde-look.xml"
updatesList="$tempDir/update.list.json"
trap cleanup EXIT
fetch_api_data
case $? in
1) echo "$WIDGETS_ERR $WIDGETS_ERR_API_FAIL" >&2; exit 1 ;;
2) echo "$WIDGETS_ERR $WIDGETS_ERR_API" >&2; exit 1 ;;
3) echo "$WIDGETS_ERR $WIDGETS_ERR_UNKNOWN" >&2; exit 1 ;;
esac
make_updates_list
jq . $updatesList
}
download_package() {
mkdir -p "$tempDir/$name/unpacked"
link=$(cat "$tempDir/link")
package="$tempDir/$name/$(basename "${link}")"
curl -s -o "$package" --connect-timeout 5 --retry 2 --max-time 120 --request GET --location --url "$link" 2>/dev/null
[ ! -s "$package" ] && return 1
bsdtar -xf "$package" -C "$tempDir/$name/unpacked" || return 2
metadata=$(find "$tempDir/$name/unpacked" -name metadata.json)
[ -z "metadata" ] && return 3
jq . "$metadata" >/dev/null 2>&1 || return 4
if ! jq -e ".KPackageStructure == \"$type\"" "$metadata" >/dev/null 2>&1; then
updateJson "$metadata" "$(jq ". + { \"KPackageStructure\": \"$type\" }" "$metadata")"
fi
updateJson "$metadata" "$(jq --arg new_value "$latestVer" '.KPlugin.Version = $new_value' "$metadata")"
if [[ $name == "apdatifier" && $trayEnabledByDefault == "true" ]]; then
if jq -e '.KPlugin.EnabledByDefault == false' "$metadata" > /dev/null; then
updateJson "$metadata" "$(jq '.KPlugin.EnabledByDefault = true' "$metadata")"
fi
fi
return 0
}
upgrade_all_packages() {
dependencies "jq"
declare -a packages
! get_packages && {
printDone "$MNG_DONE"
exit
}
makeTempDir
XML="$tempDir/api.kde-look.xml"
updatesList="$tempDir/update.list.json"
trap cleanup EXIT
trap 'kill "$bg_pid" 2>/dev/null; cleanup' INT TERM
fetch_api_data & bg_pid=$!
printWhile "$bg_pid" "$WIDGETS_FETCH"
[[ $exitCode -ne 0 ]] && exit
make_updates_list & bg_pid=$!
printWhile "$bg_pid" "$WIDGETS_CHECK"
if [[ -s "$updatesList" ]] && jq -e '(. | length) > 0' "$updatesList" > /dev/null 2>&1; then
echo
jq -c 'sort_by(.NM)[]' "$updatesList" | while read -r info; do
echo -e "$(colorize white bold $(echo "$info" | jq -r '.NM')) \t \
$(colorize yellow bold $(echo "$info" | jq -r '.TP')) \t \
$(colorize red bold $(echo "$info" | jq -r '.VO')) \t \
$(colorize white "->") \t \
$(colorize green bold $(echo "$info" | jq -r '.VN'))"
done | column -t
echo
if [[ $widgetConfirmation = true ]]; then
while true; do
printQuestion "$WIDGETS_PROCEED"; read -r answer
case "$answer" in
[Yy]*) echo; break;;
[Nn]*|"") return 0;;
*) ;;
esac
done
fi
updated=false
while read -r info; do
name=$(echo "$info" | jq -r '.NM')
contentId=$(echo "$info" | jq -r '.CN')
latestVer=$(echo "$info" | jq -r '.VN')
type=$(echo "$info" | jq -r '.TP')
echo "$(colorize blue bold "$ICO_EXEC $name ($latestVer)")"
get_download_link & printWhile $! "$WIDGETS_LINK"
[[ $exitCode -ne 0 ]] && { echo; continue; }
download_package & printWhile $! "$WIDGETS_DOWNLOADING"
[[ $exitCode -ne 0 ]] && { echo; continue; }
kpackagetool6 -t "$type" -u "$(dirname "$(find "$tempDir/$name" -name "metadata.json")")" 2>/dev/null
updated=true
sleep 1
echo
done < <(jq -c 'sort_by(.NM)[]' "$updatesList")
[[ $updated = true ]] && restartPlasmashell
else
printDone "$MNG_DONE"
fi
}
upgrade_package() {
[ -n "$1" ] && contentId="$1" || exit
[ -n "$2" ] && name="$2" || exit
dependencies "jq"
mkdir -p "$tempDir/$name/unpacked"
XML="$tempDir/$name/api.kde-look.xml"
trap 'kill "$bg_pid" 2>/dev/null; cleanup' INT TERM EXIT
{
curl -s -o "$XML" --connect-timeout 5 --retry 2 --max-time 60 --request GET --url "https://api.kde-look.org/ocs/v1/content/data/$contentId" 2>/dev/null
if xmllint --noout "$XML" 2>/dev/null; then
statuscode=$(xmllint --xpath "string(//ocs/meta/statuscode)" "$XML")
case $statuscode in
100) return 0 ;;
200) return 2 ;;
*) return 3 ;;
esac
else
return 1
fi
} & bg_pid=$!
printWhile "$bg_pid" "$WIDGETS_FETCH"
[[ $exitCode -ne 0 ]] && exit
latestVer=$(clearVer "$(xmllint --xpath "string(//id[text()='$contentId']/../version)" "$XML")")
type=$(get_package_type)
get_download_link & bg_pid=$!
printWhile "$bg_pid" "$WIDGETS_LINK"
[[ $exitCode -ne 0 ]] && exit
download_package & bg_pid=$!
printWhile "$bg_pid" "$WIDGETS_DOWNLOADING"
[[ $exitCode -ne 0 ]] && exit
kpackagetool6 -t "$type" -u "$(dirname "$(find "$tempDir/$name" -name "metadata.json")")" 2>/dev/null
sleep 1
echo
restartPlasmashell
}
list_current_packages() {
declare -a packages
! get_packages && return
local out=""
for package in "${packages[@]}"; do
local json="$package/metadata.json"; [ -s "$json" ] || continue
local name="$(jq -r '.KPlugin.Name' "$json")"; [ -z "$name" ] && continue
local currentVer=$(clearVer "$(jq -r '.KPlugin.Version' "$json")"); [ -z "$currentVer" ] && continue
out+="{\"NM\": \"${name}\","
out+="\"VO\": \"${currentVer}\"},\n"
done
echo -e $out
}
case "$1" in
"list") list_current_packages;;
"check") check_packages_updates;;
"upgrade") shift; upgrade_package $1 $2;;
"upgradeAll") upgrade_all_packages;;
*) exit;;
esac
@@ -0,0 +1,189 @@
998890 com.bxabi.bumblebee-indicator
998913 org.kde.plasma.awesomewidget
1155946 com.dschopf.plasma.qalculate
1288430 org.kde.plasma.shutdownorswitch
1377704 org.kde.mediabar
1384066 org.kde.workraveApplet
1396415 com.github.solant.plasmataker
1627256 com.github.xyz32.timekeeper
1804745 com.github.heqro.day-night-switcher
1898708 org.nielsvm.plasma.menupager
2079446 dev.vili.sahkoporssi
2100417 org.kde.mcwsremote
2100418 org.kde.olib.thermalmonitor
2107649 org.kde.panel.transparency.toggle
2112443 com.github.tilorenz.compact_pager
2113872 com.github.korapp.cloudflare-warp
2114471 de.davidhi.ddcci-brightness
2115883 org.kde.plasma.simplekickoff
2117117 com.dv.fokus
2118132 com.github.stepan-zubkov.days-to-new-year
2118492 com.github.korapp.nordvpn
2126775 com.github.tilorenz.timeprogressbar
2128047 luisbocanegra.panelspacer.extended
2128143 plasmusic-toolbar
2128477 luisbocanegra.intel.gpu.monitor
2129423 org.kde.windowtitle
2130222 luisbocanegra.panel.modes.switcher
2130541 org.kde.archupdatechecker
2130967 luisbocanegra.panel.colorizer
2131364 com.github.korapp.homeassistant
2131462 org.kde.plasma.plasm6desktopindicator
2132405 com.github.dhruv8sh.year-progress-mod
2132554 com.himdek.kde.plasma.overview
2132555 com.himdek.kde.plasma.runcommand
2134470 a2n.archupdate.plasmoid
2135509 com.github.antroids.application-title-bar
2135511 org.kde.placesWidget
2135552 org.kde.plasma.yesplaymusic-lyrics
2135642 com.github.prayag2.minimalistclock
2135653 com.github.prayag2.modernclock
2135796 com.github.exequtic.apdatifier
2135799 com.github.k-donn.plasmoid-wunderground
2135898 com.dv.uswitcher
2136288 org.kde.Big.Clock
2136291 zayron.chaac.weather
2136295 Clock.Asitoki.Color
2136299 CircleClock
2136302 zayron.almanac
2136307 Minimal.chaac.weather
2136321 com.Petik.clock
2136329 weather.bicolor.widget
2136505 org.kde.netspeedWidget
2136546 com.nemmayan.clock
2136631 com.github.scriptinator
2136636 com.github.zren.commandoutput
2136852 org.kde.latte.separator
2136860 com.github.zren.alphablackcontrol
2136933 org.kde.plasma.advancedradio
2136963 luisbocanegra.kdematerialyou.colors
2137016 org.kde.plasma.Beclock
2137185 com.github.zren.dailyforecast
2137197 com.github.zren.condensedweather
2137217 org.kde.plasma.scpmk
2137231 com.github.eatsu.spaceraspager
2137418 zayron.simple.separator
2137431 com.github.zren.simpleweather
2137675 com.gitlab.scias.advancedreboot
2137726 org.zayronxio.vector.clock
2137844 org.kde.plasma.catwalk
2138251 org.kpple.kppleMenu
2138283 ink.chyk.minimumMediaController
2138365 optimus-gpu-switcher
2138473 org.kde.plasma.videocard
2138476 lenovo-conservation-mode-switcher
2138485 com.github.boraerciyas.controlcentre
2138746 org.kde.plasma.pminhibition
2138853 org.kde.Date.Bubble.P6
2138907 org.kde.latte.spacer
2139337 split-clock
2139541 com.github.configurable_button
2139890 Plasma.Control.Hub
2140275 com.github.davide-sd.ip_address
2140856 d4rkwzd.colorpicker-tray
2141133 org.kde.MinimalMusic.P6
2142681 Audio.Wave.Widget
2142716 com.github.zren.tiledmenu
2143899 org.kde.plasma.resources-monitor
2144212 AndromedaLauncher
2144426 org.previewqt.previewqt.plasmoidpreviewqt
2144969 SoloDay.P6
2145065 com.github.liujed.rssfeeds
2145280 com.github.DenysMb.Kicker-AppsOnly
2145723 luisbocanegra.desktop.wallpaper.effects
2146553 org.kde.plasma.ginti
2147850 zayron.almanac.V2
2147871 org.kde.plasma.clearclock
2147882 org.kde.windowtitle.Fork
2148373 lyrics-on-panel-plasma6
2148469 thot.observer.ram
2148472 thot.observer.cpu
2150544 org.kde.plasma.composeEmail
2150610 com.gitlab.scias.plasmavantage
2150916 org.kde.paneltransparencybutton
2151247 org.kde.plasma.win7showdesktop
2151576 ppk.plasma6.hydrobot
2151585 com.samirgaire10.Brave-plasma6
2151600 com.samirgaire10.chatgpt-plasma6
2151622 com.samirgaire10.Deepl-plasma6
2151683 com.samirgaire10.google_gemini-plasma6
2151687 com.samirgaire10.Google-plasma6
2151694 com.samirgaire10.GoogleTranslater-plasma6
2151931 org.kde.windowbuttons
2151962 com.samirgaire10.perplexityAi-plasma6
2152642 com.samirgaire10.blackbox.AI_plasma6
2153869 org.kde.plasma.private.BusyTasksPlasmoid
2154331 com.softtechok.systemmonitorplasmoid
2154894 org.51n7.kMenu
2157390 org.latgardi.darwinmenu
2158349 Almanac.Asimetric
2158358 org.kde.losungen6
2159592 com.samirgaire10.Ollama-plasma6
2160638 com.softtechok.processmonitor
2161461 Weather.IntiSol.kde
2162586 org.kde.dwardor.fanspeedmonitor
2163165 OnlyText.Date.Kde
2163199 com.samirgaire10.DuckDuckGo-plasma6
2163340 ChatAI-Plasmoid
2163368 Circle.Music.Widget
2164459 org.kde.redshiftControl6
2164590 org.kde.plasma.mediacontroller.panel
2164679 com.github.chrtall.kppleMenu
2166656 Aligned.Music.Widget
2166934 adhe.runcommand
2167594 adhe.dittomenu
2167836 com.github.jonmagon.plasma-screendimmer
2168131 Date.Urban.kde
2168466 Rates.Mercurius.Widget
2169597 com.github.danielwjchen.folding-at-home-plasma-widget
2170656 com.samirgaire10.Github-plasma6
2171750 adhe.menu.11
2172470 org.magpie.sonomatic.separator
2173278 org.kde.plasma.powerusage
2174238 adhe.launchpadPlasma
2174265 Chaac.Complete.Weather
2175475 Redmi.Clock
2179264 org.magpie.nightglow.separator
2179317 Start.Next.Menu
2179829 com.kevinbburns.grammarly
2179833 Small.Weather.Plasma6
2180314 Soniq.Widget
2180887 DeepinMenu.Classic
2182964 adhe.menu.11 #Ignored, not a unique ID
2183258 Compact.Menu
2183752 luisbocanegra.cursor.eyes
2184730 OnlyText.Date.Kde.v2
2184747 ChatQT-Plasmoid
2185174 Seeua.Weather
2185549 org.kde.plasma.pipewiresettings
2185626 org.kde.plasma.dockio
2185931 asus-battery-health-switcher
2185962 Start.11.Simple
2186442 Music.Waves
2187344 MusicMini
2187962 MechanicalClock
2188275 com.junongx.kde.currentlyplaying
2188581 Lyndo.Kde
2189544 Plain.Circle.Weather
2189637 Solid.Plain.Music
2193670 qinvvv.qtodo.widget
2194992 org.kde.osymUpdates
2195583 org.ruiny.NowPlaying
2196105 KdeControlStation
2196149 ff.qtodo.widget
2196368 com.github.imoize.ollamacontrol
2199942 Bix.Plasma.Player
2200890 org.dhruv8sh.kara
2201084 Emulator.Spectrum.Audio
2202249 com.espaker.simpleradioplayer-plasma6
2204865 org.dr42.analogclock
2206628 runcommand.fork
2211264 com.ekaaty.vinyl-launcher
2216429 org.kde.plasma.ipaddress
2216432 org.kde.plasma.shamsi-calendar
2217171 Concepto.menu.12
2217301 hermes.observer.ram.P6
2217313 hermes.observer.cpu.P6
2224768 org.kde.plasma.dayprogress
2225123 org.kde.olib.pinpanel
2225378 material.clock
@@ -0,0 +1,983 @@
const scriptDir = Qt.resolvedUrl("sh/").toString().replace("file://", "");
const configDir = "${APDATIFIER_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/apdatifier}/"
const cacheDir = "${APDATIFIER_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/apdatifier}/"
const configFile = configDir + "config.conf"
const cacheFile = cacheDir + "updates.json"
const rulesFile = configDir + "rules.json"
const newsFile = cacheDir + "news.json"
const timestampFile = cacheDir + "lastCheck"
const procOpt = { stoppable: true }
function execute(command, callback, opt) {
const component = Qt.createComponent("../ui/components/Shell.qml")
if (component.status === Component.Ready) {
const componentObject = component.createObject(root)
if (componentObject) {
if (opt && opt.stoppable) sts.proc = componentObject
componentObject.exec(command, callback)
} else {
Error(1, "Failed to create executable DataSource object")
}
} else {
Error(1, "Executable DataSource component not ready")
}
}
const readFile = (file) => `[ -f "${file}" ] && cat "${file}"`
const writeFile = (data, redir, file) => `echo '${data}' ${redir} "${file}"`
const saveTimestamp = () => execute(writeFile(Math.round(sts.lastCheck).toString(), '>', timestampFile))
const bash = (script, ...args) => scriptDir + script + ' ' + args.join(' ')
const runInTerminal = (script, ...args) => {
const cmd = bash('terminal', script, ...args)
execute(`kstart -- bash -c '${cmd}'`)
if (script === "upgrade") {
runLater(5000, () => upgradeTimer.start())
scheduler.stop()
sts.busy = sts.upgrading = true
sts.statusMsg = i18n("Upgrade in progress") + "..."
sts.statusIco = cfg.ownIconsUI ? "toolbar_upgrade" : "akonadiconsole"
}
}
function runLater(ms, fn) {
const timer = Qt.createQmlObject('import QtQuick 2.0; Timer { repeat: false }', parent)
timer.interval = ms
timer.triggered.connect(() => {
fn()
timer.destroy()
})
timer.start()
}
const debug = true
function log(message) {
if (debug) console.log("[" + new Date().getTime().toString() + "] "+ "APDATIFIER: " + message)
}
function Error(code, err) {
if (err) {
sts.errors = sts.errors.concat([{code: code, message: err.trim(), type: ""}])
cfg.notifyErrors && notify.send("error", i18n("Exit code: ") + code, err.trim())
setStatusBar()
return true
}
return false
}
function handleError(code, err, type, onError) {
if (code && err) {
sts.errors = sts.errors.concat([{code: code, message: err.trim(), type: type}])
onError()
return true
}
return false
}
function init() {
execute(bash('init'), (cmd, out, err, code) => {
if (Error(code, err)) return
loadConfig()
})
function loadConfig() {
execute(readFile(configFile), (cmd, out, err, code) => {
if (Error(code, err)) return
if (out) {
const config = out.trim().split("\n")
const convert = value => {
if (!isNaN(parseFloat(value))) return parseFloat(value)
if (value === "true" || value === "false") return value === 'true'
return value
}
config.forEach(line => {
const match = line.match(/(\w+)="([^"]*)"/)
if (match) plasmoid.configuration[match[1]] = convert(match[2])
})
}
execute(readFile(timestampFile), (cmd, out, err, code) => {
if (out) sts.lastCheck = parseInt(out.trim()) || 0
loadCache()
})
})
}
function loadCache() {
execute(readFile(cacheFile), (cmd, out, err, code) => {
if (Error(code, err)) return
if (out && validJSON(out, cacheFile)) cache = keys(JSON.parse(out.trim()))
loadRules()
})
}
function loadRules() {
execute(readFile(rulesFile), (cmd, out, err, code) => {
if (Error(code, err)) return
if (out && validJSON(out, rulesFile)) plasmoid.configuration.rules = out
loadNews()
})
}
function loadNews() {
execute(readFile(newsFile), (cmd, out, err, code) => {
if (Error(code, err)) return
if (out && validJSON(out, newsFile)) {
const news = JSON.parse(out.trim())
let migrate = false //
for (const article of news) {
// todo: remove later
if (article.timestamp === undefined && article.date) {
migrate = true
const [d, t] = article.date.split(" | ")
const [day, month, year] = d.split(".").map(Number)
const [hour, minute] = t.split(":").map(Number)
article.timestamp = Math.floor(new Date(year, month - 1, day, hour, minute).getTime() / 1000)
delete article.date
}//
addNewsItem(article)
}
if (migrate) saveNews() //
}
onStartup()
})
}
function onStartup() {
sts.init = true
checkDependencies()
refreshListModel()
upgradingState()
}
}
function saveConfig() {
if (saveTimer.running) return
let config = ""
Object.keys(cfg).forEach(key => {
if (key.endsWith("Default")) {
let name = key.slice(0, -7)
config += `${name}="${cfg[name]}"\n`
}
})
execute(writeFile(config, ">", configFile))
}
function checkDependencies() {
const pkgs = "pacman flatpak fwupdmgr paru pikaur yay jq tmux alacritty foot ghostty gnome-terminal kitty konsole lxterminal ptyxis terminator tilix wezterm xterm yakuake"
const checkPkg = (pkgs) => `for pkg in ${pkgs}; do command -v $pkg || echo; done`
const populate = (data) => data.map(item => ({ "name": item.split("/").pop(), "value": item }))
execute(checkPkg(pkgs), (cmd, out, err, code) => {
if (Error(code, err)) return
const output = out.split("\n")
const [pacman, flatpak, fwupdmgr, paru, pikaur, yay, jq, tmux ] = output.map(Boolean)
cfg.packages = { pacman, flatpak, fwupdmgr, paru, pikaur, yay, jq, tmux }
if (!cfg.wrapper) cfg.wrapper = paru ? "paru" : yay ? "yay" : pikaur ? "pikaur" : ""
const terminals = populate(output.slice(8).filter(Boolean))
cfg.terminals = terminals.length > 0 ? terminals : null
if (!cfg.terminal) cfg.terminal = cfg.terminals.length > 0 ? cfg.terminals[0].value : ""
if (!pacman) plasmoid.configuration.arch = false
if (!pacman || (!yay && !paru && !pikaur)) plasmoid.configuration.aur = false
if (!flatpak) plasmoid.configuration.flatpak = false
if (!fwupdmgr) plasmoid.configuration.fwupd = false
if (!tmux) plasmoid.configuration.tmuxSession = false
if (!jq) {
plasmoid.configuration.widgets = false
plasmoid.configuration.feedsEnabled = false
}
})
}
function upgradePackage(name, appID, contentID) {
if (sts.upgrading) return
if (appID) {
runInTerminal("upgrade", "flatpak", appID, name)
} else if (contentID) {
runInTerminal("upgrade", "widget", contentID, name)
}
}
function management() {
runInTerminal("management")
}
function upgradingState() {
const checkProc = `pgrep -f "apdatifier.*upgrade*"`
execute(checkProc, (cmd, out, err, code) => {
if (!out) {
sts.busy = sts.upgrading = false
upgradeTimer.stop()
execute(bash('utils', "currentVersions"), (cmd, out, err, code) => {
if (Error(code, err)) return
if (!out || !validJSON(out)) return
const currentVersions = JSON.parse(out)
const newList = cache.filter(cached => {
const current = currentVersions.find(pkg => pkg.NM.replace(/ /g, "-").toLowerCase() === cached.NM)
return current && current.VO === cached.VO + cached.AC
})
if (JSON.stringify(cache) !== JSON.stringify(newList)) {
cache = newList
refreshListModel()
saveCache(cache)
}
setStatusBar()
resumeScheduler()
})
} else {
scheduler.stop()
upgradeTimer.start()
sts.busy = sts.upgrading = true
sts.statusMsg = i18n("Upgrade in progress") + "..."
sts.statusIco = cfg.ownIconsUI ? "toolbar_upgrade" : "akonadiconsole"
}
updatePlasmoidStatus()
})
}
function upgradeSystem() {
if (sts.upgrading && !cfg.tmuxSession) return
const ignorePkgs = buildIgnoreString()
runInTerminal("upgrade", "full", `${ignorePkgs}`)
}
function stopCheck() {
sts.errors = []
sts.busy = false
sts.proc?.cleanup()
setStatusBar()
resumeScheduler()
if (isOnline) {
sts.lastCheck = Date.now()
saveTimestamp()
}
}
function loadIgnorePkgs() {
return new Promise(resolve => {
if (!cfg.packages || !cfg.packages.pacman) {
resolve([])
return
}
execute("pacman-conf IgnorePkg", (cmd, out, err, code) => {
if (code || !out) {
resolve([])
return
}
const tokens = out.trim().split(/\s+/).filter(Boolean)
const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const patterns = tokens.map(token => {
let pat = token.split(/[<>=]/)[0].trim()
if (!pat) return null
let re = escapeRe(pat)
re = re.replace(/\\\*/g, ".*").replace(/\\\?/g, ".")
return new RegExp("^" + re + "$")
}).filter(Boolean)
resolve(patterns)
})
})
}
function buildIgnoreString() {
const rules = !cfg.rules ? [] : JSON.parse(cfg.rules)
if (!Array.isArray(rules)) return ''
const ignoreItems = rules.filter(rule =>
rule && rule.ignore === true
)
if (ignoreItems.length === 0) return ''
const values = ignoreItems.map(rule => rule.value)
return `--ignore ${values.join(',')}`
}
function checkUpdates() {
if (sts.upgrading) return
if (!isOnline) return
sts.errors = []
scheduler.stop()
sts.busy = true
let archRepos = [], archAur = [], flatpak = [], widgets = [], firmwares = []
const feeds = cfg.feedsEnabled ? parseCustomFeeds(cfg.feeds).map(u => `'${u}'`).join(' ') : ""
const news = feeds && cfg.packages?.jq
news ? checkNews() :
cfg.arch ? checkRepos() :
cfg.aur ? checkAur() :
cfg.flatpak ? checkFlatpak() :
cfg.widgets ? checkWidgets() :
cfg.fwupd ? checkFirmwares() :
merge()
function checkNews() {
const next = () => cfg.arch ? checkRepos() : cfg.aur ? checkAur() : cfg.flatpak ? checkFlatpak() :
cfg.widgets ? checkWidgets() : cfg.fwupd ? checkFirmwares() : merge()
sts.statusIco = cfg.ownIconsUI ? "status_news" : "news-subscribe"
sts.statusMsg = i18n("Checking latest news...")
execute(bash('utils', 'rss', feeds), (cmd, out, err, code) => {
if (out) {
const news = JSON.parse(out.trim())
if (cfg.notifyNews) {
const currentLinks = new Set(Array.from(Array(newsModel.count), (_, i) => newsModel.get(i).link))
const newItems = news.filter(item => !currentLinks.has(item.link))
if (newItems.length > 0) {
const title = i18np("%1 new article", "%1 new articles", newItems.length)
const body = newItems.map(item => `<b>${item.title}</b>: ${item.article}`).join("\n")
notify.send("news", title, body, newItems[0].link)
}
}
if (newsModel.count === 0) {
news.forEach(item => addNewsItem(item))
} else {
let newLinks = news.map(item => item.link)
let prevLinks = modelToArray(newsModel).map(item => item.link)
news.forEach(item => {
if (!prevLinks.includes(item.link)) addNewsItem(item)
})
for (let i = newsModel.count - 1; i >= 0; --i) {
let modelItem = newsModel.get(i)
if (!newLinks.includes(modelItem.link)) newsModel.remove(i)
}
}
}
if (handleError(code, err, "news", next)) return
next()
}, procOpt)
}
function checkRepos() {
const next = () => cfg.aur ? checkAur() : cfg.flatpak ? checkFlatpak() :
cfg.widgets ? checkWidgets() : cfg.fwupd ? checkFirmwares() : merge()
sts.statusIco = cfg.ownIconsUI ? "status_package" : "apdatifier-package"
sts.statusMsg = i18n("Synchronizing pacman databases...")
execute(bash('utils', 'syncdb'), (cmd, out, err, code) => {
if (handleError(code, err, "repositories", next)) return
sts.statusIco = cfg.ownIconsUI ? "status_package" : "apdatifier-package"
sts.statusMsg = i18n("Checking system updates...")
execute(`pacman -Qu --dbpath "${cfg.dbPath}" 2>&1`, (cmd, out, err, code) => {
if (handleError(code, err, "repositories", next)) return
const updates = out ? out.trim().split("\n") : []
makeArchList(updates, "repositories").then(result => {
archRepos = result
next()
})
}, procOpt)
}, procOpt)
}
function checkAur() {
const next = () => cfg.flatpak ? checkFlatpak() : cfg.widgets ? checkWidgets() : cfg.fwupd ? checkFirmwares() : merge()
sts.statusIco = cfg.ownIconsUI ? "status_package" : "apdatifier-package"
sts.statusMsg = i18n("Checking AUR updates...")
const cmdArg = cfg.wrapper === "pikaur" ? " -Qua --noconfirm" : " -Qua"
execute(cfg.wrapper + cmdArg, (cmd, out, err, code) => {
if (handleError(code, err, "aur", next)) return
const updates = out ? out.trim().split("\n") : []
makeArchList(updates, "aur").then(result => {
archAur = result
next()
})
}, procOpt)
}
function checkFlatpak() {
const next = () => cfg.widgets ? checkWidgets() : cfg.fwupd ? checkFirmwares() : merge()
sts.statusIco = cfg.ownIconsUI ? "status_flatpak" : "apdatifier-flatpak"
sts.statusMsg = i18n("Synchronizing flatpak appstream...")
execute("flatpak update --appstream >/dev/null 2>&1", (cmd, out, err, code) => {
if (handleError(code, err, "flatpak", next)) return
sts.statusIco = cfg.ownIconsUI ? "status_flatpak" : "apdatifier-flatpak"
sts.statusMsg = i18n("Checking flatpak updates...")
execute("flatpak remote-ls --app --updates --show-details", (cmd, out, err, code) => {
if (handleError(code, err, "flatpak", next)) return
const updates = out.trim()
makeFlatpakList(updates, "flatpak").then(result => {
flatpak = result
next()
})
}, procOpt)
}, procOpt)
}
function checkWidgets() {
const next = () => cfg.fwupd ? checkFirmwares() : merge()
sts.statusIco = cfg.ownIconsUI ? "status_widgets" : "start-here-kde-plasma-symbolic"
sts.statusMsg = i18n("Checking widgets updates...")
execute(bash('widgets', 'check'), (cmd, out, err, code) => {
if (handleError(code, err, "widgets", next)) return
widgets = JSON.parse(out.trim())
next()
}, procOpt)
}
function checkFirmwares() {
sts.statusIco = cfg.ownIconsUI ? "status_package" : "apdatifier-package"
sts.statusMsg = i18n("Checking firmware updates...")
execute("fwupdmgr refresh --force", (cmd, out, err, code) => {
if (handleError(code, err, "fwupdmgr", merge)) return
execute("fwupdmgr get-updates --json", (cmd, out, err, code) => {
if (handleError(code, err, "fwupdmgr", merge)) return
firmwares = JSON.parse(out).Devices.map(device => ({
NM: `${device.Vendor}-${device.Name}`.replace(/ /g, "-").toLowerCase(),
RE: "fwupd",
DE: device.Releases?.[0]?.Summary || "",
LN: device.Releases?.[0]?.Homepage || "",
IN: device.Icons?.[0] || "",
VO: device.Version || "",
VN: device.Releases?.[0]?.Version || ""
}))
merge()
}, procOpt)
}, procOpt)
}
function merge() {
const list = keys(archRepos.concat(archAur, flatpak, widgets, firmwares))
const finish = (finalList) => {
sts.errors.length > 0
? runLater(3000, () => isOnline ? finalize(finalList) : stopCheck())
: finalize(finalList)
}
loadIgnorePkgs().then(ignorePatterns => {
if (!ignorePatterns || ignorePatterns.length === 0) {
finish(list)
} else {
const filtered = list.filter(pkg => !ignorePatterns.some(re => re.test(pkg.NM)))
finish(filtered)
}
})
}
}
function makeArchList(updates, source) {
return new Promise((resolve) => {
if (updates.length === 0) {
resolve([])
} else {
const updateRe = /^([A-Za-z0-9@._+-]+)\s+(\S+)\s+->\s+(\S+)/
const parsed = updates.map(l => l.trim()).map(l => l.match(updateRe)).filter(Boolean)
if (parsed.length === 0) {
resolve([])
return
}
const pkgs = parsed.map(m => m[1]).join(' ')
execute(`LC_ALL=C.UTF-8 pacman -Sl --dbpath "${cfg.dbPath}" | awk 'index($0,"[installed:")>0'`, (cmd, out, err, code) => {
if (code && handleError(code, err, source, () => resolve([]))) return
const repositories = out.trim().split('\n')
execute("LC_ALL=C.UTF-8 pacman -Qi " + pkgs, (cmd, out, err, code) => {
if (code && handleError(code, err, source, () => resolve([]))) return
const descriptions = out.trim().split('\n\n')
execute(bash('utils', 'getIcons', pkgs), (cmd, out, err, code) => {
if (code && handleError(code, err, source, () => resolve([]))) return
const icons = out.split('\n')
const iconsMap = new Map()
icons.map(l => l.split(' ', 2))
.filter(([name, icon]) => name && icon)
.forEach(([name, icon]) => iconsMap.set(name, icon))
const versionsMap = new Map()
parsed.forEach(m => versionsMap.set(m[1], { currentVer: m[2], newVer: m[3] }))
const repositoriesMap = new Map()
repositories.forEach(line => {
const [repo, name] = line.trim().split(/\s+/)
if (!repositoriesMap.has(name)) {
repositoriesMap.set(name, repo)
}
})
const keyMap = {
'Name': 'NM',
'Description': 'DE',
'URL': 'LN',
'Groups': 'GR',
'Provides': 'PR',
'Depends On': 'DP',
'Required By': 'RQ',
'Optional For': 'OF',
'Conflicts With': 'CF',
'Replaces': 'RP',
'Installed Size': 'IS',
'Install Date': 'DT',
'Install Reason': 'RN'
}
const packagesData = descriptions.map(description => {
const pkg = {}
const lines = description.split('\n')
for (const line of lines) {
const match = line.match(/^(\w+(?:\s+\w+)*)\s+:\s*(.*)/)
if (match) {
const key = match[1].trim()
const value = match[2].trim()
if (value === "None") continue
const mappedKey = keyMap[key]
if (mappedKey) pkg[mappedKey] = value
}
}
if (iconsMap.has(pkg.NM)) pkg.IN = iconsMap.get(pkg.NM)
const versions = versionsMap.get(pkg.NM) || {}
pkg.VO = versions.currentVer || ""
pkg.VN = (versions.newVer === "latest-commit") ? i18n("latest commit") : (versions.newVer || "")
pkg.RE = repositoriesMap.get(pkg.NM) || (pkg.NM.endsWith("-git") || pkg.VN === i18n("latest commit") ? "devel" : "aur")
if (pkg.RN.includes("Explicitly")) {
pkg.RN = "explicit"
} else if (!pkg.RQ && !pkg.OF) {
pkg.RN = "orphan"
} else {
pkg.RN = "dependency"
}
delete pkg.OF
return pkg
})
resolve([...new Map(packagesData.map(item => [item.NM, item])).values()])
}, procOpt)
}, procOpt)
}, procOpt)
}
})
}
function makeFlatpakList(updates) {
return new Promise((resolve) => {
if (!updates) {
resolve([])
} else {
execute("flatpak list --app --columns=application,version,active", (cmd, out, err, code) => {
if (code && handleError(code, err, "flatpak", () => resolve([]))) return
const description = out.trim().split("\n").reduce((obj, line) => {
const [ID, VO, AC] = line.split("\t").map(entry => entry.trim())
obj[ID] = { VO, AC }
return obj
}, {})
const extendedList = updates.split("\n").map(line => {
const [NM, DE, ID, VN, BR, , RE, , CM, RT, IS, DS] = line.split("\t").map(entry => entry.trim())
const { VO, AC } = description[ID]
return {
NM: NM.replace(/ /g, "-").toLowerCase(),
DE, LN: "https://flathub.org/apps/" + ID,
ID, BR, RE, AC, CM, RT, IS, DS, VO,
VN: VO === VN ? i18n("latest commit") : VN
}
})
resolve(extendedList)
}, procOpt)
}
})
}
function sortList(list, byName) {
if (!list) return
return list.sort((a, b) => {
const name = a.NM.localeCompare(b.NM)
const repo = a.RE.localeCompare(b.RE)
if (byName || !cfg.sorting) return name
if (a.IM !== b.IM) return a.IM ? -1 : 1
const develA = a.RE.includes("devel")
const develB = b.RE.includes("devel")
if (develA !== develB) return develA ? -1 : 1
const aurA = a.RE.includes("aur")
const aurB = b.RE.includes("aur")
if (aurA !== aurB) return aurA ? -1 : 1
return repo || name
})
}
function refreshListModel(list) {
list = sortList(applyRules(list || cache)) || []
sts.count = list.length || 0
setStatusBar()
if (!list) return
listModel.clear()
list.forEach(item => listModel.append(item))
}
function finalize(list) {
sts.busy = false
resumeScheduler()
sts.lastCheck = Date.now()
saveTimestamp()
if (cfg.notifyErrors && sts.error) {
const notifyMsg = sts.errors.map(err => `<b>${err.type}</b> => ${err.message} (Exit code ${err.code})`).join('\n\n')
notify.send("error", i18np("%1 error occurred", "%1 errors occurred", sts.errors.length), notifyMsg)
}
if (!list) {
listModel.clear()
execute(writeFile("[]", '>', cacheFile))
cache = []
sts.count = 0
setStatusBar()
return
}
if (sts.error && cache.length > 0) {
var errorTypes = {}
for (var i = 0; i < sts.errors.length; i++) errorTypes[sts.errors[i].type] = true
var currentNames = {}
for (var j = 0; j < list.length; j++) currentNames[list[j].NM] = true
var cachedPackages = []
for (var k = 0; k < cache.length; k++) {
var cached = cache[k]
if (currentNames[cached.NM]) continue
if (errorTypes.repositories && cached.RE && cached.RE !== "aur" && cached.RE !== "devel") {
cachedPackages.push(cached)
} else if (errorTypes.aur && (cached.RE === "aur" || cached.RE === "devel")) {
cachedPackages.push(cached)
} else if (errorTypes.flatpak && cached.ID) {
cachedPackages.push(cached)
} else if (errorTypes.widgets && cached.CN) {
cachedPackages.push(cached)
}
}
list = list.concat(cachedPackages)
}
refreshListModel(list)
if (cfg.notifyUpdates) {
const cached = new Map(cache.map(el => [el.NM, el.VN]))
const newList = applyRules(list).filter(el => !cached.has(el.NM) || (cfg.notifyEveryBump && cached.get(el.NM) !== el.VN))
if (newList.length > 0) {
const title = i18np("+%1 new update", "+%1 new updates", newList.length)
const body = newList.map(pkg => `${pkg.NM}${pkg.VN}`).join("\n")
notify.send("updates", title, body)
}
}
cache = list
saveCache(cache)
}
function saveCache(list) {
if (JSON.stringify(list).length > 130000) {
let start = 0
const chunkSize = 200
const json = JSON.stringify(keys(sortList(JSON.parse(JSON.stringify(list)), true))).replace(/},/g, "},\n").replace(/'/g, "")
const lines = json.split("\n")
while (start < lines.length) {
const chunk = lines.slice(start, start + chunkSize).join("\n")
const redir = start === 0 ? ">" : ">>"
execute(writeFile(chunk, redir, `${cacheFile}_${Math.ceil(start / chunkSize)}`))
start += chunkSize
}
execute(bash('utils', 'combineFiles', cacheFile))
} else {
const json = toFileFormat(keys(sortList(JSON.parse(JSON.stringify(list)), true)))
execute(writeFile(json, '>', cacheFile))
}
}
function setStatusBar() {
if (sts.count > 0) {
sts.statusIco = cfg.ownIconsUI ? "status_pending" : "accept_time_event"
sts.statusMsg = sts.count + " " + i18np("update is pending", "updates are pending", sts.count)
} else {
sts.statusIco = cfg.ownIconsUI ? "status_blank" : ""
sts.statusMsg = ""
}
}
function resumeScheduler() {
if (cfg.checkMode !== "manual") {
scheduler.start()
}
}
function searchScheduler(options) {
if (!isOnline) return
if (isMetered) return
const mode = cfg.checkMode
if (mode === "manual") {
scheduler.stop()
return
}
const currTime = new Date().getTime()
const lastCheck = sts.lastCheck || 0
let nextCheck = null
if (mode === "interval") {
const interval = parseFloat(cfg.intervalMinutes)
const intervalMs = interval * 60 * 1000
nextCheck = lastCheck ? (lastCheck + intervalMs) : (currTime + intervalMs)
} else if (mode === "daily") {
const scheduled = new Date(currTime)
scheduled.setHours(cfg.dailyHour, cfg.dailyMinute, 0, 0)
if (scheduled.getTime() <= currTime) scheduled.setDate(scheduled.getDate() + 1)
nextCheck = scheduled.getTime()
} else if (mode === "weekly") {
const scheduled = new Date(currTime)
const targetDay = Number(cfg.weeklyDay)
const daysAhead = (targetDay - scheduled.getDay() + 7) % 7
scheduled.setDate(scheduled.getDate() + daysAhead)
scheduled.setHours(cfg.weeklyHour, cfg.weeklyMinute, 0, 0)
if (scheduled.getTime() <= currTime) scheduled.setDate(scheduled.getDate() + 7)
nextCheck = scheduled.getTime()
}
options = options || {}
if (options.simulate) return nextCheck
if (nextCheck && nextCheck <= (currTime + 10000) && nextCheck > lastCheck) {
checkUpdates()
}
return
}
function getCheckTime() {
const currTime = new Date().getTime()
const lastCheck = sts.lastCheck || currTime
const formatDelta = (ms1, ms2) => {
const diff = Math.max(0, Math.floor((ms1 - ms2) / 1000))
const days = Math.floor(diff / 86400)
const hours = Math.floor((diff % 86400) / 3600)
const minutes = Math.floor((diff % 3600) / 60)
const seconds = diff % 60
const parts = []
if (days > 0) parts.push([days, i18np("%1 day", "%1 days", days)])
if (hours > 0) parts.push([hours, i18np("%1 hour", "%1 hours", hours)])
if (minutes > 0) parts.push([minutes, i18np("%1 minute", "%1 minutes", minutes)])
if (parts.length === 0 && seconds > 0) return i18n("less than a minute")
if (seconds > 0) parts.push([seconds, i18np("%1 second", "%1 seconds", seconds)])
const take = parts.slice(0, 2).map(p => p[1])
return take.join(' ')
}
const lastCheckStr = (currTime > lastCheck) ? `${i18n("Last check:")} ${formatDelta(currTime, lastCheck)} ${i18n("ago")}` : ""
if (!scheduler.running) return lastCheckStr
const nextCheck = searchScheduler({ simulate: true })
if (!nextCheck) return lastCheckStr
const nextCheckStr = (currTime < nextCheck) ? `${i18n("Next check in:")} ${formatDelta(nextCheck, currTime)}` : ""
if (lastCheckStr && nextCheckStr) return `${lastCheckStr}\n${nextCheckStr}`
if (lastCheckStr && !nextCheckStr) return lastCheckStr
if (!lastCheckStr && nextCheckStr) return nextCheckStr
}
function setIndex(value, arr) {
let index = 0
for (let i = 0; i < arr.length; i++) {
if (arr[i]["value"] == value) {
index = i
break
}
}
return index
}
const defaultIcon = "apdatifier-plasmoid"
function setIcon(icon) {
return icon === "" ? defaultIcon : icon
}
function applyRules(list) {
const rules = !cfg.rules ? [] : JSON.parse(cfg.rules)
list.forEach(el => {
el.IC = el.IN ? el.IN : el.ID ? el.ID : "apdatifier-package"
el.EX = false
el.IM = false
})
function applyRule(el, rule) {
const types = {
'name' : () => el.NM === rule.value,
'regex' : () => { try { return new RegExp(rule.value).test(el.NM) } catch(e) { return false } }
}
if (types[rule.type] && types[rule.type]()) {
el.IC = rule.icon
el.EX = rule.excluded
el.IM = rule.important
}
}
rules.forEach(rule => list.forEach(el => applyRule(el, rule)))
return list.filter(el => !el.EX)
}
function keys(list) {
const keysList = ["GR", "PR", "DP", "RQ", "CF", "RP", "IS", "DT", "RN", "ID", "BR", "AC", "CM", "RT", "DS", "CN", "AU"]
list.forEach(el => {
keysList.forEach(key => {
if (!el.hasOwnProperty(key)) el[key] = ""
else if (el[key] === "") delete el[key]
})
if (el.hasOwnProperty("IC")) delete el["IC"]
if (el.hasOwnProperty("EX")) delete el["EX"]
if (el.hasOwnProperty("IM")) delete el["IM"]
})
return list
}
function switchScheduler() {
if (cfg.checkMode === "manual" || sts.busy) return
scheduler.running ? scheduler.stop() : scheduler.start()
}
function toFileFormat(obj) {
const jsonStringWithSpace = JSON.stringify(obj, null, 2)
const writebleJsonStrings = jsonStringWithSpace.replace(/'/g, "")
return writebleJsonStrings
}
function validJSON(string, file) {
try {
const json = JSON.parse(string)
if (json && typeof json === "object") return json
}
catch (e) {
file ? Error(1, `JSON data at ${file} is corrupted or broken and cannot be processed`)
: Error(1, "JSON data is broken and cannot be processed")
}
return false
}
function isValidFeedUrl(value) {
return /^https?:\/\/\S+$/i.test((value || "").trim())
}
function parseCustomFeeds(value) {
return String(value || "")
.split(/[\n|]/)
.map(feed => feed.trim())
.filter(isValidFeedUrl)
}
function serializeCustomFeeds(value) {
return (Array.isArray(value) ? value : parseCustomFeeds(value))
.map(feed => feed.trim())
.filter((feed, index, feeds) => isValidFeedUrl(feed) && feeds.indexOf(feed) === index)
.join("|")
}
function modelToArray(model) {
const array = new Array(model.count)
for (let i = 0; i < model.count; i++) {
array[i] = model.get(i)
}
return array
}
function findInsertIndex(news) {
const n = newsModel.count
let firstRemoved = n
for (let i = 0; i < n; i++) {
if (newsModel.get(i).removed) {
firstRemoved = i
break
}
}
const start = news.removed ? firstRemoved : 0
const end = news.removed ? n : firstRemoved
for (let i = start; i < end; i++) {
if (newsModel.get(i).timestamp < news.timestamp)
return i
}
return end
}
function saveNews() {
execute(writeFile(toFileFormat(modelToArray(newsModel)), '>', newsFile))
}
function addNewsItem(news) {
newsModel.insert(findInsertIndex(news), news)
}
function removeNewsItem(index) {
const item = newsModel.get(index)
if (!item || item.removed) return
let to = findInsertIndex({ timestamp: item.timestamp, removed: true })
if (to > index) to--
newsModel.move(index, to, 1)
newsModel.setProperty(to, "removed", true)
saveNews()
}
@@ -0,0 +1,14 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 23 23">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color: #232629; }
</style>
</defs>
<path
class="ColorScheme-Text"
style="fill:currentColor"
d="m22.43,12.56s-.05-.04-.07-.07c-.04-.04-.07-.08-.12-.11,0,0,0,0,0,0,0,0,0,0,0,0l-4.97-2.6v-5.2c0-.09-.03-.17-.07-.24-.02-.03-.05-.04-.07-.07-.04-.04-.07-.08-.12-.11,0,0,0,0,0,0,0,0,0,0,0,0l-4.75-2.48c-.47-.24-1.04-.24-1.5,0l-4.75,2.48s0,0,0,0c0,0,0,0,0,0-.05.03-.08.07-.12.11-.02.02-.06.04-.07.07-.05.07-.07.15-.07.24v5.2L.76,12.38s0,0,0,0c0,0,0,0,0,0-.06.03-.09.08-.13.12-.02.02-.04.03-.06.05-.05.07-.07.15-.07.24v5.48c0,.17.1.33.26.42l5.24,2.74c.08.04.17.06.26.06s.18-.02.26-.06l4.98-2.6,4.98,2.6c.08.04.17.06.26.06s.18-.02.26-.06l5.24-2.74c.16-.09.26-.24.26-.42v-5.48c0-.09-.03-.17-.07-.24Zm-16.9,2.05l-2.7-1.41c-.34-.18-.34-.62,0-.79l2.7-1.41c.45-.24,1-.24,1.45,0l2.7,1.41c.34.18.34.62,0,.79l-2.7,1.41c-.45.24-1,.24-1.45,0Zm5.44,2.75c0,.4-.23.76-.61.96l-2.75,1.44c-.37.19-.83-.05-.83-.44v-2.86c0-.4.23-.76.61-.96l2.75-1.44c.37-.19.83.05.83.44v2.86Zm-.2-10.97l-2.7-1.41c-.34-.18-.34-.62,0-.79l2.7-1.41c.45-.24,1-.24,1.45,0l2.7,1.41c.34.18.34.62,0,.79l-2.7,1.41c-.45.24-1,.24-1.45,0Zm1.25,4.71v-2.86c0-.4.23-.76.61-.96l2.75-1.44c.37-.19.83.05.83.44v2.86c0,.4-.23.76-.61.96l-2.75,1.44c-.37.19-.83-.05-.83-.44Zm3.99,3.51l-2.7-1.41c-.34-.18-.34-.62,0-.79l2.7-1.41c.45-.24,1-.24,1.45,0l2.7,1.41c.34.18.34.62,0,.79l-2.7,1.41c-.45.24-1,.24-1.45,0Zm5.44,2.75c0,.4-.23.76-.61.96l-2.75,1.44c-.37.19-.83-.05-.83-.44v-2.86c0-.4.23-.76.61-.96l2.75-1.44c.37-.19.83.05.83.44v2.86Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,14 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 23 23">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color: #232629; }
</style>
</defs>
<path
class="ColorScheme-Text"
style="fill:currentColor"
d="m21.5,6.21c0-.06-.04-.11-.06-.18-.02-.09-.04-.19-.09-.27-.04-.06-.11-.1-.16-.15-.06-.06-.11-.13-.19-.17,0,0-.02,0-.03,0,0,0-.01-.01-.02-.02l-7.71-3.55c-1.1-.51-2.39-.51-3.49,0l-7.71,3.55s-.01.01-.02.02c0,0-.02,0-.03,0-.08.04-.12.11-.19.17-.05.05-.12.09-.16.15-.05.08-.07.17-.09.27-.02.06-.06.11-.06.18,0,0,0,0,0,.01,0,0,0,0,0,.01v9.57c0,.32.19.62.49.78l9.01,4.79c.15.08.32.12.49.12s.34-.04.49-.12l8.5-4.51c.62-.33,1.01-.94,1.01-1.6V6.24s0,0,0-.01c0,0,0,0,0-.01Zm-16.07-.32l5.2-2.4c.55-.25,1.19-.25,1.73,0l5.2,2.4c.37.17.39.66.03.85l-5.13,2.72c-.6.32-1.33.32-1.93,0l-5.13-2.72c-.36-.19-.35-.68.03-.85Zm14.1,8.5c0,.55-.32,1.06-.84,1.33l-5.44,2.88c-.34.18-.77-.05-.77-.41v-5.77c0-.55.32-1.05.83-1.33l3.24-1.72,2.2-1.16c.34-.18.77.05.77.41v5.77Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,17 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color: #232629; }
</style>
</defs>
<path class="ColorScheme-Text" style="fill:currentColor"
d="m9.6094 13.32c-.68385 0-1.25.56615-1.25 1.25v.12891c0 .68385.56615 1.25 1.25 1.25h4.791c.68385 0 1.25-.56615 1.25-1.25v-.12891c0-.68385-.56615-1.25-1.25-1.25zm0 1h4.791c.14715 0 .25.10285.25.25v.12891c0 .14715-.10285.25-.25.25h-4.791c-.14715 0-.25-.10285-.25-.25v-.12891c0-.14715.10285-.25.25-.25z"/>
<g stroke-linecap="round" stroke-linejoin="round" class="ColorScheme-Text" style="fill:currentColor">
<path d="m4.9707 10.02a.75008.75008 0 00-.75.75v9.0996c0 1.0232.84592 1.8711 1.8691 1.8711h11.91c1.0232 0 1.8691-.84787 1.8691-1.8711v-9.0996a.75008.75008 0 00-.75-.75zm.75 1.5h12.648v8.3496c0 .21677-.15237.37109-.36914.37109h-11.91c-.21677 0-.36914-.15432-.36914-.37109z"/>
<path d="m7.5098 7.1602a.75008.75008 0 00-.56055.25195l-2.5391 2.8594a.75008.75008 0 00.56055 1.248h14.148a.75008.75008 0 00.5625-1.248l-2.541-2.8594a.75008.75008 0 00-.56055-.25195zm.33594 1.5h8.3965l1.207 1.3594h-10.811z"/>
<path d="m15.746 3.6992-7.4453.041016c-.57783 0-1.1067.42379-1.209 1.0117a.75008.75008 0 00-.00781.046875l-.20703 1.9082-2.3945-1.375c-.37855-.23296-.83629-.23296-1.2148 0a.75008.75008 0 00-.00781.00391l-2.7324 1.7246c-.00054767.000337.00054683.00162 0 .00195-.71708.44295-.71521 1.5381.005859 1.9707a.75008.75008 0 00.003906.00195l4.0313 2.3906a.75.75 0 001.0273-.26367.75.75 0 00-.26367-1.0273l-3.5391-2.0996 2.0898-1.3184 3.2441 1.8633a.75008.75008 0 001.1191-.57031l.30078-2.7715 6.9668-.037109.31055 2.8105a.75008.75008 0 001.127.56445l3.1641-1.8613 2.0918 1.3145-3.4746 2.0996a.75.75 0 00-.25391 1.0293.75.75 0 001.0293.25391l3.9336-2.377c.72984-.4325.72984-1.5675 0-2l-2.7031-1.6992a.75008.75008 0 00-.0059-.00391c-.35475-.21833-.84467-.24519-1.2382.0078162l-2.3047 1.3555-.21484-1.9277a.75008.75008 0 00-.0059-.046875c-.10095-.5807-.61862-1.0458-1.25-1.0195z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,14 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color: #232629; }
</style>
</defs>
<path
class="ColorScheme-Text"
style="fill:currentColor"
d="m 19.854756,20.481011 c -1.262396,-0.531251 -2.820748,-1.029352 -4.628787,-1.322602 -0.496751,-0.08075 -0.849454,-0.542301 -0.763897,-1.025102 0.05151,-0.28815 0.0777,-0.5899 0.0777,-0.905251 0,-1.996653 -1.097395,-3.618455 -2.445348,-3.618455 -1.357556,0 -2.4549513,1.621802 -2.4549513,3.618455 0,0.30005 0.025317,0.594151 0.074207,0.875501 0.082937,0.485351 -0.2697652,0.948602 -0.7691364,1.025102 -1.8831212,0.28815 -3.5008395,0.811751 -4.8121254,1.361702 -0.7988193,0.1122 -1.4439859,-0.777751 -0.9725516,-1.490052 0.1152394,-0.169151 0.231352,-0.346801 0.3369882,-0.525301 0.2793686,-0.430951 0.5587371,-0.862751 0.8276293,-1.312402 1.2327135,-1.977953 2.464554,-4.125056 3.6588544,-6.431109 0.028809,-0.0561 0.05762,-0.122401 0.086429,-0.178501 C 8.3203287,10.075296 8.560411,9.5967448 8.7917631,9.1190441 8.9070025,8.8937938 9.0231151,8.6591935 9.1287513,8.4339431 9.3120869,8.5461433 9.4945495,8.6770435 9.706695,8.8087937 10.669644,9.3901945 11.574972,9.718295 12.229743,9.8865952 11.748705,9.5304447 11.218778,9.1088441 10.679247,8.5835434 10.274162,8.1899928 9.9179674,7.7964422 9.6097889,7.4122417 10.062017,6.4372903 10.495912,5.4810389 10.880916,4.5528376 c 0.06722,-0.1496002 0.134447,-0.3000505 0.192939,-0.4403007 0.336988,-0.8151511 1.502479,-0.8151511 1.84907,-0.00935 0.163257,0.3842006 0.327385,0.7777511 0.501118,1.1721517 0.144049,0.3468005 0.308178,0.693601 0.471434,1.0497515 0.105636,0.2431004 0.221748,0.4964007 0.336989,0.7497011 0.231352,0.5066007 0.471434,1.0123514 0.721992,1.5283022 0.231352,0.4972507 0.481038,0.9936514 0.7412,1.4994026 0,0.0187 0.0096,0.02805 0.01921,0.04675 0.616356,1.228251 1.25192,2.399553 1.897086,3.534305 0.461831,0.833851 0.934139,1.640502 1.415176,2.419103 0.164128,0.290701 0.336989,0.572051 0.520325,0.853402 0.124842,0.1972 0.240954,0.39355 0.365797,0.5814 h -0.01048 c -0.336988,-0.17765 -0.702787,-0.3468 -1.097395,-0.49725 -0.683579,-0.262651 -1.319143,-0.421601 -1.868276,-0.515951 0.750802,0.365501 1.617718,0.844051 2.523047,1.481552 0.558736,0.402901 1.05898,0.796451 1.482399,1.190852 0.250558,0.674901 -0.356196,1.405902 -1.087792,1.284352 z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23"><path d=""/></svg>

After

Width:  |  Height:  |  Size: 92 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m11.5,1.63C5.99,1.63,1.5,6.11,1.5,11.63s4.49,10,10,10,10-4.49,10-10S17.01,1.63,11.5,1.63Zm-7.26,10c0-4,3.26-7.26,7.26-7.26,1.56,0,3,.5,4.19,1.34L5.56,15.78c-.83-1.18-1.32-2.61-1.32-4.16Zm7.26,7.26c-1.46,0-2.83-.44-3.97-1.19l10.06-10c.74,1.13,1.17,2.48,1.17,3.93,0,4-3.26,7.26-7.26,7.26Z"/>
</svg>

After

Width:  |  Height:  |  Size: 384 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m22.43,12.56s-.05-.04-.07-.07c-.04-.04-.07-.08-.12-.11,0,0,0,0,0,0,0,0,0,0,0,0l-4.97-2.6v-5.2c0-.09-.03-.17-.07-.24-.02-.03-.05-.04-.07-.07-.04-.04-.07-.08-.12-.11,0,0,0,0,0,0,0,0,0,0,0,0l-4.75-2.48c-.47-.24-1.04-.24-1.5,0l-4.75,2.48s0,0,0,0c0,0,0,0,0,0-.05.03-.08.07-.12.11-.02.02-.06.04-.07.07-.05.07-.07.15-.07.24v5.2L.76,12.38s0,0,0,0c0,0,0,0,0,0-.06.03-.09.08-.13.12-.02.02-.04.03-.06.05-.05.07-.07.15-.07.24v5.48c0,.17.1.33.26.42l5.24,2.74c.08.04.17.06.26.06s.18-.02.26-.06l4.98-2.6,4.98,2.6c.08.04.17.06.26.06s.18-.02.26-.06l5.24-2.74c.16-.09.26-.24.26-.42v-5.48c0-.09-.03-.17-.07-.24Zm-16.9,2.05l-2.7-1.41c-.34-.18-.34-.62,0-.79l2.7-1.41c.45-.24,1-.24,1.45,0l2.7,1.41c.34.18.34.62,0,.79l-2.7,1.41c-.45.24-1,.24-1.45,0Zm5.44,2.75c0,.4-.23.76-.61.96l-2.75,1.44c-.37.19-.83-.05-.83-.44v-2.86c0-.4.23-.76.61-.96l2.75-1.44c.37-.19.83.05.83.44v2.86Zm-.2-10.97l-2.7-1.41c-.34-.18-.34-.62,0-.79l2.7-1.41c.45-.24,1-.24,1.45,0l2.7,1.41c.34.18.34.62,0,.79l-2.7,1.41c-.45.24-1,.24-1.45,0Zm1.25,4.71v-2.86c0-.4.23-.76.61-.96l2.75-1.44c.37-.19.83.05.83.44v2.86c0,.4-.23.76-.61.96l-2.75,1.44c-.37.19-.83-.05-.83-.44Zm3.99,3.51l-2.7-1.41c-.34-.18-.34-.62,0-.79l2.7-1.41c.45-.24,1-.24,1.45,0l2.7,1.41c.34.18.34.62,0,.79l-2.7,1.41c-.45.24-1,.24-1.45,0Zm5.44,2.75c0,.4-.23.76-.61.96l-2.75,1.44c-.37.19-.83-.05-.83-.44v-2.86c0-.4.23-.76.61-.96l2.75-1.44c.37-.19.83.05.83.44v2.86Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m18.18,3.3h-3.48c-.43-1.1-1.53-1.88-2.81-1.88s-2.37.78-2.81,1.88h-3.36c-1.53,0-2.77,1.2-2.77,2.68v12.75c0,1.48,1.24,2.68,2.77,2.68h12.46c1.53,0,2.77-1.2,2.77-2.68V5.98c0-1.48-1.24-2.68-2.77-2.68Zm-6.29-.35c.59,0,1.07.47,1.07,1.04s-.48,1.04-1.07,1.04-1.07-.47-1.07-1.04.48-1.04,1.07-1.04Zm-.27,14.89h-4.95c-.54,0-.98-.43-.98-.95s.44-.95.98-.95h4.95c.54,0,.98.43.98.95s-.44.95-.98.95Zm3.22-4.36H6.68c-.54,0-.98-.43-.98-.95s.44-.95.98-.95h8.17c.54,0,.98.43.98.95s-.44.95-.98.95Zm2.56-4.36H6.68c-.54,0-.98-.43-.98-.95s.44-.95.98-.95h10.73c.54,0,.98.43.98.95s-.44.95-.98.95Z"/>
</svg>

After

Width:  |  Height:  |  Size: 667 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m21.5,6.21c0-.06-.04-.11-.06-.18-.02-.09-.04-.19-.09-.27-.04-.06-.11-.1-.16-.15-.06-.06-.11-.13-.19-.17,0,0-.02,0-.03,0,0,0-.01-.01-.02-.02l-7.71-3.55c-1.1-.51-2.39-.51-3.49,0l-7.71,3.55s-.01.01-.02.02c0,0-.02,0-.03,0-.08.04-.12.11-.19.17-.05.05-.12.09-.16.15-.05.08-.07.17-.09.27-.02.06-.06.11-.06.18,0,0,0,0,0,.01,0,0,0,0,0,.01v9.57c0,.32.19.62.49.78l9.01,4.79c.15.08.32.12.49.12s.34-.04.49-.12l8.5-4.51c.62-.33,1.01-.94,1.01-1.6V6.24s0,0,0-.01c0,0,0,0,0-.01Zm-16.07-.32l5.2-2.4c.55-.25,1.19-.25,1.73,0l5.2,2.4c.37.17.39.66.03.85l-5.13,2.72c-.6.32-1.33.32-1.93,0l-5.13-2.72c-.36-.19-.35-.68.03-.85Zm14.1,8.5c0,.55-.32,1.06-.84,1.33l-5.44,2.88c-.34.18-.77-.05-.77-.41v-5.77c0-.55.32-1.05.83-1.33l3.24-1.72,2.2-1.16c.34-.18.77.05.77.41v5.77Z"/>
</svg>

After

Width:  |  Height:  |  Size: 839 B

@@ -0,0 +1,4 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.13px" points="15.35 15.15 11.68 12.23 11.68 7.15"/>
<path d="m22.5,11.61c0-.33-.02-.65-.05-.97,0-.01,0-.02,0-.03-.03-.31-.08-.61-.14-.92,0-.03,0-.05-.01-.08-.06-.27-.13-.54-.21-.8-.02-.06-.03-.11-.05-.17-.08-.24-.17-.48-.26-.72-.03-.07-.05-.13-.08-.2-.02-.04-.03-.08-.05-.12-.1-.23-.22-.46-.34-.68,0-.02-.02-.03-.02-.05-1.06-1.97-2.72-3.5-4.78-4.4-.05-.02-.09-.04-.14-.06-.07-.03-.14-.07-.22-.1-.19-.07-.37-.13-.56-.19-.04-.01-.08-.03-.13-.04-3.14-.98-6.44-.33-8.92,1.57-.12.09-.23.19-.34.29-.11.09-.22.17-.32.27-1.69,1.53-2.84,3.65-3.17,6.08h-1.34c-.78,0-1.17.96-.62,1.51l2.85,2.85c.33.33.87.33,1.21,0l2.83-2.86c.55-.56.16-1.51-.62-1.5h-1.54c.12-.64.32-1.25.6-1.82,0-.02.02-.03.03-.04.41-.83.96-1.55,1.61-2.15.15-.14.31-.27.47-.4,0,0,0,0,0,0,1.21-.92,2.73-1.47,4.37-1.47.74,0,1.46.11,2.13.32.01,0,.03,0,.04.01.21.07.41.14.61.22.03.01.05.02.07.03,1.49.65,2.71,1.78,3.48,3.2,0,0,0,0,0,.01.11.2.2.4.29.61.02.04.03.09.05.13.07.18.14.35.2.54.01.04.02.08.03.11.06.19.11.39.15.59,0,.02,0,.04,0,.06.04.22.08.44.1.67,0,0,0,.02,0,.03.02.23.03.46.04.7,0,.03,0,.05,0,.08,0,.21-.01.42-.03.62,0,.07-.02.14-.03.21-.02.16-.04.31-.07.47-.02.11-.05.23-.08.34-.03.11-.05.22-.08.33-.04.14-.09.28-.14.41-.03.08-.06.17-.09.25-.06.15-.12.29-.19.43-.04.08-.07.16-.12.24-.06.12-.13.24-.2.35-.05.09-.11.18-.17.27-.06.1-.14.19-.2.29-.07.1-.14.19-.21.29-.07.09-.15.18-.23.26-.08.08-.15.17-.23.25-.1.11-.21.2-.32.3-.06.05-.11.11-.17.16-.16.14-.33.27-.5.39,0,0-.02.01-.03.02-1.13.78-2.46,1.23-3.84,1.29-.03,0-.05,0-.08,0-.19,0-.38,0-.58-.01-.05,0-.11,0-.16,0-.2-.02-.39-.04-.59-.07-.04,0-.07-.01-.11-.02-.21-.04-.42-.08-.63-.14-.02,0-.04-.01-.06-.02-.22-.06-.43-.13-.65-.22-.02,0-.03-.01-.05-.02-.96-.38-1.81-.94-2.52-1.68-.53-.54-1.39-.55-1.93-.02-.54.53-.55,1.4-.02,1.94.87.89,1.88,1.59,3.01,2.1v.02c.13.06.28.11.42.17.03.01.06.03.09.04.03.01.06.02.09.03.24.09.48.17.72.24.07.02.13.04.2.06.24.06.48.12.72.17.08.01.16.02.23.04.23.04.47.08.71.1.06,0,.11,0,.17,0,.25.02.5.04.75.04.16,0,.31-.02.47-.02.11,0,.21,0,.32-.02.2-.02.39-.04.58-.07.07,0,.14-.02.21-.03.93-.15,1.81-.43,2.63-.82.05-.02.1-.05.14-.07.19-.09.37-.19.55-.29.06-.03.11-.07.17-.1.25-.15.49-.31.73-.48.14-.1.29-.21.42-.32.08-.07.16-.13.24-.2.16-.14.31-.28.46-.42.06-.05.11-.11.17-.17.19-.2.38-.4.55-.61,0-.01.02-.02.03-.03.2-.24.38-.49.56-.75.01-.02.02-.04.03-.05.15-.22.29-.46.42-.69.04-.08.08-.15.12-.23.09-.18.18-.37.27-.56.04-.1.09-.2.13-.3.02-.05.04-.1.06-.15.04-.1.06-.2.1-.3.06-.17.12-.33.17-.5.05-.17.09-.34.13-.51.03-.14.07-.27.09-.41.04-.23.08-.45.1-.68.01-.09.02-.17.03-.26.03-.29.04-.59.04-.88,0-.02,0-.05,0-.07,0,0,0-.01,0-.02Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m11.5,1.5C5.98,1.5,1.5,5.98,1.5,11.5s4.48,10,10,10,10-4.48,10-10S17.02,1.5,11.5,1.5Zm6.45,6.42l-6.94,8.96c-.34.44-.86.73-1.42.77-.06,0-.11,0-.17,0-.5,0-.99-.19-1.36-.53l-2.84-2.61c-.58-.53-.62-1.43-.09-2,.53-.58,1.43-.61,2-.09l2.19,2.01,6.39-8.25c.48-.62,1.37-.73,1.99-.25.62.48.73,1.37.25,1.99Z"/>
</svg>

After

Width:  |  Height:  |  Size: 393 B

@@ -0,0 +1,6 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m14.49,15.49c-.46,0-.92-.2-1.24-.6-.58-.72-.5-1.8.18-2.41l3.92-3.5-3.93-3.55c-.68-.62-.76-1.7-.17-2.41.59-.71,1.62-.8,2.3-.18l5.38,4.85c.36.33.57.8.57,1.3,0,.5-.21.97-.57,1.3l-5.38,4.8c-.31.27-.68.41-1.06.41Z"/>
<ellipse cx="7.09" cy="4.35" rx="1.51" ry="1.58"/>
<ellipse cx="3.68" cy="11.77" rx="2.18" ry="2.28"/>
<ellipse cx="8.48" cy="19.22" rx="3.04" ry="3.19"/>
</svg>

After

Width:  |  Height:  |  Size: 473 B

@@ -0,0 +1,15 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<defs>
<style>
.cls-1 { fill: none; stroke: #000; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2.13px; }
</style>
</defs>
<ellipse cx="2.48" cy="5.61" rx="1.53" ry="1.5"/>
<line class="cls-1" x1="8" y1="5.61" x2="20.94" y2="5.61"/>
<ellipse cx="2.48" cy="9.61" rx="1.53" ry="1.5"/>
<line class="cls-1" x1="8" y1="9.61" x2="20.94" y2="9.61"/>
<ellipse cx="2.48" cy="13.61" rx="1.53" ry="1.5"/>
<line class="cls-1" x1="8" y1="13.61" x2="20.94" y2="13.61"/>
<ellipse cx="2.48" cy="17.61" rx="1.53" ry="1.5"/>
<line class="cls-1" x1="8" y1="17.61" x2="20.94" y2="17.61"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

@@ -0,0 +1,5 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m1.5,2.42v3.68c0,.45.32.82.71.82h18.58c.39,0,.71-.36.71-.82v-3.68c0-.45-.32-.82-.71-.82H2.21c-.39,0-.71.36-.71.82Zm2.14,3.07c-.63,0-1.14-.64-1.14-1.42s.51-1.42,1.14-1.42,1.14.64,1.14,1.42-.51,1.42-1.14,1.42Zm16.11-.43H7.53c-.39,0-.71-.36-.71-.82v-.34c0-.45.32-.82.71-.82h12.21c.39,0,.71.36.71.82v.34c0,.45-.32.82-.71.82Z"/>
<path d="m1.5,9.77v3.68c0,.45.32.82.71.82h18.58c.39,0,.71-.36.71-.82v-3.68c0-.45-.32-.82-.71-.82H2.21c-.39,0-.71.36-.71.82Zm2.14,3.07c-.63,0-1.14-.64-1.14-1.42s.51-1.42,1.14-1.42,1.14.64,1.14,1.42-.51,1.42-1.14,1.42Zm16.11-.43H7.53c-.39,0-.71-.36-.71-.82v-.34c0-.45.32-.82.71-.82h12.21c.39,0,.71.36.71.82v.34c0,.45-.32.82-.71.82Z"/>
<path d="m1.5,17.11v3.68c0,.45.32.82.71.82h18.58c.39,0,.71-.36.71-.82v-3.68c0-.45-.32-.82-.71-.82H2.21c-.39,0-.71.36-.71.82Zm2.14,3.07c-.63,0-1.14-.64-1.14-1.42s.51-1.42,1.14-1.42,1.14.64,1.14,1.42-.51,1.42-1.14,1.42Zm16.11-.43H7.53c-.39,0-.71-.36-.71-.82v-.34c0-.45.32-.82.71-.82h12.21c.39,0,.71.36.71.82v.34c0,.45-.32.82-.71.82Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,4 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m20.02,13.39c-.59,0-.93.29-1.07.68-.95,2.82-3.56,4.52-6.64,4.52-1.67,0-3.26-.58-4.52-1.64-.19-.16-.19-.44-.01-.61l1.5-1.46c.53-.52.16-1.42-.58-1.42l-5.62.1c-.55.01-1,.46-1,1.01v5.37c.18.8,1.1.88,1.54.45l1.39-1.36c.16-.15.4-.15.56,0,1.86,1.66,4.23,2.58,6.74,2.58,5.25,0,8.46-3.36,9.71-7.7.08-.26-.13-.53-.41-.53h-1.6Z"/>
<path d="m4.07,9.82c.59,0,.93-.29,1.07-.68.95-2.82,3.56-4.52,6.64-4.52,1.68,0,3.26.58,4.53,1.64.19.15.19.44.01.61l-1.5,1.46c-.53.52-.16,1.42.58,1.42l5.62-.1c.55-.01.99-.46.99-1.01V3.28c-.18-.8-1.1-.88-1.54-.45l-1.39,1.36c-.16.15-.4.15-.56,0-1.86-1.67-4.23-2.58-6.75-2.58-5.25,0-8.46,3.37-9.71,7.71-.08.26.13.53.41.53h1.6Z"/>
</svg>

After

Width:  |  Height:  |  Size: 743 B

@@ -0,0 +1,5 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m9.72,11.51h13.28v-3.68c0-1.31-1.04-2.37-2.33-2.37h-4.1v-2.09c0-1.09-.88-1.98-1.95-1.98h-6.72c-1.08,0-1.95.89-1.95,1.98v2.09h-3.6c-1.29,0-2.33,1.06-2.33,2.37v3.68h9.72Zm-1.66-7.97h6.37v1.91h-6.37v-1.91Z"/>
<path d="m9.72,12.93h3.71c.16,0,.29.13.29.29v.67c0,.16-.13.29-.29.29h-3.71c-.16,0-.29-.13-.29-.29v-.67c0-.16.13-.29.29-.29Z"/>
<path d="m15.12,12.93c.02.1.03.19.03.29v.67c0,.94-.77,1.71-1.71,1.71h-3.71c-.94,0-1.71-.77-1.71-1.71v-.67c0-.1.01-.2.03-.29H0v6.08c0,1.31,1.04,2.37,2.33,2.37h18.33c1.29,0,2.33-1.06,2.33-2.37v-6.08h-7.88Z"/>
</svg>

After

Width:  |  Height:  |  Size: 642 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m11.5,1.51C5.98,1.51,1.5,5.99,1.5,11.51s4.48,10,10,10,10-4.48,10-10S17.02,1.51,11.5,1.51Zm-1.48,13.45c0,.88-.62,1.59-1.4,1.59s-1.4-.71-1.4-1.59v-6.91c0-.88.62-1.59,1.4-1.59s1.4.71,1.4,1.59v6.91Zm5.75,0c0,.88-.62,1.59-1.4,1.59s-1.4-.71-1.4-1.59v-6.91c0-.88.62-1.59,1.4-1.59s1.4.71,1.4,1.59v6.91Z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

@@ -0,0 +1,12 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 18 18">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color: #000000; }
</style>
</defs>
<path
class="ColorScheme-Text"
style="fill:currentColor"
d="m 4.0214839,5.02e-5 a 0.9947634,1.0001 0 1 0 0,2 h 0.978516 v 3 c 0,1.328 -1.695715,2 -2.9999998,2 v 2 H 6.9999999 V 15.00005 l 0.976562,1 1.023438,-1 V 9.0000502 H 14 v -2 c -1.304286,0 -3.055749,-0.673171 -3,-2 v -3 h 0.978516 a 0.9947634,1.0001 0 1 0 0,-2 z"
/>
</svg>

After

Width:  |  Height:  |  Size: 567 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m20.88,17.92l-3.29-3.28c.85-1.32,1.35-2.88,1.35-4.56,0-4.73-3.91-8.57-8.72-8.57S1.5,5.35,1.5,10.08s3.91,8.57,8.72,8.57c1.55,0,3-.4,4.26-1.1.01.01.02.03.03.04l3.31,3.3c.41.4.95.63,1.53.63.55,0,1.07-.2,1.47-.57.43-.39.67-.92.68-1.5.01-.57-.21-1.11-.62-1.52ZM3.98,10.08c0-3.36,2.8-6.09,6.24-6.09s6.24,2.73,6.24,6.09-2.8,6.09-6.24,6.09-6.24-2.73-6.24-6.09Z"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

@@ -0,0 +1,4 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="2 2 18 18">
<path d="m9.2502 3.0001c-0.51888 1.549e-4 -0.95146 0.39711-0.99609 0.91406l-0.11719 1.3398c-0.23504 0.11806-0.46067 0.24888-0.67969 0.39453l-1.2207-0.57422c-0.47103-0.22177-1.0334-0.045069-1.293 0.40625l-1.75 3.0293c-0.25943 0.44992-0.13129 1.0235 0.29492 1.3203l1.0488 0.73242c-0.010017 0.14609-0.036985 0.29094-0.037109 0.4375 1.241e-4 0.14591 0.02718 0.2901 0.037109 0.43555l-1.0449 0.7207c-0.42981 0.29586-0.55992 0.87245-0.29883 1.3242l1.75 3.0293c0.2603 0.45166 0.82388 0.62761 1.2949 0.4043l1.2109-0.57422c0.22158 0.14673 0.44964 0.27991 0.6875 0.39844l0.11719 1.3477c0.044639 0.51695 0.47722 0.91391 0.99609 0.91406h3.5c0.52095-4.1e-4 0.95433-0.40065 0.99609-0.91992l0.10938-1.3438c0.23718-0.11834 0.46457-0.25009 0.68555-0.39648l1.2227 0.57422c0.47054 0.2221 1.0328 0.0463 1.293-0.4043l1.75-3.0293c0.26026-0.45085 0.13114-1.0259-0.29688-1.3223l-1.0508-0.72852c0.01082-0.13896 0.03915-0.27653 0.04102-0.41602 3.1e-5 -0.0046 3.1e-5 -0.0091 0-0.01367 1.45e-4 -0.14944-0.02697-0.29635-0.03711-0.44531l1.0449-0.72266c0.4287-0.29572 0.5587-0.87093 0.29883-1.3223l-1.75-3.0293c-0.25955-0.45132-0.82194-0.62802-1.293-0.40625l-1.2227 0.57617c-0.22098-0.14639-0.44836-0.27814-0.68555-0.39648l-0.10938-1.3359c-0.04273-0.5185-0.47584-0.91764-0.99609-0.91797zm0.91602 2h1.6621l0.07422 0.92188c0.03216 0.3873 0.28579 0.72091 0.65039 0.85547 0.4791 0.17704 0.92563 0.43516 1.3184 0.76172 0.29825 0.24821 0.71316 0.3015 1.0645 0.13672l0.84766-0.40039 0.83008 1.4355-0.76172 0.52539c-0.32136 0.22144-0.48455 0.60959-0.41797 0.99414 0.04336 0.25146 0.06621 0.50656 0.06641 0.76172-0.0037 0.25058-0.02895 0.50156-0.07422 0.74805-0.07068 0.38631 0.09129 0.77824 0.41406 1.002l0.77344 0.53516-0.83008 1.4375-0.84766-0.40039c-0.3513-0.16478-0.7662-0.11149-1.0645 0.13672-0.39273 0.32656-0.83926 0.58468-1.3184 0.76172-0.3646 0.13456-0.61823 0.46816-0.65039 0.85547l-0.07422 0.93164h-1.6602l-0.08203-0.93555c-0.03328-0.38536-0.2858-0.71699-0.64844-0.85156-0.4791-0.17704-0.92563-0.43516-1.3184-0.76172-0.29906-0.24836-0.71496-0.30092-1.0664-0.13477l-0.83789 0.39648-0.82812-1.4336 0.76172-0.52734c0.32064-0.22112 0.48374-0.60829 0.41797-0.99219-0.043342-0.25132-0.06619-0.50473-0.066407-0.75977 2.169e-4 -0.25503 0.023066-0.50844 0.066406-0.75977 0.065792-0.38246-0.095608-0.76845-0.41406-0.99023l-0.76758-0.53516 0.83203-1.4395 0.84766 0.40039c0.35219 0.16508 0.76815 0.11099 1.0664-0.13867 0.39026-0.32576 0.83238-0.58189 1.3086-0.75977 0.36187-0.13518 0.61353-0.46668 0.64648-0.85156z"/>
<circle cx="11" cy="11" r="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,5 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m9.15,17.22h-2.11V2.27c0-.42-.38-.77-.86-.77h-1.15c-.47,0-.86.34-.86.77v14.96h-2.11c-.49,0-.74.52-.4.84l3.55,3.29c.22.2.58.2.8,0l3.55-3.29c.34-.31.09-.84-.4-.84Z"/>
<path d="m19.62,9.26l-.42-1.08h-3.54l-.42,1.1c-.16.43-.3.72-.42.87s-.3.23-.56.23c-.22,0-.42-.08-.59-.24s-.26-.34-.26-.54c0-.12.02-.24.06-.36s.1-.3.2-.52l2.23-5.58c.06-.16.14-.35.23-.58s.18-.41.28-.56.23-.27.4-.36.37-.14.61-.14.45.05.61.14.3.21.4.35.19.3.26.47.16.39.26.67l2.27,5.54c.18.42.27.73.27.92s-.08.38-.25.55-.37.25-.61.25c-.14,0-.26-.02-.36-.07s-.18-.12-.25-.2-.14-.21-.22-.39-.14-.33-.2-.46Zm-3.49-2.38h2.6l-1.31-3.54-1.29,3.54Z"/>
<path d="m14.27,19.5l4.47-5.28h-3.84c-.27,0-.47-.06-.61-.18s-.2-.27-.2-.46.07-.36.2-.48.34-.18.61-.18h4.95c.64,0,.96.29.96.88,0,.28-.05.51-.16.67s-.32.44-.65.81l-4.18,4.91h4.61c.27,0,.48.06.61.17s.2.27.2.47-.07.37-.2.49-.34.18-.61.18h-5.65c-.38,0-.67-.08-.87-.25s-.29-.4-.29-.68c0-.1.02-.19.05-.27s.07-.16.13-.23.13-.16.22-.28.18-.21.25-.29Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m11.5,1.51C5.98,1.51,1.5,5.99,1.5,11.51s4.48,10,10,10,10-4.48,10-10S17.02,1.51,11.5,1.51Zm5.17,10.81l-5.87,3.76c-1.03.66-2.37-.08-2.37-1.3v-6.54c0-1.22,1.35-1.95,2.37-1.3l5.87,3.76c.59.38.59,1.25,0,1.63Z"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m11.76,1.5C6.23,1.5,1.76,5.98,1.76,11.5s4.48,10,10,10,10-4.48,10-10S17.28,1.5,11.76,1.5Zm3.85,12.29c0,.86-.7,1.56-1.56,1.56h-4.58c-.86,0-1.56-.7-1.56-1.56v-4.58c0-.86.7-1.56,1.56-1.56h4.58c.86,0,1.56.7,1.56,1.56v4.58Z"/>
</svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1,15 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 18 18">
<defs>
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text { color: #000000; }
</style>
</defs>
<path
class="ColorScheme-Text"
style="fill:currentColor"
d="M 4.0214844,0 C 3.7305148,4.096652e-4 3.4543107,0.12887668 3.265625,0.3515625 L 11.914062,9 H 14 V 7 C 12.695714,7 10.944251,6.326829 11,5 V 2 h 0.978516 c 1.345107,0.019125 1.345107,-2.01912521 0,-2 z M 3.71875,6.6328125 C 3.1820953,6.8759067 2.5561157,7 2,7 V 9 H 6.0859375 Z M 7,9.9140625 V 15 L 7.9765625,16 9,15 v -3.085938 z"/>
<path
class="ColorScheme-Text"
style="fill:currentColor"
d="M -1e-7,9.9999999e-8 V 1.5000001 L 14.5,16 H 16 V 14.5 L 1.4999999,9.9999999e-8 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 805 B

@@ -0,0 +1,3 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
<path d="m20.33,1.42H2.89C1.63,1.42.61,2.47.61,3.77v15.31c0,1.29,1.02,2.34,2.28,2.34h17.44c1.26,0,2.28-1.05,2.28-2.34V3.77c0-1.29-1.02-2.34-2.28-2.34Zm-9.99,10.67l-3.1,3.29c-.21.22-.48.33-.76.33-.26,0-.53-.1-.73-.3-.42-.42-.43-1.1-.02-1.53l2.38-2.53-2.35-2.45c-.41-.43-.4-1.11.01-1.53.41-.42,1.08-.41,1.49.01l3.07,3.2c.4.42.4,1.09,0,1.51Zm6.5,3.62h-4.07c-.58,0-1.05-.48-1.05-1.08s.47-1.08,1.05-1.08h4.07c.58,0,1.05.48,1.05,1.08s-.47,1.08-1.05,1.08Z"/>
</svg>

After

Width:  |  Height:  |  Size: 537 B

@@ -0,0 +1,52 @@
import QtQuick
import org.kde.kirigami as Kirigami
Rectangle {
id: badge
property var iconName: ""
property var iconColor: ""
property var position: ""
width: Math.min(parent.width, parent.height) / 3
height: width
radius: width / 2
color: cfg.counterColor ? cfg.counterColor : Kirigami.Theme.backgroundColor
anchors {
topMargin: state.includes("top") ? cfg.badgeOffsetX : 0
leftMargin: state.includes("Left") ? cfg.badgeOffsetY : 0
rightMargin: state.includes("Right") ? cfg.badgeOffsetY : 0
bottomMargin: state.includes("bottom") ? cfg.badgeOffsetX : 0
}
state: position || "topRight"
states: [
State {
name: "topLeft"
AnchorChanges { target: badge; anchors.top: parent.top; anchors.left: parent.left }
},
State {
name: "topRight"
AnchorChanges { target: badge; anchors.top: parent.top; anchors.right: parent.right }
},
State {
name: "bottomLeft"
AnchorChanges { target: badge; anchors.bottom: parent.bottom; anchors.left: parent.left }
},
State {
name: "bottomRight"
AnchorChanges { target: badge; anchors.bottom: parent.bottom; anchors.right: parent.right }
}
]
transitions: Transition { AnchorAnimation { duration: 120 } }
Kirigami.Icon {
anchors.fill: parent
source: cfg.ownIconsUI ? Qt.resolvedUrl("../assets/icons/" + iconName + ".svg") : iconName
color: iconColor
isMask: cfg.ownIconsUI
}
}
@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import "../../tools/tools.js" as JS
RowLayout {
property alias labelText: label.text
ComboBox {
id: comboBox
implicitWidth: 250
textRole: "name"
model: [
{"name": i18n("None"), "value": ""},
{"name": i18n("Check updates"), "value": "checkUpdates"},
{"name": i18n("Upgrade system"), "value": "upgradeSystem"},
{"name": i18n("Pause auto check"), "value": "switchScheduler"},
{"name": i18n("Management"), "value": "management"}
]
onCurrentIndexChanged: {
var action = model[currentIndex]["value"]
switch (type) {
case "middle": cfg_middleAction = action; break;
case "right": cfg_rightAction = action; break;
case "scrollUp": cfg_scrollUpAction = action; break;
case "scrollDown": cfg_scrollDownAction = action; break;
}
}
Component.onCompleted: {
currentIndex = JS.setIndex(plasmoid.configuration[type + "Action"], model)
}
}
Label {
id: label
text: ""
}
property string type: ""
}
@@ -0,0 +1,64 @@
import QtQuick
import org.kde.notification
import "../../tools/tools.js" as JS
Item {
Component {
id: notifyComponent
Notification {
componentName: "apdatifier"
flags: cfg.notifyPersistent ? Notification.Persistent : Notification.CloseOnTimeout
}
}
Component {
id: upgradeAction
NotificationAction {
label: i18n("Upgrade system")
onActivated: JS.upgradeSystem()
}
}
Component {
id: openArticleAction
NotificationAction {
property string link
label: i18n("Read article")
onActivated: Qt.openUrlExternally(link)
}
}
function send(event, title, body, link) {
const params = {
updates: {
icon: "apdatifier-packages",
urgency: "DefaultUrgency"
},
news: {
icon: "news-subscribe",
urgency: "HighUrgency"
},
error: {
icon: "error",
urgency: "CriticalUrgency"
}
}
const { icon, urgency } = params[event]
const action = (event === "updates" && cfg.notifyUpdatesAction) ? upgradeAction.createObject(root)
: (event === "news" && cfg.notifyNewsAction) ? openArticleAction.createObject(root, { link: link })
: []
if (cfg.notifySound) event += "Sound"
notifyComponent.createObject(root, {
eventId: event,
iconName: icon,
title: title,
text: body,
actions: action,
urgency: urgency
})?.sendEvent()
}
}
@@ -0,0 +1,61 @@
import QtQuick
Item {
id: root
required property Item target
required property bool running
property real scaleTo: 1.20
property real opacityTo: 0.60
property int duration: 400
onRunningChanged: {
if (!running && target) {
target.scale = 1
target.opacity = 1
}
}
SequentialAnimation {
id: scaleAnimation
running: root.running
loops: Animation.Infinite
NumberAnimation {
target: root.target
property: "scale"
to: root.scaleTo
duration: root.duration
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.target
property: "scale"
to: 1.00
duration: root.duration
easing.type: Easing.InOutQuad
}
}
SequentialAnimation {
id: opacityAnimation
running: root.running
loops: Animation.Infinite
NumberAnimation {
target: root.target
property: "opacity"
to: root.opacityTo
duration: root.duration
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.target
property: "opacity"
to: 1.00
duration: root.duration
easing.type: Easing.InOutQuad
}
}
}
@@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Controls.Basic
import org.kde.kirigami as Kirigami
ScrollBar {
id: root
property bool showByTimer: false
visible: root.visualSize < 1.0
Timer {
id: hideTimer
interval: 1200
running: false
repeat: false
onTriggered: root.showByTimer = false
}
onActiveChanged: {
if (active) {
showByTimer = true
hideTimer.restart()
}
}
HoverHandler {
id: hoverHandler
cursorShape: Qt.PointingHandCursor
onHoveredChanged: scroll.opacity = hovered ? 1 : 0.5
}
opacity: (!cfg.autoHideScrollBar || root.active || root.hovered || root.showByTimer) ? 0.8 : 0
Behavior on opacity { NumberAnimation { duration: 300 } }
contentItem: Rectangle {
id: scroll
implicitWidth: 5
radius: width / 2
color: Kirigami.Theme.highlightColor
opacity: 0.5
border.width: 1
border.color: Qt.darker(Qt.rgba(color.r, color.g, color.b, 0.8), 2)
Behavior on opacity { NumberAnimation { duration: 300 } }
}
}
@@ -0,0 +1,44 @@
import QtQuick
import org.kde.plasma.plasma5support as Plasma5Support
Plasma5Support.DataSource {
id: executable
engine: "executable"
connectedSources: []
onNewData: (sourceName, data) => {
var cmd = sourceName
var out = data["stdout"]
var err = data["stderr"]
var code = data["exit code"]
exited(cmd, out, err, code)
const cb = listeners[cmd]
if (cb) cb(cmd, out, err, code)
}
signal exited(string cmd, string out, string err, int code)
property var listeners: ({})
function exec(cmd, callback) {
listeners[cmd] = execCallback.bind(executable, callback)
connectSource(cmd)
}
function execCallback(callback, cmd, out, err, code) {
cleanup()
if (callback) callback(cmd, out, err, code)
}
function cleanup() {
for (var cmd in listeners) {
delete listeners[cmd]
disconnectSource(cmd)
}
if (typeof sts !== 'undefined' && sts?.proc === executable) sts.proc = null
executable.destroy()
}
}
@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.plasma.components
ToolButton {
property string tooltipText: ""
property string iconSource: ""
property color iconColor: Kirigami.Theme.colorSet
property alias buttonTooltip: tooltip
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
hoverEnabled: enabled
highlighted: enabled
ToolTip {
id: tooltip
text: tooltipText
}
Kirigami.Icon {
height: parent.height
width: parent.height
anchors.centerIn: parent
source: iconSource
color: iconColor
scale: cfg.ownIconsUI ? 0.7 : 0.9
isMask: cfg.ownIconsUI
smooth: true
}
}
@@ -0,0 +1,819 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Dialogs
import QtQuick.Controls
import org.kde.ksvg
import org.kde.kcmutils
import org.kde.iconthemes
import org.kde.kquickcontrolsaddons
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import "../../tools/tools.js" as JS
SimpleKCM {
property alias cfg_hideIconPolicy: hideIconPolicy.value
property string cfg_selectedIcon: plasmoid.configuration.selectedIcon
property string cfg_busyIndicator: plasmoid.configuration.busyIndicator
property string cfg_pauseBadgePosition: plasmoid.configuration.pauseBadgePosition
property string cfg_updatedBadgePosition: plasmoid.configuration.updatedBadgePosition
property alias cfg_badgeOffsetX: badgeOffsetX.value
property alias cfg_badgeOffsetY: badgeOffsetY.value
property string cfg_counterMode: plasmoid.configuration.counterMode
property string cfg_counterSidePosition: plasmoid.configuration.counterSidePosition
property string cfg_counterBadgePosition: plasmoid.configuration.counterBadgePosition
property alias cfg_counterOffsetX: counterOffsetX.value
property alias cfg_counterOffsetY: counterOffsetY.value
property string cfg_counterColor: plasmoid.configuration.counterColor
property alias cfg_counterRadius: counterRadius.value
property alias cfg_counterOpacity: counterOpacity.value
property string cfg_counterFontFamily: plasmoid.configuration.counterFontFamily
property alias cfg_counterFontBold: counterFontBold.checked
property alias cfg_counterFontSize: counterFontSize.value
property alias cfg_counterGapsInner: counterGapsInner.value
property alias cfg_counterGapsOuter: counterGapsOuter.value
property alias cfg_ownIconsUI: ownIconsUI.checked
property alias cfg_autoHideScrollBar: autoHideScrollBar.checked
property int cfg_defaultTab: plasmoid.configuration.defaultTab
property alias cfg_switchDefaultTab: switchDefaultTab.checked
property alias cfg_sorting: sorting.checked
property alias cfg_showStatusText: showStatusText.checked
property alias cfg_showToolBar: showToolBar.checked
property alias cfg_searchButton: searchButton.checked
property alias cfg_intervalButton: intervalButton.checked
property alias cfg_sortButton: sortButton.checked
property alias cfg_viewButton: viewButton.checked
property alias cfg_managementButton: managementButton.checked
property alias cfg_upgradeButton: upgradeButton.checked
property alias cfg_checkButton: checkButton.checked
property alias cfg_pinButton: pinButton.checked
property alias cfg_settingsButton: settingsButton.checked
property alias cfg_tabBarVisible: tabBarVisible.checked
property alias cfg_tabBarTexts: tabBarTexts.checked
property string cfg_compactInfo: plasmoid.configuration.compactInfo || "repository"
property alias cfg_extendedShowRepository: extendedShowRepository.checked
property alias cfg_extendedShowInstallReason: extendedShowInstallReason.checked
property alias cfg_highlightVersionDiff: highlightVersionDiff.checked
property alias cfg_colorizeInstallReason: colorizeInstallReason.checked
readonly property bool inTray: (plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
readonly property bool onDesktop: plasmoid.location === PlasmaCore.Types.Floating
readonly property bool horizontal: plasmoid.location === PlasmaCore.Types.TopEdge || plasmoid.location === PlasmaCore.Types.BottomEdge
readonly property bool allowOnlyCounterBadge: inTray || !horizontal
readonly property bool counterEnabled: cfg_counterMode !== "disabled"
readonly property bool sideMode: cfg_counterMode === "side"
readonly property bool badgeMode: cfg_counterMode === "badge"
readonly property var positions: [
{ name: i18n("Disabled"), value: "disabled" },
{ name: i18n("Top-Left"), value: "topLeft" },
{ name: i18n("Top-Right"), value: "topRight" },
{ name: i18n("Bottom-Left"), value: "bottomLeft" },
{ name: i18n("Bottom-Right"), value: "bottomRight" }
]
property int currentTab
signal tabChanged(currentTab: int)
onCurrentTabChanged: tabChanged(currentTab)
header: Kirigami.NavigationTabBar {
actions: [
Kirigami.Action {
icon.name: "view-list-icons"
text: i18n("Panel Icon")
checked: currentTab === 0
onTriggered: currentTab = 0
},
Kirigami.Action {
icon.name: "view-split-left-right"
text: i18n("Full View")
checked: currentTab === 1
onTriggered: currentTab = 1
}
]
}
Kirigami.FormLayout {
id: iconViewTab
visible: currentTab === 0
Kirigami.Separator {
Kirigami.FormData.label: i18n("Icon")
Kirigami.FormData.isSection: true
}
Button {
id: iconButton
implicitWidth: iconFrame.width + Kirigami.Units.smallSpacing
implicitHeight: implicitWidth
hoverEnabled: true
FrameSvgItem {
id: iconFrame
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.large + fixedMargins.left + fixedMargins.right
height: width
imagePath: "widgets/background"
Kirigami.Icon {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.large
height: width
source: JS.setIcon(cfg_selectedIcon)
}
}
IconDialog {
id: iconDialog
onIconNameChanged: cfg_selectedIcon = iconName || JS.defaultIcon
}
onClicked: menu.opened ? menu.close() : menu.open()
Menu {
id: menu
y: +parent.height
MenuItem {
text: i18n("Default") + " 1"
icon.name: "apdatifier-plasmoid"
enabled: cfg_selectedIcon !== JS.defaultIcon
onClicked: cfg_selectedIcon = JS.defaultIcon
}
MenuItem {
text: i18n("Default") + " 2"
icon.name: "apdatifier-packages"
enabled: cfg_selectedIcon !== icon.name
onClicked: cfg_selectedIcon = icon.name
}
MenuItem {
text: i18n("Default") + " 3"
icon.name: "apdatifier-package"
enabled: cfg_selectedIcon !== icon.name
onClicked: cfg_selectedIcon = icon.name
}
MenuItem {
text: i18n("Default") + " 4"
icon.name: "apdatifier-flatpak"
enabled: cfg_selectedIcon !== icon.name
onClicked: cfg_selectedIcon = icon.name
}
MenuItem {
text: i18n("Select...")
icon.name: "document-open-folder"
onClicked: iconDialog.open()
}
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
ToolTip {
text: cfg_selectedIcon === JS.defaultIcon ? i18n("Default icon") : cfg_selectedIcon
delay: Kirigami.Units.toolTipDelay
visible: iconButton.hovered
}
}
RowLayout {
Kirigami.FormData.label: i18n("Hide") + ":"
Label {
text: i18n("when less than")
}
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
id: hideIconPolicy
from: 0
to: 999
stepSize: 1
}
Label {
text: i18np("update", "updates", hideIconPolicy.value)
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("If the widget is on a panel, you can access the hidden icon in Panel Configuration mode.")
}
}
RowLayout {
Kirigami.FormData.label: i18n("Busy indicator") + ":"
RowLayout {
spacing: Kirigami.Units.largeSpacing
ButtonGroup {
id: busyIndicatorGroup
onCheckedButtonChanged: {
if (checkedButton) {
cfg_busyIndicator = checkedButton.modeValue
}
}
}
RadioButton {
text: i18n("Spinner")
property string modeValue: "spinner"
ButtonGroup.group: busyIndicatorGroup
}
RadioButton {
text: i18n("Pulse")
property string modeValue: "pulse"
ButtonGroup.group: busyIndicatorGroup
}
Component.onCompleted: {
const current = plasmoid.configuration.busyIndicator || "spinner"
for (let i = 0; i < busyIndicatorGroup.buttons.length; ++i) {
const b = busyIndicatorGroup.buttons[i]
if (b.modeValue === current) {
b.checked = true
break
}
}
}
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Spinner uses the standard Plasmoid busy indicator. Pulse animates the panel icon. On the desktop, the busy indicator is always off.")
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Counter")
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Mode") + ":"
RowLayout {
id: counterMode
spacing: Kirigami.Units.largeSpacing
ButtonGroup {
id: counterModeGroup
onCheckedButtonChanged: {
if (checkedButton) {
cfg_counterMode = checkedButton.modeValue
}
}
}
RadioButton {
text: i18n("Off")
property string modeValue: "disabled"
ButtonGroup.group: counterModeGroup
}
RadioButton {
text: i18n("Side")
property string modeValue: "side"
ButtonGroup.group: counterModeGroup
enabled: !allowOnlyCounterBadge
}
RadioButton {
text: i18n("Badge")
property string modeValue: "badge"
ButtonGroup.group: counterModeGroup
}
Component.onCompleted: {
if (allowOnlyCounterBadge && plasmoid.configuration.counterMode === "side") {
plasmoid.configuration.counterMode = "badge"
}
for (let i = 0; i < counterModeGroup.buttons.length; ++i) {
const b = counterModeGroup.buttons[i]
if (b.modeValue === plasmoid.configuration.counterMode) {
b.checked = true
break
}
}
}
}
}
RowLayout {
Kirigami.FormData.label: i18n("Position") + ":"
ComboBox {
id: counterBadgePosition
enabled: badgeMode
textRole: "name"
model: positions.slice(1).concat([{ name: i18n("Center"), value: "center" }])
onCurrentIndexChanged: cfg_counterBadgePosition = model[currentIndex].value
Component.onCompleted: currentIndex = JS.setIndex(plasmoid.configuration.counterBadgePosition, model)
}
}
RowLayout {
Kirigami.FormData.label: i18n("Side") + ":"
enabled: sideMode
spacing: Kirigami.Units.largeSpacing
RadioButton {
text: i18n("Left")
checked: cfg_counterSidePosition === "left"
onCheckedChanged: {
if (checked) cfg_counterSidePosition = "left"
}
}
RadioButton {
text: i18n("Right")
checked: cfg_counterSidePosition === "right"
onCheckedChanged: {
if (checked) cfg_counterSidePosition = "right"
}
}
}
RowLayout {
Kirigami.FormData.label: i18n("Gaps") + ":"
enabled: sideMode
spacing: Kirigami.Units.largeSpacing
RowLayout {
Label { text: i18n("Inner") + ":" }
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
enabled: sideMode
id: counterGapsInner
from: 0
to: 99
stepSize: 1
}
}
RowLayout {
Label { text: i18n("Outer") + ":" }
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
enabled: sideMode
id: counterGapsOuter
from: 0
to: 99
stepSize: 1
}
}
}
RowLayout {
Kirigami.FormData.label: i18n("Offset") + ":"
enabled: badgeMode
Label { text: "X:" }
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
id: counterOffsetX
from: -5
to: 5
stepSize: 1
}
Label { text: "Y:" }
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
id: counterOffsetY
from: -5
to: 5
stepSize: 1
}
}
Item {
Kirigami.FormData.isSection: true
}
ComboBox {
Kirigami.FormData.label: i18n("Font family") + ":"
Layout.preferredWidth: Kirigami.Units.gridUnit * 12
enabled: counterEnabled
editable: true
textRole: "name"
model: {
let fonts = Qt.fontFamilies()
let arr = []
arr.push({"name": i18n("Default system font"), "value": ""})
for (let i = 0; i < fonts.length; i++) {
arr.push({"name": fonts[i], "value": fonts[i]})
}
return arr
}
onCurrentIndexChanged: cfg_counterFontFamily = model[currentIndex]["value"]
Component.onCompleted: currentIndex = JS.setIndex(plasmoid.configuration.counterFontFamily, model)
}
Slider {
Kirigami.FormData.label: i18n("Font size") + ":"
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
enabled: counterEnabled
id: counterFontSize
from: 2
to: 8
stepSize: 1
}
CheckBox {
Kirigami.FormData.label: i18n("Font bold") + ":"
enabled: counterEnabled
id: counterFontBold
text: i18n("Enable")
onCheckedChanged: cfg_counterFontBold = counterFontBold.checked
}
Item {
Kirigami.FormData.isSection: true
}
Button {
Kirigami.FormData.label: i18n("Badge color") + ":"
Layout.leftMargin: Kirigami.Units.gridUnit
enabled: badgeMode
id: counterColor
implicitWidth: Kirigami.Units.gridUnit * 4
implicitHeight: Kirigami.Units.gridUnit
background: Item {
Rectangle {
id: bg
anchors.fill: parent
radius: counterRadius.value
border.width: 1
border.color: "black"
color: cfg_counterColor ? cfg_counterColor : Kirigami.Theme.backgroundColor
}
Text {
anchors.centerIn: parent
text: bg.color.toString().toUpperCase()
font.bold: true
color: Kirigami.ColorUtils.brightnessForColor(bg.color) === Kirigami.ColorUtils.Dark ? "white" : "black"
}
}
onPressed: menuColor.opened ? menuColor.close() : menuColor.open()
Menu {
id: menuColor
y: +parent.height
MenuItem {
text: i18n("Default color")
icon.name: "edit-clear"
enabled: cfg_counterColor && cfg_counterColor !== Kirigami.Theme.backgroundColor
onClicked: cfg_counterColor = ""
}
MenuItem {
text: i18n("Select...")
icon.name: "document-open-folder"
onClicked: colorDialog.open()
}
}
ColorDialog {
id: colorDialog
visible: false
title: i18n("Select counter background color")
selectedColor: cfg_counterColor
onAccepted: {
cfg_counterColor = selectedColor
}
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
ToolTip {
text: cfg_counterColor ? cfg_counterColor : i18n("Default background color from current theme")
delay: Kirigami.Units.toolTipDelay
visible: !cfg_counterColor && counterColor.hovered
}
}
RowLayout {
Kirigami.FormData.label: i18n("Badge radius") + ":"
enabled: badgeMode
Slider {
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
id: counterRadius
from: 0
to: 100
stepSize: 1
}
Label {
text: counterRadius.value
}
}
RowLayout {
Kirigami.FormData.label: i18n("Badge opacity") + ":"
enabled: badgeMode
Slider {
Layout.preferredWidth: Kirigami.Units.gridUnit * 10
id: counterOpacity
from: 0
to: 10
stepSize: 1
}
Label {
text: counterOpacity.value / 10
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Icon Badges")
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Paused") + ":"
ComboBox {
id: pauseBadgePosition
textRole: "name"
model: positions
onCurrentIndexChanged: cfg_pauseBadgePosition = model[currentIndex].value
Component.onCompleted: currentIndex = JS.setIndex(plasmoid.configuration.pauseBadgePosition, model)
}
}
RowLayout {
Kirigami.FormData.label: i18n("Updated") + ":"
ComboBox {
id: updatedBadgePosition
textRole: "name"
model: positions
onCurrentIndexChanged: cfg_updatedBadgePosition = model[currentIndex].value
Component.onCompleted: currentIndex = JS.setIndex(plasmoid.configuration.updatedBadgePosition, model)
}
}
RowLayout {
Kirigami.FormData.label: i18n("Offset") + ":"
Label { text: "X:" }
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
id: badgeOffsetX
from: -5
to: 5
stepSize: 1
}
Label { text: "Y:" }
SpinBox {
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
id: badgeOffsetY
from: -5
to: 5
stepSize: 1
}
}
}
Kirigami.FormLayout {
id: listViewTab
visible: currentTab === 1
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: "UI:"
CheckBox {
id: ownIconsUI
text: i18n("Use built-in icons")
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Override custom icon theme and use default Apdatifier icons instead.")
}
}
Item {
Kirigami.FormData.isSection: true
}
CheckBox {
Kirigami.FormData.label: i18n("Scrollbar") + ":"
id: autoHideScrollBar
text: i18n("Auto-hide scrollbar")
}
Item {
Kirigami.FormData.isSection: true
}
ButtonGroup {
id: viewGroup
}
RadioButton {
Kirigami.FormData.label: i18n("Default view") + ":"
id: extendedView
ButtonGroup.group: viewGroup
text: i18n("Extended")
Component.onCompleted: checked = !plasmoid.configuration.defaultTab
}
RadioButton {
ButtonGroup.group: viewGroup
text: i18n("Compact")
onCheckedChanged: cfg_defaultTab = checked
Component.onCompleted: checked = plasmoid.configuration.defaultTab
}
CheckBox {
Kirigami.FormData.label: i18n("Behavior") + ":"
id: switchDefaultTab
text: i18n("Always switch to default view")
}
Item {
Kirigami.FormData.isSection: true
}
ButtonGroup {
id: sortGroup
}
RadioButton {
id: sorting
Kirigami.FormData.label: i18n("Sorting") + ":"
text: i18n("By repository")
checked: true
Component.onCompleted: checked = plasmoid.configuration.sorting
ButtonGroup.group: sortGroup
}
RadioButton {
text: i18n("By name")
Component.onCompleted: checked = !plasmoid.configuration.sorting
ButtonGroup.group: sortGroup
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Toolbar")
Kirigami.FormData.isSection: true
}
CheckBox {
id: showStatusText
Kirigami.FormData.label: i18n("Header") + ":"
text: i18n("Show status")
}
CheckBox {
id: showToolBar
text: i18n("Show tool bar")
}
RowLayout {
enabled: showToolBar.checked
ToolButton {
id: searchButton
icon.name: "search"
checkable: true
}
ToolButton {
id: intervalButton
icon.name: "media-playback-paused"
checkable: true
}
ToolButton {
id: sortButton
icon.name: "sort-name"
checkable: true
}
}
RowLayout {
enabled: showToolBar.checked
ToolButton {
id: viewButton
icon.name: "view-split-top-bottom"
checkable: true
}
ToolButton {
id: managementButton
icon.name: "tools"
checkable: true
}
ToolButton {
id: upgradeButton
icon.name: "akonadiconsole"
checkable: true
}
}
RowLayout {
enabled: showToolBar.checked
ToolButton {
id: checkButton
icon.name: "view-refresh"
checkable: true
}
ToolButton {
id: settingsButton
icon.name: "settings-configure"
checkable: true
enabled: !inTray
}
ToolButton {
id: pinButton
icon.name: "pin"
checkable: true
enabled: !inTray && !onDesktop
ToolTip {
text: !pinButton.enabled ? "Pinning button is not available in the system tray and on the desktop" : ""
}
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Additional Package Info")
Kirigami.FormData.isSection: true
}
ComboBox {
Kirigami.FormData.label: i18n("Compact view") + ":"
id: compactInfo
textRole: "name"
model: [
{ name: i18n("Repository"), value: "repository" },
{ name: i18n("Install reason"), value: "installReason" },
{ name: i18n("Versions"), value: "versionDiff" }
]
onCurrentIndexChanged: cfg_compactInfo = model[currentIndex].value
Component.onCompleted: currentIndex = JS.setIndex(plasmoid.configuration.compactInfo || "repository", model)
}
Item {
Kirigami.FormData.isSection: true
}
CheckBox {
Kirigami.FormData.label: i18n("Extended view") + ":"
id: extendedShowRepository
text: i18n("Repository")
}
CheckBox {
id: extendedShowInstallReason
text: i18n("Install reason")
}
Item {
Kirigami.FormData.isSection: true
}
CheckBox {
id: highlightVersionDiff
text: i18n("Highlight version differences")
}
CheckBox {
id: colorizeInstallReason
text: i18n("Colorize install reason")
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Tab Bar")
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Footer") + ":"
CheckBox {
id: tabBarVisible
text: i18n("Show tab bar")
}
}
CheckBox {
id: tabBarTexts
text: i18n("Show tab texts")
enabled: tabBarVisible.checked
}
Item {
Kirigami.FormData.isSection: true
}
}
}
@@ -0,0 +1,800 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Dialogs
import org.kde.kcmutils
import org.kde.kirigami as Kirigami
import "../components" as QQC
import "../../tools/tools.js" as JS
SimpleKCM {
property string cfg_checkMode: plasmoid.configuration.checkMode
property alias cfg_intervalMinutes: intervalMinutes.value
property alias cfg_dailyHour: dailyHour.value
property alias cfg_dailyMinute: dailyMinute.value
property alias cfg_weeklyDay: weeklyDay.currentIndex
property alias cfg_weeklyHour: weeklyHour.value
property alias cfg_weeklyMinute: weeklyMinute.value
property alias cfg_arch: arch.checked
property alias cfg_aur: aur.checked
property alias cfg_flatpak: flatpak.checked
property alias cfg_fwupd: fwupd.checked
property alias cfg_widgets: widgets.checked
property bool cfg_feedsEnabled: plasmoid.configuration.feedsEnabled
property alias cfg_feedsFetchCount: feedsFetchCount.value
property string cfg_feeds: plasmoid.configuration.feeds
property string cfg_middleAction: plasmoid.configuration.middleAction
property string cfg_rightAction: plasmoid.configuration.rightAction
property string cfg_scrollUpAction: plasmoid.configuration.scrollUpAction
property string cfg_scrollDownAction: plasmoid.configuration.scrollDownAction
property alias cfg_notifyUpdates: notifyUpdates.checked
property alias cfg_notifyUpdatesAction: notifyUpdatesAction.checked
property alias cfg_notifyEveryBump: notifyEveryBump.checked
property alias cfg_notifyNews: notifyNews.checked
property alias cfg_notifyNewsAction: notifyNewsAction.checked
property alias cfg_notifyErrors: notifyErrors.checked
property alias cfg_notifySound: notifySound.checked
property alias cfg_notifyPersistent: notifyPersistent.checked
property alias cfg_checkConn: checkConn.checked
property alias cfg_respectMeteredConn: respectMeteredConn.checked
property var cfg: plasmoid.configuration
property var pkg: plasmoid.configuration.packages
property var terminals: plasmoid.configuration.terminals
property var feeds: []
property alias cfg_dbPath: dbPath.text
property bool systemWide: !/^\/home\/[^/]+\/\.local/.test(JS.scriptDir)
property int installButton
property var dialogTitles: {
"0": i18n("Install Development version"),
"1": i18n("Install Stable version"),
"2": i18n("Uninstall widget")
}
property var dialogSubtitles: {
"0": i18n("Note: version with the latest commits may be unstable."),
"1": i18n("Note: if you haven't installed the Devel version before, there's no need to install the Stable version."),
"2": i18n("Removal of the widget and all related files, including the directory with its configuration.")
}
property int currentTab
signal tabChanged(currentTab: int)
onCurrentTabChanged: tabChanged(currentTab)
Component.onCompleted: {
feeds = JS.parseCustomFeeds(cfg_feeds)
cfg_feeds = JS.serializeCustomFeeds(feeds)
feedsUrl.syncToModel()
JS.checkDependencies()
}
header: Kirigami.NavigationTabBar {
actions: [
Kirigami.Action {
icon.name: "search"
text: i18n("Search")
checked: currentTab === 0
onTriggered: currentTab = 0
},
Kirigami.Action {
icon.name: "notification-active"
text: i18n("Notifications")
checked: currentTab === 1
onTriggered: currentTab = 1
},
Kirigami.Action {
icon.name: "followmouse-symbolic"
text: i18n("Mouse actions")
checked: currentTab === 2
onTriggered: currentTab = 2
},
Kirigami.Action {
icon.name: "documentinfo"
text: i18n("Misc")
checked: currentTab === 3
onTriggered: currentTab = 3
}
]
}
ColumnLayout {
Kirigami.InlineMessage {
id: configMsg
Layout.fillWidth: true
icon.source: "document-save"
text: "<b>" + i18n("Configuration is automatically saved in a config file and loaded at every startup, ensuring you never lose your settings. The config file is stored in ") + "~/.config/apdatifier" + "</b>"
type: Kirigami.MessageType.Positive
visible: plasmoid.configuration.configMsg
actions: [
Kirigami.Action {
text: "OK"
icon.name: "checkmark"
onTriggered: plasmoid.configuration.configMsg = false
}
]
}
Kirigami.FormLayout {
id: searchTab
visible: currentTab === 0
Kirigami.Separator {
Kirigami.FormData.label: i18n("Schedule")
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Mode") + ":"
ComboBox {
id: checkMode
textRole: "name"
model: [
{ name: i18n("Manual"), value: "manual" },
{ name: i18n("Interval"), value: "interval" },
{ name: i18n("Daily"), value: "daily" },
{ name: i18n("Weekly"), value: "weekly" }]
currentIndex: JS.setIndex(cfg_checkMode, model)
onCurrentIndexChanged: cfg_checkMode = model[currentIndex].value
}
}
RowLayout {
visible: cfg_checkMode === "interval"
Kirigami.FormData.label: i18n("Interval") + ":"
SpinBox {
id: intervalMinutes
from: 30
to: 43200
stepSize: 5
value: intervalMinutes.value
onValueChanged: cfg_intervalMinutes = value
}
Label { text: i18n("minutes") }
}
RowLayout {
visible: cfg_checkMode === "daily"
Kirigami.FormData.label: i18n("Daily time") + ":"
SpinBox {
id: dailyHour
from: 0
to: 23
stepSize: 1
value: dailyHour.value
onValueChanged: cfg_dailyHour = value
Layout.preferredWidth: 50
}
Label { text: ":" }
SpinBox {
id: dailyMinute
from: 0
to: 59
stepSize: 1
value: dailyMinute.value
onValueChanged: cfg_dailyMinute = value
Layout.preferredWidth: 50
}
}
RowLayout {
visible: cfg_checkMode === "weekly"
Kirigami.FormData.label: i18n("Schedule") + ":"
ComboBox {
id: weeklyDay
textRole: "name"
model: [
{ name: i18n("Sunday"), value: 0 },
{ name: i18n("Monday"), value: 1 },
{ name: i18n("Tuesday"), value: 2 },
{ name: i18n("Wednesday"), value: 3 },
{ name: i18n("Thursday"), value: 4 },
{ name: i18n("Friday"), value: 5 },
{ name: i18n("Saturday"), value: 6 }
]
currentIndex: JS.setIndex(plasmoid.configuration.weeklyDay, model)
onCurrentIndexChanged: plasmoid.configuration.weeklyDay = model[currentIndex].value
}
RowLayout {
spacing: Kirigami.Units.smallSpacing
SpinBox {
id: weeklyHour
from: 0
to: 23
stepSize: 1
value: weeklyHour.value
onValueChanged: cfg_weeklyHour = value
Layout.preferredWidth: 50
}
Label {
text: ":"
verticalAlignment: Text.AlignVCenter
}
SpinBox {
id: weeklyMinute
from: 0
to: 59
stepSize: 1
value: weeklyMinute.value
onValueChanged: cfg_weeklyMinute = value
Layout.preferredWidth: 50
}
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Updates")
Kirigami.FormData.isSection: true
}
RowLayout {
CheckBox {
id: arch
text: i18n("Arch Official Repositories")
enabled: pkg.pacman
}
}
RowLayout {
spacing: Kirigami.Units.gridUnit
visible: pkg.pacman
CheckBox {
id: aur
text: i18n("Arch User Repository") + " (AUR)"
enabled: pkg.paru || pkg.yay || pkg.pikaur
}
Kirigami.UrlButton {
url: "https://github.com/exequtic/apdatifier#supported-pacman-wrappers"
text: instTip.text
font.pointSize: instTip.font.pointSize
color: instTip.color
visible: !aur.enabled
}
}
RowLayout {
spacing: Kirigami.Units.gridUnit
CheckBox {
id: flatpak
text: i18n("Flatpak applications")
enabled: pkg.flatpak
}
Kirigami.UrlButton {
id: instTip
url: "https://flathub.org/setup"
text: i18n("Not installed")
font.pointSize: Kirigami.Theme.smallFont.pointSize
color: Kirigami.Theme.neutralTextColor
visible: !pkg.flatpak
}
}
RowLayout {
CheckBox {
id: widgets
text: i18n("Plasma Widgets")
enabled: pkg.jq
}
}
RowLayout {
spacing: Kirigami.Units.gridUnit
CheckBox {
id: fwupd
text: i18n("Firmware")
enabled: pkg.fwupdmgr
}
}
Item {
Kirigami.FormData.isSection: true
}
}
ColumnLayout {
visible: currentTab === 0
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
ColumnLayout {
id: feedsUrl
Layout.fillWidth: true
enabled: pkg.jq
spacing: Kirigami.Units.largeSpacing
function syncFromModel() {
var items = []
for (var i = 0; i < feedsModel.count; i++) {
items.push(feedsModel.get(i).url)
}
feeds = items
cfg_feeds = JS.serializeCustomFeeds(feeds)
}
function syncToModel() {
feedsModel.clear()
for (var i = 0; i < feeds.length; i++) {
feedsModel.append({url: feeds[i]})
}
}
function reset() {
feedsModel.clear()
var defaults = JS.parseCustomFeeds(plasmoid.configuration.feedsDefault)
for (var i = 0; i < defaults.length; i++) {
feedsModel.append({url: defaults[i]})
}
syncFromModel()
}
ListModel {
id: feedsModel
}
ListView {
id: feedsList
Layout.fillWidth: true
implicitHeight: contentHeight
model: feedsModel
headerPositioning: ListView.OverlayHeader
header: Kirigami.InlineViewHeader {
width: feedsList.width
text: i18n("RSS Feeds")
actions: [
Kirigami.Action {
icon.name: "rating"
enabled: cfg_feedsEnabled && pkg.jq
onTriggered: recommendedFeedsDialog.open()
},
Kirigami.Action {
icon.name: cfg_feedsEnabled ? "news-subscribe" : "news-unsubscribe"
text: cfg_feedsEnabled ? i18n("Enabled") : i18n("Disabled")
onTriggered: cfg_feedsEnabled = !cfg_feedsEnabled
},
Kirigami.Action {
icon.name: "edit-reset"
text: i18n("Reset")
enabled: cfg_feedsEnabled && pkg.jq
onTriggered: feedsUrl.reset()
},
Kirigami.Action {
icon.name: "list-add-symbolic"
text: i18n("Add")
enabled: cfg_feedsEnabled && pkg.jq
onTriggered: {
feedsModel.insert(0, {url: ""})
feedsUrl.syncFromModel()
}
}
]
}
delegate: RowLayout {
required property string url
required property int index
width: ListView.view.width
spacing: Kirigami.Units.smallSpacing
TextField {
Layout.fillWidth: true
text: url
placeholderText: "https://..."
font.family: "monospace"
font.pointSize: Kirigami.Theme.smallFont.pointSize
selectByMouse: true
implicitHeight: Kirigami.Units.gridUnit * 1.8
enabled: cfg_feedsEnabled
onTextChanged: {
feedsModel.setProperty(index, "url", text)
feedsUrl.syncFromModel()
}
}
Button {
icon.name: "delete-symbolic"
implicitWidth: Kirigami.Units.gridUnit * 2
implicitHeight: Kirigami.Units.gridUnit * 2
ToolTip {
text: i18n("Remove")
delay: Kirigami.Units.toolTipDelay
}
onClicked: {
feedsModel.remove(index)
feedsUrl.syncFromModel()
}
}
}
add: Transition { NumberAnimation { property: "x"; from: 80; to: 0; duration: 320; easing.type: Easing.OutCubic } }
moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 300; easing.type: Easing.OutCubic } }
move: Transition { NumberAnimation { properties: "x,y"; duration: 300; easing.type: Easing.OutCubic } }
removeDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 280; easing.type: Easing.OutCubic } }
remove: Transition { ParallelAnimation {
NumberAnimation { property: "opacity"; to: 0; duration: 220; easing.type: Easing.InQuad }
NumberAnimation { property: "x"; to: 80; duration: 300; easing.type: Easing.InCubic } } }
}
RowLayout {
enabled: pkg.jq && cfg_feedsEnabled && cfg_feeds !== ""
SpinBox {
id: feedsFetchCount
from: 1
to: 10
stepSize: 1
}
Label {
text: i18np(
"latest article from each feed",
"latest articles from each feed",
feedsFetchCount.value
)
}
}
}
Kirigami.Dialog {
id: recommendedFeedsDialog
title: i18n("Recommended RSS Feeds")
preferredWidth: Kirigami.Units.gridUnit * 25
ColumnLayout {
spacing: 0
Repeater {
model: [
{ name: "This week in Plasma", url: "https://blogs.kde.org/categories/this-week-in-plasma/index.xml" },
{ name: "This week in KDE Apps", url: "https://blogs.kde.org/categories/this-week-in-kde-apps/index.xml" },
{ name: "KDE Community", url: "https://kde.org/index.xml" },
{ name: "CachyOS's Blog", url: "https://cachyos.org/rss.xml" },
{ name: "Apdatifier Release Notes", url: "https://github.com/exequtic/apdatifier/releases.atom" }
]
delegate: Frame {
Layout.fillWidth: true
Layout.topMargin: index > 0 ? 2 : 0
RowLayout {
anchors.fill: parent
Label {
text: modelData.name
font.bold: true
Layout.fillWidth: true
}
Button {
icon.name: "list-add"
text: i18n("Add")
Layout.preferredWidth: Kirigami.Units.gridUnit * 6
onClicked: {
if (!feeds.includes(modelData.url)) {
feedsModel.insert(0, {url: modelData.url})
feedsUrl.syncFromModel()
}
}
}
}
}
}
}
}
}
Kirigami.FormLayout {
id: notificationsTab
visible: currentTab === 1
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
spacing: Kirigami.Units.largeSpacing * 2
CheckBox {
Kirigami.FormData.label: i18n("Notifications") + ":"
id: notifyUpdates
text: i18n("For new updates")
}
CheckBox {
id: notifyUpdatesAction
text: i18n("Action button")
enabled: notifyUpdates.checked
}
}
RowLayout {
CheckBox {
id: notifyEveryBump
text: i18n("For every version bump")
enabled: notifyUpdates.checked
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("If the option is <b>enabled</b>, notifications will be sent when a new version of the package is bumped, even if the package is already on the list. <b>More notifications.</b> <br><br>If the option is <b>disabled</b>, notifications will only be sent for packages that are not yet on the list. <b>Less notifications.</b>")
}
}
RowLayout {
spacing: Kirigami.Units.largeSpacing * 2
CheckBox {
id: notifyNews
text: i18n("For news")
}
CheckBox {
id: notifyNewsAction
text: i18n("Action button")
enabled: notifyNews.checked
}
}
CheckBox {
id: notifyErrors
text: i18n("For errors")
}
CheckBox {
id: notifySound
text: i18n("With sound")
enabled: notifyUpdates.checked || notifyNews.checked || notifyErrors.checked
}
CheckBox {
id: notifyPersistent
text: i18n("Persistent")
enabled: notifyUpdates.checked || notifyNews.checked || notifyErrors.checked
}
Item {
Kirigami.FormData.isSection: true
}
Kirigami.Separator {
Layout.fillWidth: true
}
RowLayout {
id: notifyTip
Label {
horizontalAlignment: Text.AlignHCenter
Layout.maximumWidth: 250
font.pointSize: instTip.font.pointSize
text: i18n("To further configure, click the button below -> Application Settings -> Apdatifier")
wrapMode: Text.WordWrap
}
}
Item {
Kirigami.FormData.isSection: true
}
Button {
anchors.horizontalCenter: notifyTip.horizontalCenter
enabled: notifyUpdates.checked || notifyNews.checked || notifyErrors.checked
icon.name: "settings-configure"
text: i18n("Configure...")
onClicked: KCMLauncher.openSystemSettings("kcm_notifications")
}
Item {
Kirigami.FormData.isSection: true
}
}
Kirigami.FormLayout {
id: mouseActionsTab
visible: currentTab === 2
Item {
Kirigami.FormData.isSection: true
}
QQC.ComboBox {
Kirigami.FormData.label: i18n("Middle click") + ":"
type: "middle"
}
QQC.ComboBox {
Kirigami.FormData.label: i18n("Right click") + ":"
type: "right"
Kirigami.ContextualHelpButton {
toolTipText: i18n("Do not enable this option if the widget is not used in the system tray; otherwise, you will not be able to open the settings by right-clicking.")
}
}
QQC.ComboBox {
Kirigami.FormData.label: i18n("Scroll up") + ":"
type: "scrollUp"
}
QQC.ComboBox {
Kirigami.FormData.label: i18n("Scroll down") + ":"
type: "scrollDown"
}
Item {
Kirigami.FormData.isSection: true
}
}
Kirigami.FormLayout {
id: miscTab
visible: currentTab === 3
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Layout.preferredWidth: miscTab.width - Kirigami.Units.largeSpacing * 10
Button {
Layout.fillWidth: true
Layout.maximumWidth: 500
icon.name: "backup"
text: i18n("Restore hidden tooltips")
onClicked: {
plasmoid.configuration.configMsg = true
plasmoid.configuration.newsMsg = true
plasmoid.configuration.version = "v0"
}
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Layout.preferredWidth: miscTab.width - Kirigami.Units.largeSpacing * 10
Button {
enabled: !systemWide
Layout.fillWidth: true
Layout.maximumWidth: 500
icon.name: "folder-git-symbolic"
text: i18n("Install Development version")
onClicked: {
installButton = 0
installDialog.open()
}
}
}
RowLayout {
Layout.preferredWidth: miscTab.width - Kirigami.Units.largeSpacing * 10
Button {
enabled: !systemWide
Layout.fillWidth: true
Layout.maximumWidth: 500
icon.name: "run-build"
text: i18n("Install Stable version")
onClicked: {
installButton = 1
installDialog.open()
}
}
}
RowLayout {
Layout.preferredWidth: miscTab.width - Kirigami.Units.largeSpacing * 10
Button {
enabled: !systemWide
Layout.fillWidth: true
Layout.maximumWidth: 500
icon.name: "delete"
text: i18n("Uninstall widget")
onClicked: {
installButton = 2
installDialog.open()
}
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Layout.preferredWidth: miscTab.width - Kirigami.Units.largeSpacing * 10
Label {
text: "pacman DBPath:"
}
TextArea {
id: dbPath
Layout.fillWidth: true
Layout.maximumWidth: 320
readOnly: true
Component.onCompleted: dbPath.text = plasmoid.configuration.dbPath
}
Button {
icon.name: "document-open"
onClicked: folderDialog.open()
}
Button {
icon.name: "edit-reset"
ToolTip.delay: 0
ToolTip.visible: hovered
ToolTip.text: i18n("Reset to default. By default, the path is the same as for checkupdates.")
onClicked: {
dbPath.text = plasmoid.configuration.dbPathDefault
pathError.visible = false
}
}
FolderDialog {
id: folderDialog
onAccepted: {
const path = selectedFolder.toString().substring(7)
if (path.includes(' ')) {
pathError.visible = true
} else {
dbPath.text = path
pathError.visible = false
}
}
}
}
Label {
id: pathError
text: i18n("Path must not contain spaces")
color: Kirigami.Theme.negativeTextColor
visible: false
}
Kirigami.PromptDialog {
id: installDialog
title: dialogTitles[installButton]
subtitle: dialogSubtitles[installButton]
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
onAccepted: {
if (installButton === 0) {
JS.execute(JS.runInTerminal("utils", "installDev"))
} else if (installButton === 1) {
JS.execute(JS.runInTerminal("utils", "installStable"))
} else {
JS.execute(JS.runInTerminal("utils", "uninstall"))
}
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Network")
Kirigami.FormData.isSection: true
}
CheckBox {
id: checkConn
text: i18n("Connectivity check")
}
CheckBox {
id: respectMeteredConn
text: i18n("Respect metered connection")
}
}
}
}
@@ -0,0 +1,247 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.iconthemes
import org.kde.kirigami as Kirigami
import "../../tools/tools.js" as JS
ColumnLayout {
ListModel {
id: rulesModel
Component.onCompleted: {
JS.execute(JS.readFile(JS.rulesFile), (cmd, out, err, code) => {
if (JS.Error(code, err)) return
if (out && JS.validJSON(out, JS.rulesFile)) {
JSON.parse(out).forEach(el => {
if (el.type !== "name" && el.type !== "regex") return
rulesModel.append({
type: el.type,
value: el.value,
icon: el.icon,
excluded: el.excluded,
ignore: ('ignore' in el) ? el.ignore : false,
important: ('important' in el) ? el.important : false
})
})
}
})
}
}
ListModel {
id: typesModel
Component.onCompleted: {
let types = [
{name: i18n("Name"), type: "name", tip: i18n("Exact package name")},
{name: i18n("Regex"), type: "regex", tip: i18n("See Help")}
]
for (var i = 0; i < types.length; ++i) {
typesModel.append({name: types[i].name, type: types[i].type, tip: types[i].tip})
}
}
}
Component {
id: rule
Item {
id: ruleItem
required property int index
required property var model
width: rulesList.width - Kirigami.Units.largeSpacing * 2
height: swipeListItem.height
Kirigami.SwipeListItem {
id: swipeListItem
width: ruleItem.width
down: false
hoverEnabled: true
separatorVisible: true
contentItem: RowLayout {
Kirigami.ListItemDragHandle {
listItem: swipeListItem
listView: rulesList
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.minimumWidth: Kirigami.Units.iconSizes.small
Layout.maximumWidth: Kirigami.Units.iconSizes.small
onMoveRequested: {
var to = Math.max(0, Math.min(newIndex, rulesModel.count - 1))
rulesModel.move(oldIndex, to, 1)
}
}
ComboBox {
implicitWidth: 120
id: type
model: typesModel
textRole: "name"
currentIndex: -1
property bool ready: false
onCurrentIndexChanged: {
if (!ready) return
rulesList.model.set(index, {
"type": model.get(currentIndex).type,
"value": "",
"ignore": false
})
}
Component.onCompleted: {
var currentType = rulesList.model.get(index).type
for (var i = 0; i < model.count; ++i) {
if (model.get(i).type === currentType) {
currentIndex = i
break
}
}
ready = true
}
}
TextField {
id: valueField
Layout.fillWidth: true
text: model.value
placeholderText: type.model.get(type.currentIndex).tip
enabled: true
onTextChanged: {
if (type.model.get(type.currentIndex).type === "name") {
var filtered = valueField.text.replace(/[^a-z0-9_\-+.]/g, "")
if (valueField.text !== filtered) {
valueField.text = filtered
return
}
}
if (model.value !== valueField.text) {
model.value = valueField.text
}
}
}
ToolButton {
ToolTip { text: model.icon }
icon.name: model.icon
onClicked: iconDialog.open()
IconDialog {
id: iconDialog
onIconNameChanged: model.icon = iconName
}
}
ToolButton {
ToolTip { text: i18n("Mark as important") }
icon.name: model.important ? "flag-red" : "flag"
checkable: true
checked: model.important
onClicked: model.important = !model.important
}
ToolButton {
ToolTip { text: model.excluded ? i18n("Show in the list") : i18n("Exclude from the list") }
icon.name: model.excluded ? "hint" : "view-visible"
checkable: true
checked: model.excluded
onClicked: model.excluded = !model.excluded
}
ToolButton {
ToolTip {
text: i18n("Ignore a package upgrade. <b>Be careful in skipping packages, since partial upgrades are unsupported.</b>")
}
icon.name: model.ignore ? "process-stop" : "software-updates-updates"
enabled: type.model.get(type.currentIndex).type === "name"
checkable: true
checked: model.ignore
opacity: enabled ? 1 : 0.5
onClicked: model.ignore = !model.ignore
}
ToolButton {
ToolTip { text: i18n("Remove") }
icon.name: 'delete'
onClicked: rulesList.model.remove(index)
}
}
}
}
}
Kirigami.InlineViewHeader {
Layout.fillWidth: true
text: i18n("Rules")
actions: [
Kirigami.Action {
icon.name: "help-about-symbolic"
text: i18n("Help")
checkable: true
onTriggered: rulesMsg.visible = checked
},
Kirigami.Action {
icon.name: "list-add-symbolic"
text: i18n("Add")
onTriggered: rulesModel.append({
type: "name", value: "",
icon: plasmoid.configuration.ownIconsUI ? "apdatifier-package" : "server-database",
excluded: false, important: false, ignore: false
})
},
Kirigami.Action {
icon.name: "dialog-ok-apply"
text: i18n("Apply")
onTriggered: {
var array = []
for (var i = 0; i < rulesModel.count; i++) {
var item = rulesModel.get(i)
if (item.value.trim() !== "")
array.push({
type: item.type, value: item.value, icon: item.icon,
excluded: item.excluded, important: item.important, ignore: item.ignore
})
}
var rules = JS.toFileFormat(array)
plasmoid.configuration.rules = rules
JS.execute(JS.writeFile(rules, '>', JS.rulesFile))
rulesModel.clear()
for (var i = 0; i < array.length; i++)
rulesModel.append(array[i])
}
}
]
}
Kirigami.InlineMessage {
id: rulesMsg
Layout.fillWidth: true
icon.source: "showinfo"
text: i18n("Rules apply top-to-bottom: later rules override earlier ones.<br><br>Regex examples:<br><b>^nvidia</b> — names starting with nvidia<br><b>wayland$</b> — names ending with wayland<br><b>.*qt.*</b> — names containing qt<br><b>(ttf|font|otf)</b> — any of ttf, font, or otf<br><b>linux(-zen|-lts)?$</b> — linux, linux-zen or linux-lts")
visible: false
}
ListView {
Layout.fillHeight: true
Layout.fillWidth: true
id: rulesList
model: rulesModel
delegate: rule
clip: true
boundsBehavior: Flickable.StopAtBounds
reuseItems: true
moveDisplaced: Transition {
YAnimator { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
}
ScrollBar.vertical: ScrollBar { active: true }
Label {
anchors.centerIn: parent
visible: rulesModel.count === 0
text: i18n("No rules yet")
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.3
opacity: 0.6
}
}
}
@@ -0,0 +1,99 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.kirigami as Kirigami
Kirigami.Page {
id: supportPage
leftPadding: Kirigami.Units.gridUnit
rightPadding: Kirigami.Units.gridUnit
header: Item {
height: layout.implicitHeight + (Kirigami.Units.gridUnit * 2)
ColumnLayout {
id: layout
width: parent.width - (Kirigami.Units.gridUnit * 2)
anchors.centerIn: parent
Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: i18n("Thanks for using my widget! If you appreciate my work, you can support me by starring the GitHub repository or buying me a coffee ;)")
font.bold: true
wrapMode: Text.WordWrap
}
}
}
Menu {
id: menu
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
MenuItem {
text: "buymeacoffee.com"
icon.name: "internet-web-browser-symbolic"
onTriggered: Qt.openUrlExternally("https://buymeacoffee.com/evgk")
}
MenuItem {
text: "nowpayments.io (crypto)"
icon.name: "internet-web-browser-symbolic"
onTriggered: Qt.openUrlExternally("https://nowpayments.io/donation/exequtic")
}
}
RowLayout {
anchors.centerIn: parent
Layout.fillWidth: true
Layout.fillHeight: true
Image {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
source: "../assets/art/apdatifier-donate.png"
sourceSize.width: supportPage.width / 2
sourceSize.height: supportPage.height
HoverHandler {
id: handlerDonate
cursorShape: menu.opened ? Qt.ArrowCursor : Qt.PointingHandCursor
}
TapHandler {
onTapped: (event) => menu.opened ? menu.close() : menu.popup(event.position.x, event.position.y)
}
Label {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
text: "Donate"
font.bold: true
visible: handlerDonate.hovered && !menu.opened
}
}
Image {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
source: "../assets/art/apdatifier-githubstar.png"
sourceSize.width: supportPage.width / 2
sourceSize.height: supportPage.height
HoverHandler {
id: handlerGithub
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: Qt.openUrlExternally("https://github.com/exequtic/apdatifier")
}
Label {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
font.bold: true
visible: handlerGithub.hovered
text: "https://github.com"
}
}
}
}
@@ -0,0 +1,766 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Dialogs
import org.kde.kcmutils
import org.kde.kirigami as Kirigami
import "../../tools/tools.js" as JS
SimpleKCM {
property string cfg_terminal: plasmoid.configuration.terminal
property alias cfg_tmuxSession: tmuxSession.checked
property alias cfg_idleInhibit: idleInhibit.checked
property alias cfg_termFont: termFont.checked
property alias cfg_mngHideFlatpak: mngHideFlatpak.checked
property alias cfg_mngHideHeaders: mngHideHeaders.checked
property alias cfg_mngHideHelp: mngHideHelp.checked
property alias cfg_mngHideNums: mngHideNums.checked
property alias cfg_fzfOptions: fzfOptions.text
property string cfg_wrapper: plasmoid.configuration.wrapper
property alias cfg_archFlags: archFlags.text
property alias cfg_sudoBin: sudoBin.text
property alias cfg_rebootSystem: rebootSystem.checked
property string cfg_mirrors: plasmoid.configuration.mirrors
property bool mirrorsGeneratorEnabled: cfg_mirrors !== "false"
property alias cfg_mirrorsAge: mirrorsAge.value
property alias cfg_mirrorCount: mirrorCount.value
property alias cfg_mirrorProgress: mirrorProgress.checked
property var countryList: []
property string cfg_dynamicUrl: plasmoid.configuration.dynamicUrl
property alias cfg_flatpakRemoveUnused: flatpakRemoveUnused.checked
property string cfg_flatpakFlags: plasmoid.configuration.flatpakFlags
property alias cfg_widgetConfirmation: widgetConfirmation.checked
property alias cfg_restartShell: restartShell.checked
property alias cfg_restartCommand: restartCommand.text
property alias cfg_trayEnabledByDefault: trayEnabledByDefault.checked
property var pkg: plasmoid.configuration.packages
property var terminals: plasmoid.configuration.terminals
property alias cfg_preExec: preExec.text
property alias cfg_postExec: postExec.text
property int currentTab
signal tabChanged(currentTab: int)
onCurrentTabChanged: tabChanged(currentTab)
header: Kirigami.NavigationTabBar {
actions: [
Kirigami.Action {
icon.name: "akonadiconsole"
text: i18n("General")
checked: currentTab === 0
onTriggered: currentTab = 0
},
Kirigami.Action {
icon.name: "apdatifier-package"
text: "Arch"
checked: currentTab === 1
onTriggered: currentTab = 1
},
Kirigami.Action {
icon.name: "apdatifier-flatpak"
text: "Flatpak"
checked: currentTab === 2
onTriggered: currentTab = 2
},
Kirigami.Action {
icon.name: "start-here-kde-plasma-symbolic"
text: i18n("Widgets")
checked: currentTab === 3
onTriggered: currentTab = 3
}
]
}
Kirigami.FormLayout {
id: terminalTab
visible: currentTab === 0
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Terminal") + ":"
ComboBox {
model: terminals
textRole: "name"
enabled: terminals
onCurrentIndexChanged: cfg_terminal = model[currentIndex]["value"]
Component.onCompleted: {
if (terminals) {
currentIndex = JS.setIndex(plasmoid.configuration.terminal, terminals)
if (!plasmoid.configuration.terminal) {
plasmoid.configuration.terminal = model[0]["value"]
}
}
}
}
Kirigami.UrlButton {
url: "https://github.com/exequtic/apdatifier#supported-terminals"
text: i18n("Not installed")
font.pointSize: Kirigami.Theme.smallFont.pointSize
color: Kirigami.Theme.neutralTextColor
visible: !terminals
}
}
RowLayout {
CheckBox {
id: termFont
text: i18n("Use NerdFont icons")
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("If your terminal utilizes any <b>Nerd Font</b>, icons from that font will be used.")
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
CheckBox {
id: tmuxSession
text: i18n("tmux session")
enabled: pkg.tmux
}
}
RowLayout {
CheckBox {
id: idleInhibit
text: i18n("Idle Inhibit")
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Disables automatic sleep and screen lock while upgrading.")
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Pre/post upgrade scripts")
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Pre-exec") + ":"
TextField {
id: preExec
placeholderText: i18n("Command or script path")
placeholderTextColor: "grey"
}
Button {
icon.name: "document-open"
onClicked: fileDialogPreExec.open()
}
FileDialog {
id: fileDialogPreExec
fileMode: FileDialog.OpenFile
onAccepted: preExec.text = selectedFile.toString().substring(7)
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Running your command or script BEFORE the upgrade.<br>For example, you can specify your command to update the mirrorlist if you have unofficial repositories.")
}
}
RowLayout {
Kirigami.FormData.label: i18n("Post-exec") + ":"
TextField {
id: postExec
placeholderText: i18n("Command or script path")
placeholderTextColor: "grey"
}
Button {
icon.name: "document-open"
onClicked: fileDialogPostExec.open()
}
FileDialog {
id: fileDialogPostExec
fileMode: FileDialog.OpenFile
onAccepted: postExec.text = selectedFile.toString().substring(7)
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Running your command or script AFTER the upgrade.<br>For example, you can specify your command to upgrade something else.")
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Management")
Kirigami.FormData.isSection: true
}
CheckBox {
id: mngHideFlatpak
text: i18n("Without Flatpak options")
}
CheckBox {
id: mngHideHeaders
text: i18n("Hide headers")
}
CheckBox {
id: mngHideHelp
text: i18n("Hide help")
}
CheckBox {
id: mngHideNums
text: i18n("Hide option numbers")
}
RowLayout {
Kirigami.FormData.label: i18n("Fzf options") + ":"
TextField {
id: fzfOptions
placeholderText: "--style full --layout default"
placeholderTextColor: "grey"
Layout.preferredWidth: 250
onTextChanged: {
var sanitized = fzfOptions.text.replace(/["'\\$`\n\r]/g, "")
if (sanitized !== fzfOptions.text)
fzfOptions.text = sanitized
}
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Override default fzf options.<br>See <b>fzf --help</b> for available options.")
}
}
}
Kirigami.FormLayout {
id: archTab
visible: currentTab === 1
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
enabled: plasmoid.configuration.aur
Kirigami.FormData.label: i18n("Wrapper") + ":"
spacing: Kirigami.Units.largeSpacing * 2
ButtonGroup {
id: wrappersGroup
}
RadioButton {
ButtonGroup.group: wrappersGroup
text: "paru"
enabled: pkg.paru
checked: plasmoid.configuration.wrapper === text
onCheckedChanged: if (checked) cfg_wrapper = text
}
RadioButton {
ButtonGroup.group: wrappersGroup
text: "yay"
enabled: pkg.yay
checked: plasmoid.configuration.wrapper === text
onCheckedChanged: if (checked) cfg_wrapper = text
}
RadioButton {
ButtonGroup.group: wrappersGroup
text: "pikaur"
enabled: pkg.pikaur
checked: plasmoid.configuration.wrapper === text
onCheckedChanged: if (checked) cfg_wrapper = text
}
}
RowLayout {
visible: !plasmoid.configuration.aur
Label {
horizontalAlignment: Text.AlignHCenter
font.pointSize: Kirigami.Theme.smallFont.pointSize
font.bold: true
color: Kirigami.Theme.negativeTextColor
text: i18n("AUR disabled in search settings")
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Upgrade options") + ":"
Label {
text: plasmoid.configuration.aur ? cfg_wrapper+" "+"-Syu" : cfg_sudoBin+" "+"pacman -Syu"
}
TextField {
id: archFlags
onTextChanged: {
var allow = /^[a-z0-9\- ]*$/
if (!allow.test(archFlags.text))
archFlags.text = archFlags.text.replace(/[^a-z0-9\- ]/g, "")
}
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: "sudobin:"
spacing: 0
enabled: pkg.pacman
TextField {
id: sudoBin
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Reboot system") + ":"
CheckBox {
id: rebootSystem
text: i18n("Suggest after upgrading")
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("This option suggests restarting the system after upgrading critical packages.<br><br><b>Note that not all critical packages require a full system restart; some may only need a session restart or no action at all, such as when an updated package is not currently running (e.g., an alternative kernel) or not in use (e.g., an alternative driver).</b>")
}
}
Kirigami.Separator {
Kirigami.FormData.label: i18n("Pacman Mirrorlist Generator")
Kirigami.FormData.isSection: true
}
RowLayout {
height: 30
Label {
horizontalAlignment: Text.AlignHCenter
font.pointSize: Kirigami.Theme.smallFont.pointSize
font.bold: true
color: Kirigami.Theme.negativeTextColor
text: i18n("Only for official repositories")+"!\n"+" (core, extra, multilib, ...)"
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Also see https://archlinux.org/mirrorlist (click button to open link)")
onClicked: Qt.openUrlExternally("https://archlinux.org/mirrorlist")
}
}
Item {
Layout.preferredHeight: Kirigami.Units.smallSpacing * 2
}
ButtonGroup {
id: generator
}
RowLayout{
Kirigami.FormData.label: i18n("Generator") + ":"
RadioButton {
ButtonGroup.group: generator
id: mirrors
text: i18n("Disabled")
enabled: pkg.pacman
checked: {
plasmoid.configuration.mirrors === "false"
}
onCheckedChanged: {
if (checked) cfg_mirrors = "false"
}
Component.onCompleted: {
if (!checked && !enabled) {
checked = true
plasmoid.configuration.mirrors = "false"
}
}
}
}
RadioButton {
ButtonGroup.group: generator
text: i18n("Always ask")
checked: plasmoid.configuration.mirrors === "alwaysAsk"
onCheckedChanged: {
if (checked) cfg_mirrors = "alwaysAsk"
}
}
RowLayout{
RadioButton {
ButtonGroup.group: generator
text: i18n("Ask if older than")
checked: plasmoid.configuration.mirrors === "age"
onCheckedChanged: {
if (checked) cfg_mirrors = "age"
}
}
SpinBox {
id: mirrorsAge
from: 1
to: 999
stepSize: 1
value: mirrorsAge
}
Label {
text: i18np("day", "days", mirrorsAge.value)
}
}
RadioButton {
ButtonGroup.group: generator
text: i18n("No ask, force refresh")
checked: plasmoid.configuration.mirrors === "force"
onCheckedChanged: {
if (checked) cfg_mirrors = "force"
}
}
Item {
Layout.preferredHeight: Kirigami.Units.smallSpacing * 2
}
RowLayout {
Kirigami.FormData.label: i18n("Protocol") + ":"
CheckBox {
id: http
text: "http"
onClicked: updateUrl()
enabled: mirrorsGeneratorEnabled
}
CheckBox {
id: https
text: "https"
onClicked: updateUrl()
enabled: mirrorsGeneratorEnabled
}
}
RowLayout {
Kirigami.FormData.label: i18n("IP version") + ":"
CheckBox {
id: ipv4
text: "IPv4"
onClicked: updateUrl()
enabled: mirrorsGeneratorEnabled
}
CheckBox {
id: ipv6
text: "IPv6"
onClicked: updateUrl()
enabled: mirrorsGeneratorEnabled
}
}
CheckBox {
Kirigami.FormData.label: i18n("Mirror status") + ":"
id: mirrorstatus
text: i18n("Enable")
onClicked: updateUrl()
enabled: mirrorsGeneratorEnabled
}
RowLayout {
Kirigami.FormData.label: i18n("Number output") + ":"
SpinBox {
id: mirrorCount
from: 0
to: 10
stepSize: 1
value: mirrorCount
enabled: mirrorsGeneratorEnabled
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("Number of servers to write to mirrorlist file. 0 for all.")
}
}
CheckBox {
Kirigami.FormData.label: i18n("Show progress") + ":"
id: mirrorProgress
text: i18n("Enable")
enabled: mirrorsGeneratorEnabled
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Country") + ":"
enabled: mirrorsGeneratorEnabled
Label {
textFormat: Text.RichText
text: {
var matchResult = cfg_dynamicUrl.match(/country=([A-Z]+)/g)
if (matchResult !== null) {
var countries = matchResult.map(str => str.split("=")[1]).join(", ")
return countries
} else {
return '<a style="color: ' + Kirigami.Theme.negativeTextColor + '">' + i18n("Select at least one!") + '</a>'
}
}
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("You must select at least one country, otherwise all will be chosen by default. <br><br><b>The more countries you select, the longer it will take to generate the mirrors!</b> <br><br>It is optimal to choose <b>1-2</b> countries closest to you.")
}
}
ColumnLayout {
Layout.maximumWidth: archTab.width / 2.5
Layout.maximumHeight: 200
enabled: mirrorsGeneratorEnabled
ScrollView {
Layout.preferredWidth: archTab.width / 2.5
Layout.preferredHeight: 200
GridLayout {
columns: 1
Repeater {
model: countryListModel
delegate: CheckBox {
text: model.text
checked: model.checked
onClicked: {
model.checked = checked
checked ? countryList.push(model.code) : countryList.splice(countryList.indexOf(model.code), 1)
updateUrl()
}
}
}
}
}
}
Item {
Kirigami.FormData.isSection: true
}
}
Kirigami.FormLayout {
id: flatpakTab
visible: currentTab === 2
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Uninstall unused") + ":"
CheckBox {
id: flatpakRemoveUnused
text: i18n("Enable")
}
}
Item {
Kirigami.FormData.isSection: true
}
ButtonGroup {
id: flatpakFlags
}
RowLayout{
Kirigami.FormData.label: i18n("Upgrade options") + ":"
RadioButton {
ButtonGroup.group: flatpakFlags
text: i18n("Normal")
checked: plasmoid.configuration.flatpakFlags === ""
onCheckedChanged: {
if (checked) cfg_flatpakFlags = ""
}
}
}
RadioButton {
ButtonGroup.group: flatpakFlags
text: i18n("Normal, skip questions")
checked: plasmoid.configuration.flatpakFlags === "--assumeyes"
onCheckedChanged: {
if (checked) cfg_flatpakFlags = "--assumeyes"
}
}
RadioButton {
ButtonGroup.group: flatpakFlags
text: i18n("Non interactive, skip questions")
checked: plasmoid.configuration.flatpakFlags === "--noninteractive"
onCheckedChanged: {
if (checked) cfg_flatpakFlags = "--noninteractive"
}
}
RadioButton {
ButtonGroup.group: flatpakFlags
text: i18n("Verbose")
checked: plasmoid.configuration.flatpakFlags === "--verbose"
onCheckedChanged: {
if (checked) cfg_flatpakFlags = "--verbose"
}
}
}
Kirigami.FormLayout {
id: widgetsTab
visible: currentTab === 3
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Upgrade confirmation") + ":"
CheckBox {
id: widgetConfirmation
text: i18n("Enable")
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Restart plasmashell") + ":"
CheckBox {
id: restartShell
text: i18n("Suggest after upgrading")
}
Kirigami.ContextualHelpButton {
toolTipText: i18n("After upgrading widget, the old version will still remain in memory until you restart plasmashell. To avoid doing this manually, enable this option.")
}
}
RowLayout {
Kirigami.FormData.label: i18n("Command") + ":"
TextField {
id: restartCommand
enabled: restartShell.checked
}
}
Item {
Kirigami.FormData.isSection: true
}
RowLayout {
Kirigami.FormData.label: i18n("Apdatifier tray icon") + ":"
CheckBox {
id: trayEnabledByDefault
text: i18n("Enabled by default")
}
}
}
Component.onCompleted: {
if (tmuxSession.checked && !pkg.tmux) tmuxSession.checked = plasmoid.configuration.tmuxSession = false
if(cfg_dynamicUrl) {
var urlParams = plasmoid.configuration.dynamicUrl.split("?")[1].split("&")
for (var i = 0; i < urlParams.length; i++) {
var param = urlParams[i]
if (param.includes("use_mirror_status=on")) mirrorstatus.checked = true
if (/protocol=http\b/.test(param)) http.checked = true
if (param.includes("protocol=https")) https.checked = true
if (param.includes("ip_version=4")) ipv4.checked = true
if (param.includes("ip_version=6")) ipv6.checked = true
if (param.includes("country=")) {
var country = decodeURIComponent(param.split("=")[1])
countryList.push(country)
for (var j = 0; j < countryListModel.count; ++j) {
if (countryListModel.get(j).code === country) {
countryListModel.get(j).checked = true
}
}
}
}
}
}
function updateUrl() {
var params = ""
if (http.checked) params += "&protocol=http"
if (https.checked) params += "&protocol=https"
if (ipv4.checked) params += "&ip_version=4"
if (ipv6.checked) params += "&ip_version=6"
if (mirrorstatus.checked) params += "&use_mirror_status=on"
for (var i = 0; i < countryList.length; i++) {
params += "&country=" + countryList[i]
}
var baseUrl = "https://archlinux.org/mirrorlist/?"
cfg_dynamicUrl = baseUrl + params.substring(1)
}
ListModel {
id: countryListModel
function createCountryList() {
let countries =
"Australia:AU, Austria:AT, Azerbaijan:AZ, Bangladesh:BD, Belarus:BY, Belgium:BE, " +
"Brazil:BR, Bulgaria:BG, Cambodia:KH, Canada:CA, Chile:CL, China:CN, Colombia:CO, " +
"Croatia:HR, Czech Republic:CZ, Denmark:DK, Ecuador:EC, Estonia:EE, Finland:FI, " +
"France:FR, Georgia:GE, Germany:DE, Greece:GR, Hong Kong:HK, Hungary:HU, Iceland:IS, " +
"India:IN, Indonesia:ID, Iran:IR, Israel:IL, Italy:IT, Japan:JP, Kazakhstan:KZ, Kenya:KE, " +
"Latvia:LV, Lithuania:LT, Luxembourg:LU, Malaysia:MY, Mauritius:MU, Mexico:MX, Moldova:MD, " +
"Morocco:MA, Nepal:NP, Netherlands:NL, New Caledonia:NC, New Zealand:NZ, North Macedonia:MK, " +
"Norway:NO, Paraguay:PY, Poland:PL, Portugal:PT, Romania:RO, Russia:RU, Réunion:RE, " +
"Saudi Arabia:SA, Serbia:RS, Singapore:SG, Slovakia:SK, Slovenia:SI, South Africa:ZA, " +
"South Korea:KR, Spain:ES, Sweden:SE, Switzerland:CH, Taiwan:TW, Thailand:TH, Turkey:TR, " +
"Ukraine:UA, United Arab Emirates:AE, United Kingdom:GB, United States:US, Uzbekistan:UZ, Vietnam:VN"
countries.split(", ").map(item => {
let [country, code] = item.split(":")
countryListModel.append({text: country, code: code, checked: false})
})
}
Component.onCompleted: createCountryList()
}
}
@@ -0,0 +1,144 @@
import QtQuick
import QtQuick.Layouts
import QtNetwork
import org.kde.plasma.plasmoid
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import "components"
import "representation" as Rep
import "../tools/tools.js" as JS
PlasmoidItem {
id: root
compactRepresentation: Rep.Panel {}
fullRepresentation: Rep.Expanded {
Layout.minimumWidth: Kirigami.Units.gridUnit * 24
Layout.minimumHeight: Kirigami.Units.gridUnit * 16
anchors.fill: parent
focus: true
}
switchWidth: Kirigami.Units.gridUnit * 24
switchHeight: Kirigami.Units.gridUnit * 16
Plasmoid.busy: plasmoid.location === PlasmaCore.Types.Floating ? false : ((cfg.busyIndicator || "spinner") === "spinner" ? sts.busy : false)
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground
Plasmoid.icon: plasmoid.configuration.selectedIcon
function updatePlasmoidStatus() {
if (sts.count >= cfg.hideIconPolicy || sts.busy || sts.error || panelConfigurationMode) {
Plasmoid.status = PlasmaCore.Types.ActiveStatus
} else {
Plasmoid.status = inTray ? PlasmaCore.Types.PassiveStatus : PlasmaCore.Types.HiddenStatus
}
}
property int hideIconPolicy: cfg.hideIconPolicy
property int countUpdates: sts.count
onHideIconPolicyChanged: updatePlasmoidStatus()
onCountUpdatesChanged: updatePlasmoidStatus()
toolTipMainText: sts.paused ? i18n("Auto check disabled") : ""
toolTipSubText: sts.busy ? sts.statusMsg : sts.checkTimeTooltip
hideOnWindowDeactivate: !pinned
property bool isOnline: cfg.checkConn ? (NetworkInformation.reachability === NetworkInformation.Reachability.Online) : true
property bool isMetered: NetworkInformation.isMetered && cfg.respectMeteredConn
property bool inTray: (plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
property bool onDesktop: plasmoid.location === PlasmaCore.Types.Floating
property bool horizontal: plasmoid.location === PlasmaCore.Types.TopEdge || plasmoid.location === PlasmaCore.Types.BottomEdge
property bool panelConfigurationMode: Plasmoid.containment.corona?.editMode ?? false
property bool pinned: false
property bool listCompactMode: false
property var cache: []
property string checkMode: plasmoid.configuration.checkMode
property bool sorting: plasmoid.configuration.sorting
property string rules: plasmoid.configuration.rules || ""
property var pkg: plasmoid.configuration.packages || ""
property var cfg: plasmoid.configuration
property var configuration: JSON.stringify(cfg)
QtObject {
id: sts
property bool init: false
property var errors: []
property int count: 0
property double lastCheck: 0
property bool busy: true
property bool upgrading: false
property bool error: !busy && errors.length > 0
property bool paused: !busy && !scheduler.running && cfg.checkMode !== "manual"
property string statusMsg: ""
property string statusIco: ""
property string checkTimeTooltip: ""
property var proc: null
}
ListModel {
id: listModel
}
ListModel {
id: newsModel
}
ListModel {
id: activeNewsModel
}
Notification {
id: notify
}
Plasmoid.contextualActions: [
PlasmaCore.Action {
text: i18n("Check updates")
icon.name: "view-refresh"
enabled: !sts.upgrading
onTriggered: sts.proc ? JS.stopCheck() : JS.checkUpdates()
},
PlasmaCore.Action {
text: i18n("Upgrade system")
icon.name: "akonadiconsole"
enabled: (cfg.terminal && cfg.tmuxSession && sts.count) || (cfg.terminal && !sts.busy && sts.count)
onTriggered: JS.upgradeSystem()
},
PlasmaCore.Action {
text: i18n("Management")
icon.name: "tools"
enabled: cfg.terminal && pkg.pacman
onTriggered: JS.management()
}
]
Timer {
id: scheduler
interval: 10 * 1000
repeat: true
triggeredOnStart: true
onTriggered: JS.searchScheduler()
}
Timer {
id: upgradeTimer
interval: 1000
repeat: true
onTriggered: JS.upgradingState()
}
Timer {
id: saveTimer
interval: 1000
onTriggered: JS.saveConfig()
}
onIsOnlineChanged: (!isOnline && sts.proc) && JS.stopCheck()
onCheckModeChanged: sts.init && scheduler.restart()
onSortingChanged: sts.init && JS.refreshListModel()
onRulesChanged: sts.init && JS.refreshListModel()
onConfigurationChanged: saveTimer.start()
Component.onCompleted: JS.init()
}
@@ -0,0 +1,396 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kitemmodels
import org.kde.plasma.extras
import org.kde.plasma.plasmoid
import org.kde.plasma.components
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import "../components"
import "../scrollview" as View
import "../../tools/tools.js" as JS
Representation {
property string currVersion: "v2.9.9"
property bool searchFieldOpen: false
property bool expanded: root.expanded
onExpandedChanged: {
if (plasmoid.configuration.switchDefaultTab && !expanded)
listCompactMode = (plasmoid.configuration.defaultTab !== 0)
}
function svg(icon) {
return Qt.resolvedUrl("../assets/icons/" + icon + ".svg")
}
property var backgroundHidden: (Plasmoid.formFactor === PlasmaCore.Types.Planar) && (Plasmoid.userBackgroundHints === PlasmaCore.Types.ShadowBackground)
onBackgroundHiddenChanged: topHeader.background.visible = bottomHeader.background.visible = !backgroundHidden
property bool activeNewsItems: false
function checkActiveNewsItems() {
activeNewsItems = false
for (let i = 0; i < newsModel.count; ++i) {
if (newsModel.get(i).removed === false) {
activeNewsItems = true
return
}
}
}
Connections {
target: newsModel
function onDataChanged() {
checkActiveNewsItems()
}
function onCountChanged() {
checkActiveNewsItems()
}
}
Component.onCompleted: checkActiveNewsItems()
header: PlasmoidHeading {
id: topHeader
visible: (cfg.showStatusText || cfg.showToolBar) && !sts.error
contentItem: RowLayout {
id: toolBar
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.iconSizes.medium
Layout.maximumHeight: Kirigami.Units.iconSizes.medium
RowLayout {
id: status
Layout.alignment: cfg.showToolBar ? Qt.AlignLeft : Qt.AlignHCenter
spacing: Kirigami.Units.smallSpacing / 2
visible: cfg.showStatusText
Item {
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
Kirigami.Icon {
height: parent.height
width: parent.height
anchors.centerIn: parent
source: cfg.ownIconsUI ? svg(sts.statusIco) : sts.statusIco
color: Kirigami.Theme.colorSet
scale: cfg.ownIconsUI ? 0.7 : 0.9
isMask: cfg.ownIconsUI
smooth: true
}
}
Label {
Layout.maximumWidth: toolBar.width - toolBarButtons.width - Kirigami.Units.iconSizes.smallMedium
Layout.alignment: Qt.AlignLeft
text: sts.statusMsg
elide: Text.ElideRight
font.bold: true
}
}
RowLayout {
id: toolBarButtons
Layout.alignment: Qt.AlignRight
spacing: Kirigami.Units.smallSpacing
visible: cfg.showToolBar
ToolbarButton {
id: searchButton
tooltipText: i18n("Filter by package name")
iconSource: cfg.ownIconsUI ? svg("toolbar_search") : "search"
visible: cfg.searchButton && sts.count
enabled: visible && swipeView.currentIndex === 0
onClicked: {
if (searchFieldOpen) searchField.text = ""
searchFieldOpen = !searchField.visible
searchField.focus = searchFieldOpen
}
}
ToolbarButton {
tooltipText: sts.paused ? i18n("Enable auto search updates") : i18n("Disable auto search updates")
iconSource: cfg.ownIconsUI ? (!sts.paused ? svg("toolbar_pause") : svg("toolbar_start"))
: (!sts.paused ? "media-playback-paused" : "media-playback-playing")
iconColor: sts.paused ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.colorSet
enabled: !sts.busy
visible: enabled && cfg.intervalButton && cfg.checkMode !== "manual"
onClicked: JS.switchScheduler()
}
ToolbarButton {
tooltipText: cfg.sorting ? i18n("Sort packages by name") : i18n("Sort packages by repository")
iconSource: cfg.ownIconsUI ? svg("toolbar_sort") : "sort-name"
visible: cfg.sortButton && !sts.busy && sts.count
enabled: visible && swipeView.currentIndex === 0
onClicked: cfg.sorting = !cfg.sorting
}
ToolbarButton {
tooltipText: listCompactMode ? i18n("Extended") : i18n("Compact")
iconSource: cfg.ownIconsUI ? (listCompactMode ? svg("tab_extended") : svg("tab_compact"))
: (listCompactMode ? "view-split-top-bottom" : "view-split-left-right")
visible: cfg.viewButton && sts.count && swipeView.currentIndex === 0
enabled: visible
onClicked: listCompactMode = !listCompactMode
}
ToolbarButton {
id: managementButton
tooltipText: i18n("Management")
iconSource: cfg.ownIconsUI ? svg("toolbar_management") : "tools"
enabled: !sts.busy && pkg.pacman !== "" && cfg.terminal
visible: enabled && cfg.managementButton
onClicked: { buttonTooltip.hide(); JS.management() }
}
ToolbarButton {
id: upgradeButton
tooltipText: i18n("Upgrade system")
iconSource: cfg.ownIconsUI ? svg("toolbar_upgrade") : "akonadiconsole"
enabled: !sts.busy && sts.count && cfg.terminal
visible: enabled && cfg.upgradeButton
onClicked: { buttonTooltip.hide(); JS.upgradeSystem() }
}
ToolbarButton {
tooltipText: sts.busy ? i18n("Stop checking") : i18n("Check updates")
iconSource: cfg.ownIconsUI ? (sts.busy ? svg("toolbar_stop") : svg("toolbar_check"))
: (sts.busy ? "media-playback-stopped" : "view-refresh")
visible: cfg.checkButton && !sts.upgrading
onClicked: sts.proc ? JS.stopCheck() : JS.checkUpdates()
}
ToolbarButton {
tooltipText: i18n("Open settings")
iconSource: cfg.ownIconsUI ? svg("toolbar_settings") : "settings-configure"
visible: cfg.settingsButton && !inTray && !sts.busy
onClicked: plasmoid.internalAction("configure").trigger()
}
ToolbarButton {
tooltipText: pinned ? i18n("Unpin window") : i18n("Keep open")
iconSource: cfg.ownIconsUI ? (pinned ? svg("toolbar_unpin") : svg("toolbar_pin"))
: (pinned ? "window-unpin" : "window-pin")
visible: cfg.pinButton && !inTray && !onDesktop
onClicked: pinned = !pinned
}
}
}
}
footer: PlasmoidHeading {
id: bottomHeader
spacing: 0
topPadding: 0
height: Kirigami.Units.iconSizes.medium
visible: cfg.tabBarVisible && cfg.feedsEnabled && !sts.error
contentItem: TabBar {
id: tabBar
Layout.fillWidth: true
Layout.fillHeight: true
position: TabBar.Footer
currentIndex: swipeView.currentIndex
onCurrentIndexChanged: {
swipeView.currentIndex = currentIndex
if (swipeView.currentIndex >= 1) {
searchFieldOpen = false
searchField.text = ""
}
}
Repeater {
model: [
{
id: "updates",
icon: "status_package",
fallback: "kpackagekit-updates",
label: i18n("Updates"),
},
cfg.feedsEnabled ? {
id: "news",
icon: "status_news",
fallback: "news-subscribe",
label: i18n("News"),
} : null
].filter(Boolean)
delegate: TabButton {
required property var modelData
contentItem: RowLayout {
Kirigami.Theme.inherit: true
Item { Layout.fillWidth: true }
Kirigami.Icon {
Layout.preferredHeight: Kirigami.Units.iconSizes.small
Layout.preferredWidth: Kirigami.Units.iconSizes.small
source: cfg.ownIconsUI ? svg(modelData.icon) : modelData.fallback
color: (modelData.id === "news" && activeNewsItems) ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.colorSet
isMask: cfg.ownIconsUI
smooth: true
}
Label { text: modelData.label; visible: cfg.tabBarTexts }
Item { Layout.fillWidth: true }
}
}
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Loader {
Layout.fillWidth: true
active: sts.busy
sourceComponent: ProgressBar { from: 0; to: 100; indeterminate: true }
}
TextField {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing * 2
Layout.bottomMargin: Kirigami.Units.smallSpacing * 2
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
Layout.rightMargin: Kirigami.Units.smallSpacing * 2
id: searchField
clearButtonShown: true
visible: searchFieldOpen && sts.count
placeholderText: i18n("Filter by package name")
onTextChanged: modelList.setFilterFixedString(text)
}
Kirigami.InlineMessage {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing * 2
Layout.bottomMargin: Kirigami.Units.smallSpacing * 2
text: "<b>" + i18n("Check out release notes")+" "+currVersion+"</b>"
type: Kirigami.MessageType.Positive
visible: !searchFieldOpen && isOnline &&
plasmoid.configuration.version.localeCompare(currVersion, undefined, { numeric: true, sensitivity: 'base' }) < 0
actions: [
Kirigami.Action {
tooltip: i18n("Select...")
icon.name: "application-menu"
expandible: true
Kirigami.Action {
text: "GitHub"
icon.name: "internet-web-browser-symbolic"
onTriggered: Qt.openUrlExternally("https://github.com/exequtic/apdatifier/releases")
}
Kirigami.Action {
text: i18n("Dismiss")
icon.name: "dialog-close"
onTriggered: plasmoid.configuration.version = currVersion
}
}
]
}
Kirigami.InlineMessage {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing * 2
Layout.bottomMargin: Kirigami.Units.smallSpacing * 2
text: "<b>" + i18n("No internet connection") + "</b>"
type: Kirigami.MessageType.Error
visible: !isOnline
}
SwipeView {
id: swipeView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
currentIndex: 0
Repeater {
model: [
{ component: "updates" },
cfg.feedsEnabled ? { component: "news" } : null
].filter(Boolean)
delegate: Loader {
required property var modelData
sourceComponent: modelData.component === "updates" ? updatesComp : newsComp
}
}
Component { id: updatesComp; View.Updates {} }
Component { id: newsComp; View.News { id: newsPage } }
}
}
Rectangle {
z: 9998
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
visible: errorLoader.active
}
Loader {
z: 9999
id: errorLoader
anchors.centerIn: parent
active: !sts.busy && sts.error
sourceComponent: ColumnLayout {
spacing: Kirigami.Units.largeSpacing * 2
Layout.fillWidth: true
RowLayout {
Kirigami.Icon {
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
source: cfg.ownIconsUI ? svg("status_error") : "error"
isMask: cfg.ownIconsUI
}
Kirigami.Heading {
text: i18np("%1 error occurred", "%1 errors occurred", sts.errors.length)
type: Kirigami.Heading.Primary
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
ScrollView {
Layout.preferredWidth: Kirigami.Units.gridUnit * 20
Layout.preferredHeight: Kirigami.Units.gridUnit * 8
TextArea {
width: parent.width
height: parent.height
readOnly: true
wrapMode: Text.Wrap
font.family: "Monospace"
font.pointSize: Kirigami.Theme.smallFont.pointSize
text: sts.errors.map(err => err?.type ? `${err.type}: ${err.message}` : `${err.message}`).join("\n\n")
color: Kirigami.Theme.textColor
}
}
Button {
Layout.alignment: Qt.AlignHCenter
icon.name: "checkmark"
text: "OK"
onClicked: {
sts.errors = []
JS.setStatusBar()
}
}
}
}
KSortFilterProxyModel {
id: modelList
sourceModel: listModel
filterRoleName: "name"
filterRowCallback: (sourceRow, sourceParent) => {
return sourceModel.data(sourceModel.index(sourceRow, 0, sourceParent), filterRole).includes(searchField.text)
}
}
}
@@ -0,0 +1,271 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import Qt5Compat.GraphicalEffects
import "../components" as QQC
import "../../tools/tools.js" as JS
MouseArea {
id: mouseArea
property bool wasExpanded: false
readonly property bool counterSide: !inTray && horizontal && plasmoid.configuration.counterMode === "side"
readonly property bool counterEnabled: plasmoid.configuration.counterMode !== "disabled"
readonly property bool counterCenter: plasmoid.configuration.counterBadgePosition === "center"
readonly property bool pauseBadgeEnabled: plasmoid.configuration.pauseBadgePosition !== "disabled"
readonly property bool updatedBadgeEnabled: plasmoid.configuration.updatedBadgePosition !== "disabled"
readonly property bool onDesktop: plasmoid.location === PlasmaCore.Types.Floating
readonly property bool pulseActive: (plasmoid.configuration.busyIndicator === "pulse") && sts.busy && !onDesktop
function darkColor(color) {
return Kirigami.ColorUtils.brightnessForColor(color) === Kirigami.ColorUtils.Dark
}
readonly property bool isDarkText: darkColor(Kirigami.Theme.textColor)
readonly property color lightText: isDarkText ? Kirigami.Theme.backgroundColor : Kirigami.Theme.textColor
readonly property color darkText: isDarkText ? Kirigami.Theme.textColor : Kirigami.Theme.backgroundColor
readonly property string errorIcon: cfg.ownIconsUI ? "status_error" : "error"
readonly property string updatedIcon: cfg.ownIconsUI ? "status_updated" : "checkmark"
readonly property string pausedIcon: cfg.ownIconsUI ? "toolbar_pause" : "media-playback-paused"
readonly property real panelIconSize: horizontal ? parent.height : parent.width
readonly property real iconSize: inTray ? Math.min(parent.width, parent.height) : panelIconSize
readonly property real counterPixelSize: Math.max(iconSize / 4, iconSize * (cfg.counterFontSize / 10))
implicitHeight: iconSize
implicitWidth: viewLoader.item ? viewLoader.item.implicitWidth : iconSize
Layout.preferredHeight: implicitHeight
Layout.minimumHeight: implicitHeight
Layout.preferredWidth: implicitWidth
Layout.minimumWidth: implicitWidth
hoverEnabled: true
acceptedButtons: cfg.rightAction ? Qt.AllButtons : Qt.LeftButton | Qt.MiddleButton
onEntered: sts.checkTimeTooltip = JS.getCheckTime()
onPressed: mouse => {
wasExpanded = expanded
if (!cfg.rightAction && mouse.button == Qt.RightButton) mouse.accepted = false
}
onClicked: mouse => {
if (mouse.button == Qt.LeftButton) expanded = !wasExpanded
if (mouse.button == Qt.MiddleButton && cfg.middleAction) JS[cfg.middleAction]()
if (mouse.button == Qt.RightButton && cfg.rightAction) JS[cfg.rightAction]()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
if (scrollTimer.running) return
scrollTimer.start()
var action = event.angleDelta.y > 0 ? cfg.scrollUpAction : cfg.scrollDownAction
if (!action) return
JS[action]()
}
}
Timer {
id: scrollTimer
interval: 500
}
Loader {
id: viewLoader
anchors.fill: parent
sourceComponent: counterSide ? iconWithCounterSide : iconWithCounterBadge
}
Component {
id: iconWithCounterSide
RowLayout {
implicitHeight: mouseArea.iconSize
layoutDirection: cfg.counterSidePosition === "left" ? Qt.RightToLeft : Qt.LeftToRight
spacing: 0
anchors.centerIn: parent
Item { Layout.preferredWidth: Kirigami.Units.smallSpacing + cfg.counterGapsOuter }
Item {
Layout.preferredHeight: mouseArea.iconSize
Layout.preferredWidth: mouseArea.iconSize
Kirigami.Icon {
id: panelIcon
width: parent.width
height: parent.height
source: JS.setIcon(plasmoid.icon)
active: mouseArea.containsMouse
layer.enabled: sts.error
layer.effect: sts.error ? errorShadowEffect : null
Loader {
anchors.fill: parent
sourceComponent: badgesLayer
active: sts.init
}
}
QQC.Pulse {
target: panelIcon
running: mouseArea.pulseActive
}
}
Item {
Layout.preferredWidth: cfg.counterGapsInner
visible: counterText.visible
}
Label {
id: counterText
visible: mouseArea.counterEnabled && !sts.busy && sts.count
font.family: plasmoid.configuration.counterFontFamily || Kirigami.Theme.defaultFont.family
font.pixelSize: Math.round(mouseArea.counterPixelSize)
font.bold: cfg.counterFontBold
fontSizeMode: Text.FixedSize
text: sts.count
}
Item { Layout.preferredWidth: Kirigami.Units.smallSpacing + cfg.counterGapsOuter }
}
}
Component {
id: iconWithCounterBadge
Item {
implicitWidth: mouseArea.iconSize
implicitHeight: mouseArea.iconSize
Item {
width: mouseArea.iconSize
height: mouseArea.iconSize
anchors.centerIn: parent
Kirigami.Icon {
id: trayIcon
anchors.fill: parent
source: JS.setIcon(plasmoid.icon)
active: mouseArea.containsMouse
layer.enabled: sts.error
layer.effect: sts.error ? errorShadowEffect : null
}
QQC.Pulse {
target: trayIcon
running: mouseArea.pulseActive
}
Rectangle {
id: frame
anchors.centerIn: parent
width: parent.width
height: parent.height
opacity: 0
}
Rectangle {
id: counterFrame
width: mouseArea.counterCenter ? frame.width : counter.implicitWidth + 2
height: mouseArea.counterCenter ? frame.height : counter.implicitHeight
radius: cfg.counterRadius
opacity: cfg.counterOpacity / 10
color: cfg.counterColor ? cfg.counterColor : Kirigami.Theme.backgroundColor
visible: mouseArea.counterEnabled && !sts.busy && sts.count
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 0
radius: 2
samples: radius * 2
color: Qt.rgba(0, 0, 0, 0.5)
}
state: cfg.counterBadgePosition
states: [
State { name: "center"; AnchorChanges { target: counterFrame; anchors.horizontalCenter: frame.horizontalCenter; anchors.verticalCenter: frame.verticalCenter } },
State { name: "topLeft"; AnchorChanges { target: counterFrame; anchors.top: frame.top; anchors.left: frame.left } },
State { name: "topRight"; AnchorChanges { target: counterFrame; anchors.top: frame.top; anchors.right: frame.right } },
State { name: "bottomLeft"; AnchorChanges { target: counterFrame; anchors.bottom: frame.bottom; anchors.left: frame.left } },
State { name: "bottomRight"; AnchorChanges { target: counterFrame; anchors.bottom: frame.bottom; anchors.right: frame.right } }
]
anchors {
topMargin: state.includes("top") ? cfg.counterOffsetX : 0
leftMargin: state.includes("Left") ? cfg.counterOffsetY : 0
rightMargin: state.includes("Right") ? cfg.counterOffsetY : 0
bottomMargin: state.includes("bottom") ? cfg.counterOffsetX : 0
}
transitions: Transition { AnchorAnimation { duration: 120 } }
}
Label {
id: counter
anchors.centerIn: counterFrame
text: sts.count
font.family: plasmoid.configuration.counterFontFamily || Kirigami.Theme.defaultFont.family
font.pixelSize: Math.round(mouseArea.counterPixelSize)
font.bold: cfg.counterFontBold
color: cfg.counterColor ? (mouseArea.darkColor(counterFrame.color) ? mouseArea.lightText : mouseArea.darkText) : Kirigami.Theme.textColor
visible: counterFrame.visible
}
Loader {
anchors.fill: parent
sourceComponent: badgesLayer
active: sts.init
}
}
}
}
Component {
id: badgesLayer
Item {
anchors.fill: parent
QQC.Badge {
iconName: updatedIcon
position: cfg.updatedBadgePosition
iconColor: Kirigami.Theme.positiveTextColor
visible: !sts.busy && !sts.count && updatedBadgeEnabled
}
QQC.Badge {
iconName: pausedIcon
position: cfg.pauseBadgePosition
iconColor: Kirigami.Theme.neutralTextColor
visible: sts.paused && pauseBadgeEnabled
}
}
}
Component {
id: errorShadowEffect
DropShadow {
horizontalOffset: 0
verticalOffset: 0
radius: 6
samples: 16
color: "red"
spread: 0.35
// transparentBorder: true
}
}
}
@@ -0,0 +1,133 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.plasma.components
import org.kde.kirigami as Kirigami
import "../../tools/tools.js" as JS
import "../components"
Item {
ListView {
model: newsModel
spacing: Kirigami.Units.largeSpacing * 2
topMargin: spacing
rightMargin: spacing
leftMargin: spacing
bottomMargin: spacing
anchors.fill: parent
ScrollBar.vertical: Scroll {
policy: newsModel.count > 0 ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
delegate: Kirigami.AbstractCard {
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
GridLayout {
id: delegateLayout
anchors {
left: parent.left
top: parent.top
right: parent.right
}
rowSpacing: Kirigami.Units.largeSpacing
columnSpacing: Kirigami.Units.largeSpacing
columns: width > Kirigami.Units.gridUnit * 20 ? 4 : 2
opacity: model.removed ? 0.5 : 1.0
Kirigami.Icon {
Layout.fillHeight: true
Layout.maximumHeight: Kirigami.Units.iconSizes.huge
Layout.preferredWidth: height
source: model.title.includes("Arch") ? "apdatifier-plasmoid" :
model.title.includes("Plasma") ? "note-symbolic" :
model.title.includes("Apps") ? "applications-all-symbolic" :
model.title.includes("Community") ? "start-here-kde-plasma-symbolic"
: "news-subscribe"
}
RowLayout {
ColumnLayout {
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: model.title
font.bold: true
}
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: Qt.formatDateTime(new Date(model.timestamp * 1000), Qt.DefaultLocaleShortDate)
opacity: 0.6
}
Kirigami.Separator {
Layout.fillWidth: true
}
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: model.article
}
}
ColumnLayout {
Controls.Button {
ToolTip { id: tip; text: i18n("Read article") }
icon.name: "internet-web-browser-symbolic"
onClicked: {
tip.hide()
Qt.openUrlExternally(model.link)
JS.removeNewsItem(index)
}
}
Controls.Button {
visible: !model.removed
ToolTip { text: i18n("Mark as read") }
icon.name: model.removed ? "backup" : "delete"
onClicked: JS.removeNewsItem(index)
}
}
}
}
}
}
Loader {
anchors.centerIn: parent
active: newsModel.count === 0
sourceComponent: Kirigami.PlaceholderMessage {
icon.name: "news-subscribe"
text: i18n("No unread news")
}
}
add: Transition { animations: [ NumberAnimation { properties: "opacity,scale"; from: 0; to: 1; duration: 380; easing.type: Easing.OutBack; easing.overshoot: 1.12 } ] }
addDisplaced: Transition { animations: [
NumberAnimation { property: "y"; duration: 340; easing.type: Easing.OutCubic },
NumberAnimation { properties: "opacity,scale"; to: 1; duration: 340; easing.type: Easing.OutBack; easing.overshoot: 1.08 }
] }
displaced: Transition { animations: [
NumberAnimation { property: "y"; duration: 340; easing.type: Easing.OutBack; easing.overshoot: 1.05 },
NumberAnimation { properties: "opacity,scale"; to: 1; duration: 260; easing.type: Easing.OutQuad }
] }
move: Transition { animations: [
NumberAnimation { property: "y"; duration: 360; easing.type: Easing.OutBack; easing.overshoot: 1.06 },
NumberAnimation { properties: "opacity,scale"; to: 1; duration: 260; easing.type: Easing.OutQuad }
] }
moveDisplaced: Transition { animations: [
NumberAnimation { property: "y"; duration: 340; easing.type: Easing.OutBack; easing.overshoot: 1.04 },
NumberAnimation { properties: "opacity,scale"; to: 1; duration: 260; easing.type: Easing.OutQuad }
] }
remove: Transition { animations: [
NumberAnimation { property: "x"; to: width + 120; duration: 300; easing.type: Easing.InBack; easing.overshoot: 1.15 },
NumberAnimation { property: "opacity"; to: 0; duration: 240; easing.type: Easing.InQuad },
NumberAnimation { property: "scale"; to: 0.92; duration: 300; easing.type: Easing.InBack }
] }
removeDisplaced: Transition { animations: [
NumberAnimation { property: "y"; duration: 340; easing.type: Easing.OutBack; easing.overshoot: 1.03 },
NumberAnimation { properties: "opacity,scale"; to: 1; duration: 260; easing.type: Easing.OutQuad }
] }
}
}
@@ -0,0 +1,409 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import org.kde.ksvg
import org.kde.plasma.extras
import org.kde.kirigami as Kirigami
import "../../tools/tools.js" as JS
import "../components"
Item {
id: view
property bool compactMode: listCompactMode
clip: true
ListView {
id: listView
ScrollBar.vertical: Scroll {
policy: !sts.count ? ScrollBar.AlwaysOff : ScrollBar.AsNeeded
}
anchors.fill: parent
model: modelList
boundsBehavior: Flickable.StopAtBounds
spacing: view.compactMode ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.smallSpacing
delegate: Item {
width: listView.width
height: delegateItem.height
Rectangle {
id: delegateItem
anchors {
left: parent.left
right: parent.right
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
}
height: mainColumn.implicitHeight
clip: true
color: "transparent"
property bool expanded: false
property bool hovered: false
property var pkg: []
Behavior on height {
NumberAnimation {
duration: 220
easing.type: Easing.OutBack
easing.overshoot: 0.7
}
}
HoverHandler {
id: hoverHandler
cursorShape: Qt.PointingHandCursor
onHoveredChanged: delegateItem.hovered = hovered
}
MouseArea {
anchors.fill: parent
onClicked: delegateItem.expanded = !delegateItem.expanded
}
Rectangle {
property color hColor: Kirigami.Theme.highlightColor
property bool highlighted: (delegateItem.hovered && !listView.moving) || delegateItem.expanded
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: highlighted ? Qt.rgba(hColor.r, hColor.g, hColor.b, 0.4) : "transparent"
border.width: highlighted ? 1 : 0
border.color: Qt.darker(Qt.rgba(hColor.r, hColor.g, hColor.b, 0.8), 2)
Behavior on color {
ColorAnimation {
duration: 100
}
}
}
ColumnLayout {
id: mainColumn
width: parent.width
spacing: 0
RowLayout {
id: mainRow
spacing: Kirigami.Units.largeSpacing
Kirigami.Icon {
id: pkgIcon
Layout.preferredWidth: view.compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.medium
Layout.preferredHeight: view.compactMode ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.medium
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Kirigami.Units.largeSpacing
source: model.IC
isMask: false
}
// Extended
RowLayout {
visible: !view.compactMode
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
ColumnLayout {
Label {
Layout.fillWidth: true
text: model.NM
color: model.IM ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.textColor
elide: Text.ElideRight
font.weight: Font.DemiBold
}
Loader {
Layout.fillWidth: true
sourceComponent: versionDiffLabel
onLoaded: {
item.oldVer = model.VO
item.newVer = model.VN
}
}
}
ColumnLayout {
Loader {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Kirigami.Units.largeSpacing
sourceComponent: infoChip
active: cfg.extendedShowRepository !== false
visible: active
onLoaded: item.text = model.RE
}
Loader {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Kirigami.Units.largeSpacing
sourceComponent: infoChip
active: model.RN && cfg.extendedShowInstallReason !== false
visible: active
onLoaded: {
item.text = model.RN
item.reason = model.RN || ""
}
}
}
}
// Compact
RowLayout {
visible: view.compactMode
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Label {
Layout.fillWidth: true
text: model.NM
color: model.IM ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.textColor
elide: Text.ElideRight
font.weight: Font.DemiBold
}
Loader {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
sourceComponent: infoChip
active: (cfg.compactInfo || "repository") === "repository"
visible: active
onLoaded: item.text = model.RE
}
Loader {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
sourceComponent: infoChip
active: model.RN && (cfg.compactInfo || "repository") === "installReason"
visible: active
onLoaded: {
item.text = model.RN
item.reason = model.RN || ""
}
}
Loader {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
sourceComponent: versionDiffLabel
active: (cfg.compactInfo || "repository") === "versionDiff"
visible: active
onLoaded: {
item.oldVer = model.VO
item.newVer = model.VN
item.hAlign = Text.AlignRight
}
}
}
}
Loader {
id: detailsLoader
Layout.fillWidth: true
active: delegateItem.expanded
visible: active
sourceComponent: ColumnLayout {
spacing: 0
Item {
Layout.topMargin: Kirigami.Units.largeSpacing
}
GridLayout {
id: details
width: parent.width
columns: 2
rowSpacing: Kirigami.Units.smallSpacing / 4
columnSpacing: 40
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.rightMargin: Kirigami.Units.largeSpacing
Repeater {
id: repeater
model: delegateItem.pkg.length
delegate: Label {
property bool header: !(index % 2)
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
horizontalAlignment: Text.AlignLeft
font: Kirigami.Theme.smallFont
opacity: header ? 0.6 : 1
text: {
if (header)
return "<b>" + delegateItem.pkg[index] + ":</b>"
if (delegateItem.pkg[index].indexOf("://") !== -1)
return "<a href=\"" + delegateItem.pkg[index] + "\">" + delegateItem.pkg[index] + "</a>"
return delegateItem.pkg[index]
}
textFormat: header ? Text.StyledText : delegateItem.pkg[index].indexOf("://") !== -1 ? Text.StyledText : Text.PlainText
wrapMode: header ? Text.NoWrap : Text.WordWrap
onLinkActivated: Qt.openUrlExternally(link)
}
}
}
Item {
Layout.preferredHeight: Kirigami.Units.smallSpacing * 2
}
ToolButton {
id: upgradeButton
// Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: (model.ID || model.CN) && cfg.terminal
icon.name: "edit-download"
text: i18n("Upgrade package")
onClicked: JS.upgradePackage(model.NM, model.ID, model.CN)
}
Item {
Layout.preferredHeight: Kirigami.Units.smallSpacing * 2
visible: upgradeButton.visible
}
Component.onCompleted: {
const F = [
["TP", i18n("Package type")],
["DE", i18n("Description")],
["AU", i18n("Author")],
["RE", i18n("Repository")],
["LN", "URL"],
["ID", i18n("App ID")],
["BR", i18n("Branch")],
["CM", i18n("Commit")],
["RT", i18n("Runtime")],
["GR", i18n("Groups")],
["PR", i18n("Provides")],
["DP", i18n("Depends on")],
["RQ", i18n("Required by")],
["CF", i18n("Conflicts with")],
["RP", i18n("Replaces")],
["IS", i18n("Installed size")],
["DS", i18n("Download size")],
["DT", i18n("Install date")],
["RN", i18n("Install reason")]
]
const details = []
for (const [k, l] of F) {
if (!model[k]) continue
let value = model[k]
if (k === "RN") {
value = model.RN === "explicit" ? i18n("Explicitly installed")
: model.RN === "dependency" ? i18n("Installed as a dependency")
: i18n("Orphaned dependency")
}
details.push(l, value)
}
delegateItem.pkg = details
}
}
}
}
}
}
Loader {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
active: !sts.count
sourceComponent: Kirigami.PlaceholderMessage {
width: parent.width - (Kirigami.Units.largeSpacing * 4)
icon.name: "checkmark"
text: i18n("System updated")
}
}
}
Component {
id: infoChip
Rectangle {
property alias text: label.text
property string reason: ""
readonly property color bgColor: {
if (reason) {
if (!cfg.colorizeInstallReason)
return Kirigami.Theme.highlightColor
else if (reason === "explicit")
return Kirigami.Theme.positiveBackgroundColor
else if (reason === "dependency")
return Kirigami.Theme.neutralBackgroundColor
else
return Kirigami.Theme.negativeBackgroundColor
}
return Kirigami.Theme.highlightColor
}
radius: 10
color: Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.3)
implicitWidth: label.contentWidth + 16
implicitHeight: label.contentHeight
border.width: 1
border.color: Qt.lighter(Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.5), 1.2)
Label {
id: label
anchors.centerIn: parent
font: Kirigami.Theme.smallFont
color: Qt.lighter(bgColor, 3)
}
}
}
Component {
id: versionDiffLabel
Label {
property string oldVer: ""
property string newVer: ""
property int hAlign: Text.AlignLeft
horizontalAlignment: hAlign
text: cfg.highlightVersionDiff ? versionDiff(oldVer, newVer) : `${oldVer} ${newVer}`
font: Kirigami.Theme.smallFont
textFormat: Text.StyledText
elide: Text.ElideRight
opacity: 0.7
function versionDiff(oldVer, newVer) {
if (oldVer === newVer) return oldVer
let i = 0
while (i < oldVer.length &&
i < newVer.length &&
oldVer[i] === newVer[i]) {
++i
}
const prefix = oldVer.slice(0, i)
const oldDiff = oldVer.slice(i)
const newDiff = newVer.slice(i)
return `${prefix}<font color="${Kirigami.Theme.negativeTextColor}">${oldDiff}</font> ${prefix}<font color="${Kirigami.Theme.positiveTextColor}">${newDiff}</font>`
}
}
}
}
@@ -0,0 +1,24 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Evgeny Kazantsev",
"Email": "exequtic@gmail.com"
}
],
"Category": "System Information",
"Description": "Arch Update Notifier",
"Icon": "apdatifier-plasmoid",
"Id": "com.github.exequtic.apdatifier",
"Name": "Apdatifier",
"EnabledByDefault": false,
"Version": "2.9.9",
"License": "MIT",
"BugReportUrl": "https://github.com/exequtic/apdatifier/issues",
"Website": "https://github.com/exequtic/apdatifier"
},
"KPackageStructure": "Plasma/Applet",
"X-Plasma-API-Minimum-Version": "6.0",
"X-Plasma-NotificationArea": "true",
"X-Plasma-NotificationAreaCategory": "SystemServices"
}
@@ -0,0 +1,38 @@
> Version 7 of Zren's i18n scripts.
## New Translations
Fill out [`template.pot`](template.pot) with your translations then open a [new issue](https://github.com/exequtic/apdatifier/issues/new), name the file with the extension `.txt`, attach the txt file to the issue (drag and drop).
Or if you know how to make a pull request:
Copy the [`template.pot`](template.pot) file to [`./po`](po) directory and name it your locale's code (Eg: `en`/`de`/`fr`) with the extension `.po`. Then fill out all the `msgstr ""`.
## Scripts
* `sh ./merge` will parse the `i18n()` calls in the `*.qml` files and write it to the `template.pot` file. Then it will merge any changes into the `*.po` language files.
* `sh ./build` will convert the `*.po` files to it's binary `*.mo` version and move it to `contents/locale/...`
## Links
* https://zren.github.io/kde/docs/widget/#translations-i18n
* https://github.com/Zren/plasma-applet-lib/tree/master/package/translate
## Status
| Locale | Lines | % Done|
|----------|---------|-------|
| Template | 330 | |
| de | 261/330 | 79% |
| es | 261/330 | 79% |
| fr | 316/330 | 95% |
| hu_HU | 306/330 | 92% |
| ko | 261/330 | 79% |
| nl | 261/330 | 79% |
| pl | 261/330 | 79% |
| pt_BR | 299/330 | 90% |
| ru | 330/330 | 100% |
| tr | 261/330 | 79% |
| uk | 261/330 | 79% |
| zh_CN | 294/330 | 89% |
| zh_HK | 294/330 | 89% |
| zh_TW | 294/330 | 89% |
@@ -0,0 +1,53 @@
#!/bin/bash
# Version: 6
# This script will convert the *.po files to *.mo files, rebuilding the package/contents/locale folder.
# Feature discussion: https://phabricator.kde.org/D5209
# Eg: contents/locale/fr_CA/LC_MESSAGES/plasma_applet_org.kde.plasma.eventcalendar.mo
DIR=`cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd`
plasmoidName=com.github.exequtic.apdatifier
widgetName="${plasmoidName##*.}" # Strip namespace
website=https://github.com/exequtic/apdatifier
bugAddress="$website"
packageRoot=".." # Root of translatable sources
projectName="plasma_applet_${plasmoidName}" # project name
#---
if [ -z "$plasmoidName" ]; then
echo "[build] Error: Couldn't read plasmoidName."
exit
fi
if [ -z "$(which msgfmt)" ]; then
echo "[build] Error: msgfmt command not found. Need to install gettext"
echo "[build] Running 'sudo apt install gettext'"
sudo apt install gettext
echo "[build] gettext installation should be finished. Going back to installing translations."
fi
#---
echo "[build] Compiling messages"
catalogs=`find ./po -name '*.po' | sort`
for cat in $catalogs; do
catLocale=`basename ${cat%.*}`
msgfmt -o "${catLocale}.mo" "$cat"
installPath="$DIR/../contents/locale/${catLocale}/LC_MESSAGES/${projectName}.mo"
echo "[build] Install ${cat} to ${installPath}"
mkdir -p "$(dirname "$installPath")"
mv "${catLocale}.mo" "${installPath}"
done
echo "[build] Done building messages"
if [ "$1" = "--restartplasma" ]; then
echo "[build] Restarting plasmashell"
killall plasmashell
kstart5 plasmashell
echo "[build] Done restarting plasmashell"
else
echo "[build] (re)install the plasmoid and restart plasmashell to test."
fi
@@ -0,0 +1,165 @@
#!/bin/bash
# Version: 22
# https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems
# https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems/Outside_KDE_repositories
# https://invent.kde.org/sysadmin/l10n-scripty/-/blob/master/extract-messages.sh
DIR=`cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd`
plasmoidName=com.github.exequtic.apdatifier
widgetName="${plasmoidName##*.}" # Strip namespace
website=https://github.com/exequtic/apdatifier
bugAddress="$website"
packageRoot=".." # Root of translatable sources
projectName="plasma_applet_${plasmoidName}" # project name
#---
if [ -z "$plasmoidName" ]; then
echo "[merge] Error: Couldn't read plasmoidName."
exit
fi
if [ -z "$(which xgettext)" ]; then
echo "[merge] Error: xgettext command not found. Need to install gettext"
echo "[merge] Running 'sudo apt install gettext'"
sudo apt install gettext
echo "[merge] gettext installation should be finished. Going back to merging translations."
fi
#---
echo "[merge] Extracting messages"
potArgs="--from-code=UTF-8 --width=200 --add-location=file"
# See Ki18n's extract-messages.sh for a full example:
# https://invent.kde.org/sysadmin/l10n-scripty/-/blob/master/extract-messages.sh#L25
# The -kN_ and -kaliasLocale keywords are mentioned in the Outside_KDE_repositories wiki.
# We don't need -kN_ since we don't use intltool-extract but might as well keep it.
# I have no idea what -kaliasLocale is used for. Googling aliasLocale found only listed kde1 code.
# We don't need to parse -ki18nd since that'll extract messages from other domains.
find "${packageRoot}" -name '*.cpp' -o -name '*.h' -o -name '*.c' -o -name '*.qml' -o -name '*.js' -o -name 'messages' | sort > "${DIR}/infiles.list"
xgettext \
${potArgs} \
--files-from="${DIR}/infiles.list" \
-C -kde \
-ci18n \
-ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 \
-kki18n:1 -kki18nc:1c,2 -kki18np:1,2 -kki18ncp:1c,2,3 \
-kxi18n:1 -kxi18nc:1c,2 -kxi18np:1,2 -kxi18ncp:1c,2,3 \
-kkxi18n:1 -kkxi18nc:1c,2 -kkxi18np:1,2 -kkxi18ncp:1c,2,3 \
-kI18N_NOOP:1 -kI18NC_NOOP:1c,2 \
-kI18N_NOOP2:1c,2 -kI18N_NOOP2_NOSTRIP:1c,2 \
-ktr2i18n:1 -ktr2xi18n:1 \
-kN_:1 \
-kaliasLocale \
--package-name="${widgetName}" \
--msgid-bugs-address="${bugAddress}" \
-D "${packageRoot}" \
-D "${DIR}" \
-o "template.pot.new" \
|| \
{ echo "[merge] error while calling xgettext. aborting."; exit 1; }
sed -i 's/"Content-Type: text\/plain; charset=CHARSET\\n"/"Content-Type: text\/plain; charset=UTF-8\\n"/' "template.pot.new"
sed -i 's/# SOME DESCRIPTIVE TITLE./'"# Translation of ${widgetName} in LANGUAGE"'/' "template.pot.new"
sed -i 's/# Copyright (C) YEAR THE PACKAGE'"'"'S COPYRIGHT HOLDER/'"# Copyright (C) $(date +%Y)"'/' "template.pot.new"
if [ -f "template.pot" ]; then
newPotDate=`grep "POT-Creation-Date:" template.pot.new | sed 's/.\{3\}$//'`
oldPotDate=`grep "POT-Creation-Date:" template.pot | sed 's/.\{3\}$//'`
sed -i 's/'"${newPotDate}"'/'"${oldPotDate}"'/' "template.pot.new"
changes=`diff "template.pot" "template.pot.new"`
if [ ! -z "$changes" ]; then
# There's been changes
sed -i 's/'"${oldPotDate}"'/'"${newPotDate}"'/' "template.pot.new"
mv "template.pot.new" "template.pot"
addedKeys=`echo "$changes" | grep "> msgid" | cut -c 9- | sort`
removedKeys=`echo "$changes" | grep "< msgid" | cut -c 9- | sort`
echo ""
echo "Added Keys:"
echo "$addedKeys"
echo ""
echo "Removed Keys:"
echo "$removedKeys"
echo ""
else
# No changes
rm "template.pot.new"
fi
else
# template.pot didn't already exist
mv "template.pot.new" "template.pot"
fi
potMessageCount=`expr $(grep -Pzo 'msgstr ""\n(\n|$)' "template.pot" | grep -c 'msgstr ""')`
echo "| Locale | Lines | % Done|" > "./Status.md"
echo "|----------|---------|-------|" >> "./Status.md"
entryFormat="| %-8s | %7s | %5s |"
templateLine=`perl -e "printf(\"$entryFormat\", \"Template\", \"${potMessageCount}\", \"\")"`
echo "$templateLine" >> "./Status.md"
rm "${DIR}/infiles.list"
echo "[merge] Done extracting messages"
#---
echo "[merge] Merging messages"
catalogs=`find ./po -name '*.po' | sort`
for cat in $catalogs; do
echo "[merge] $cat"
catLocale=`basename ${cat%.*}`
widthArg=""
catUsesGenerator=`grep "X-Generator:" "$cat"`
if [ -z "$catUsesGenerator" ]; then
widthArg="--width=400"
fi
compendiumArg=""
if [ ! -z "$COMPENDIUM_DIR" ]; then
langCode=`basename "${cat%.*}"`
compendiumPath=`realpath "$COMPENDIUM_DIR/compendium-${langCode}.po"`
if [ -f "$compendiumPath" ]; then
echo "compendiumPath=$compendiumPath"
compendiumArg="--compendium=$compendiumPath"
fi
fi
cp "$cat" "$cat.new"
sed -i 's/"Content-Type: text\/plain; charset=CHARSET\\n"/"Content-Type: text\/plain; charset=UTF-8\\n"/' "$cat.new"
msgmerge \
${widthArg} \
--add-location=file \
--no-fuzzy-matching \
${compendiumArg} \
-o "$cat.new" \
"$cat.new" "${DIR}/template.pot"
sed -i 's/# SOME DESCRIPTIVE TITLE./'"# Translation of ${widgetName} in ${catLocale}"'/' "$cat.new"
sed -i 's/# Translation of '"${widgetName}"' in LANGUAGE/'"# Translation of ${widgetName} in ${catLocale}"'/' "$cat.new"
sed -i 's/# Copyright (C) YEAR THE PACKAGE'"'"'S COPYRIGHT HOLDER/'"# Copyright (C) $(date +%Y)"'/' "$cat.new"
poEmptyMessageCount=`expr $(grep -Pzo 'msgstr ""\n(\n|$)' "$cat.new" | grep -c 'msgstr ""')`
poMessagesDoneCount=`expr $potMessageCount - $poEmptyMessageCount`
poCompletion=`perl -e "printf(\"%d\", $poMessagesDoneCount * 100 / $potMessageCount)"`
poLine=`perl -e "printf(\"$entryFormat\", \"$catLocale\", \"${poMessagesDoneCount}/${potMessageCount}\", \"${poCompletion}%\")"`
echo "$poLine" >> "./Status.md"
# mv "$cat" "$cat.old"
mv "$cat.new" "$cat"
done
echo "[merge] Done merging messages"
#---
# Populate ReadMe.md
echo "[merge] Updating translate/ReadMe.md"
sed -i -E 's`share\/plasma\/plasmoids\/(.+)\/translate`share/plasma/plasmoids/'"${plasmoidName}"'/translate`' ./ReadMe.md
if [[ "$website" == *"github.com"* ]]; then
sed -i -E 's`\[new issue\]\(https:\/\/github\.com\/(.+)\/(.+)\/issues\/new\)`[new issue]('"${website}"'/issues/new)`' ./ReadMe.md
fi
sed -i '/^|/ d' ./ReadMe.md # Remove status table from ReadMe
cat ./Status.md >> ./ReadMe.md
rm ./Status.md
echo "[merge] Done"

Some files were not shown because too many files have changed in this diff Show More