feat: add KDE Plasma podcast plasmoid

This commit is contained in:
Matteo Benedetto
2026-09-05 18:03:44 +02:00
commit 646033e16f
13 changed files with 1030 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import QtQuick
import org.kde.plasma.configuration
ConfigModel {
ConfigCategory {
name: i18n("Podcast")
icon: "audio-x-generic"
source: "configGeneral.qml"
}
}
+107
View File
@@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import "../ui/podcastCatalog.js" as PodcastCatalog
Kirigami.FormLayout {
id: page
property alias cfg_favoritePodcastIds: favoriteIds.text
property alias cfg_maxEpisodes: maxEpisodes.value
property alias cfg_refreshIntervalSec: refreshInterval.value
property alias cfg_favoritesOnly: favoritesOnly.checked
function selectedIds() {
return favoriteIds.text.split(",").map(function (id) { return id.trim() }).filter(function (id) { return id.length > 0 })
}
function hasId(id) {
return selectedIds().indexOf(String(id)) >= 0
}
function toggleId(id, enabled) {
var ids = selectedIds()
var value = String(id)
var index = ids.indexOf(value)
if (enabled && index < 0)
ids.push(value)
else if (!enabled && index >= 0)
ids.splice(index, 1)
favoriteIds.text = ids.join(",")
}
QQC2.TextField {
id: favoriteIds
Kirigami.FormData.label: i18n("Preferiti:")
placeholderText: i18n("Seleziona dall'elenco sotto")
visible: false
}
QQC2.Label {
Kirigami.FormData.label: i18n("Programmi:")
text: i18n("Seleziona i programmi da mettere in cima alla lista.")
wrapMode: Text.WordWrap
Layout.maximumWidth: 440
}
Rectangle {
Kirigami.FormData.label: ""
Layout.preferredWidth: 440
Layout.preferredHeight: 240
color: Kirigami.Theme.backgroundColor
border.color: Kirigami.Theme.disabledTextColor
radius: 4
QQC2.ScrollView {
anchors.fill: parent
anchors.margins: 6
clip: true
Column {
width: parent.width
spacing: 2
Repeater {
model: PodcastCatalog.catalog
delegate: QQC2.CheckBox {
width: parent.width
text: modelData.title + " (" + modelData.id + ")"
checked: page.hasId(modelData.id)
onClicked: page.toggleId(modelData.id, checked)
}
}
}
}
}
QQC2.SpinBox {
id: maxEpisodes
Kirigami.FormData.label: i18n("Episodi da mostrare:")
from: 1
to: 30
}
QQC2.SpinBox {
id: refreshInterval
Kirigami.FormData.label: i18n("Aggiornamento (secondi):")
from: 120
to: 86400
stepSize: 60
editable: true
}
QQC2.CheckBox {
id: favoritesOnly
Kirigami.FormData.label: i18n("Filtro:")
text: i18n("Mostra solo i preferiti")
}
QQC2.Label {
Kirigami.FormData.label: ""
text: i18n("Le credenziali vengono richieste nel widget e conservate in KWallet, non nella configurazione Plasma.")
wrapMode: Text.WordWrap
Layout.maximumWidth: 440
color: Kirigami.Theme.disabledTextColor
}
}
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0">
<kcfgfile name=""/>
<group name="General">
<entry name="favoritePodcastIds" type="String">
<default></default>
<label>ID dei programmi preferiti</label>
</entry>
<entry name="maxEpisodes" type="Int">
<default>8</default>
<min>1</min>
<max>30</max>
<label>Numero massimo di episodi</label>
</entry>
<entry name="refreshIntervalSec" type="Int">
<default>900</default>
<min>120</min>
<max>86400</max>
<label>Intervallo aggiornamento (secondi)</label>
</entry>
<entry name="favoritesOnly" type="Bool">
<default>false</default>
<label>Mostra solo i programmi preferiti</label>
</entry>
</group>
</kcfg>
+2
View File
@@ -0,0 +1,2 @@
module org.enne2.ilpost.credentials
plugin ilpostcredentialsplugin
+277
View File
@@ -0,0 +1,277 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami
import "../lib" as Credentials
PlasmoidItem {
id: root
property string favoriteIds: String(Plasmoid.configuration.favoritePodcastIds || "")
property int maxEpisodes: Number(Plasmoid.configuration.maxEpisodes || 8)
property int refreshSeconds: Number(Plasmoid.configuration.refreshIntervalSec || 900)
property bool favoritesOnly: Boolean(Plasmoid.configuration.favoritesOnly)
Credentials.IlPostApiClient {
id: api
}
Plasmoid.title: i18n("Podcast de Il Post")
Plasmoid.icon: "audio-x-generic"
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
preferredRepresentation: fullRepresentation
switchWidth: 320
switchHeight: 360
function refresh() {
api.refresh(root.favoriteIds, root.maxEpisodes, root.favoritesOnly)
}
onFavoriteIdsChanged: if (api.authenticated) refresh()
onMaxEpisodesChanged: if (api.authenticated) refresh()
onFavoritesOnlyChanged: if (api.authenticated) refresh()
Component.onCompleted: api.restore()
Connections {
target: api
function onAuthenticatedChanged() {
if (api.authenticated)
root.refresh()
}
}
Timer {
interval: Math.max(120, root.refreshSeconds) * 1000
repeat: true
running: api.authenticated
onTriggered: root.refresh()
}
compactRepresentation: Component {
Item {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
Kirigami.Icon {
anchors.fill: parent
source: Plasmoid.icon
color: api.authenticated ? Kirigami.Theme.textColor : Kirigami.Theme.disabledTextColor
}
MouseArea {
anchors.fill: parent
onClicked: Plasmoid.expanded = !Plasmoid.expanded
}
}
}
fullRepresentation: Component {
Item {
implicitWidth: 380
implicitHeight: 430
Layout.minimumWidth: 300
Connections {
target: api
function onLoginSucceeded() {
passwordField.clear()
}
}
Layout.minimumHeight: 260
Layout.preferredWidth: 380
Layout.preferredHeight: 430
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: i18n("Podcast de Il Post")
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 2
Layout.fillWidth: true
elide: Text.ElideRight
}
PC3.Button {
icon.name: "view-refresh"
display: QQC2.AbstractButton.IconOnly
enabled: !api.loading && api.authenticated
onClicked: root.refresh()
QQC2.ToolTip.text: i18n("Aggiorna")
QQC2.ToolTip.visible: hovered
}
}
PC3.Label {
Layout.fillWidth: true
visible: api.statusMessage.length > 0
text: api.statusMessage
color: api.authenticated ? Kirigami.Theme.disabledTextColor : Kirigami.Theme.negativeTextColor
elide: Text.ElideRight
}
ColumnLayout {
visible: !api.authenticated
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PC3.Label {
Layout.fillWidth: true
text: i18n("Accedi per visualizzare gli episodi. La password sarà salvata solo in KWallet.")
wrapMode: Text.WordWrap
}
QQC2.TextField {
id: emailField
Layout.fillWidth: true
placeholderText: i18n("Username / email Il Post")
inputMethodHints: Qt.ImhEmailCharactersOnly
enabled: !api.loading
}
QQC2.TextField {
id: passwordField
Layout.fillWidth: true
placeholderText: i18n("Password")
echoMode: TextInput.Password
enabled: !api.loading
onAccepted: loginButton.clicked()
}
PC3.Button {
id: loginButton
Layout.fillWidth: true
text: api.loading ? i18n("Accesso…") : i18n("Accedi e salva in KWallet")
enabled: !api.loading && emailField.text.length > 0 && passwordField.text.length > 0
onClicked: api.login(emailField.text, passwordField.text)
}
}
QQC2.ScrollView {
visible: api.authenticated
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ListView {
id: episodeList
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
model: api.episodes
delegate: Rectangle {
required property var modelData
width: episodeList.width
implicitHeight: episodeColumn.implicitHeight + 16
radius: 5
color: modelData.favorite
? Qt.rgba(0.25, 0.45, 0.25, 0.16)
: Kirigami.Theme.backgroundColor
border.color: modelData.favorite
? Kirigami.Theme.positiveTextColor
: Kirigami.Theme.disabledTextColor
border.width: 1
RowLayout {
id: episodeRow
anchors.fill: parent
anchors.margins: 8
spacing: Kirigami.Units.smallSpacing
Item {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
Layout.alignment: Qt.AlignTop
Image {
id: coverImage
anchors.fill: parent
source: modelData.podcastImage || ""
asynchronous: true
cache: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
visible: status === Image.Ready
}
Kirigami.Icon {
anchors.centerIn: parent
width: 36
height: 36
source: "audio-x-generic"
visible: !coverImage.visible
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.width: 1
border.color: Kirigami.Theme.disabledTextColor
opacity: 0.45
}
}
ColumnLayout {
id: episodeColumn
Layout.fillWidth: true
spacing: 2
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: modelData.podcastTitle || i18n("Podcast")
color: modelData.favorite ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.disabledTextColor
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
Layout.fillWidth: true
elide: Text.ElideRight
}
PC3.Label {
visible: modelData.favorite
text: i18n("Preferito")
color: Kirigami.Theme.positiveTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
}
}
PC3.Label {
Layout.fillWidth: true
text: modelData.title || i18n("Senza titolo")
font.bold: true
wrapMode: Text.WordWrap
}
PC3.Label {
Layout.fillWidth: true
text: modelData.date ? new Date(modelData.date).toLocaleDateString(Qt.locale()) : ""
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
}
PC3.Button {
visible: Boolean(modelData.audioUrl)
text: i18n("Riproduci in Dragon Player")
onClicked: api.playAudio(modelData.audioUrl)
}
}
}
}
}
}
RowLayout {
visible: api.authenticated
Layout.fillWidth: true
PC3.Label {
text: i18n("%1 episodi · preferiti in cima", api.episodes.length)
color: Kirigami.Theme.disabledTextColor
Layout.fillWidth: true
}
PC3.Button {
text: i18n("Disconnetti")
onClicked: api.clearCredentials()
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
var catalog = [{"id":227193,"title":"Tienimi Bordone"},{"id":233679,"title":"Timbuctu"},{"id":232180,"title":"Globo"},{"id":227474,"title":"Morning"},{"id":236472,"title":"Sigmund"},{"id":235598,"title":"L'invasione"},{"id":236670,"title":"Altre Indagini"},{"id":233120,"title":"Comodino"},{"id":233226,"title":"Amare parole"},{"id":234755,"title":"Per fare il Post"},{"id":230388,"title":"Ci vuole una scienza"},{"id":230293,"title":"Indagini"},{"id":231758,"title":"Tienimi Morning"},{"id":235926,"title":"In soldoni"},{"id":227196,"title":"Un podcast su Sanremo"},{"id":227412,"title":"Audiopost"},{"id":235470,"title":"Wild Baricco"},{"id":234841,"title":"10 risposte sui migranti"},{"id":234250,"title":"L'ombelico di un mondo"},{"id":227194,"title":"Joypad"},{"id":232871,"title":"Cosa c'entra?"},{"id":234368,"title":"Ripassini"},{"id":234296,"title":"Tredici"},{"id":229701,"title":"Politics"},{"id":233678,"title":"Vicini e lontani"},{"id":233516,"title":"Il grande vecchio"},{"id":233318,"title":"Le onorate"},{"id":228236,"title":"M49 - Una storia di orsi e persone"},{"id":233144,"title":"La nave"},{"id":228710,"title":"La fine del mondo"},{"id":232619,"title":"L'erba del vicino"},{"id":231622,"title":"Le basi"},{"id":231088,"title":"La bomba"},{"id":231013,"title":"Un furto di quart'ordine"},{"id":232518,"title":"Racconti a Natale"},{"id":230754,"title":"La fabbrica dei soldi"},{"id":230476,"title":"Certe cose"},{"id":229228,"title":"Cose spiegate bene"},{"id":227496,"title":"Un podcast sull'Eurovision"},{"id":227768,"title":"Da Vermicino in poi"},{"id":227322,"title":"Mai più"},{"id":227244,"title":"La guerra dei gelati"},{"id":227259,"title":"L'anno del jazz"},{"id":228752,"title":"Strade blu"},{"id":227246,"title":"Immaginare l'apocalisse"},{"id":227195,"title":"Konrad"},{"id":227197,"title":"Storie dalla libreria"},{"id":227198,"title":"Weekly Post"},{"id":227736,"title":"Wembley"}];
+16
View File
@@ -0,0 +1,16 @@
{
"KPackageStructure": "Plasma/Applet",
"KPlugin": {
"Id": "org.enne2.ilpost.podcasts",
"Name": "Il Post Podcasts",
"Name[it]": "Podcast de Il Post",
"Description": "Visualizza gli episodi più recenti dei podcast de Il Post",
"Description[it]": "Visualizza gli episodi più recenti dei podcast de Il Post",
"Icon": "audio-x-generic",
"Category": "Multimedia",
"License": "GPL-3.0-or-later",
"Version": "0.1.0",
"Authors": [{ "Name": "enne2" }]
},
"X-Plasma-API-Minimum-Version": "6.0"
}