diff --git a/package/contents/config/main.xml b/package/contents/config/main.xml index 42b8cb3..74c9973 100644 --- a/package/contents/config/main.xml +++ b/package/contents/config/main.xml @@ -5,6 +5,10 @@ + + + false + Shaders6/Ps3menu.frag.qsb diff --git a/package/contents/ui/KomplexModel.qml b/package/contents/ui/KomplexModel.qml index 7449fd8..8ac911a 100644 --- a/package/contents/ui/KomplexModel.qml +++ b/package/contents/ui/KomplexModel.qml @@ -58,14 +58,6 @@ Rectangle property var pack: wallpaper.configuration.shader_package - // onPackChanged: () => - // { - // if(mainItem.ready) - // { - // shaderPackModel.loadJson(pack) - // } - // } - id: mainItem color: "black" @@ -108,11 +100,7 @@ Rectangle property var bufferB property var bufferC property var bufferD - - // ShaderChannel { id: bufferA; visible: false; anchors.fill: parent } - // ShaderChannel { id: bufferB; visible: false; anchors.fill: parent } - // ShaderChannel { id: bufferC; visible: false; anchors.fill: parent } - // ShaderChannel { id: bufferD; visible: false; anchors.fill: parent } + iTime: mainItem.iTime iMouse: mainItem.iMouse iResolution: mainItem.iResolution @@ -230,35 +218,7 @@ Rectangle // Recursive helper function to parse channels function parseChannel(channel, json, typeDefault = 2, autodestroy = true) { - var source = getFilePath(json.source) - - channel.frameBufferChannel = json.frame_buffer_channel !== undefined ? json.frame_buffer_channel : -1 - channel.type = json.type !== undefined ? json.type : typeDefault - channel.visible = false - channel.iMouse = Qt.binding(() => { return mainItem.iMouse; }) - channel.iTime = Qt.binding(() => { return mainItem.iTime; }) - channel.iResolutionScale = json.resolution_scale ? json.resolution_scale : 1.0 - channel.iResolution = Qt.binding(() => { return json.resolution_x ? Qt.vector3d(json.resolution_x, json.resolution_y, 1.0) : Qt.vector3d(mainItem.iResolution.x,mainItem.iResolution.y,1.0); }) - channel.mouseBias = json.mouse_scale ? json.mouse_scale : 1.0 - channel.iTimeScale = json.time_scale ? json.time_scale : 1.0 - channel.iTimeDelta = Qt.binding(() => { return mainItem.iTimeDelta; }) - channel.width = Qt.binding(() => channel.iResolution.x) - channel.height = Qt.binding(() => channel.iResolution.y) - - channel.iChannelTime = Qt.binding(() => { - return [ - mainItem.iTime * channel.iTimeScale, - mainItem.iTime * channel.iTimeScale, - mainItem.iTime * channel.iTimeScale, - mainItem.iTime * channel.iTimeScale - ]; - }); - - channel.iFrameRate = Qt.binding(() => { return mainItem.iFrameRate; }) - channel.iFrame = mainItem.iFrame - channel.invert = json.invert ? json.invert : false - - channel.source = source + var component = Qt.createComponent("./ShaderChannel.qml") if (json.channel0) { @@ -271,9 +231,8 @@ Rectangle } else if(typeof json.channel0 === "object") { - var component = Qt.createComponent("./ShaderChannel.qml") - - if (component.status === Component.Ready) { + if (component.status === Component.Ready) + { channel.iChannel0 = component.createObject(mainItem, { }) parseChannel(channel.iChannel0, json.channel0) } @@ -293,9 +252,8 @@ Rectangle } else if(typeof json.channel1 === "object") { - var component = Qt.createComponent("./ShaderChannel.qml") - - if (component.status === Component.Ready) { + if (component.status === Component.Ready) + { channel.iChannel1 = component.createObject(mainItem, { }) parseChannel(channel.iChannel1, json.channel1) } @@ -315,9 +273,8 @@ Rectangle } else if(typeof json.channel2 === "object") { - var component = Qt.createComponent("./ShaderChannel.qml") - - if (component.status === Component.Ready) { + if (component.status === Component.Ready) + { channel.iChannel2 = component.createObject(mainItem, { }) parseChannel(channel.iChannel2, json.channel2) } @@ -337,9 +294,8 @@ Rectangle } else if(typeof json.channel3 === "object") { - var component = Qt.createComponent("./ShaderChannel.qml") - - if (component.status === Component.Ready) { + if (component.status === Component.Ready) + { channel.iChannel3 = component.createObject(mainItem, { }) parseChannel(channel.iChannel3, json.channel3) } @@ -348,6 +304,37 @@ Rectangle console.log('Uknown channel type 3 ' + typeof json.channel3) } + channel.frameBufferChannel = json.frame_buffer_channel !== undefined ? json.frame_buffer_channel : -1 + channel.type = json.type !== undefined ? json.type : typeDefault + channel.visible = false + channel.iMouse = Qt.binding(() => { return mainItem.iMouse; }) + channel.iTime = Qt.binding(() => { return mainItem.iTime; }) + channel.iResolutionScale = json.resolution_scale ? json.resolution_scale : 1.0 + channel.iResolution = Qt.binding(() => { return json.resolution_x ? Qt.vector3d(json.resolution_x, json.resolution_y, 1.0) : Qt.vector3d(mainItem.iResolution.x,mainItem.iResolution.y,1.0); }) + channel.mouseBias = json.mouse_scale ? json.mouse_scale : 1.0 + channel.iTimeScale = json.time_scale ? json.time_scale : 1.0 + channel.iTimeDelta = Qt.binding(() => { return mainItem.iTimeDelta; }) + channel.width = Qt.binding(() => channel.iResolution.x) + channel.height = Qt.binding(() => channel.iResolution.y) + channel.materialTexture = json.materialTexture !== undefined ? getFilePath(json.materialTexture) : "" + channel.materialShader = json.materialShader !== undefined ? getFilePath(json.materialShader) : "" + + channel.iChannelTime = Qt.binding(() => { + return [ + mainItem.iTime * channel.iTimeScale, + mainItem.iTime * channel.iTimeScale, + mainItem.iTime * channel.iTimeScale, + mainItem.iTime * channel.iTimeScale + ]; + }); + + channel.iFrameRate = Qt.binding(() => { return mainItem.iFrameRate; }) + channel.iFrame = mainItem.iFrame + channel.invert = json.invert ? json.invert : false + + var source = getFilePath(json.source) + channel.source = source + if(autodestroy) data.channels.push(channel) } @@ -364,11 +351,13 @@ Rectangle var component = Qt.createComponent("./ShaderChannel.qml") - if (component.status === Component.Ready) { + if (component.status === Component.Ready) + { channelOutput.bufferA = component.createObject(channelOutput, { visible: false }) channelOutput.bufferB = component.createObject(channelOutput, { visible: false }) channelOutput.bufferC = component.createObject(channelOutput, { visible: false }) channelOutput.bufferD = component.createObject(channelOutput, { visible: false }) + data.buffers.set("{bufferA}", channelOutput.bufferA) data.buffers.set("{bufferB}", channelOutput.bufferB) data.buffers.set("{bufferC}", channelOutput.bufferC) @@ -401,11 +390,11 @@ Rectangle // Generate a new ShaderEffectSource for the requested buffer function createBufferAssociation(buffer) { - var component = Qt.createComponent('./ShaderBuffer.qml') var result - if (component.status === Component.Ready) { + if (component.status === Component.Ready) + { result = component.createObject(mainItem, { x:0, y:0, diff --git a/package/contents/ui/PexelsImageHub.qml b/package/contents/ui/PexelsImageHub.qml new file mode 100644 index 0000000..f0af06e --- /dev/null +++ b/package/contents/ui/PexelsImageHub.qml @@ -0,0 +1,434 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Layouts + +import Komplex.Pexels.Image as Pexels + +Item +{ + id: mainItem + anchors.fill: parent + + Pexels.SearchModel + { + id: searchModel + } + + ColumnLayout + { + anchors.fill: parent + + RowLayout + { + Layout.fillHeight: false + Layout.fillWidth: true + Layout.margins: 6 + + TextField + { + Layout.preferredHeight: 32 + Layout.fillWidth: true + + id: searchField + placeholderText: "Search" + onEditingFinished: mainItem.updateSearch() + Keys.onPressed: (event) => + { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) + { + searchField.focus = false; // Unfocus the TextField + event.accepted = true; // Prevent further propagation of the key event + } + } + } + + Button + { + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 + + icon.name: "search-symbolic" + + onClicked: mainItem.updateSearch() + } + } + + Component + { + id: highlight + Rectangle { + width: view.cellWidth; height: view.cellHeight + color: palette.highlight; radius: 5 + x: view.currentItem.x + y: view.currentItem.y + Behavior on x { SpringAnimation { spring: 3; damping: 0.2 } } + Behavior on y { SpringAnimation { spring: 3; damping: 0.2 } } + } + } + + Rectangle + { + Layout.fillHeight: true + Layout.fillWidth: true + color: palette.base + clip: true + + RowLayout + { + anchors.fill: parent + + GridView + { + // The standard size + property int idealCellHeight: 300 + property int idealCellWidth: 300 + cellWidth: width / Math.floor(width / idealCellWidth) + cellHeight: idealCellHeight + + id: view + + model: searchModel + highlight: highlight + highlightFollowsCurrentItem: false + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 6 + + delegate: Column + { + property int itemIndex: index + property int originalHeight: imageHeight + property int originalWidth: imageWidth + property int imageId: id + property string author: photographer + property string authorUrl: photographerUrl + property string imageUrl: original + property string thumbnailUrl: thumbnail + property string altText: alt + property string largeThumbnail: large + + id: entry + + leftPadding: Math.floor((width - thumbnailImage.width) / 2) + topPadding: 10 + rightPadding: Math.floor((width - thumbnailImage.width) / 2) + bottomPadding: 10 + width: view.cellWidth + + Image + { + width: 280 + height: 200 + id: thumbnailImage + source: thumbnail + anchors.horizontalCenter: parent.horizontalCenter + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => { + view.currentIndex = parent.parent.itemIndex + } + } + + Rectangle + { + color: palette.base + anchors.fill: parent + visible: thumbnailImage.status === Image.Loading + + RowLayout + { + anchors.fill: parent + + BusyIndicator + { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 64 + Layout.preferredWidth: 64 + visible: running + } + } + } + } + Text + { + property string externalLink: photographerUrl + + elide: Text.ElideRight + topPadding: 4 + bottomPadding: 2 + text: "

" + photographer + "

" + anchors.horizontalCenter: parent.horizontalCenter + width: 280 + color: palette.link + font.bold: true + + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => { + Qt.openUrlExternally(parent.externalLink) + } + } + } + Text + { + leftPadding: 8 + rightPadding: 8 + text: qsTr(alt) + anchors.horizontalCenter: parent.horizontalCenter + elide: Text.ElideRight + width: 280 + color: palette.text + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + maximumLineCount: 2 + font.italic: true + } + RowLayout + { + visible: parent.itemIndex === view.currentIndex + width: 280 + Button + { + Layout.topMargin: 4 + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 + icon.name: "emblem-downloads" + onClicked: { + downloadDialog.imageHeight = entry.originalHeight + downloadDialog.imageWidth = entry.originalWidth + downloadDialog.photographer = entry.author + downloadDialog.photographerUrl = entry.authorUrl + downloadDialog.alt = entry.altText + downloadDialog.thumbnail = entry.largeThumbnail + downloadDialog.imageUrl = entry.imageUrl + downloadDialog.id = entry.imageId + downloadDialog.open() + } + } + } + + } + populate: Transition + { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 1000 } + } + } + } + } + + RowLayout + { + Layout.margins: 6 + Layout.fillWidth: true + + Button + { + text: "Previous" + enabled: searchModel.previousPage !== "" + onClicked: searchModel.back() + } + + RowLayout + { + Layout.margins: 6 + Layout.fillWidth: true + + Text + { + visible: searchModel.totalResults > 0 + color: palette.text + text: ((searchModel.resultsPerPage * searchModel.currentPage) - searchModel.resultsPerPage + 1) + "-" + (searchModel.resultsPerPage * searchModel.currentPage) + " of " + searchModel.totalResults + } + + Text + { + color: palette.text + Layout.fillWidth: true + text: "Page " + searchModel.currentPage + " of " + Math.ceil(searchModel.totalResults / searchModel.resultsPerPage) + } + + Text + { + color: palette.link + text: "Photos provided by Pexels" + font.bold: true + + onLinkActivated: (link) => Qt.openUrlExternally(link) + } + } + Button + { + text: "Next" + enabled: searchModel.nextPage !== "" + onClicked: searchModel.next() + } + } + } + + Rectangle + { + color: palette.base + anchors.fill: parent + visible: searchModel.status === Pexels.SearchModel.Searching + + RowLayout + { + anchors.fill: parent + + BusyIndicator + { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 128 + Layout.preferredWidth: 128 + visible: running + } + } + } + + Dialog + { + property string photographer + property string photographerUrl + property string imageUrl + property string thumbnail + property string alt + property int imageHeight + property int imageWidth + property int id + + id: downloadDialog + modal: Qt.WindowModal + width: 600 + height: 420 + + anchors.centerIn: parent + clip: true + + ColumnLayout + { + anchors.fill: parent + + Text + { + property string externalLink: downloadDialog.photographerUrl + + elide: Text.ElideRight + topPadding: 4 + bottomPadding: 2 + text: "

" + downloadDialog.photographer + "

" + width: 280 + color: palette.link + font.bold: true + + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => { + Qt.openUrlExternally(parent.externalLink) + } + } + } + + Image + { + Layout.fillHeight: true + Layout.fillWidth: true + + fillMode: Image.PreserveAspectCrop + source: downloadDialog.thumbnail + } + + Text + { + Layout.fillWidth: true + leftPadding: 8 + rightPadding: 8 + text: qsTr(downloadDialog.alt) + elide: Text.ElideRight + width: 280 + color: palette.text + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + maximumLineCount: 4 + font.italic: true + } + + Button + { + Layout.fillWidth: true + text: "Download (" + downloadDialog.imageWidth + "x" + downloadDialog.imageHeight + ")" + icon.name: "image-symbolic" + + onClicked: () => + { + downloadDialog.close(); + progressDialog.thumbnail = downloadDialog.thumbnail + progressDialog.photographer = downloadDialog.photographer + progressDialog.open() + searchModel.download(downloadDialog.imageUrl, downloadDialog.id) + } + } + } + } + + Dialog + { + property string photographer + property string thumbnail + + modal: Qt.WindowModal + width: 600 + height: 420 + + anchors.centerIn: parent + clip: true + + id: progressDialog + + ColumnLayout + { + anchors.fill: parent + + Image + { + Layout.fillHeight: true + Layout.fillWidth: true + + fillMode: Image.PreserveAspectCrop + source: downloadDialog.thumbnail + } + + Text + { + text: "Downloading Photo..." + color: palette.text + } + + ProgressBar + { + value: searchModel.downloadProgress + Layout.fillWidth: true + Layout.preferredHeight: 6 + } + } + + Connections + { + target: searchModel + function onDownloadFinished() + { + progressDialog.close() + } + } + } + + function updateSearch() + { + console.log(searchField.text) + searchModel.query = searchField.text + } + +} diff --git a/package/contents/ui/PexelsVideoHub.qml b/package/contents/ui/PexelsVideoHub.qml new file mode 100644 index 0000000..0399037 --- /dev/null +++ b/package/contents/ui/PexelsVideoHub.qml @@ -0,0 +1,587 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Layouts +import QtMultimedia + +import Komplex.Pexels.Video as Pexels + +Item +{ + property alias selectedFile: searchModel.lastSavedFile + + id: mainItem + anchors.fill: parent + + Pexels.SearchModel + { + id: searchModel + } + + ColumnLayout + { + anchors.fill: parent + + RowLayout + { + Layout.fillHeight: false + Layout.fillWidth: true + Layout.margins: 6 + + TextField + { + Layout.preferredHeight: 32 + Layout.fillWidth: true + + id: searchField + placeholderText: "Search" + onEditingFinished: mainItem.updateSearch() + Keys.onPressed: (event) => + { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) + { + searchField.focus = false; // Unfocus the TextField + event.accepted = true; // Prevent further propagation of the key event + } + } + } + + Button + { + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 + + icon.name: "search-symbolic" + + onClicked: mainItem.updateSearch() + } + } + + Component + { + id: highlight + Rectangle + { + width: view.cellWidth; height: view.cellHeight + color: palette.highlight; radius: 5 + x: view.currentItem.x + y: view.currentItem.y + Behavior on x { SpringAnimation { spring: 3; damping: 0.2 } } + Behavior on y { SpringAnimation { spring: 3; damping: 0.2 } } + } + } + + Rectangle + { + Layout.fillHeight: true + Layout.fillWidth: true + color: palette.base + clip: true + + RowLayout + { + anchors.fill: parent + + GridView + { + // The standard size + property int idealCellHeight: 220 + property int idealCellWidth: 250 + cellWidth: width / Math.floor(width / idealCellWidth) + cellHeight: idealCellHeight + + id: view + + model: searchModel + highlight: highlight + highlightFollowsCurrentItem: false + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 6 + + delegate: Column + { + property int itemIndex: index + property int originalHeight: videoHeight + property int originalWidth: videoWidth + property int videoId: id + property string author: user + property string authorUrl: userUrl + property string videoUrl: videoUrl + property string thumbnailUrl: thumbnail + + id: entry + + leftPadding: Math.floor((width - thumbnailImage.width) / 2) + topPadding: 10 + rightPadding: Math.floor((width - thumbnailImage.width) / 2) + bottomPadding: 10 + width: view.cellWidth + + Image + { + width: 250 + height: 140 + id: thumbnailImage + source: thumbnail + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle + { + color: palette.base + anchors.fill: parent + visible: thumbnailImage.status === Image.Loading + + RowLayout + { + anchors.fill: parent + + BusyIndicator + { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 64 + Layout.preferredWidth: 64 + visible: running + } + } + } + + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => + { + view.currentIndex = parent.parent.itemIndex + searchModel.currentIndex = view.currentIndex + } + } + } + + Text + { + property string externalLink: authorUrl + + elide: Text.ElideRight + topPadding: 4 + bottomPadding: 2 + text: "

" + author + "

" + anchors.horizontalCenter: parent.horizontalCenter + width: 280 + color: palette.link + + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => + { + Qt.openUrlExternally(parent.externalLink) + } + cursorShape: Qt.PointingHandCursor + } + } + + RowLayout + { + visible: parent.itemIndex === view.currentIndex + width: thumbnailImage.width + Button + { + Layout.topMargin: 4 + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 32 + Layout.fillWidth: true + icon.name: "emblem-downloads" + text: qsTr("Preview & Download") + + onClicked: () => + { + downloadDialog.user = entry.author + downloadDialog.userUrl = entry.authorUrl + downloadDialog.thumbnail = entry.thumbnailUrl + downloadDialog.id = entry.videoId + downloadDialog.open() + searchModel.videoModel.update() + } + } + } + } + populate: Transition + { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 1000 } + } + } + } + } + + RowLayout + { + Layout.margins: 6 + Layout.fillWidth: true + + Button + { + text: "Previous" + enabled: searchModel.previousPage !== "" + onClicked: searchModel.back() + } + + RowLayout + { + Layout.margins: 6 + Layout.fillWidth: true + + Text + { + visible: searchModel.totalResults > 0 + color: palette.text + text: ((searchModel.resultsPerPage * searchModel.currentPage) - searchModel.resultsPerPage + 1) + "-" + (searchModel.resultsPerPage * searchModel.currentPage) + " of " + searchModel.totalResults + } + + Text + { + color: palette.text + Layout.fillWidth: true + text: "Page " + searchModel.currentPage + " of " + Math.ceil(searchModel.totalResults / searchModel.resultsPerPage) + } + + Text + { + color: palette.text + text: "Videos provided by Pexels" + font.bold: true + + onLinkActivated: (link) => Qt.openUrlExternally(link) + + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => + { + Qt.openUrlExternally("https://www.pexels.com") + } + cursorShape: Qt.PointingHandCursor + } + } + } + Button + { + text: "Next" + enabled: searchModel.nextPage !== "" + onClicked: searchModel.next() + } + } + } + + Rectangle + { + color: palette.base + anchors.fill: parent + visible: searchModel.status === Pexels.SearchModel.Searching + + RowLayout + { + anchors.fill: parent + + BusyIndicator + { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 128 + Layout.preferredWidth: 128 + visible: running + } + } + } + + Dialog + { + property string user + property string userUrl + property string thumbnail + property string preview + property int id + + id: downloadDialog + modal: Qt.WindowModal + width: 600 + height: 440 + + anchors.centerIn: parent + clip: true + + ColumnLayout + { + anchors.fill: parent + + Text + { + property string externalLink: downloadDialog.userUrl + + elide: Text.ElideRight + text: "

" + downloadDialog.user + "

" + width: 280 + color: palette.link + + MouseArea + { + anchors.fill: parent + onClicked: (mouse) => + { + Qt.openUrlExternally(parent.externalLink) + } + cursorShape: Qt.PointingHandCursor + } + } + + Rectangle + { + Layout.fillWidth: true + Layout.preferredHeight: 300 + color: "black" + ColumnLayout + { + anchors.fill: parent + spacing: 0 + + VideoOutput + { + property alias duration: mediaPlayer.duration + property alias mediaSource: mediaPlayer.source + property alias metaData: mediaPlayer.metaData + property alias playbackRate: mediaPlayer.playbackRate + property alias position: mediaPlayer.position + property alias seekable: mediaPlayer.seekable + property alias volume: audioOutput.volume + + signal sizeChanged + signal fatalError + + id: videoOutput + + visible: true + Layout.preferredWidth: 500 + Layout.preferredHeight: 281 + Layout.alignment: Qt.AlignHCenter + fillMode: VideoOutput.PreserveAspectCrop + smooth: true + + onHeightChanged: this.sizeChanged() + + MediaPlayer + { + id: mediaPlayer + videoOutput: videoOutput + source: Qt.resolvedUrl(downloadSelector.currentValue ? downloadSelector.currentValue : "") + + audioOutput: AudioOutput + { + id: audioOutput + volume: 0 + } + + onErrorOccurred: function(error, errorString) + { + if (MediaPlayer.NoError !== error) + { + console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString) + videoOutput.fatalError() + } + } + + onSourceChanged: + { + if(mediaPlayer.source !== "") + mediaPlayer.play() + else + mediaPlayer.stop() + } + } + + function start() { mediaPlayer.play() } + function stop() { mediaPlayer.stop() } + function seek(position) { mediaPlayer.setPosition(position); } + + Image + { + visible: !mediaPlayer.playing + anchors.fill: parent + + fillMode: Image.PreserveAspectFill + source: downloadDialog.thumbnail + } + } + + Rectangle + { + Layout.fillWidth: true + Layout.preferredHeight: 18 + color: palette.alternateBase + RowLayout + { + anchors.fill: parent + spacing: 0 + + ProgressBar + { + Layout.fillWidth: true + Layout.fillHeight: true + from: 0 + to: mediaPlayer.duration + value: mediaPlayer.position + } + Button + { + Layout.preferredHeight: 18 + Layout.preferredWidth: 18 + icon.name: mediaPlayer.playing ? "stop-symbolic" : "play-symbolic" + icon.height: 16 + icon.width: 16 + onClicked: () => + { + if(mediaPlayer.playing) + mediaPlayer.stop() + else + mediaPlayer.play() + } + } + } + } + } + } + + Text + { + text: qsTr("Download Options") + color: palette.text + font.bold: true + Layout.fillHeight: true + verticalAlignment: Text.AlignBottom + } + + RowLayout + { + Layout.fillWidth: true + Layout.preferredHeight: downloadSelector.height + ComboBox + { + id: downloadSelector + Layout.fillWidth: true + model: searchModel.videoModel + textRole: "text" + valueRole: "url" + } + + Button + { + enabled: downloadSelector.currentIndex >= 0 + Layout.preferredHeight: downloadSelector.height + Layout.preferredWidth: downloadSelector.height + icon.name: "image-symbolic" + + id: downloadButton + + onClicked: () => + { + downloadDialog.close(); + progressDialog.thumbnail = downloadDialog.thumbnail + progressDialog.author = downloadDialog.user + progressDialog.open() + searchModel.videoModel.download(downloadSelector.currentIndex) + } + } + } + } + + Connections + { + target: searchModel.videoModel + function onStatusChanged() + { + if(searchModel.videoModel.status === 0) + downloadSelector.currentIndex = 0 + } + } + + onClosed: () => + { + mediaPlayer.stop() + } + + Rectangle + { + color: palette.base + anchors.fill: parent + visible: searchModel.videoModel.status === 1 + + RowLayout + { + anchors.fill: parent + + BusyIndicator + { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 128 + Layout.preferredWidth: 128 + visible: running + } + } + } + } + + Dialog + { + property string author + property string thumbnail + + modal: Qt.WindowModal + width: 600 + height: 420 + + anchors.centerIn: parent + clip: true + + id: progressDialog + + ColumnLayout + { + anchors.fill: parent + + Image + { + Layout.fillHeight: true + Layout.fillWidth: true + + fillMode: Image.PreserveAspectCrop + source: downloadDialog.thumbnail + } + + Text + { + text: "Downloading Video..." + color: palette.text + } + + ProgressBar + { + value: searchModel.videoModel.downloadProgress + Layout.fillWidth: true + Layout.preferredHeight: 6 + } + } + + Connections + { + target: searchModel.videoModel + function onDownloadFinished() + { + progressDialog.close() + } + } + } + + function updateSearch() + { + console.log(searchField.text) + searchModel.query = searchField.text + } +} diff --git a/package/contents/ui/ShaderChannel.qml b/package/contents/ui/ShaderChannel.qml index 295d047..85780c9 100644 --- a/package/contents/ui/ShaderChannel.qml +++ b/package/contents/ui/ShaderChannel.qml @@ -43,7 +43,8 @@ Item VideoChannel, ShaderChannel, CubeMapChannel, - AudioChannel + AudioChannel, + SceneChannel } property int type: ShaderChannel.Type.ImageChannel @@ -70,6 +71,9 @@ Item property real iTimeScale: 1 // This is used to scale the time for the shader, allowing for slow motion or fast forward effects per channel property int frameBufferChannel: -1 property bool blending: false + property string materialTexture:"" + property string materialShader:"" + property var windowModel // bind to ShaderEffectSource.live to prevent data being updated causing a refresh between intended frames. I think this may be why // the shaders are using so many resources. This is likely to cause issues of its own since we have no way of knowing the progress @@ -80,8 +84,20 @@ Item // but god damn does it feel hacky. Try to find a better way to actually limit framerate onIFrameChanged: () => { - active = true; - active = false; + // this method of frame limiting breaks video playback + // even when the video is the source of an item being limited + // in this way + + // if(type === ShaderChannel.ShaderChannel) + // { + // active = true; + // active = false; + + // return; + // } + + // if(active === false) + // active = true; } property bool invert @@ -162,10 +178,14 @@ Item PropertyChanges { loader.sourceComponent: channelAudio - - // loader.width: 512 - // loader.height: 2 - // loader.and + } + }, + State + { + when: channel.type === ShaderChannel.Type.SceneChannel + PropertyChanges + { + loader.sourceComponent: Qt.createComponent(channel.source) } } ] @@ -206,35 +226,42 @@ Item Component { id: channelVideo - - VideoOutput + Rectangle { - property alias duration: mediaPlayer.duration - property alias mediaSource: mediaPlayer.source - property alias metaData: mediaPlayer.metaData - property alias playbackRate: mediaPlayer.playbackRate - property alias position: mediaPlayer.position - property alias seekable: mediaPlayer.seekable - property alias volume: audioOutput.volume - - signal sizeChanged - signal fatalError - - id: videoOutput - - visible: true anchors.fill: parent - fillMode: VideoOutput.PreserveAspectCrop - smooth: true + color: "black" - onHeightChanged: this.sizeChanged() + VideoOutput + { + property alias duration: mediaPlayer.duration + property alias mediaSource: mediaPlayer.source + property alias metaData: mediaPlayer.metaData + property alias playbackRate: mediaPlayer.playbackRate + property alias position: mediaPlayer.position + property alias seekable: mediaPlayer.seekable + property alias volume: audioOutput.volume + property bool loaded: false - MediaPlayer + signal sizeChanged + signal fatalError + + id: videoComponent + + visible: true + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectCrop + smooth: true + + onHeightChanged: this.sizeChanged() + } + + MediaPlayer { id: mediaPlayer - videoOutput: videoOutput + videoOutput: videoComponent loops: MediaPlayer.Infinite source: Qt.resolvedUrl(channel.source) + playbackRate: channel.iTimeScale >= 0.01 ? channel.iTimeScale : 0.01 audioOutput: AudioOutput { @@ -242,22 +269,96 @@ Item volume: 0 } - onErrorOccurred: function(error, errorString) + onErrorOccurred: (error, errorString) => { if (MediaPlayer.NoError !== error) { console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString) - videoOutput.fatalError() + videoComponent.fatalError() } } onSourceChanged: { - if(mediaPlayer.source != "") - mediaPlayer.play() - else - mediaPlayer.stop() + if(!videoComponent.loaded) + return; + delayedStartTimer.start() } + + onMediaStatusChanged: + { + switch(mediaStatus) + { + case MediaPlayer.NoMedia: + console.log("No media loaded") + break; + case MediaPlayer.LoadingMedia: + console.log("Video loading") + break; + case MediaPlayer.LoadedMedia: + console.log("Video Loaded") + break; + case MediaPlayer.BufferingMedia: + console.log("Video buffering") + break; + case MediaPlayer.StalledMedia: + console.log("Video stalled") + break; + case MediaPlayer.BufferedMedia: + console.log("Video buffered") + break; + case MediaPlayer.EndOfMedia: + console.log("Video EOF") + break; + case MediaPlayer.InvalidMedia: + console.log("Video invalid") + break; + } + } + + onPlaybackStateChanged: + { + switch(playbackState) + { + case MediaPlayer.PlayingState: + console.log("Video playback started") + break; + case MediaPlayer.PausedState: + console.log("Video playback paused") + break; + case MediaPlayer.StoppedState: + console.log("Video playback stopped") + break; + } + } + + Component.onCompleted: + { + videoComponent.loaded = true + delayedStartTimer.start() + } + + function autoStart() + { + if(mediaPlayer.source !== "" && mediaPlayer.source !== undefined) + { + console.log("Starting playback of " + mediaPlayer.source) + mediaPlayer.play() + } + else + { + console.log("Stopping playback") + mediaPlayer.stop() + } + } + } + + Timer + { + id: delayedStartTimer + interval: 500 + repeat: false + onTriggered: mediaPlayer.autoStart() } function start() { mediaPlayer.play() } @@ -395,14 +496,6 @@ Item blending: channel.blending } - - // ShaderEffectSource - // { - // anchors.fill: parent - // sourceItem: channelShaderOutput - // hideSource: true - // visible: true - // } } } @@ -530,4 +623,129 @@ Item } } } -} \ No newline at end of file + + // Scene is directly loaded, no comp needed + + // 3D Model + Component + { + id: modelComponent + + Item + { + anchors.fill: parent + id: channelModelContent + + // recursive frame buffer + ShaderEffectSource + { + id: frameBufferSource + sourceItem: channel.frameBufferChannel === -1 ? null : channelModelContent + sourceRect: Qt.rect(0,0, channelModelContent.width, channelModelContent.height) + wrapMode: ShaderEffectSource.ClampToEdge + live: channel.active + mipmap: true + recursive: true + textureSize: Qt.size(channelModelContent.width, channelModelContent.height) + visible: false + textureMirroring: ShaderEffectSource.NoMirroring + width: channel.iResolution.x + height: channel.iResolution.y + } + + View3D + { + id: view3d + anchors.fill: parent + + property real lastX: 0 + property real lastY: 0 + property bool mousePressed: false + property real yaw: 0 + property real pitch: 0 + + environment: SceneEnvironment + { + backgroundMode: SceneEnvironment.SkyBoxCubeMap + skyBoxCubeMap: CubeMapTexture + { + source: channel.source !== "" ? Qt.resolvedUrl(channel.source) + "/%p.jpg" : "" + } + } + + camera: PerspectiveCamera + { + id: camera + position: Qt.vector3d(0, 0, 10) + } + + function updateCamera() + { + yaw -= (data.iMouse.x - lastX) * 0.5 + pitch -= (data.iMouse.y - lastY) * -0.5 + pitch = Math.max(-89, Math.min(89, pitch)) + lastX = data.iMouse.x + lastY = data.iMouse.y + + var radYaw = yaw * Math.PI / 180 + var radPitch = pitch * Math.PI / 180 + var x = Math.cos(radPitch) * Math.sin(radYaw) + var y = Math.sin(radPitch) + var z = Math.cos(radPitch) * Math.cos(radYaw) + + camera.eulerRotation = Qt.vector3d((radPitch * 180 / Math.PI), (radYaw * 180 / Math.PI), 0) + } + + Connections + { + target: channel + function onIMouseChanged() + { + view3d.updateCamera() + } + } + + Model + { + source: Qt.resolvedUrl(channel.source) + scale: Qt.vector3d(channel.scale) + geometry: Komplex.GeometryProvider + { + source: Qt.resolvedUrl(channel.source) + } + materials: + [ + CustomMaterial + { + property var iChannel0: channel.frameBufferChannel === 0 ? frameBufferSource : channelSource0 + property var iChannel1: channel.frameBufferChannel === 1 ? frameBufferSource : channelSource1 + property var iChannel2: channel.frameBufferChannel === 2 ? frameBufferSource : channelSource2 + property var iChannel3: channel.frameBufferChannel === 3 ? frameBufferSource : channelSource3 + + property var iResolution: channel.iResolution + property var iTime: data.iTime + property var iTimeDelta: channel.iTimeDelta + property var iChannelTime: channel.iChannelTime + property var iSampleRate: channel.iSampleRate + property var iFrame: channel.iFrame + property var iFrameRate: channel.iFrameRate + property var iMouse: data.iMouse + property var iDate: channel.iDate + + property var iChannelResolution: channel.iResolution + + Texture + { + id: baseColorMap + source: Qt.resolvedUrl(channel.materialTexture) + } + + cullMode: PrincipledMaterial.NoCulling + fragmentShader: Qt.resolvedUrl(channel.materialShader) + } + ] + } + } + } + } +} diff --git a/package/contents/ui/ShaderChannelConfiguration.qml b/package/contents/ui/ShaderChannelConfiguration.qml index ab8a366..aa840cf 100644 --- a/package/contents/ui/ShaderChannelConfiguration.qml +++ b/package/contents/ui/ShaderChannelConfiguration.qml @@ -74,6 +74,7 @@ Item property int resolution_y property bool enabled property bool invert + property bool changed id: window @@ -116,6 +117,16 @@ Item type: ShaderChannel.Type.ImageChannel } + ListElement + { + file: true + name: "Scene" + icon: "./icons/image.svg" + title: "Select a scene file" + filter: "Image Files (*.qml)" + type: ShaderChannel.Type.SceneChannel + } + ListElement { file: true @@ -574,8 +585,8 @@ Item function accept() { // copy over temp values - source = tmp_source type = tmp_type + source = tmp_source timeScale = tmp_timeScale resolution_scale = tmp_resolution_scale resolution_x = tmp_resolution_x @@ -629,6 +640,12 @@ Item // Function to reset the selection to default values function resetSelection() { + if((tmp_source !== source) || (tmp_enabled !== enabled) || + (tmp_invert !== invert) || (tmp_resolution_scale !== resolution_scale) || + (tmp_resolution_x !== resolution_x) || (tmp_resolution_y !== resolution_y) || + (tmp_timeScale !== timeScale) || (tmp_type !== type)) + changed = true; + tmp_source = source tmp_timeScale = timeScale tmp_resolution_scale = resolution_scale diff --git a/package/contents/ui/ShaderToyHub.qml b/package/contents/ui/ShaderToyHub.qml new file mode 100644 index 0000000..cbc4700 --- /dev/null +++ b/package/contents/ui/ShaderToyHub.qml @@ -0,0 +1,577 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Layouts +import QtMultimedia +import QtWebView + +import Komplex.ShaderToy as ShaderToy + +Item +{ + id: mainItem + anchors.fill: parent + + ShaderToy.SearchModel + { + id: searchModel + } + + ColumnLayout + { + anchors.fill: parent + + RowLayout + { + Layout.fillHeight: false + Layout.fillWidth: true + Layout.margins: 6 + + TextField + { + Layout.preferredHeight: 32 + Layout.fillWidth: true + + id: searchField + placeholderText: "Search" + onEditingFinished: mainItem.updateSearch() + Keys.onPressed: (event) => + { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) + { + searchField.focus = false; // Unfocus the TextField + event.accepted = true; // Prevent further propagation of the key event + } + } + } + + Button + { + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 + + icon.name: "search-symbolic" + + onClicked: mainItem.updateSearch() + } + } + + Component + { + id: highlight + Rectangle + { + width: view.cellWidth; height: view.cellHeight + color: palette.highlight; radius: 5 + x: view.currentItem.x + y: view.currentItem.y + Behavior on x { SpringAnimation { spring: 3; damping: 0.2 } } + Behavior on y { SpringAnimation { spring: 3; damping: 0.2 } } + } + } + + Rectangle + { + Layout.fillHeight: true + Layout.fillWidth: true + color: palette.base + clip: true + + RowLayout + { + anchors.fill: parent + + GridView + { + // The standard size + property int idealCellHeight: 200 + property int idealCellWidth: 250 + cellWidth: width / Math.floor(width / idealCellWidth) + cellHeight: idealCellHeight + + id: view + + model: searchModel + highlight: highlight + highlightFollowsCurrentItem: false + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 6 + + delegate: Column + { + property int itemIndex: index + property string shaderThumbnail: thumbnail + property string shaderEmbed: embedUrl + property string shaderId: id + property string author: username + property string shaderDescription: model.description + + id: entry + + leftPadding: Math.floor((width - thumbnailImage.width) / 2) + topPadding: 10 + rightPadding: Math.floor((width - thumbnailImage.width) / 2) + bottomPadding: 10 + width: view.cellWidth + + Image + { + width: 250 + height: 140 + id: thumbnailImage + source: thumbnail + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle + { + color: palette.base + anchors.fill: parent + visible: thumbnailImage.status === Image.Loading + + RowLayout + { + anchors.fill: parent + + BusyIndicator + { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 64 + Layout.preferredWidth: 64 + visible: running + } + } + } + + Rectangle + { + color: palette.dark + anchors.fill: parent + visible: thumbnailImage.status === Image.Error + + Text + { + color: palette.text + anchors.centerIn: parent + text: qsTr("Error Loading Image") + } + } + + MouseArea + { + anchors.fill: parent + onClicked: + (mouse) => { + view.currentIndex = parent.parent.itemIndex + //searchModel.currentIndex = view.currentIndex + } + } + } + + RowLayout + { + visible: parent.itemIndex === view.currentIndex + width: thumbnailImage.width + Button + { + Layout.topMargin: 4 + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 32 + Layout.fillWidth: true + icon.name: "emblem-downloads" + text: qsTr("Preview & Download") + onClicked: () => + { + downloadDialog.open() + } + } + } + + Dialog + { + id: downloadDialog + modal: Qt.WindowModal + width: 600 + height: 440 + + parent: mainItem + anchors.centerIn: mainItem + clip: true + + ColumnLayout + { + anchors.fill: parent + + WebView + { + Layout.fillHeight: true + Layout.fillWidth: true + id: shaderPreview + Layout.preferredWidth: 500 + Layout.preferredHeight: 281 + Layout.alignment: Qt.AlignHCenter + url: "" + } + + Text + { + Layout.preferredHeight: 64 + Layout.fillHeight: true + Layout.fillWidth: true + id: shaderDescription + text: model.description + elide: Text.ElideRight + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + maximumLineCount: 3 + color: palette.text + } + + Button + { + Layout.fillWidth: true + icon.name: "image-symbolic" + + id: downloadButton + text: qsTr("Convert to Komplex Pack") + + onClicked: () => + { + workingThumbnail.source = model.thumbnail + searchModel.convert(model.index); + downloadDialog.close(); + } + } + } + + onClosed: () => + { + shaderPreview.loadHtml(``) + } + + onOpened: () => + { + shaderDescription.text = model.description + shaderPreview.loadHtml(`