overte/interface/resources/qml/hifi/AvatarApp.qml
armored-dragon 9a989e5ada
Fixed lingering references to avatarIcon.
Signed-off-by: armored-dragon <publicmail@armoreddragon.com>
2024-10-03 10:38:01 -05:00

864 lines
31 KiB
QML

import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQml.Models 2.1
import QtGraphicalEffects 1.0
import controlsUit 1.0 as HifiControls
import stylesUit 1.0
import "avatarapp"
Rectangle {
id: root
width: 480
height: 706
property bool keyboardEnabled: true
property bool keyboardRaised: false
property bool punctuationMode: false
HifiConstants { id: hifi }
HifiControls.Keyboard {
id: keyboard
z: 1000
raised: parent.keyboardEnabled && parent.keyboardRaised && HMD.active
numeric: parent.punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
}
color: style.colors.white
property string getAvatarsMethod: 'getAvatars'
signal sendToScript(var message);
function emitSendToScript(message) {
sendToScript(message);
}
ListModel { // the only purpose of this model is to convert JS object to ListElement
id: currentAvatarModel
dynamicRoles: true;
function makeAvatarEntry(avatarObject) {
clear();
append(avatarObject);
return get(count - 1);
}
}
property var jointNames: []
property var currentAvatarSettings;
property bool wearablesFrozen;
function fetchAvatarModelName(marketId, avatar) {
var xmlhttp = new XMLHttpRequest();
var url = "https://highfidelity.com/api/v1/marketplace/items/" + marketId;
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState === XMLHttpRequest.DONE && xmlhttp.status === 200) {
try {
var marketResponse = JSON.parse(xmlhttp.responseText.trim())
if (marketResponse.status === 'success') {
avatar.modelName = marketResponse.data.title;
}
}
catch(err) {
//console.error(err);
}
}
}
xmlhttp.open("GET", url, true);
xmlhttp.send();
}
function getAvatarModelName() {
if (currentAvatar === null) {
return '';
}
if (currentAvatar.modelName !== undefined) {
return currentAvatar.modelName;
} else {
var marketId = allAvatars.extractMarketId(currentAvatar.avatarUrl);
if (marketId !== '') {
fetchAvatarModelName(marketId, currentAvatar);
}
}
var avatarUrl = currentAvatar.avatarUrl;
var splitted = avatarUrl.split('/');
return splitted[splitted.length - 1];
}
property string avatarName: currentAvatar ? currentAvatar.name : ''
property string avatarUrl: currentAvatar ? currentAvatar.thumbnailUrl : null
property bool isAvatarInFavorites: currentAvatar ? allAvatars.findAvatar(currentAvatar.name) !== undefined : false
property int avatarWearablesCount: currentAvatar ? currentAvatar.wearables.count : 0
property var currentAvatar: null;
function setCurrentAvatar(avatar, bookmarkName) {
var currentAvatarObject = allAvatars.makeAvatarObject(avatar, bookmarkName);
currentAvatar = currentAvatarModel.makeAvatarEntry(currentAvatarObject);
}
property url externalAvatarThumbnailUrl: '../../images/avatarapp/guy-in-circle.svg'
function fromScript(message) {
if (message.method === 'initialize') {
jointNames = message.data.jointNames;
emitSendToScript({'method' : getAvatarsMethod});
} else if (message.method === 'wearableUpdated') {
adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties, message.updateUI);
} else if (message.method === 'wearablesUpdated') {
var wearablesModel = currentAvatar.wearables;
wearablesModel.clear();
message.wearables.forEach(function(wearable) {
wearablesModel.append(wearable);
});
adjustWearables.refresh(currentAvatar);
} else if (message.method === 'scaleChanged') {
currentAvatar.avatarScale = message.value;
updateCurrentAvatarInBookmarks(currentAvatar);
} else if (message.method === 'externalAvatarApplied') {
currentAvatar.avatarUrl = message.avatarURL;
currentAvatar.thumbnailUrl = allAvatars.makeThumbnailUrl(message.avatarURL);
currentAvatar.entry.avatarUrl = currentAvatar.avatarUrl;
currentAvatar.modelName = undefined;
updateCurrentAvatarInBookmarks(currentAvatar);
} else if (message.method === 'settingChanged') {
currentAvatarSettings[message.name] = message.value;
} else if (message.method === 'changeSettings') {
currentAvatarSettings = message.settings;
} else if (message.method === 'bookmarkLoaded') {
setCurrentAvatar(message.data.currentAvatar, message.data.name);
var avatarIndex = allAvatars.findAvatarIndex(currentAvatar.name);
allAvatars.move(avatarIndex, 0, 1);
view.setPage(0);
} else if (message.method === 'bookmarkAdded') {
var avatar = allAvatars.findAvatar(message.bookmarkName);
if (avatar !== undefined) {
var avatarObject = allAvatars.makeAvatarObject(message.bookmark, message.bookmarkName);
for(var prop in avatarObject) {
avatar[prop] = avatarObject[prop];
}
if (currentAvatar.name === message.bookmarkName) {
currentAvatar = currentAvatarModel.makeAvatarEntry(avatarObject);
}
} else {
allAvatars.addAvatarEntry(message.bookmark, message.bookmarkName);
}
updateCurrentAvatarInBookmarks(currentAvatar);
} else if (message.method === 'bookmarkDeleted') {
pageOfAvatars.isUpdating = true;
var index = pageOfAvatars.findAvatarIndex(message.name);
var absoluteIndex = view.currentPage * view.itemsPerPage + index
allAvatars.remove(absoluteIndex)
pageOfAvatars.remove(index);
var itemsOnPage = pageOfAvatars.count;
var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage;
if (newItemIndex <= (allAvatars.count - 1)) {
pageOfAvatars.append(allAvatars.get(newItemIndex));
} else {
if (!pageOfAvatars.hasGetAvatars()) {
pageOfAvatars.appendGetAvatars();
}
}
pageOfAvatars.isUpdating = false;
} else if (message.method === getAvatarsMethod) {
var getAvatarsData = message.data;
allAvatars.populate(getAvatarsData.bookmarks);
setCurrentAvatar(getAvatarsData.currentAvatar, '');
displayNameInput.text = getAvatarsData.displayName;
currentAvatarSettings = getAvatarsData.currentAvatarSettings;
var bookmarkAvatarIndex = allAvatars.findAvatarIndexByValue(currentAvatar);
if (bookmarkAvatarIndex === -1) {
currentAvatar.name = '';
} else {
currentAvatar.name = allAvatars.get(bookmarkAvatarIndex).name;
allAvatars.move(bookmarkAvatarIndex, 0, 1);
}
view.setPage(0);
} else if (message.method === 'updateAvatarInBookmarks') {
updateCurrentAvatarInBookmarks(currentAvatar);
} else if (message.method === 'selectAvatarEntity') {
adjustWearables.selectWearableByID(message.entityID);
} else if (message.method === 'wearablesFrozenChanged') {
wearablesFrozen = message.wearablesFrozen;
}
}
function updateCurrentAvatarInBookmarks(avatar) {
var bookmarkAvatarIndex = allAvatars.findAvatarIndexByValue(avatar);
if (bookmarkAvatarIndex === -1) {
avatar.name = '';
view.setPage(0);
} else {
var bookmarkAvatar = allAvatars.get(bookmarkAvatarIndex);
avatar.name = bookmarkAvatar.name;
view.selectAvatar(bookmarkAvatar);
}
}
property bool isInManageState: false
Component.onDestruction: {
keyboard.raised = false;
}
AvatarAppStyle {
id: style
}
AvatarAppHeader {
id: header
z: 100
property string currentPage: "Avatar"
property bool mainPageVisible: !settings.visible && !adjustWearables.visible
Binding on currentPage {
when: settings.visible
value: "Avatar Settings"
}
Binding on currentPage {
when: adjustWearables.visible
value: "Adjust Wearables"
}
Binding on currentPage {
when: header.mainPageVisible
value: "Avatar"
}
pageTitle: currentPage
settingsButtonVisible: mainPageVisible
onSettingsClicked: {
displayNameInput.focus = false;
root.keyboardRaised = false;
settings.open(currentAvatarSettings, currentAvatar.avatarScale);
}
}
Settings {
id: settings
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.bottom: parent.bottom
z: 3
onSaveClicked: function() {
var avatarSettings = {
dominantHand : settings.dominantHandIsLeft ? 'left' : 'right',
hmdAvatarAlignmentType : settings.hmdAvatarAlignmentTypeIsEyes ? 'eyes' : 'head',
collisionsEnabled : settings.environmentCollisionsOn,
otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn,
animGraphOverrideUrl : settings.avatarAnimationOverrideJSON,
collisionSoundUrl : settings.avatarCollisionSoundUrl
};
emitSendToScript({'method' : 'saveSettings', 'settings' : avatarSettings, 'avatarScale': settings.scaleValue})
close();
}
onCancelClicked: function() {
emitSendToScript({'method' : 'revertScale', 'avatarScale' : avatarScaleBackup});
close();
}
onScaleChanged: {
emitSendToScript({'method' : 'setScale', 'avatarScale' : scale})
}
}
AdjustWearables {
id: adjustWearables
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.bottom: parent.bottom
jointNames: root.jointNames
onWearableUpdated: {
emitSendToScript({'method' : 'adjustWearable', 'entityID' : id, 'wearableIndex' : index, 'properties' : properties})
}
onWearableDeleted: {
emitSendToScript({'method' : 'deleteWearable', 'entityID' : id, 'avatarName' : avatarName});
}
onAdjustWearablesOpened: {
emitSendToScript({'method' : 'adjustWearablesOpened', 'avatarName' : avatarName});
}
onAdjustWearablesClosed: {
emitSendToScript({'method' : 'adjustWearablesClosed', 'save' : status, 'avatarName' : avatarName});
}
onWearableSelected: {
emitSendToScript({'method' : 'selectWearable', 'entityID' : id});
}
onAddWearable: {
emitSendToScript({'method' : 'addWearable', 'avatarName' : avatarName, 'url' : url});
}
z: 3
}
Rectangle {
id: mainBlock
anchors.left: parent.left
anchors.leftMargin: 30
anchors.right: parent.right
anchors.rightMargin: 30
anchors.top: header.bottom
anchors.bottom: favoritesBlock.top
// TextStyle1
RalewaySemiBold {
size: 24;
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 34
}
// TextStyle1
RalewaySemiBold {
id: displayNameLabel
size: 24;
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 25
text: 'Display Name'
}
InputField {
id: displayNameInput
font.family: "Fira Sans"
font.pixelSize: 15
anchors.left: displayNameLabel.right
anchors.leftMargin: 30
anchors.verticalCenter: displayNameLabel.verticalCenter
anchors.right: parent.right
width: 232
text: 'ThisIsDisplayName'
onEditingFinished: {
emitSendToScript({'method' : 'changeDisplayName', 'displayName' : text})
focus = false;
}
onFocusChanged: {
root.keyboardRaised = focus;
}
}
ShadowImage {
id: avatarImage
width: 134
height: 134
anchors.top: displayNameLabel.bottom
anchors.topMargin: 31
Binding on source {
when: avatarUrl !== ''
value: avatarUrl
}
visible: avatarImage.status !== Image.Loading && avatarImage.status !== Image.Error
fillMode: Image.PreserveAspectCrop
}
ShadowImage {
id: customAvatarImage
anchors.fill: avatarImage;
visible: avatarUrl === '' || avatarImage.status === Image.Error
source: externalAvatarThumbnailUrl
}
ShadowRectangle {
anchors.fill: avatarImage;
color: 'white'
visible: avatarImage.status === Image.Loading
radius: avatarImage.radius
dropShadowRadius: avatarImage.dropShadowRadius;
dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset
dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset
Spinner {
id: spinner
visible: parent.visible
anchors.fill: parent;
}
}
AvatarWearablesIndicator {
anchors.right: avatarImage.right
anchors.bottom: avatarImage.bottom
anchors.rightMargin: -radius
anchors.bottomMargin: 6.08
wearablesCount: avatarWearablesCount
visible: avatarWearablesCount !== 0
}
RowLayout {
id: star
anchors.top: avatarImage.top
anchors.topMargin: 11
anchors.left: avatarImage.right
anchors.leftMargin: 30.5
anchors.right: parent.right
spacing: 12.3
Image {
width: 21.2
height: 19.3
source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg'
Layout.alignment: Qt.AlignVCenter
}
// TextStyle5
FiraSansSemiBold {
size: 22;
Layout.fillWidth: true
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
elide: Qt.ElideRight
Layout.alignment: Qt.AlignVCenter
}
}
MouseArea {
enabled: !isAvatarInFavorites
anchors.fill: star
onClicked: {
createFavorite.onSaveClicked = function() {
var entry = currentAvatar.entry;
var wearables = [];
for(var i = 0; i < currentAvatar.wearables.count; ++i) {
wearables.push(currentAvatar.wearables.get(i));
}
entry.avatarEntites = wearables;
currentAvatar.name = createFavorite.favoriteNameText;
emitSendToScript({'method': 'addAvatar', 'name' : currentAvatar.name});
createFavorite.close();
}
var avatarThumbnail = (avatarUrl === '' || avatarImage.status === Image.Error) ?
externalAvatarThumbnailUrl : avatarUrl;
createFavorite.open(root.currentAvatar.wearables.count, avatarThumbnail);
}
}
// TextStyle3
RalewayRegular {
id: avatarNameLabel
size: 22;
text: getAvatarModelName();
elide: Qt.ElideRight
anchors.right: linkLabel.left
anchors.left: avatarImage.right
anchors.leftMargin: 30
anchors.top: star.bottom
anchors.topMargin: 11
property bool hasMarketId: currentAvatar && allAvatars.extractMarketId(currentAvatar.avatarUrl) !== '';
MouseArea {
enabled: avatarNameLabel.hasMarketId
anchors.fill: parent;
onClicked: emitSendToScript({'method' : 'navigate', 'url' : allAvatars.makeMarketItemUrl(currentAvatar.avatarUrl)})
}
color: hasMarketId ? style.colors.blueHighlight : 'black'
}
// TextStyle3
RalewayRegular {
id: wearablesLabel
size: 22;
anchors.left: avatarImage.right
anchors.leftMargin: 30
anchors.top: avatarNameLabel.bottom
anchors.topMargin: 16
color: 'black'
text: 'Wearables'
}
SquareLabel {
id: linkLabel
anchors.right: parent.right
anchors.verticalCenter: avatarNameLabel.verticalCenter
glyphText: "."
glyphSize: 22
onClicked: {
popup.showSpecifyAvatarUrl(currentAvatar.avatarUrl, function() {
var url = popup.inputText.text;
emitSendToScript({'method' : 'applyExternalAvatar', 'avatarURL' : url})
}, function(link) {
Qt.openUrlExternally(link);
});
}
}
SquareLabel {
id: adjustLabel
anchors.right: parent.right
anchors.verticalCenter: wearablesLabel.verticalCenter
glyphText: "\ue02e"
onClicked: {
if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) {
adjustWearables.open(currentAvatar);
} else {
Window.alert("You cannot use wearables on this domain.")
}
}
}
SquareLabel {
anchors.right: adjustLabel.left
anchors.verticalCenter: wearablesLabel.verticalCenter
anchors.rightMargin: 15
glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock;
onClicked: {
if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) {
emitSendToScript({'method' : 'toggleWearablesFrozen'});
} else {
Window.alert("You cannot use wearables on this domain.")
}
}
}
}
Rectangle {
id: favoritesBlock
height: 407
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: style.colors.lightGrayBackground
// TextStyle1
RalewaySemiBold {
id: favoritesLabel
size: 24;
anchors.top: parent.top
anchors.topMargin: 15
anchors.left: parent.left
anchors.leftMargin: 30
text: "Favorites"
}
// TextStyle8
RalewaySemiBold {
id: manageLabel
color: style.colors.blueHighlight
size: 20;
anchors.top: parent.top
anchors.topMargin: 20
anchors.right: parent.right
anchors.rightMargin: 30
text: isInManageState ? "Back" : "Manage"
MouseArea {
anchors.fill: parent
onClicked: {
isInManageState = isInManageState ? false : true;
}
}
}
Item {
anchors.left: parent.left
anchors.leftMargin: 30
anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: favoritesLabel.bottom
anchors.topMargin: 20
anchors.bottom: parent.bottom
GridView {
id: view
anchors.fill: parent
interactive: false;
currentIndex: currentAvatarIndexInBookmarksPage();
function currentAvatarIndexInBookmarksPage() {
return (currentAvatar && currentAvatar.name !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatarIndex(currentAvatar.name) : -1;
}
property int horizontalSpacing: 18
property int verticalSpacing: 44
property int thumbnailWidth: 92
property int thumbnailHeight: 92
function selectAvatar(avatar) {
emitSendToScript({'method' : 'selectAvatar', 'name' : avatar.name})
}
function deleteAvatar(avatar) {
emitSendToScript({'method' : 'deleteAvatar', 'name' : avatar.name})
}
AvatarsModel {
id: allAvatars
}
property int itemsPerPage: 8
property int totalPages: Math.ceil((allAvatars.count + 1) / itemsPerPage)
property int currentPage: 0;
onCurrentPageChanged: {
currentIndex = Qt.binding(currentAvatarIndexInBookmarksPage);
}
property bool hasNext: currentPage < (totalPages - 1)
property bool hasPrev: currentPage > 0
function setPage(pageIndex) {
pageOfAvatars.isUpdating = true;
pageOfAvatars.clear();
var start = pageIndex * itemsPerPage;
var end = Math.min(start + itemsPerPage, allAvatars.count);
for(var itemIndex = 0; start < end; ++start, ++itemIndex) {
var avatarItem = allAvatars.get(start)
pageOfAvatars.append(avatarItem);
}
if (pageOfAvatars.count !== itemsPerPage) {
pageOfAvatars.appendGetAvatars();
}
currentPage = pageIndex;
pageOfAvatars.isUpdating = false;
}
model: AvatarsModel {
id: pageOfAvatars
property bool isUpdating: false;
property var getMoreAvatarsEntry: {'thumbnailUrl' : '', 'name' : '', 'getMoreAvatars' : true}
function appendGetAvatars() {
append(getMoreAvatarsEntry);
}
function hasGetAvatars() {
return count != 0 && get(count - 1).getMoreAvatars
}
function removeGetAvatars() {
if (hasGetAvatars()) {
remove(count - 1)
}
}
}
flow: GridView.FlowLeftToRight
cellHeight: thumbnailHeight + verticalSpacing
cellWidth: thumbnailWidth + horizontalSpacing
delegate: Item {
id: delegateRoot
height: GridView.view.cellHeight
width: GridView.view.cellWidth
Item {
id: container
width: 92
height: 92
Behavior on y {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: "hovered"
when: favoriteAvatarMouseArea.containsMouse;
PropertyChanges { target: favoriteAvatarMouseArea; anchors.bottomMargin: -5 }
PropertyChanges { target: container; y: -5 }
PropertyChanges { target: favoriteAvatarImage; dropShadowRadius: 10 }
PropertyChanges { target: favoriteAvatarImage; dropShadowVerticalOffset: 6 }
},
State {
name: "getMoreAvatarsHovered"
when: getMoreAvatarsMouseArea.containsMouse;
PropertyChanges { target: getMoreAvatarsMouseArea; anchors.bottomMargin: -5 }
PropertyChanges { target: container; y: -5 }
PropertyChanges { target: getMoreAvatarsImage; dropShadowRadius: 10 }
PropertyChanges { target: getMoreAvatarsImage; dropShadowVerticalOffset: 6 }
}
]
property bool highlighted: delegateRoot.GridView.isCurrentItem
AvatarThumbnail {
id: favoriteAvatarImage
externalAvatarThumbnailUrl: root.externalAvatarThumbnailUrl
avatarUrl: thumbnailUrl
border.color: container.highlighted ? style.colors.blueHighlight : 'transparent'
border.width: container.highlighted ? 4 : 0
wearablesCount: {
return !getMoreAvatars ? wearables.count : 0
}
visible: !getMoreAvatars
MouseArea {
id: favoriteAvatarMouseArea
anchors.fill: parent
anchors.margins: 0
enabled: !container.highlighted
hoverEnabled: enabled
onClicked: {
if (isInManageState) {
var currentItem = delegateRoot.GridView.view.model.get(index);
popup.showDeleteFavorite(currentItem.name, function() {
view.deleteAvatar(currentItem);
});
} else {
if (delegateRoot.GridView.view.currentIndex !== index) {
var currentItem = delegateRoot.GridView.view.model.get(index);
popup.showLoadFavorite(currentItem.name, function() {
view.selectAvatar(currentItem);
});
}
}
}
}
}
Rectangle {
anchors.fill: favoriteAvatarImage
color: '#AFAFAF'
opacity: 0.4
radius: 5
visible: isInManageState && !container.highlighted && !getMoreAvatars
}
HiFiGlyphs {
anchors.fill: parent
text: "{"
visible: isInManageState && !container.highlighted && !getMoreAvatars
horizontalAlignment: Text.AlignHCenter
size: 56
}
ShadowRectangle {
id: getMoreAvatarsImage
width: 92
height: 92
radius: 5
color: style.colors.blueHighlight
visible: getMoreAvatars && !isInManageState
HiFiGlyphs {
anchors.centerIn: parent
color: 'white'
size: 60
text: "K"
}
MouseArea {
id: getMoreAvatarsMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
popup.showBuyAvatars(null, function(link) {
emitSendToScript({'method' : 'navigate', 'url' : link})
});
}
}
}
}
// TextStyle7
FiraSansRegular {
id: text
size: 18;
lineHeightMode: Text.FixedHeight
lineHeight: 16.9;
width: view.thumbnailWidth
height: view.verticalSpacing
elide: Qt.ElideRight
anchors.top: container.bottom
anchors.topMargin: 8
anchors.horizontalCenter: container.horizontalCenter
verticalAlignment: Text.AlignTop
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: getMoreAvatars ? 'Get More Avatars' : name
visible: !getMoreAvatars || !isInManageState
}
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 40
height: 40
color: 'transparent'
PageIndicator {
x: 1
hasNext: view.hasNext
hasPrev: view.hasPrev
onClicked: view.setPage(view.currentPage - 1)
}
}
spacing: 0
Rectangle {
width: 40
height: 40
color: 'transparent'
PageIndicator {
x: -1
isPrevious: false
hasNext: view.hasNext
hasPrev: view.hasPrev
onClicked: view.setPage(view.currentPage + 1)
}
}
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
}
}
MessageBoxes {
id: popup
}
CreateFavoriteDialog {
avatars: allAvatars
id: createFavorite
}
}