overte/interface/resources/qml/hifi/AvatarApp.qml
2018-07-31 22:03:34 +03:00

843 lines
30 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 "../controls-uit" as HifiControls
import "../styles-uit"
import "avatarapp"
Rectangle {
id: root
width: 480
height: 706
property bool keyboardEnabled: true
property bool keyboardRaised: false
property bool punctuationMode: false
HifiControls.Keyboard {
id: keyboard
z: 1000
raised: parent.keyboardEnabled && parent.keyboardRaised
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;
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;
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'updateAvatarInBookmarks') {
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'selectAvatarEntity') {
adjustWearables.selectWearableByID(message.entityID);
}
}
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.onCompleted: {
}
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
avatarIconVisible: mainPageVisible
settingsButtonVisible: mainPageVisible
onSettingsClicked: {
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',
collisionsEnabled : settings.avatarCollisionsOn,
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});
}
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;
}
}
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'
anchors.verticalCenter: parent.verticalCenter
}
// TextStyle5
FiraSansSemiBold {
size: 22;
Layout.fillWidth: true
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
elide: Qt.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
}
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 {
anchors.right: parent.right
anchors.verticalCenter: wearablesLabel.verticalCenter
glyphText: "\ue02e"
visible: avatarWearablesCount !== 0
onClicked: {
adjustWearables.open(currentAvatar);
}
}
// TextStyle3
RalewayRegular {
size: 22;
anchors.right: parent.right
anchors.verticalCenter: wearablesLabel.verticalCenter
font.underline: true
text: "Add"
color: 'black'
visible: avatarWearablesCount === 0
MouseArea {
anchors.fill: parent
onClicked: {
popup.showGetWearables(function() {
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
}, function(link) {
emitSendToScript({'method' : 'navigate', 'url' : link})
});
}
}
}
}
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(function() {
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://BodyMart'})
}, 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
}
}