feat(@desktop/chat): Display gif popup next to emoji
This commit is contained in:
parent
2df8e938ae
commit
58506fbd97
4
Makefile
4
Makefile
|
@ -221,6 +221,10 @@ DEFAULT_TOKEN := 220a1abb4b6943a093c35d0ce4fb0732
|
||||||
INFURA_TOKEN ?= $(DEFAULT_TOKEN)
|
INFURA_TOKEN ?= $(DEFAULT_TOKEN)
|
||||||
NIM_PARAMS += -d:INFURA_TOKEN:"$(INFURA_TOKEN)"
|
NIM_PARAMS += -d:INFURA_TOKEN:"$(INFURA_TOKEN)"
|
||||||
|
|
||||||
|
DEFAULT_TENOR_API_KEY := DU7DWZ27STB2
|
||||||
|
TENOR_API_KEY ?= $(DEFAULT_TENOR_API_KEY)
|
||||||
|
NIM_PARAMS += -d:TENOR_API_KEY:"$(TENOR_API_KEY)"
|
||||||
|
|
||||||
# generate a status-desktop.log file with chronicles output. This file will be truncated each time the app starts
|
# generate a status-desktop.log file with chronicles output. This file will be truncated each time the app starts
|
||||||
NIM_PARAMS += -d:chronicles_sinks="textlines[stdout],textlines[nocolors,file(status-desktop.log,truncate)]"
|
NIM_PARAMS += -d:chronicles_sinks="textlines[stdout],textlines[nocolors,file(status-desktop.log,truncate)]"
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import ../../status/ens as status_ens
|
||||||
import ../../status/chat/[chat, message]
|
import ../../status/chat/[chat, message]
|
||||||
import ../../status/profile/profile
|
import ../../status/profile/profile
|
||||||
import web3/[conversions, ethtypes]
|
import web3/[conversions, ethtypes]
|
||||||
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages, message_item]
|
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, communities, community_list, community_item, format_input, ens, activity_notification_list, channel, messages, message_item, gif]
|
||||||
import ../utils/image_utils
|
import ../utils/image_utils
|
||||||
import ../../status/tasks/[qt, task_runner_impl]
|
import ../../status/tasks/[qt, task_runner_impl]
|
||||||
import ../../status/tasks/marathon/mailserver/worker
|
import ../../status/tasks/marathon/mailserver/worker
|
||||||
|
@ -81,6 +81,7 @@ QtObject:
|
||||||
callResult: string
|
callResult: string
|
||||||
reactions*: ReactionView
|
reactions*: ReactionView
|
||||||
stickers*: StickersView
|
stickers*: StickersView
|
||||||
|
gif*: GifView
|
||||||
groups*: GroupsView
|
groups*: GroupsView
|
||||||
transactions*: TransactionsView
|
transactions*: TransactionsView
|
||||||
communities*: CommunitiesView
|
communities*: CommunitiesView
|
||||||
|
@ -98,6 +99,7 @@ QtObject:
|
||||||
self.activityNotificationList.delete
|
self.activityNotificationList.delete
|
||||||
self.reactions.delete
|
self.reactions.delete
|
||||||
self.stickers.delete
|
self.stickers.delete
|
||||||
|
self.gif.delete
|
||||||
self.groups.delete
|
self.groups.delete
|
||||||
self.transactions.delete
|
self.transactions.delete
|
||||||
self.communities.delete
|
self.communities.delete
|
||||||
|
@ -121,6 +123,7 @@ QtObject:
|
||||||
result.channelView.activeChannel
|
result.channelView.activeChannel
|
||||||
)
|
)
|
||||||
result.stickers = newStickersView(status, result.channelView.activeChannel)
|
result.stickers = newStickersView(status, result.channelView.activeChannel)
|
||||||
|
result.gif = newGifView()
|
||||||
result.groups = newGroupsView(status,result.channelView.activeChannel)
|
result.groups = newGroupsView(status,result.channelView.activeChannel)
|
||||||
result.transactions = newTransactionsView(status)
|
result.transactions = newTransactionsView(status)
|
||||||
|
|
||||||
|
@ -356,6 +359,12 @@ QtObject:
|
||||||
QtProperty[QVariant] stickers:
|
QtProperty[QVariant] stickers:
|
||||||
read = getStickers
|
read = getStickers
|
||||||
|
|
||||||
|
proc getGif*(self: ChatsView): QVariant {.slot.} =
|
||||||
|
newQVariant(self.gif)
|
||||||
|
|
||||||
|
QtProperty[QVariant] gif:
|
||||||
|
read = getGif
|
||||||
|
|
||||||
proc getGroups*(self: ChatsView): QVariant {.slot.} =
|
proc getGroups*(self: ChatsView): QVariant {.slot.} =
|
||||||
newQVariant(self.groups)
|
newQVariant(self.groups)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import # vendor libs
|
||||||
|
NimQml
|
||||||
|
|
||||||
|
import gif_list
|
||||||
|
import ../../../status/gif
|
||||||
|
|
||||||
|
|
||||||
|
QtObject:
|
||||||
|
type GifView* = ref object of QObject
|
||||||
|
items*: GifList
|
||||||
|
client: GifClient
|
||||||
|
|
||||||
|
proc setup(self: GifView) =
|
||||||
|
self.QObject.setup
|
||||||
|
|
||||||
|
proc delete*(self: GifView) =
|
||||||
|
self.QObject.delete
|
||||||
|
|
||||||
|
proc newGifView*(): GifView =
|
||||||
|
new(result, delete)
|
||||||
|
result = GifView()
|
||||||
|
result.client = newGifClient()
|
||||||
|
result.items = newGifList()
|
||||||
|
result.setup()
|
||||||
|
|
||||||
|
proc getItemsList*(self: GifView): QVariant {.slot.} =
|
||||||
|
result = newQVariant(self.items)
|
||||||
|
|
||||||
|
proc itemsLoaded*(self: GifView) {.signal.}
|
||||||
|
|
||||||
|
QtProperty[QVariant] items:
|
||||||
|
read = getItemsList
|
||||||
|
notify = itemsLoaded
|
||||||
|
|
||||||
|
proc load*(self: GifView) {.slot.} =
|
||||||
|
let data = self.client.getTrendings()
|
||||||
|
self.items.setNewData(data)
|
||||||
|
self.itemsLoaded()
|
||||||
|
|
||||||
|
proc search*(self: GifView, query: string) {.slot.} =
|
||||||
|
let data = self.client.search(query)
|
||||||
|
self.items.setNewData(data)
|
||||||
|
self.itemsLoaded()
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import NimQml, Tables, sequtils
|
||||||
|
|
||||||
|
import ../../../status/gif
|
||||||
|
|
||||||
|
type
|
||||||
|
GifRoles {.pure.} = enum
|
||||||
|
Url = UserRole + 1
|
||||||
|
Id = UserRole + 2
|
||||||
|
Title = UserRole + 3
|
||||||
|
|
||||||
|
QtObject:
|
||||||
|
type
|
||||||
|
GifList* = ref object of QAbstractListModel
|
||||||
|
gifs*: seq[GifItem]
|
||||||
|
|
||||||
|
proc setup(self: GifList) = self.QAbstractListModel.setup
|
||||||
|
|
||||||
|
proc delete(self: GifList) = self.QAbstractListModel.delete
|
||||||
|
|
||||||
|
proc newGifList*(): GifList =
|
||||||
|
new(result, delete)
|
||||||
|
result.gifs = @[]
|
||||||
|
result.setup()
|
||||||
|
|
||||||
|
proc setNewData*(self: GifList, gifs: seq[GifItem]) =
|
||||||
|
self.beginResetModel()
|
||||||
|
self.gifs = gifs
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
method rowCount(self: GifList, index: QModelIndex = nil): int = self.gifs.len
|
||||||
|
|
||||||
|
method data(self: GifList, index: QModelIndex, role: int): QVariant =
|
||||||
|
if not index.isValid:
|
||||||
|
return
|
||||||
|
|
||||||
|
if index.row < 0 or index.row >= self.gifs.len:
|
||||||
|
return
|
||||||
|
|
||||||
|
let gif = self.gifs[index.row]
|
||||||
|
case role.GifRoles:
|
||||||
|
of GifRoles.Url: result = newQVariant(gif.url)
|
||||||
|
of GifRoles.Id: result = newQVariant(gif.id)
|
||||||
|
of GifRoles.Title: result = newQVariant(gif.title)
|
||||||
|
|
||||||
|
method roleNames(self: GifList): Table[int, string] =
|
||||||
|
{
|
||||||
|
GifRoles.Url.int:"url",
|
||||||
|
GifRoles.Id.int:"id",
|
||||||
|
GifRoles.Title.int:"title"
|
||||||
|
}.toTable
|
|
@ -0,0 +1,60 @@
|
||||||
|
import httpclient
|
||||||
|
import json
|
||||||
|
import strformat
|
||||||
|
import os
|
||||||
|
|
||||||
|
# set via `nim c` param `-d:TENOR_API_KEY:[api_key]`; should be set in CI/release builds
|
||||||
|
const TENOR_API_KEY {.strdefine.} = ""
|
||||||
|
let TENOR_API_KEY_ENV = $getEnv("TENOR_API_KEY")
|
||||||
|
|
||||||
|
let TENOR_API_KEY_RESOLVED =
|
||||||
|
if TENOR_API_KEY_ENV != "":
|
||||||
|
TENOR_API_KEY_ENV
|
||||||
|
else:
|
||||||
|
TENOR_API_KEY
|
||||||
|
|
||||||
|
const baseUrl = "https://g.tenor.com/v1/"
|
||||||
|
let defaultParams = fmt("&media_filter=basic&limit=10&key={TENOR_API_KEY_RESOLVED}")
|
||||||
|
|
||||||
|
type
|
||||||
|
GifItem* = object
|
||||||
|
id*: int
|
||||||
|
title*: string
|
||||||
|
url*: string
|
||||||
|
|
||||||
|
proc toGifItem(jsonMsg: JsonNode): GifItem =
|
||||||
|
return GifItem(
|
||||||
|
id: jsonMsg{"id"}.getInt,
|
||||||
|
title: jsonMsg{"title"}.getStr,
|
||||||
|
url: jsonMsg{"media"}[0]["gif"]["url"].getStr
|
||||||
|
)
|
||||||
|
|
||||||
|
proc `$`*(self: GifItem): string =
|
||||||
|
return fmt"GifItem(id:{self.id}, title:{self.title}, url:{self.url})"
|
||||||
|
|
||||||
|
type
|
||||||
|
GifClient* = ref object
|
||||||
|
client: HttpClient
|
||||||
|
|
||||||
|
proc newGifClient*(): GifClient =
|
||||||
|
result = GifClient()
|
||||||
|
result.client = newHttpClient()
|
||||||
|
|
||||||
|
proc tenorQuery(self: GifClient, path: string): seq[GifItem] =
|
||||||
|
try:
|
||||||
|
let content = self.client.getContent(fmt("{baseUrl}{path}{defaultParams}"))
|
||||||
|
let doc = content.parseJson()
|
||||||
|
|
||||||
|
var items: seq[GifItem] = @[]
|
||||||
|
for json in doc["results"]:
|
||||||
|
items.add(toGifItem(json))
|
||||||
|
|
||||||
|
return items
|
||||||
|
except:
|
||||||
|
return @[]
|
||||||
|
|
||||||
|
proc search*(self: GifClient, query: string): seq[GifItem] =
|
||||||
|
return self.tenorQuery(fmt("search?q={query}"))
|
||||||
|
|
||||||
|
proc getTrendings*(self: GifClient): seq[GifItem] =
|
||||||
|
return self.tenorQuery("trending?")
|
|
@ -230,6 +230,15 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusSettingsLineButton {
|
||||||
|
text: qsTr("GIF Widget")
|
||||||
|
isSwitch: true
|
||||||
|
switchChecked: appSettings.isGifWidgetEnabled
|
||||||
|
onClicked: {
|
||||||
|
appSettings.isGifWidgetEnabled = !appSettings.isGifWidgetEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StatusSettingsLineButton {
|
// StatusSettingsLineButton {
|
||||||
// //% "Node Management"
|
// //% "Node Management"
|
||||||
// text: qsTrId("node-management")
|
// text: qsTrId("node-management")
|
||||||
|
|
|
@ -348,6 +348,7 @@ StatusAppLayout {
|
||||||
property bool isBrowserEnabled: false
|
property bool isBrowserEnabled: false
|
||||||
property bool isActivityCenterEnabled: false
|
property bool isActivityCenterEnabled: false
|
||||||
property bool showOnlineUsers: false
|
property bool showOnlineUsers: false
|
||||||
|
property bool isGifWidgetEnabled: false
|
||||||
property bool displayChatImages: false
|
property bool displayChatImages: false
|
||||||
property bool useCompactMode: true
|
property bool useCompactMode: true
|
||||||
property bool timelineEnabled: true
|
property bool timelineEnabled: true
|
||||||
|
|
|
@ -79,6 +79,28 @@ Rectangle {
|
||||||
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
|
messageInputField.insert(start, text.replace(/\n/g, "<br/>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function togglePopup(popup, btn) {
|
||||||
|
if (popup !== stickersPopup) {
|
||||||
|
stickersPopup.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popup !== gifPopup) {
|
||||||
|
gifPopup.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popup !== emojiPopup) {
|
||||||
|
emojiPopup.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popup.opened) {
|
||||||
|
popup.close()
|
||||||
|
btn.highlighted = false
|
||||||
|
} else {
|
||||||
|
popup.open()
|
||||||
|
btn.highlighted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property var interpretMessage: function (msg) {
|
property var interpretMessage: function (msg) {
|
||||||
if (msg.startsWith("/shrug")) {
|
if (msg.startsWith("/shrug")) {
|
||||||
return msg.replace("/shrug", "") + " ¯\\\\\\_(ツ)\\_/¯"
|
return msg.replace("/shrug", "") + " ¯\\\\\\_(ツ)\\_/¯"
|
||||||
|
@ -614,6 +636,23 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusGifPopup {
|
||||||
|
id: gifPopup
|
||||||
|
width: 360
|
||||||
|
height: 440
|
||||||
|
x: parent.width - width - Style.current.halfPadding
|
||||||
|
y: -height
|
||||||
|
gifSelected: function (event, url) {
|
||||||
|
messageInputField.text = url
|
||||||
|
control.sendMessage(event)
|
||||||
|
gifBtn.highlighted = false
|
||||||
|
messageInputField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
onClosed: {
|
||||||
|
gifBtn.highlighted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StatusEmojiPopup {
|
StatusEmojiPopup {
|
||||||
id: emojiPopup
|
id: emojiPopup
|
||||||
width: 360
|
width: 360
|
||||||
|
@ -1042,15 +1081,22 @@ Rectangle {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
icon.name: "emojiBtn"
|
icon.name: "emojiBtn"
|
||||||
type: "secondary"
|
type: "secondary"
|
||||||
onClicked: {
|
onClicked: togglePopup(emojiPopup, emojiBtn)
|
||||||
stickersPopup.close()
|
|
||||||
if (emojiPopup.opened) {
|
|
||||||
emojiPopup.close()
|
|
||||||
highlighted = false
|
|
||||||
} else {
|
|
||||||
emojiPopup.open()
|
|
||||||
highlighted = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusIconButton {
|
||||||
|
id: gifBtn
|
||||||
|
visible: appSettings.isGifWidgetEnabled
|
||||||
|
anchors.right: emojiBtn.left
|
||||||
|
anchors.rightMargin: 2
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
icon.name: "wallet"
|
||||||
|
type: "secondary"
|
||||||
|
onClicked: {
|
||||||
|
if (!gifPopup.opened) {
|
||||||
|
chatsModel.gif.load()
|
||||||
|
}
|
||||||
|
togglePopup(gifPopup, gifBtn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1063,16 +1109,7 @@ Rectangle {
|
||||||
visible: !isEdit && profileModel.network.current === Constants.networkMainnet && emojiBtn.visible
|
visible: !isEdit && profileModel.network.current === Constants.networkMainnet && emojiBtn.visible
|
||||||
width: visible ? 32 : 0
|
width: visible ? 32 : 0
|
||||||
type: "secondary"
|
type: "secondary"
|
||||||
onClicked: {
|
onClicked: togglePopup(stickersPopup, stickersBtn)
|
||||||
emojiPopup.close()
|
|
||||||
if (stickersPopup.opened) {
|
|
||||||
stickersPopup.close()
|
|
||||||
highlighted = false
|
|
||||||
} else {
|
|
||||||
stickersPopup.open()
|
|
||||||
highlighted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import QtQuick 2.13
|
||||||
|
import QtQuick.Controls 2.13
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtGraphicalEffects 1.0
|
||||||
|
import StatusQ.Components 0.1
|
||||||
|
import "../../imports"
|
||||||
|
import "../../shared"
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: popup
|
||||||
|
property var loading: true
|
||||||
|
property var gifSelected: function () {}
|
||||||
|
property var searchGif: Backpressure.debounce(searchBox, 500, function (query) {
|
||||||
|
loading = true
|
||||||
|
chatsModel.gif.search(query)
|
||||||
|
});
|
||||||
|
property alias searchString: searchBox.text
|
||||||
|
modal: false
|
||||||
|
width: 360
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Style.current.radius
|
||||||
|
color: Style.current.background
|
||||||
|
border.color: Style.current.border
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: DropShadow{
|
||||||
|
verticalOffset: 3
|
||||||
|
radius: 8
|
||||||
|
samples: 15
|
||||||
|
fast: true
|
||||||
|
cached: true
|
||||||
|
color: "#22000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
searchBox.text = ""
|
||||||
|
searchBox.forceActiveFocus(Qt.MouseFocusReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: chatsModel.gif
|
||||||
|
onItemsLoaded: {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int headerMargin: 8
|
||||||
|
|
||||||
|
id: gifHeader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: searchBox.height + gifHeader.headerMargin
|
||||||
|
|
||||||
|
SearchBox {
|
||||||
|
id: searchBox
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: gifHeader.headerMargin
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: gifHeader.headerMargin
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: gifHeader.headerMargin
|
||||||
|
Keys.onReleased: {
|
||||||
|
Qt.callLater(searchGif, searchBox.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.rightMargin: Style.current.smallPadding / 2
|
||||||
|
Layout.topMargin: Style.current.smallPadding
|
||||||
|
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||||
|
Layout.preferredHeight: 400 - gifHeader.height
|
||||||
|
sourceComponent: loading ? gifLoading : gifItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: gifLoading
|
||||||
|
StatusLoadingIndicator {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: gifItems
|
||||||
|
ScrollView {
|
||||||
|
id: scrollView
|
||||||
|
property ScrollBar vScrollBar: ScrollBar.vertical
|
||||||
|
clip: true
|
||||||
|
topPadding: Style.current.smallPadding
|
||||||
|
leftPadding: Style.current.smallPadding
|
||||||
|
rightPadding: Style.current.smallPadding
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: gifSectionsRepeater
|
||||||
|
model: chatsModel.gif.items
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: animation.width
|
||||||
|
height: animation.height
|
||||||
|
|
||||||
|
AnimatedImage {
|
||||||
|
id: animation
|
||||||
|
source: model.url
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: function (event) {
|
||||||
|
gifSelected(event, model.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*##^##
|
||||||
|
Designer {
|
||||||
|
D{i:0;formeditorColor:"#ffffff";height:440;width:360}
|
||||||
|
}
|
||||||
|
##^##*/
|
|
@ -5,6 +5,7 @@ StatusChatInput 1.0 StatusChatInput.qml
|
||||||
StatusEmojiCategoryButton 1.0 StatusEmojiCategoryButton.qml
|
StatusEmojiCategoryButton 1.0 StatusEmojiCategoryButton.qml
|
||||||
StatusEmojiPopup 1.0 StatusEmojiPopup.qml
|
StatusEmojiPopup 1.0 StatusEmojiPopup.qml
|
||||||
StatusEmojiSection 1.0 StatusEmojiSection.qml
|
StatusEmojiSection 1.0 StatusEmojiSection.qml
|
||||||
|
StatusGifPopup 1.0 StatusGifPopup.qml
|
||||||
StatusIconButton 1.0 StatusIconButton.qml
|
StatusIconButton 1.0 StatusIconButton.qml
|
||||||
StatusImageIdenticon 1.0 StatusImageIdenticon.qml
|
StatusImageIdenticon 1.0 StatusImageIdenticon.qml
|
||||||
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml
|
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml
|
||||||
|
|
Loading…
Reference in New Issue