Compare commits

...

3 Commits

Author SHA1 Message Date
Digital Artifex fb47b92353 Added video search page and model 2026-06-19 02:06:05 -04:00
Digital Artifex 49db588226 Renamed SearchVideosPaginator 2026-06-19 02:04:31 -04:00
Digital Artifex f2596aaa40 Added Video icon 2026-06-19 02:03:19 -04:00
11 changed files with 872 additions and 11 deletions
+4 -1
View File
@@ -25,7 +25,9 @@ qt_standard_project_setup()
qt_add_executable(${CMAKE_PROJECT_NAME} qt_add_executable(${CMAKE_PROJECT_NAME}
README.md README.md
LICENSE) LICENSE
images/icons/icons8-video.svg
)
qt_add_resources(${CMAKE_PROJECT_NAME} "configuration" qt_add_resources(${CMAKE_PROJECT_NAME} "configuration"
PREFIX "/" PREFIX "/"
FILES FILES
@@ -56,6 +58,7 @@ qt_add_resources(${CMAKE_PROJECT_NAME} "app_images"
"images/icons/icons8-store.svg" "images/icons/icons8-store.svg"
"images/icons/icons8-user-account.svg" "images/icons/icons8-user-account.svg"
"images/icons/icons8-view-more.svg" "images/icons/icons8-view-more.svg"
"images/icons/icons8-video.svg"
"images/kero/kero_build_1.png" "images/kero/kero_build_1.png"
"images/kero/kero_build_2.png" "images/kero/kero_build_2.png"
"images/kero/kero_build_3.png" "images/kero/kero_build_3.png"
+1
View File
@@ -17,6 +17,7 @@ qt6_add_qml_module(KomplexHubContent
"pages/SettingsPage.qml" "pages/SettingsPage.qml"
"pages/UserProfilePage.qml" "pages/UserProfilePage.qml"
"pages/WallpaperSettingsPage.qml" "pages/WallpaperSettingsPage.qml"
QML_FILES pages/VideoSearchPage.qml
) )
+23 -2
View File
@@ -83,7 +83,27 @@ Rectangle {
currentMenuButton = this currentMenuButton = this
searchContainer.preferredHeight = 50 searchContainer.preferredHeight = 50
// searchContainer.searchTerm = (pageLoader.page as ImageSearchPage).query }
}
MenuButton {
Layout.alignment: Qt.AlignTop
height: 64
width: 100
text: "Videos"
icon.height: 32
icon.width: 32
icon.url: "qrc:/images/icons/icons8-video.svg"
onTriggered: () => {
pageLoader.page = "pages/VideoSearchPage.qml"
currentMenuButton.selected = false
currentMenuButton = this
searchContainer.preferredHeight = 50
} }
} }
@@ -287,7 +307,8 @@ Rectangle {
{ {
return ( return (
pageLoader.item instanceof ImageSearchPage || pageLoader.item instanceof ImageSearchPage ||
pageLoader.item instanceof LiveSearchPage pageLoader.item instanceof LiveSearchPage ||
pageLoader.item instanceof VideoSearchPage
); );
} }
} }
+225
View File
@@ -0,0 +1,225 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import KomplexHub
import KomplexHub.Controls
import KomplexHub.Kero
import KomplexHubPlugin
Item
{
property string query
readonly property bool searchable: true
property int resultsPerRow: (resultsContainer.width - (Constants.largeMargin * 2)) / (resultWidth + Constants.mediumMargin)
property int rows: ((resultsContainer.height - (Constants.largeMargin * 2)) / (resultHeight + Constants.mediumMargin))
property int resultsPerPage: resultsPerRow * rows
property int emptyWidth: (resultsContainer.width - (Constants.largeMargin * 2)) - (resultsPerRow * (resultWidth + Constants.mediumMargin))
property int resultWidth: 256
property int resultHeight: 245
id: videoSearchPageRoot
VideoSearchModel
{
id: searchModel
query: videoSearchPageRoot.query
resultsPerPage: videoSearchPageRoot.resultsPerPage
}
Rectangle
{
anchors.fill: parent
color: palette.base
Rectangle
{
id: resultsContainer
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: buttonBoxLayout.top
color: "transparent"
ScrollView
{
id: resultsView
anchors.fill: parent
RowLayout
{
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.top: parent.top
anchors.margins: Constants.largeMargin
Item
{
Layout.fillHeight: true
Layout.preferredWidth: emptyWidth / 2
Layout.maximumWidth: emptyWidth / 2
}
GridLayout
{
Layout.fillHeight: true
Layout.fillWidth: true
columns: videoSearchPageRoot.resultsPerRow
columnSpacing: Constants.mediumMargin
rowSpacing: Constants.mediumMargin
Repeater
{
model: searchModel
delegate: Item {
width: resultWidth
height: resultHeight
required property string uuid
required property string author
required property string authorId
required property string authorUrl
required property string thumbnail
SearchResultItem
{
anchors.fill: parent
title: parent.author
author: parent.author
description: "Video provided by Pexels"
thumbnail: parent.thumbnail
uuid: parent.uuid
}
}
}
}
}
}
}
RowLayout
{
id: buttonBoxLayout
height: 35
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Constants.largeMargin
SquareButton
{
Layout.fillHeight: true
Layout.preferredWidth: 128
icon.source: "qrc:/images/icons/icons8-back.svg"
icon.height: 16
icon.width: 16
text: qsTr("Previous")
enabled: searchModel.hasPreviousPage
onTriggered: () => searchModel.previousPage()
}
Item { Layout.fillWidth: true }
SquareButton
{
Layout.fillHeight: true
Layout.preferredWidth: 128
icon.source: "qrc:/images/icons/icons8-next.svg"
icon.height: 16
icon.width: 16
text: qsTr("Next")
enabled: searchModel.hasNextPage
onTriggered: () => searchModel.nextPage()
}
}
}
KeroErrorAvatar
{
id: errorOverlay
anchors.fill: parent
opacity: 0
}
KeroNoResultsAvatar
{
id: noResultsOverlay
anchors.fill: parent
opacity: 0
}
KeroLoadingAnimation
{
id: loadingOverlay
anchors.fill: parent
opacity: 0
}
states: [
State
{
name: "loading"
when: searchModel.state == VideoSearchModel.Loading
PropertyChanges
{
target: loadingOverlay
opacity: 1
}
PropertyChanges
{
target: resultsView
opacity: 0
}
},
State
{
name: "empty"
when: searchModel.totalResults === 0
PropertyChanges
{
target: noResultsOverlay
opacity: 1
}
PropertyChanges
{
target: resultsView
opacity: 0
}
PropertyChanges
{
target: buttonBoxLayout
visible: false
}
},
State
{
name: "error"
when: searchModel.state == VideoSearchModel.Error
PropertyChanges
{
target: errorOverlay
opacity: 1
}
PropertyChanges
{
target: resultsView
opacity: 0
}
}
]
}
+3 -1
View File
@@ -38,7 +38,7 @@ qt_add_qml_module(
SOURCES paginators/featuredcubemapspaginator.h SOURCES paginators/featuredcubemapspaginator.h
SOURCES paginators/searchshaderspaginator.h SOURCES paginators/searchshaderspaginator.h
SOURCES paginators/searchcubemapspaginator.h SOURCES paginators/searchcubemapspaginator.h
SOURCES paginators/searchvideospaginator.h SOURCES paginators/videosearchpaginator.h
SOURCES featuredpacksmodel.h SOURCES featuredpacksmodel.h
SOURCES featuredpacksmodel.cpp SOURCES featuredpacksmodel.cpp
SOURCES featuredimagesmodel.h SOURCES featuredimagesmodel.h
@@ -52,6 +52,8 @@ qt_add_qml_module(
SOURCES paginators/videopaginator.h SOURCES paginators/videopaginator.h
SOURCES imagesearchmodel.cpp SOURCES imagesearchmodel.cpp
SOURCES imagesearchmodel.h SOURCES imagesearchmodel.h
SOURCES videosearchmodel.h
SOURCES videosearchmodel.cpp
) )
+1 -1
View File
@@ -94,7 +94,7 @@ auto FeaturedVideosModel::data(const QModelIndex &index, int role) const -> QVar
case WidthRole: case WidthRole:
data = dataPoint.width; data = dataPoint.width;
break; break;
} }
return data; return data;
} }
@@ -16,8 +16,8 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/> * along with this program. If not, see <https://www.gnu.org/licenses/>
*/ */
#ifndef SEARCHVIDEOSPAGINATOR_H #ifndef VIDEOSEARCHPAGINATOR_H
#define SEARCHVIDEOSPAGINATOR_H #define VIDEOSEARCHPAGINATOR_H
#include <QObject> #include <QObject>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
@@ -29,12 +29,12 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include "wallpaperpaginator.h" #include "videopaginator.h"
class KOMPLEX_EXPORT SearchVideosPaginator : public WallpaperPaginator class KOMPLEX_EXPORT VideoSearchPaginator : public VideoPaginator
{ {
public: public:
explicit SearchVideosPaginator(QObject *parent = nullptr) : WallpaperPaginator(parent) explicit VideoSearchPaginator(QObject *parent = nullptr) : VideoPaginator(parent)
{ {
setUri( setUri(
QString::fromUtf8(KOMPLEX_ENDPOINT_VIDEOS_SEARCH) QString::fromUtf8(KOMPLEX_ENDPOINT_VIDEOS_SEARCH)
@@ -42,4 +42,4 @@ public:
} }
}; };
#endif // SEARCHVIDEOSPAGINATOR_H #endif // VIDEOSEARCHPAGINATOR_H
+288
View File
@@ -0,0 +1,288 @@
#include "videosearchmodel.h"
VideoSearchModel::VideoSearchModel(QObject *parent) : QAbstractListModel{parent}
{
setState(Loading);
m_paginator = new VideoSearchPaginator(this);
PaginationNotifier *notifier = static_cast<PaginationNotifier*>(m_paginator);
QObject::connect(
notifier,
&PaginationNotifier::resultsPerPageChanged,
this,
&VideoSearchModel::resultsPerPageChanged
);
QObject::connect(
notifier,
&PaginationNotifier::totalResultsChanged,
this,
&VideoSearchModel::totalResultsChanged
);
QObject::connect(
notifier,
&PaginationNotifier::totalPagesChanged,
this,
&VideoSearchModel::totalPagesChanged
);
QObject::connect(
notifier,
&PaginationNotifier::pageChanged,
this,
&VideoSearchModel::pageChanged
);
QObject::connect(
notifier,
&PaginationNotifier::dataChanged,
this,
&VideoSearchModel::resetDataModel
);
QObject::connect(
m_paginator,
&VideoSearchPaginator::queryChanged,
this,
&VideoSearchModel::queryChanged
);
}
auto VideoSearchModel::rowCount(const QModelIndex &) const -> int
{
if(m_paginator != nullptr)
{
return m_paginator->count();
}
return 0;
}
auto VideoSearchModel::data(const QModelIndex &index, int role) const -> QVariant
{
if(index.row() < 0 || m_paginator == nullptr)
{
return {};
}
VideoCache dataPoint = m_data.at(index.row());
QVariant data;
switch(static_cast<DataRole>(role))
{
case UuidRole:
data = dataPoint.id;
break;
case AuthorRole:
data = dataPoint.author;
break;
case AuthorIdRole:
data = dataPoint.authorId;
break;
case AuthorUrlRole:
data = dataPoint.authorUrl;
break;
case DurationRole:
data = dataPoint.duration;
break;
case ThumbnailRole:
data = dataPoint.thumbnail;
break;
case UrlRole:
data = dataPoint.url;
break;
case HeightRole:
data = dataPoint.height;
break;
case WidthRole:
data = dataPoint.width;
break;
}
return data;
}
auto VideoSearchModel::index(int row, int column, const QModelIndex &parent) const -> QModelIndex
{
Q_UNUSED(parent)
if(row < 0 || row >= m_data.count())
return {};
return createIndex(row, column, &m_data[row]);
}
auto VideoSearchModel::columnCount(const QModelIndex &) const -> int
{
return 0;
}
auto VideoSearchModel::parent(const QModelIndex &) const -> QModelIndex
{
return {};
}
auto VideoSearchModel::setData(const QModelIndex &, const QVariant &, int) -> bool
{
return false;
}
auto VideoSearchModel::state() const -> VideoSearchModel::State
{
return m_state;
}
auto VideoSearchModel::setState(State state) -> void
{
if (m_state == state)
{
return;
}
m_state = state;
emit stateChanged();
}
auto VideoSearchModel::resetState() -> void
{
setState(Idle);
}
auto VideoSearchModel::resetDataModel() -> void
{
setState(Loading);
//invalidate previous model data
beginRemoveRows(QModelIndex(), 0, m_data.count());
m_data.clear();
endRemoveRows();
//signal new data
beginInsertRows(QModelIndex(), 0, m_paginator->count() - 1);
auto dataset = m_paginator->data();
for(const VideoCache& data : std::as_const(dataset))
{
m_data.append(data);
}
endInsertRows();
setState(Idle);
}
auto VideoSearchModel::hasNextPage() -> bool
{
return m_paginator->page() < m_paginator->totalPages();
}
auto VideoSearchModel::hasPreviousPage() -> bool
{
return m_paginator->page() > 0;
}
auto VideoSearchModel::roleNames() const -> QHash<int, QByteArray>
{
return m_dataRoles;
}
auto VideoSearchModel::errorString() const -> QString
{
return m_errorString;
}
auto VideoSearchModel::setErrorString(const QString &errorString) -> void
{
if (m_errorString == errorString)
{
return;
}
m_errorString = errorString;
emit errorStringChanged();
}
auto VideoSearchModel::resultsPerPage() const -> qsizetype
{
if(m_paginator != nullptr)
{
return m_paginator->resultsPerPage();
}
return 0;
}
auto VideoSearchModel::setResultsPerPage(qsizetype resultsPerPage) -> void
{
if(m_paginator != nullptr)
{
m_paginator->setResultsPerPage(resultsPerPage);
}
}
auto VideoSearchModel::totalResults() const -> qsizetype
{
if(m_paginator != nullptr)
{
return m_paginator->totalResults();
}
return 0;
}
auto VideoSearchModel::nextPage() const -> void
{
if(m_paginator != nullptr)
{
m_paginator->next();
}
}
auto VideoSearchModel::previousPage() const -> void
{
if(m_paginator != nullptr)
{
m_paginator->previous();
}
}
auto VideoSearchModel::query() const -> QString
{
if(m_paginator != nullptr)
{
return m_paginator->query();
}
return {};
}
auto VideoSearchModel::setQuery(const QString &query) -> void
{
if(m_paginator != nullptr)
{
m_paginator->setQuery(query);
}
}
qsizetype VideoSearchModel::page() const
{
if(m_paginator != nullptr)
{
return m_paginator->page();
}
return 0;
}
auto VideoSearchModel::totalPages() const -> qsizetype
{
if(m_paginator != nullptr)
{
return m_paginator->totalPages();
}
return 0;
}
+315
View File
@@ -0,0 +1,315 @@
/*
* Komplex Wallpaper Engine
* Copyright (C) 2026 @DigitalArtifex
* https://digitalartifex.dev - https://github.com/DigitalArtifex
*
* 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 3 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, see <https://www.gnu.org/licenses/>
*/
#ifndef VideoSearchModel_H
#define VideoSearchModel_H
#include <QObject>
#include <QQmlEngine>
#include <QList>
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QAbstractListModel>
#include <QProcess>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QEventLoop>
#include "paginators/videosearchpaginator.h"
/**
* @brief The VideoSearchModel class
*/
class KOMPLEX_EXPORT VideoSearchModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
/**
* @brief Used to control the UI state
*/
enum State
{
Idle,
Loading,
Error
};
Q_ENUM(State)
/**
* @brief Data roles used by the view to request data
*/
enum DataRole {
UuidRole = Qt::UserRole + 1,
AuthorRole,
AuthorIdRole,
AuthorUrlRole,
DurationRole,
ThumbnailRole,
UrlRole,
HeightRole,
WidthRole
};
Q_ENUM(DataRole)
explicit VideoSearchModel(QObject *parent = nullptr);
auto rowCount(const QModelIndex &parent = QModelIndex()) const -> int override;
/**
* @brief data
* Used by the view to pull data from the model.
* Required by QAbstractListModel
* @param index
* @param role
* @return
*/
auto data(
const QModelIndex &index,
int role = Qt::DisplayRole
) const -> QVariant override;
/**
* @brief index
* Used by the view to create an index for the data point.
* Required by QAbstractListModel
* @param row
* @param column
* @param parent
* @return
*/
auto index(
int row,
int column,
const QModelIndex &parent = QModelIndex()
) const -> QModelIndex override;
/**
* @brief columnCount
* Required by QAbstractListModel, but just returns 0
* since we dont use cols
* @param parent
* @return
*/
auto columnCount(
const QModelIndex &parent = QModelIndex()
) const -> int override;
/**
* @brief parent
* @param index
* @return
*/
auto parent(const QModelIndex &index) const -> QModelIndex override;
/**
* @brief setData
* Required by QAbstractListModel but not used
* @param index
* @param value
* @param role
* @return true always
*/
auto setData(
const QModelIndex &index,
const QVariant &value,
int role = Qt::EditRole
) -> bool override;
/**
* @brief state
* Current Model State
* @return
*/
auto state() const -> State;
/**
* @brief roleNames
* Used to tell the view what data roles are available
* and their QML friendly names
* @return
*/
auto roleNames() const -> QHash<int, QByteArray> override;
/**
* @brief errorString
* User-friendly error string to be reported to the user
* @return
*/
auto errorString() const -> QString;
/**
* @brief resultsPerPage
* @return
*/
auto resultsPerPage() const -> qsizetype;
/**
* @brief setResultsPerPage
* @param resultsPerPage
*/
auto setResultsPerPage(qsizetype resultsPerPage) -> void;
/**
* @brief totalResults
* Total number of results available
* @return
*/
auto totalResults() const -> qsizetype;
/**
* @brief page
* @return
*/
auto page() const -> qsizetype;
/**
* @brief totalPages
* Total pages based on the current resultsPerPage
* @return
*/
auto totalPages() const -> qsizetype;
/**
* @brief nextPage
* Function for the QML frontend
*/
Q_INVOKABLE auto nextPage() const -> void;
/**
* @brief previousPage
* Function for the QML frontend
*/
Q_INVOKABLE auto previousPage() const -> void;
/**
* @brief query
* Current search query
* @return
*/
auto query() const -> QString;
/**
* @brief setQuery
* Sets the current
* @param query
*/
auto setQuery(const QString &query) -> void;
protected:
/**
* @brief setErrorString
* @param errorString
*/
auto setErrorString(const QString &errorString) -> void;
/**
* @brief setState
* @param state
*/
auto setState(State state) -> void;
/**
* @brief resetState
*/
auto resetState() -> void;
auto resetDataModel() -> void;
auto hasNextPage() -> bool;
auto hasPreviousPage() -> bool;
signals:
auto stateChanged() -> void;
auto errorStringChanged() -> void;
auto resultsPerPageChanged() -> void;
auto pageChanged() -> void;
auto totalResultsChanged() -> void;
auto totalPagesChanged() -> void;
auto queryChanged() -> void;
private:
static inline const QHash<int, QByteArray> m_dataRoles =
{
{
static_cast<int>(UuidRole),
QByteArray("uuid")
},
{
static_cast<int>(AuthorRole),
QByteArray("author")
},
{
static_cast<int>(AuthorIdRole),
QByteArray("authorId")
},
{
static_cast<int>(AuthorUrlRole),
QByteArray("authorUrl")
},
{
static_cast<int>(DurationRole),
QByteArray("duration")
},
{
static_cast<int>(ThumbnailRole),
QByteArray("thumbnail")
},
{
static_cast<int>(UrlRole),
QByteArray("url")
},
{
static_cast<int>(HeightRole),
QByteArray("height")
},
{
static_cast<int>(WidthRole),
QByteArray("width")
}
};
State m_state = Idle;
QString m_errorString = QString();
VideoSearchPaginator *m_paginator = nullptr;
mutable QList<VideoCache> m_data;
Q_PROPERTY(State state READ state WRITE setState RESET resetState NOTIFY stateChanged FINAL)
Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL)
Q_PROPERTY(qsizetype resultsPerPage READ resultsPerPage WRITE setResultsPerPage NOTIFY resultsPerPageChanged FINAL)
Q_PROPERTY(qsizetype totalResults READ totalResults NOTIFY totalResultsChanged FINAL)
Q_PROPERTY(qsizetype totalPages READ totalPages NOTIFY totalPagesChanged FINAL)
Q_PROPERTY(qsizetype page READ page NOTIFY pageChanged FINAL)
Q_PROPERTY(bool hasNextPage READ hasNextPage NOTIFY pageChanged FINAL)
Q_PROPERTY(bool hasPreviousPage READ hasPreviousPage NOTIFY pageChanged FINAL)
Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged FINAL)
};
Q_DECLARE_METATYPE(VideoSearchModel)
#endif // VideoSearchModel_H
+1
View File
@@ -0,0 +1 @@
Icons are licesened through Icons8.com via donation. They can not be distributed for use outside Komplex and its associated applications. All rights reserved to Icons8.
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<path d="M0 0 C31.68 0 63.36 0 96 0 C96 27.72 96 55.44 96 84 C64.32 84 32.64 84 0 84 C0 56.28 0 28.56 0 0 Z M4 4 C4 5.98 4 7.96 4 10 C5.98 10 7.96 10 10 10 C10 8.02 10 6.04 10 4 C8.02 4 6.04 4 4 4 Z M14 4 C14 5.98 14 7.96 14 10 C16.64 10 19.28 10 22 10 C22 8.02 22 6.04 22 4 C19.36 4 16.72 4 14 4 Z M26 4 C26 5.98 26 7.96 26 10 C28.64 10 31.28 10 34 10 C34 8.02 34 6.04 34 4 C31.36 4 28.72 4 26 4 Z M38 4 C38 5.98 38 7.96 38 10 C40.64 10 43.28 10 46 10 C46 8.02 46 6.04 46 4 C43.36 4 40.72 4 38 4 Z M50 4 C50 5.98 50 7.96 50 10 C52.64 10 55.28 10 58 10 C58 8.02 58 6.04 58 4 C55.36 4 52.72 4 50 4 Z M62 4 C62 5.98 62 7.96 62 10 C64.64 10 67.28 10 70 10 C70 8.02 70 6.04 70 4 C67.36 4 64.72 4 62 4 Z M74 4 C74 5.98 74 7.96 74 10 C76.64 10 79.28 10 82 10 C82 8.02 82 6.04 82 4 C79.36 4 76.72 4 74 4 Z M86 4 C86 5.98 86 7.96 86 10 C87.98 10 89.96 10 92 10 C92 8.02 92 6.04 92 4 C90.02 4 88.04 4 86 4 Z M4 14 C4 32.48 4 50.96 4 70 C33.04 70 62.08 70 92 70 C92 51.52 92 33.04 92 14 C62.96 14 33.92 14 4 14 Z M4 74 C4 75.98 4 77.96 4 80 C5.98 80 7.96 80 10 80 C10 78.02 10 76.04 10 74 C8.02 74 6.04 74 4 74 Z M14 74 C14 75.98 14 77.96 14 80 C16.64 80 19.28 80 22 80 C22 78.02 22 76.04 22 74 C19.36 74 16.72 74 14 74 Z M26 74 C26 75.98 26 77.96 26 80 C28.64 80 31.28 80 34 80 C34 78.02 34 76.04 34 74 C31.36 74 28.72 74 26 74 Z M38 74 C38 75.98 38 77.96 38 80 C40.64 80 43.28 80 46 80 C46 78.02 46 76.04 46 74 C43.36 74 40.72 74 38 74 Z M50 74 C50 75.98 50 77.96 50 80 C52.64 80 55.28 80 58 80 C58 78.02 58 76.04 58 74 C55.36 74 52.72 74 50 74 Z M62 74 C62 75.98 62 77.96 62 80 C64.64 80 67.28 80 70 80 C70 78.02 70 76.04 70 74 C67.36 74 64.72 74 62 74 Z M74 74 C74 75.98 74 77.96 74 80 C76.64 80 79.28 80 82 80 C82 78.02 82 76.04 82 74 C79.36 74 76.72 74 74 74 Z M86 74 C86 75.98 86 77.96 86 80 C87.98 80 89.96 80 92 80 C92 78.02 92 76.04 92 74 C90.02 74 88.04 74 86 74 Z " fill="#DADADA" transform="translate(2,8)"/>
<path d="M0 0 C4.9473515 0 5.80671024 0.11866791 9.64453125 2.625 C10.48564453 3.16640625 11.32675781 3.7078125 12.19335938 4.265625 C13.05767578 4.83796875 13.92199219 5.4103125 14.8125 6 C15.69615234 6.57234375 16.57980469 7.1446875 17.49023438 7.734375 C19.66693565 9.14628934 21.83627774 10.56830696 24 12 C23.49355928 16.53837775 23.49355928 16.53837775 21.31347656 18.57739258 C20.59643555 19.01204834 19.87939453 19.4467041 19.140625 19.89453125 C18.35429688 20.37470703 17.56796875 20.85488281 16.7578125 21.34960938 C15.51644531 22.07374023 15.51644531 22.07374023 14.25 22.8125 C13.03183594 23.56563477 13.03183594 23.56563477 11.7890625 24.33398438 C7.27598993 27.03521266 5.5061171 28 0 28 C0 18.76 0 9.52 0 0 Z M4 7 C4 11.62 4 16.24 4 21 C7.23396985 19.70641206 9.88479853 18.59717733 12.8125 16.875 C14.3903125 15.946875 14.3903125 15.946875 16 15 C16 14.34 16 13.68 16 13 C13.82248821 11.40222476 13.82248821 11.40222476 11.125 9.875 C10.22007813 9.33617187 9.31515625 8.79734375 8.3828125 8.2421875 C6.15227978 6.8459107 6.15227978 6.8459107 4 7 Z " fill="#DADADA" transform="translate(40,36)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB