commit 87872cb4fab6b9fc41cb35d4f3c87e59019ae1c4 Author: john Date: Thu Aug 21 11:36:02 2025 +0200 init diff --git a/contents/config/main.xml b/contents/config/main.xml new file mode 100644 index 0000000..a66b98e --- /dev/null +++ b/contents/config/main.xml @@ -0,0 +1,27 @@ + + + + + + + + apod + + + + 0 + + + + #000000 + + + + 0 + + + + diff --git a/contents/ui/ActionContextMenu.qml b/contents/ui/ActionContextMenu.qml new file mode 100644 index 0000000..184ad1a --- /dev/null +++ b/contents/ui/ActionContextMenu.qml @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2022 Fushan Wen + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 + +QQC2.Menu { + id: contextMenu + + /** + * Always show all actions regardless of visible property + */ + property bool showAllActions: false + + /** + * A list of extra actions in the context menu. + */ + property list actions + + Repeater { + model: contextMenu.actions + delegate: QQC2.MenuItem { + text: modelData.text || modelData.tooltip + icon.name: modelData.iconName + onTriggered: modelData.trigger() + enabled: modelData.enabled + visible: modelData.visible || contextMenu.showAllActions + + Accessible.description: modelData.Accessible.description + } + } + + onClosed: if (parent) parent.forceActiveFocus() +} diff --git a/contents/ui/WallpaperDelegate.qml b/contents/ui/WallpaperDelegate.qml new file mode 100644 index 0000000..dbae067 --- /dev/null +++ b/contents/ui/WallpaperDelegate.qml @@ -0,0 +1,260 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + SPDX-FileCopyrightText: 2022 Fushan Wen + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import Qt5Compat.GraphicalEffects as GE +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami as Kirigami + +FocusScope { + // FocusScope can pass Tab to inline buttons + id: delegate + + readonly property int shadowOffset: thumbnail.shadow.size - thumbnail.shadow.yOffset + readonly property bool isNull: wallpaperImage.status !== Image.Ready + + /** + * The background color of the preview area when the image is loaded + */ + property color backgroundColor + + /** + * The local path of the wallpaper + */ + property url localUrl + + /** + * The website of the wallpaper + */ + property url infoUrl + + /** + * The title of the wallpaper + */ + property string title + + /** + * The author of the wallpaper + */ + property string author + + /** + * Set it to true when a thumbnail is actually available: when false, + * only an icon ("edit-none") will be shown instead of the actual thumbnail. + */ + property bool thumbnailAvailable: false + + /** + * Set it to true when a thumbnail is still being loaded: when false, + * the BusyIndicator will be shown. + */ + property bool thumbnailLoading: false + + /** + * A list of extra actions for the thumbnails. They will be shown as + * icons on the bottom-right corner of the thumbnail on mouse over + */ + property list actions + + ActionContextMenu { + id: contextMenu + showAllActions: thumbnailAvailable + actions: delegate.actions + } + + Keys.onMenuPressed: contextMenu.popup(delegate, thumbnail.x, thumbnail.y + thumbnail.height) + Keys.onSpacePressed: contextMenu.popup(delegate, thumbnail.x, thumbnail.y + thumbnail.height) + + onThumbnailLoadingChanged: { + if (!thumbnailLoading) { + if (wallpaperImage.source === Qt.resolvedUrl(delegate.localUrl)) { + wallpaperImage.source = ""; + } + wallpaperImage.source = delegate.localUrl; + } else { + wallpaperImage.source = delegate.localUrl; + } + } + + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: contextMenu.popup() + } + + TapHandler { + id: openUrlTapHandler + enabled: hoverHandler.enabled + acceptedButtons: Qt.LeftButton + onTapped: Qt.openUrlExternally(delegate.infoUrl) + } + + HoverHandler { + id: hoverHandler + enabled: delegate.thumbnailAvailable && delegate.infoUrl.toString().length > 0 + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + QQC2.ToolTip { + text: delegate.infoUrl.toString() + visible: hoverHandler.enabled && !contextMenu.opened + && (hoverHandler.hovered + || thumbnailArea.activeFocus + || (Kirigami.Settings.isMobile && openUrlTapHandler.pressed)) + } + + // From kdeclarative/src/qmlcontrols/kcmcontrols/qml/GridDelegate.qml + Kirigami.ShadowedRectangle { + id: thumbnail + anchors.fill: parent + radius: Kirigami.Units.cornerRadius + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + + shadow.xOffset: 0 + shadow.yOffset: 2 + shadow.size: 10 + shadow.color: Qt.rgba(0, 0, 0, 0.3) + + color: thumbnailArea.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor + + Rectangle { + id: thumbnailArea + radius: Math.round(Kirigami.Units.cornerRadius / 2) + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + + color: !delegate.thumbnailAvailable || delegate.thumbnailLoading ? Kirigami.Theme.backgroundColor : delegate.backgroundColor + + activeFocusOnTab: true + Accessible.name: delegate.thumbnailAvailable ? i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis", "Today's picture") + : delegate.thumbnailLoading ? i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis", "Loading") + : i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis", "Unavailable") + Accessible.description: delegate.thumbnailAvailable ? i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis for an image %1 title %2 author", "%1 Author: %2. Right-click on the image to see more actions.", delegate.title, delegate.author) + : delegate.thumbnailLoading ? i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis", "The wallpaper is being fetched from the Internet.") + : i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis", "Failed to fetch the wallpaper from the Internet.") + + Image { + id: wallpaperImage + + anchors.fill: parent + autoTransform: false + cache: false + fillMode: cfg_FillMode || Image.PreserveAspectCrop + smooth: true + + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.CopyAction + Drag.mimeData: { + "text/uri-list" : [delegate.localUrl], + "text/plain": delegate.title, + } + + DragHandler { + id: dragHandler + + onActiveChanged: if (active) { + parent.grabToImage((result) => { + parent.Drag.imageSource = result.url; + parent.Drag.active = dragHandler.active; + }); + } else { + parent.Drag.active = false; + parent.Drag.imageSource = ""; + } + } + + // CachedProvider will load the image from cache, but we would like to show the real loading status. + layer.enabled: delegate.thumbnailLoading + layer.effect: GE.HueSaturation { + cached: true + + lightness: 0.5 + saturation: 0.9 + + layer.enabled: true + layer.effect: GE.GaussianBlur { + cached: true + + radius: 128 + deviation: 12 + samples: 63 + + transparentBorder: false + } + } + } + + Loader { + active: delegate.thumbnailLoading || !delegate.thumbnailAvailable + + anchors.centerIn: parent + opacity: 0.5 + visible: active + + width: Kirigami.Units.iconSizes.large + height: width + + sourceComponent: delegate.thumbnailLoading ? busyIndicator : fallbackIcon + + Component { + id: busyIndicator + + QQC2.BusyIndicator { + anchors.fill: parent + } + } + + // "None/There's nothing here" indicator + Component { + id: fallbackIcon + + Kirigami.Icon { + anchors.fill: parent + source: "edit-none" + } + } + } + + RowLayout { + anchors { + right: parent.right + rightMargin: Kirigami.Units.smallSpacing + bottom: parent.bottom + bottomMargin: Kirigami.Units.smallSpacing + } + + // Always show above thumbnail content + z: 9999 + + Repeater { + model: delegate.actions + delegate: QQC2.Button { + icon.name: modelData.icon.name + activeFocusOnTab: visible + onClicked: modelData.trigger() + enabled: modelData.enabled + visible: modelData.visible + + Accessible.name: modelData.tooltip + Accessible.description: modelData.Accessible.description + + QQC2.ToolTip { + visible: modelData.tooltip.length > 0 + && ((Kirigami.Settings.isMobile ? parent.pressed : parent.hovered) + || (parent.activeFocus && (parent.focusReason === Qt.TabFocusReason || parent.focusReason === Qt.BacktabFocusReason))) + text: modelData.tooltip + } + } + } + } + } + } +} diff --git a/contents/ui/WallpaperPreview.qml b/contents/ui/WallpaperPreview.qml new file mode 100644 index 0000000..880a703 --- /dev/null +++ b/contents/ui/WallpaperPreview.qml @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2022 Fushan Wen + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick + +import org.kde.kirigami 2.12 as Kirigami // For Action and Units + +import org.kde.plasma.wallpapers.potd 1.0 + +Column { + id: wallpaperPreview + + spacing: 0 + + /** + * The background color of the preview area when the image is loaded + */ + property alias backgroundColor: delegate.backgroundColor + + /** + * The shadow height needs to be considered in the padding. + */ + property alias shadowOffset: delegate.shadowOffset + + // Wallpaper preview (including save button) + WallpaperDelegate { + id: delegate + + width: Math.round(root.screenSize.width / 10 + Kirigami.Units.smallSpacing * 2) * Screen.devicePixelRatio + height: Math.round(root.screenSize.height / 10 + Kirigami.Units.smallSpacing * 2) * Screen.devicePixelRatio + + localUrl: backend.localUrl + infoUrl: backend.infoUrl + title: backend.title + author: backend.author + + thumbnailAvailable: !delegate.isNull + thumbnailLoading: backend.loading + + actions: [ + Kirigami.Action { + icon.name: "document-save" + enabled: backend.localUrl.length > 0 + visible: enabled + tooltip: i18ndc("plasma_wallpaper_org.kde.potd", "@action:inmenu wallpaper preview menu", "Save Image as…") + onTriggered: backend.saveImage() + + Accessible.description: i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis for a button and a menu item", "Save today's picture to local disk") + }, + Kirigami.Action { + icon.name: "internet-services" + enabled: backend.infoUrl.toString().length > 0 + visible: false + tooltip: i18ndc("plasma_wallpaper_org.kde.potd", "@action:inmenu wallpaper preview menu, will open the website of the wallpaper", "Open Link in Browser…") + onTriggered: Qt.openUrlExternally(backend.infoUrl) + + Accessible.description: i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis for a menu item", "Open the website of today's picture in the default browser") + } + ] + } +} diff --git a/contents/ui/config.qml b/contents/ui/config.qml new file mode 100644 index 0000000..9e45c23 --- /dev/null +++ b/contents/ui/config.qml @@ -0,0 +1,218 @@ +/* + * SPDX-FileCopyrightText: 2016 Weng Xuetian + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls 2.8 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kquickcontrols 2.0 as KQC2 +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.wallpapers.potd 1.0 + +Kirigami.FormLayout { + id: root + twinFormLayouts: parentLayout + + property string cfg_Provider + property int cfg_FillMode + property alias cfg_Color: colorButton.color + property int cfg_UpdateOverMeteredConnection + property alias formLayout: root + + readonly property size screenSize: Qt.size(Screen.width, Screen.height) + + PotdBackend { + id: backend + identifier: cfg_Provider + arguments: { + if (identifier === "bing") { + // Bing supports 1366/1920/UHD resolutions + const w = screenSize.width * Screen.devicePixelRatio > 1920 ? 3840 : 1920; + const h = screenSize.height * Screen.devicePixelRatio > 1080 ? 2160 : 1080; + return [w, h]; + } + return []; + } + updateOverMeteredConnection: cfg_UpdateOverMeteredConnection + } + + onCfg_FillModeChanged: { + resizeComboBox.setMethod() + } + + QQC2.ComboBox { + id: resizeComboBox + Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label:listbox", "Positioning:") + model: [ + { + 'label': i18ndc("plasma_wallpaper_org.kde.potd", "@item:inlistbox", "Scaled and cropped"), + 'fillMode': Image.PreserveAspectCrop + }, + { + 'label': i18ndc("plasma_wallpaper_org.kde.potd", "@item:inlistbox", "Scaled"), + 'fillMode': Image.Stretch + }, + { + 'label': i18ndc("plasma_wallpaper_org.kde.potd", "@item:inlistbox", "Scaled, keep proportions"), + 'fillMode': Image.PreserveAspectFit + }, + { + 'label': i18ndc("plasma_wallpaper_org.kde.potd", "@item:inlistbox", "Centered"), + 'fillMode': Image.Pad + }, + { + 'label': i18ndc("plasma_wallpaper_org.kde.potd", "@item:inlistbox", "Tiled"), + 'fillMode': Image.Tile + } + ] + + textRole: "label" + onActivated: cfg_FillMode = model[currentIndex]["fillMode"] + Component.onCompleted: setMethod(); + + function setMethod() { + for (var i = 0; i < model.length; i++) { + if (model[i]["fillMode"] === cfg_FillMode) { + resizeComboBox.currentIndex = i; + break; + } + } + } + } + + KQC2.ColorButton { + id: colorButton + Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label:chooser", "Background color:") + dialogTitle: i18ndc("plasma_wallpaper_org.kde.potd", "@title:window", "Select Background Color") + } + + Row { + Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label:listbox", "Provider:") + + QQC2.ComboBox { + id: providerComboBox + model: PotdProviderModel { } + currentIndex: model.indexOf(cfg_Provider) + textRole: "display" + valueRole: "id" + onCurrentValueChanged: { + if (currentIndex < 0) { + return; + } + cfg_Provider = currentValue; + } + } + + Kirigami.ContextualHelpButton { + anchors.verticalCenter: providerComboBox.verticalCenter + visible: providerComboBox.model.isNSFW(providerComboBox.currentIndex) + toolTipText: i18ndc("plasma_wallpaper_org.kde.potd", "@info:tooltip", "This wallpaper provider does not filter out images that may be sensitive or objectionable. Use caution if these images will be displayed in public.") + } + } + + QQC2.CheckBox { + id: updateOverMeteredConnectionCheckBox + + checked: cfg_UpdateOverMeteredConnection === 1 + visible: backend.networkInformationAvailable + text: i18ndc("plasma_wallpaper_org.kde.potd", "@option:check", "Update when using metered network connection") + + onToggled: { + cfg_UpdateOverMeteredConnection = checked ? 1 : 0; + } + } + + Kirigami.Separator { + id: previewSeparator + Kirigami.FormData.isSection: true + visible: wallpaperPreview.visible + } + + WallpaperPreview { + id: wallpaperPreview + Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label", "Today's picture:") + backgroundColor: cfg_Color + visible: !!cfg_Provider // provider is not empty + } + + Item { + width: wallpaperPreview.implicitWidth + height: wallpaperPreview.shadowOffset + } + + Item { + Kirigami.FormData.isSection: false + } + + Kirigami.SelectableLabel { + id: titleLabel + Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label", "Title:") + Layout.fillWidth: true + Layout.maximumWidth: wallpaperPreview.implicitWidth * 1.5 + visible: wallpaperPreview.visible && backend.title.length > 0 + font.bold: true + text: backend.title + Accessible.name: titleLabel.Kirigami.FormData.label + } + + Item { + Kirigami.FormData.isSection: false + } + + Kirigami.SelectableLabel { + id: authorLabel + Kirigami.FormData.label: i18ndc("plasma_wallpaper_org.kde.potd", "@label", "Author:") + Layout.fillWidth: true + Layout.maximumWidth: wallpaperPreview.implicitWidth * 1.5 + visible: wallpaperPreview.visible && backend.author.length > 0 + text: backend.author + Accessible.name: authorLabel.Kirigami.FormData.label + } + + Kirigami.InlineMessage { + id: saveMessage + + Kirigami.FormData.isSection: true + Layout.fillWidth: true + + showCloseButton: true + + actions: [ + Kirigami.Action { + icon.name: "document-open-folder" + text: i18ndc("plasma_wallpaper_org.kde.potd", "@action:button", "Open Containing Folder") + visible: backend.saveStatus === PotdBackend.Successful + onTriggered: Qt.openUrlExternally(backend.savedFolder) + + Accessible.description: i18ndc("plasma_wallpaper_org.kde.potd", "@info:whatsthis for a button", "Open the destination folder where the wallpaper image was saved.") + } + ] + + onLinkActivated: Qt.openUrlExternally(backend.savedUrl) + + Connections { + target: backend + + function onSaveStatusChanged() { + switch (backend.saveStatus) { + case PotdBackend.Successful: + saveMessage.text = backend.saveStatusMessage; + saveMessage.type = Kirigami.MessageType.Positive; + break; + case PotdBackend.Failed: + saveMessage.text = backend.saveStatusMessage; + saveMessage.type = Kirigami.MessageType.Error; + break; + default: + return; + } + + saveMessage.visible = true; + } + } + } +} diff --git a/contents/ui/main.qml b/contents/ui/main.qml new file mode 100644 index 0000000..de0f669 --- /dev/null +++ b/contents/ui/main.qml @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2016 Weng Xuetian + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Window 2.15 + +import org.kde.plasma.core as PlasmaCore +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.plasma.wallpapers.potd 1.0 +import org.kde.plasma.plasmoid 2.0 + +WallpaperItem { + id: root + + contextualActions: [ + PlasmaCore.Action { + text: i18nd("plasma_wallpaper_org.kde.potd", "Open Wallpaper Image") + icon.name: "document-open" + onTriggered: Qt.openUrlExternally(backend.localUrl); + } + ] + + QQC2.StackView { + id: imageView + anchors.fill: parent + + readonly property int fillMode: root.configuration.FillMode + readonly property size sourceSize: Qt.size(imageView.width * Screen.devicePixelRatio, imageView.height * Screen.devicePixelRatio) + property Item pendingImage + property bool doesSkipAnimation: true + + onFillModeChanged: Qt.callLater(imageView.loadImage) + onSourceSizeChanged: Qt.callLater(imageView.loadImage) + + function loadImage() { + if (backend.localUrl.length === 0) { + return; + } + if (imageView.pendingImage) { + imageView.pendingImage.statusChanged.disconnect(replaceWhenLoaded); + imageView.pendingImage.destroy(); + imageView.pendingImage = null; + } + + imageView.doesSkipAnimation = imageView.empty || sourceSize !== imageView.currentItem.sourceSize; + imageView.pendingImage = imageComponent.createObject(imageView, { + "source": backend.localUrl, + "fillMode": imageView.fillMode, + "opacity": imageView.doesSkipAnimation ? 1 : 0, + "sourceSize": imageView.sourceSize, + "width": imageView.width, + "height": imageView.height, + }); + imageView.pendingImage.statusChanged.connect(imageView.replaceWhenLoaded); + imageView.replaceWhenLoaded(); + } + + function replaceWhenLoaded() { + if (imageView.pendingImage.status === Image.Loading) { + return; + } + imageView.pendingImage.statusChanged.disconnect(imageView.replaceWhenLoaded); + imageView.replace(imageView.pendingImage, {}, imageView.doesSkipAnimation ? QQC2.StackView.Immediate : QQC2.StackView.Transition); + imageView.pendingImage = null; + } + + PotdBackend { + id: backend + identifier: root.configuration.Provider + arguments: { + if (identifier === "bing") { + // Bing supports 1366/1920/UHD resolutions + const w = imageView.sourceSize.width > 1920 ? 3840 : 1920; + const h = imageView.sourceSize.height > 1080 ? 2160 : 1080; + return [w, h]; + } + return []; + } + updateOverMeteredConnection: root.configuration.UpdateOverMeteredConnection + + onImageChanged: Qt.callLater(imageView.loadImage) + onLocalUrlChanged: Qt.callLater(imageView.loadImage) + } + + Component { + id: imageComponent + + Image { + asynchronous: true + cache: false + autoTransform: true + smooth: true + + QQC2.StackView.onActivated: root.accentColorChanged() + QQC2.StackView.onDeactivated: destroy() + QQC2.StackView.onRemoved: destroy() + } + } + + Rectangle { + id: backgroundColor + anchors.fill: parent + color: root.configuration.Color + Behavior on color { + ColorAnimation { duration: Kirigami.Units.longDuration } + } + } + + replaceEnter: Transition { + OpacityAnimator { + id: replaceEnterOpacityAnimator + to: 1 + // As the wallpaper is updated once a day, the transition should be longer. + duration: Math.round(Kirigami.Units.veryLongDuration * 5) + } + } + // Keep the old image around till the new one is fully faded in + // If we fade both at the same time you can see the background behind glimpse through + replaceExit: Transition { + PauseAnimation { + // 500: The exit transition starts first and can be completed earlier than the enter transition + duration: replaceEnterOpacityAnimator.duration + 500 + } + } + } +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..362fd37 --- /dev/null +++ b/metadata.json @@ -0,0 +1,19 @@ +{ + "KPackageStructure": "Plasma/Wallpaper", + "KPlugin": { + "Authors": [ + { + "Email": "potd@johnbotr.is", + "Name": "John Botris" + } + ], + "BugReportUrl": "https://google.com", + "Description": "A new picture from the Internet each day", + "EnabledByDefault": true, + "Icon": "office-calendar", + "Id": "botris.dev.potd", + "License": "LGPL-2.1", + "Name": "My Picture of the day", + "Website": "https://kde.org/" + } +}