Browse Source

Add q-app

pull/138/head
AlphaX-Projects 2 years ago committed by GitHub
parent
commit
a4481c14ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 626
      qortal-ui-plugins/plugins/core/q-app/app-browser/app-browser.src.js
  2. 55
      qortal-ui-plugins/plugins/core/q-app/app-browser/index.html
  3. 55
      qortal-ui-plugins/plugins/core/q-app/index.html
  4. 55
      qortal-ui-plugins/plugins/core/q-app/publish-app/index.html
  5. 670
      qortal-ui-plugins/plugins/core/q-app/publish-app/publish-app.src.js
  6. 1041
      qortal-ui-plugins/plugins/core/q-app/q-apps.src.js

626
qortal-ui-plugins/plugins/core/q-app/app-browser/app-browser.src.js

@ -0,0 +1,626 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../../epml'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@material/mwc-button'
import '@material/mwc-icon'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class AppBrowser extends LitElement {
static get properties() {
return {
url: { type: String },
name: { type: String },
service: { type: String },
identifier: { type: String },
path: { type: String },
displayUrl: {type: String },
followedNames: { type: Array },
blockedNames: { type: Array },
theme: { type: String, reflect: true }
}
}
static get observers() {
return ['_kmxKeyUp(amount)']
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--paper-input-container-focus-color: var(--mdc-theme-primary);
}
#websitesWrapper paper-button {
float: right;
}
#websitesWrapper .buttons {
width: auto !important;
}
.address-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100px;
background-color: var(--white);
height: 36px;
}
.address-bar-button mwc-icon {
width: 20px;
}
.iframe-container {
position: absolute;
top: 36px;
left: 0;
right: 0;
bottom: 0;
border-top: 1px solid var(--black);
}
.iframe-container iframe {
display: block;
width: 100%;
height: 100%;
border: none;
background-color: var(--white);
}
input[type=text] {
margin: 0;
padding: 2px 0 0 20px;
border: 0;
height: 34px;
font-size: 16px;
background-color: var(--white);
}
paper-progress {
--paper-progress-active-color: var(--mdc-theme-primary);
}
.float-right {
float: right;
}
`
}
constructor() {
super()
this.url = 'about:blank'
const urlParams = new URLSearchParams(window.location.search);
this.name = urlParams.get('name');
this.service = urlParams.get('service');
this.identifier = urlParams.get('identifier') != null ? urlParams.get('identifier') : null;
this.path = urlParams.get('path') != null ? ((urlParams.get('path').startsWith("/") ? "" : "/") + urlParams.get('path')) : "";
this.followedNames = []
this.blockedNames = []
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
// Build initial display URL
let displayUrl = "qortal://" + this.service + "/" + this.name;
if (this.identifier != null && data.identifier != "" && this.identifier != "default") displayUrl = displayUrl.concat("/" + this.identifier);
if (this.path != null && this.path != "/") displayUrl = displayUrl.concat(this.path);
this.displayUrl = displayUrl;
const getFollowedNames = async () => {
let followedNames = await parentEpml.request('apiCall', {
url: `/lists/followedNames?apiKey=${this.getApiKey()}`
})
this.followedNames = followedNames
setTimeout(getFollowedNames, this.config.user.nodeSettings.pingInterval)
}
const getBlockedNames = async () => {
let blockedNames = await parentEpml.request('apiCall', {
url: `/lists/blockedNames?apiKey=${this.getApiKey()}`
})
this.blockedNames = blockedNames
setTimeout(getBlockedNames, this.config.user.nodeSettings.pingInterval)
}
const render = () => {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`;
}
const authorizeAndRender = () => {
parentEpml.request('apiCall', {
url: `/render/authorize/${this.name}?apiKey=${this.getApiKey()}`,
method: "POST"
}).then(res => {
if (res.error) {
// Authorization problem - API key incorrect?
}
else {
render()
}
})
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
this.config = JSON.parse(c)
if (!configLoaded) {
authorizeAndRender()
setTimeout(getFollowedNames, 1)
setTimeout(getBlockedNames, 1)
configLoaded = true
}
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
})
}
render() {
return html`
<div id="websitesWrapper" style="width:auto; padding:10px; background: var(--white);">
<div class="layout horizontal center">
<div class="address-bar">
<mwc-button @click=${() => this.goBack()} title="${translate("general.back")}" class="address-bar-button"><mwc-icon>arrow_back_ios</mwc-icon></mwc-button>
<mwc-button @click=${() => this.goForward()} title="${translate("browserpage.bchange1")}" class="address-bar-button"><mwc-icon>arrow_forward_ios</mwc-icon></mwc-button>
<mwc-button @click=${() => this.refresh()} title="${translate("browserpage.bchange2")}" class="address-bar-button"><mwc-icon>refresh</mwc-icon></mwc-button>
<mwc-button @click=${() => this.goBackToList()} title="${translate("browserpage.bchange3")}" class="address-bar-button"><mwc-icon>home</mwc-icon></mwc-button>
<input disabled style="width: 550px; color: var(--black);" id="address" type="text" value="${this.displayUrl}"></input>
<mwc-button @click=${() => this.delete()} title="${translate("browserpage.bchange4")} ${this.service} ${this.name} ${translate("browserpage.bchange5")}" class="address-bar-button float-right"><mwc-icon>delete</mwc-icon></mwc-button>
${this.renderBlockUnblockButton()}
${this.renderFollowUnfollowButton()}
</div>
<div class="iframe-container">
<iframe id="browser-iframe" src="${this.url}" sandbox="allow-scripts allow-forms allow-downloads">
<span style="color: var(--black);">${translate("browserpage.bchange6")}</span>
</iframe>
</div>
</div>
</div>
`
}
firstUpdated() {
this.changeTheme()
this.changeLanguage()
window.addEventListener('contextmenu', (event) => {
event.preventDefault()
this._textMenu(event)
})
window.addEventListener('click', () => {
parentEpml.request('closeCopyTextMenu', null)
})
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
}
}
window.addEventListener("message", (event) => {
if (event == null || event.data == null || event.data.length == 0 || event.data.action == null) {
return;
}
let response = "{\"error\": \"Request could not be fulfilled\"}";
let data = event.data;
console.log("UI received event: " + JSON.stringify(data));
switch (data.action) {
case "GET_USER_ACCOUNT":
// For now, we will return this without prompting the user, but we may need to add a prompt later
let account = {};
account["address"] = this.selectedAddress.address;
account["publicKey"] = this.selectedAddress.base58PublicKey;
response = JSON.stringify(account);
break;
case "LINK_TO_QDN_RESOURCE":
case "QDN_RESOURCE_DISPLAYED":
// Links are handled by the core, but the UI also listens for these actions in order to update the address bar.
// Note: don't update this.url here, as we don't want to force reload the iframe each time.
let url = "qortal://" + data.service + "/" + data.name;
this.path = data.path != null ? ((data.path.startsWith("/") ? "" : "/") + data.path) : null;
if (data.identifier != null && data.identifier != "" && data.identifier != "default") url = url.concat("/" + data.identifier);
if (this.path != null && this.path != "/") url = url.concat(this.path);
this.name = data.name;
this.service = data.service;
this.identifier = data.identifier;
this.displayUrl = url;
return;
case "PUBLISH_QDN_RESOURCE":
// Use "default" if user hasn't specified an identifer
if (data.identifier == null) {
data.identifier = "default";
}
// Params: data.service, data.name, data.identifier, data.data64,
// TODO: prompt user for publish. If they confirm, call `POST /arbitrary/{service}/{name}/{identifier}/base64` and sign+process transaction
// then set the response string from the core to the `response` variable (defined above)
// If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}`
break;
case "SEND_CHAT_MESSAGE":
// Params: data.groupId, data.destinationAddress, data.message
// TODO: prompt user to send chat message. If they confirm, sign+process a CHAT transaction
// then set the response string from the core to the `response` variable (defined above)
// If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}`
break;
case "JOIN_GROUP":
// Params: data.groupId
// TODO: prompt user to join group. If they confirm, sign+process a JOIN_GROUP transaction
// then set the response string from the core to the `response` variable (defined above)
// If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}`
break;
case "DEPLOY_AT":
// Params: data.creationBytes, data.name, data.description, data.type, data.tags, data.amount, data.assetId, data.fee
// TODO: prompt user to deploy an AT. If they confirm, sign+process a DEPLOY_AT transaction
// then set the response string from the core to the `response` variable (defined above)
// If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}`
break;
case "GET_WALLET_BALANCE":
// Params: data.coin (QORT / LTC / DOGE / DGB / RVN / ARRR)
// TODO: prompt user to share wallet balance. If they confirm, call `GET /crosschain/:coin/walletbalance`, or for QORT, call `GET /addresses/balance/:address`
// then set the response string from the core to the `response` variable (defined above)
// If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}`
break;
case "SEND_COIN":
// Params: data.coin, data.destinationAddress, data.amount, data.fee
// TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction
// then set the response string from the core to the `response` variable (defined above)
// If they decline, send back JSON that includes an `error` key, such as `{"error": "User declined request"}`
break;
default:
console.log("Unhandled message: " + JSON.stringify(data));
return;
}
// Parse response
let responseObj;
try {
responseObj = JSON.parse(response);
} catch (e) {
// Not all responses will be JSON
responseObj = response;
}
// Respond to app
if (responseObj.error != null) {
event.ports[0].postMessage({
result: null,
error: responseObj
});
}
else {
event.ports[0].postMessage({
result: responseObj,
error: null
});
}
});
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
renderFollowUnfollowButton() {
// Only show the follow/unfollow button if we have permission to modify the list on this node
if (this.followedNames == null || !Array.isArray(this.followedNames)) {
return html``
}
if (this.followedNames.indexOf(this.name) === -1) {
// render follow button
return html`<mwc-button @click=${() => this.follow()} title="${translate("browserpage.bchange7")} ${this.name}" class="address-bar-button float-right"><mwc-icon>add_to_queue</mwc-icon></mwc-button>`
}
else {
// render unfollow button
return html`<mwc-button @click=${() => this.unfollow()} title="${translate("browserpage.bchange8")} ${this.name}" class="address-bar-button float-right"><mwc-icon>remove_from_queue</mwc-icon></mwc-button>`
}
}
renderBlockUnblockButton() {
// Only show the block/unblock button if we have permission to modify the list on this node
if (this.blockedNames == null || !Array.isArray(this.blockedNames)) {
return html``
}
if (this.blockedNames.indexOf(this.name) === -1) {
// render block button
return html`<mwc-button @click=${() => this.block()} title="${translate("browserpage.bchange9")} ${this.name}" class="address-bar-button float-right"><mwc-icon>block</mwc-icon></mwc-button>`
}
else {
// render unblock button
return html`<mwc-button @click=${() => this.unblock()} title="${translate("browserpage.bchange10")} ${this.name}" class="address-bar-button float-right"><mwc-icon>radio_button_unchecked</mwc-icon></mwc-button>`
}
}
// Navigation
goBack() {
window.history.back();
}
goForward() {
window.history.forward();
}
refresh() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ""}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ""}`;
}
goBackToList() {
window.location = "../index.html";
}
follow() {
this.followName(this.name);
}
unfollow() {
this.unfollowName(this.name);
}
block() {
this.blockName(this.name);
}
unblock() {
this.unblockName(this.name);
}
delete() {
this.deleteCurrentResource();
}
async followName(name) {
let items = [
name
]
let namesJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/followedNames?apiKey=${this.getApiKey()}`,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: `${namesJsonString}`
})
if (ret === true) {
// Successfully followed - add to local list
// Remove it first by filtering the list - doing it this way ensures the UI updates
// immediately, as apposed to only adding if it doesn't already exist
this.followedNames = this.followedNames.filter(item => item != name);
this.followedNames.push(name)
}
else {
let err1string = get("browserpage.bchange11")
parentEpml.request('showSnackBar', `${err1string}`)
}
return ret
}
async unfollowName(name) {
let items = [
name
]
let namesJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/followedNames?apiKey=${this.getApiKey()}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: `${namesJsonString}`
})
if (ret === true) {
// Successfully unfollowed - remove from local list
this.followedNames = this.followedNames.filter(item => item != name);
}
else {
let err2string = get("browserpage.bchange12")
parentEpml.request('showSnackBar', `${err2string}`)
}
return ret
}
async blockName(name) {
let items = [
name
]
let namesJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/blockedNames?apiKey=${this.getApiKey()}`,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: `${namesJsonString}`
})
if (ret === true) {
// Successfully blocked - add to local list
// Remove it first by filtering the list - doing it this way ensures the UI updates
// immediately, as apposed to only adding if it doesn't already exist
this.blockedNames = this.blockedNames.filter(item => item != name);
this.blockedNames.push(name)
}
else {
let err3string = get("browserpage.bchange13")
parentEpml.request('showSnackBar', `${err3string}`)
}
return ret
}
async unblockName(name) {
let items = [
name
]
let namesJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/blockedNames?apiKey=${this.getApiKey()}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: `${namesJsonString}`
})
if (ret === true) {
// Successfully unblocked - remove from local list
this.blockedNames = this.blockedNames.filter(item => item != name);
}
else {
let err4string = get("browserpage.bchange14")
parentEpml.request('showSnackBar', `${err4string}`)
}
return ret
}
async deleteCurrentResource() {
if (this.followedNames.indexOf(this.name) != -1) {
// Following name - so deleting won't work
let err5string = get("browserpage.bchange15")
parentEpml.request('showSnackBar', `${err5string}`)
return;
}
let identifier = this.identifier == null ? "default" : resource.identifier;
let ret = await parentEpml.request('apiCall', {
url: `/arbitrary/resource/${this.service}/${this.name}/${identifier}?apiKey=${this.getApiKey()}`,
method: 'DELETE'
})
if (ret === true) {
this.goBackToList();
}
else {
let err6string = get("browserpage.bchange16")
parentEpml.request('showSnackBar', `${err6string}`)
}
return ret
}
_textMenu(event) {
const getSelectedText = () => {
var text = ''
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString()
} else if (typeof this.shadowRoot.selection != 'undefined' && this.shadowRoot.selection.type == 'Text') {
text = this.shadowRoot.selection.createRange().text
}
return text
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText()
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('app-browser', AppBrowser)

55
qortal-ui-plugins/plugins/core/q-app/app-browser/index.html

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<app-browser></app-browser>
<script src="app-browser.js"></script>
</body>
</html>

55
qortal-ui-plugins/plugins/core/q-app/index.html

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<q-apps></q-apps>
<script src="q-apps.js"></script>
</body>
</html>

55
qortal-ui-plugins/plugins/core/q-app/publish-app/index.html

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/font/material-icons.css">
<link rel="stylesheet" href="/font/switch-theme.css">
<script>
const checkBack = localStorage.getItem('qortalTheme')
if (checkBack === 'dark') {
newtheme = 'dark';
} else {
newtheme = 'light';
}
document.querySelector('html').setAttribute('theme', newtheme);
</script>
<style>
html {
--scrollbarBG: #a1a1a1;
--thumbBG: #6a6c75;
}
*::-webkit-scrollbar {
width: 11px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
}
*::-webkit-scrollbar-track {
background: var(--scrollbarBG);
}
*::-webkit-scrollbar-thumb {
background-color: var(--thumbBG);
border-radius: 6px;
border: 3px solid var(--scrollbarBG);
}
html,
body {
margin: 0;
font-family: "Roboto", sans-serif;
background: var(--plugback);
}
</style>
</head>
<body>
<publish-app></publish-app>
<script src="publish-app.js"></script>
</body>
</html>

670
qortal-ui-plugins/plugins/core/q-app/publish-app/publish-app.src.js

@ -0,0 +1,670 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../../epml'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
})
import '@material/mwc-button'
import '@material/mwc-textfield'
import '@material/mwc-select'
import '@material/mwc-list/mwc-list-item.js'
import '@polymer/paper-progress/paper-progress.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class PublishApp extends LitElement {
static get properties() {
return {
name: { type: String },
service: { type: String },
identifier: { type: String },
category: { type: String },
uploadType: { type: String },
showName: { type: Boolean },
showService: { type: Boolean },
showIdentifier: { type: Boolean },
showMetadata: { type: Boolean },
tags: { type: Array },
serviceLowercase: { type: String },
metadata: { type: Array },
categories: { type: Array },
names: { type: Array },
myRegisteredName: { type: String },
selectedName: { type: String },
path: { type: String },
portForwardingEnabled: { type: Boolean },
amount: { type: Number },
generalMessage: { type: String },
successMessage: { type: String },
errorMessage: { type: String },
loading: { type: Boolean },
btnDisable: { type: Boolean },
theme: { type: String, reflect: true }
}
}
static get observers() {
return ['_kmxKeyUp(amount)']
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--paper-input-container-focus-color: var(--mdc-theme-primary);
--lumo-primary-text-color: rgb(0, 167, 245);
--lumo-primary-color-50pct: rgba(0, 167, 245, 0.5);
--lumo-primary-color-10pct: rgba(0, 167, 245, 0.1);
--lumo-primary-color: hsl(199, 100%, 48%);
--lumo-base-color: var(--white);
--lumo-body-text-color: var(--black);
--lumo-secondary-text-color: var(--sectxt);
--lumo-contrast-60pct: var(--vdicon);
--_lumo-grid-border-color: var(--border);
--_lumo-grid-secondary-border-color: var(--border2);
}
input[type=text] {
padding: 6px 6px 6px 6px;
color: var(--black);
}
input[type=file]::file-selector-button {
border: 1px solid transparent;
padding: 6px 6px 6px 6px;
border-radius: 5px;
color: #fff;
background-color: var(--mdc-theme-primary);
transition: 1s;
}
input[type=file]::file-selector-button:hover {
color: #000;
background-color: #81ecec;
border: 1px solid transparent;
}
#publishWrapper paper-button {
float: right;
}
#publishWrapper .buttons {
width: auto !important;
}
mwc-textfield {
margin: 0;
}
paper-progress {
--paper-progress-active-color: var(--mdc-theme-primary);
}
.upload-text {
display: block;
font-size: 14px;
color: var(--black);
}
.address-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100px;
background-color: var(--white);
height: 36px;
}
.address-bar-button mwc-icon {
width: 30px;
}
`
}
constructor() {
super()
this.showName = false;
this.showService = false
this.showIdentifier = false
this.showMetadata = false
const urlParams = new URLSearchParams(window.location.search)
this.name = urlParams.get('name')
this.service = urlParams.get('service')
this.identifier = urlParams.get('identifier')
this.category = urlParams.get('category')
this.uploadType = urlParams.get('uploadType') !== "null" ? urlParams.get('uploadType') : "file"
if (urlParams.get('showName') === "true") {
this.showName = true
}
if (urlParams.get('showService') === "true") {
this.showService = true
}
if (urlParams.get('showIdentifier') === "true") {
this.showIdentifier = true
}
if (urlParams.get('showMetadata') === "true") {
this.showMetadata = true
}
if (this.identifier != null) {
if (this.identifier === "null" || this.identifier.trim().length == 0) {
this.identifier = null
}
}
// Default to true so the message doesn't appear and disappear quickly
this.portForwardingEnabled = true
this.names = []
this.myRegisteredName = ''
this.selectedName = 'invalid'
this.path = ''
this.successMessage = ''
this.generalMessage = ''
this.errorMessage = ''
this.loading = false
this.btnDisable = false
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
const fetchNames = () => {
parentEpml.request('apiCall', {url: `/names/address/${this.selectedAddress.address}?limit=0&reverse=true`}).then(res => {
setTimeout(() => {
this.names = res
if (res[0] != null) {
this.myRegisteredName = res[0].name;
}
}, 1)
})
setTimeout(fetchNames, this.config.user.nodeSettings.pingInterval)
}
const fetchCategories = () => {
parentEpml.request('apiCall', {url: `/arbitrary/categories`}).then(res => {
setTimeout(() => {
this.categories = res
}, 1)
})
setTimeout(fetchCategories, this.config.user.nodeSettings.pingInterval)
}
const fetchPeersSummary = () => {
parentEpml.request('apiCall', {url: `/peers/summary`}).then(res => {
setTimeout(() => {
this.portForwardingEnabled = (res.inboundConnections != null && res.inboundConnections > 0);
}, 1)
})
setTimeout(fetchPeersSummary, this.config.user.nodeSettings.pingInterval)
}
let configLoaded = false
parentEpml.ready().then(() => {
parentEpml.subscribe('selected_address', async selectedAddress => {
this.selectedAddress = {}
selectedAddress = JSON.parse(selectedAddress)
if (!selectedAddress || Object.entries(selectedAddress).length === 0) return
this.selectedAddress = selectedAddress
})
parentEpml.subscribe('config', c => {
if (!configLoaded) {
setTimeout(fetchNames, 1)
setTimeout(fetchCategories, 1)
setTimeout(fetchPeersSummary, 1)
configLoaded = true
}
this.config = JSON.parse(c)
})
parentEpml.subscribe('copy_menu_switch', async value => {
if (value === 'false' && window.getSelection().toString().length !== 0) {
this.clearSelection()
}
})
})
}
render() {
return html`
<div id="publishWrapper" style="width: auto; padding:10px; background: var(--white); height: 100vh;">
<div class="layout horizontal center" style=" padding:12px 15px;">
<div class="address-bar">
<mwc-button @click=${() => this.goBack()} class="address-bar-button"><mwc-icon>arrow_back_ios</mwc-icon> ${translate("general.back")}</mwc-button>
</div>
<paper-card style="width:100%; max-width:740px;">
<div style="margin:0; margin-top:20px;">
<h3 style="margin:0; padding:8px 0; text-transform: capitalize; color: var(--black);">${translate("publishpage.pchange1")} / ${translate("publishpage.pchange2")} Q-App</h3>
<p style="font-style: italic; font-size: 14px; color: var(--black);" ?hidden="${this.portForwardingEnabled}">${translate("publishpage.pchange3")}</p>
</div>
</paper-card>
<!-- TODO: adapt this dropdown to list all names on the account. Right now it's hardcoded to a single name -->
<p style="display: ${this.showName ? 'block' : 'none'}">
<mwc-select id="registeredName" label="${translate("publishpage.pchange4")}" @selected=${(e) => this.selectName(e)} style="min-width: 130px; max-width:100%; width:100%;">
<mwc-list-item selected value=""></mwc-list-item>
<mwc-list-item value="${this.myRegisteredName}">${this.myRegisteredName}</mwc-list-item>
</mwc-select>
</p>
<div style="display: ${this.showMetadata ? 'block' : 'none'}">
<p>
<mwc-textfield style="width:100%;" label="${translate("publishpage.pchange5")}" id="title" type="text" value="${this.metadata != null && this.metadata.title != null ? this.metadata.title : ''}" maxLength="80"></mwc-textfield><!--charCounter="true"-->
</p>
<p>
<mwc-textfield style="width:100%;" label="${translate("publishpage.pchange6")}" id="description" type="text" value="${this.metadata != null && this.metadata.description != null ? this.metadata.description : ''}" maxLength="500"></mwc-textfield><!--charCounter="true"-->
</p>
<p>
<mwc-select id="category" label="${translate("publishpage.pchange7")}" index="0" style="min-width: 130px; max-width:100%; width:100%;">
${this.categories.map((c, index) => html`
<mwc-list-item value="${c.id}">${c.name}</mwc-list-item>
`)}
</mwc-select>
</p>
<p>
<mwc-textfield style="width:19.85%;" id="tag1" type="text" value="${this.metadata != null && this.metadata.tags != null && this.metadata.tags[0] != null ? this.metadata.tags[0] : ''}" placeholder="${translate("publishpage.pchange8")} 1" maxLength="20"></mwc-textfield>
<mwc-textfield style="width:19.85%;" id="tag2" type="text" value="${this.metadata != null && this.metadata.tags != null && this.metadata.tags[1] != null ? this.metadata.tags[1] : ''}" placeholder="${translate("publishpage.pchange8")} 2" maxLength="20"></mwc-textfield>
<mwc-textfield style="width:19.85%;" id="tag3" type="text" value="${this.metadata != null && this.metadata.tags != null && this.metadata.tags[2] != null ? this.metadata.tags[2] : ''}" placeholder="${translate("publishpage.pchange8")} 3" maxLength="20"></mwc-textfield>
<mwc-textfield style="width:19.85%;" id="tag4" type="text" value="${this.metadata != null && this.metadata.tags != null && this.metadata.tags[3] != null ? this.metadata.tags[3] : ''}" placeholder="${translate("publishpage.pchange8")} 4" maxLength="20"></mwc-textfield>
<mwc-textfield style="width:19.85%;" id="tag5" type="text" value="${this.metadata != null && this.metadata.tags != null && this.metadata.tags[4] != null ? this.metadata.tags[4] : ''}" placeholder="${translate("publishpage.pchange8")} 5" maxLength="20"></mwc-textfield>
</p>
</div>
${this.renderUploadField()}
<p style="display: ${this.showService ? 'block' : 'none'}">
<mwc-textfield style="width:100%;" label="${translate("publishpage.pchange9")}" id="service" type="text" value="${this.service}"></mwc-textfield>
</p>
<p style="display: ${this.showIdentifier ? 'block' : 'none'}">
<mwc-textfield style="width:100%;" label="${translate("publishpage.pchange10")}" id="identifier" type="text" value="${this.identifier != null ? this.identifier : ''}"></mwc-textfield>
</p>
<p style="break-word; color: var(--black);">${this.generalMessage}</p>
<p style="color:red">${this.errorMessage}</p>
<p style="color: green; word-break: break-word;">${this.successMessage}</p>
${this.loading ? html` <paper-progress indeterminate style="width:100%; margin:4px;"></paper-progress> ` : ''}
<div class="buttons">
<div>
<mwc-button ?disabled=${this.btnDisable} style="width:100%;" raised icon="send" @click=${(e) => this.doPublish(e)}> ${translate("publishpage.pchange11")}</mwc-button>
</div>
</div>
</div>
</div>
`
}
firstUpdated() {
this.changeTheme()
this.changeLanguage()
window.addEventListener('contextmenu', (event) => {
event.preventDefault()
this._textMenu(event)
})
window.addEventListener('click', () => {
parentEpml.request('closeCopyTextMenu', null)
})
window.addEventListener('storage', () => {
const checkLanguage = localStorage.getItem('qortalLanguage')
const checkTheme = localStorage.getItem('qortalTheme')
use(checkLanguage)
if (checkTheme === 'dark') {
this.theme = 'dark'
} else {
this.theme = 'light'
}
document.querySelector('html').setAttribute('theme', this.theme)
})
window.onkeyup = (e) => {
if (e.keyCode === 27) {
parentEpml.request('closeCopyTextMenu', null)
}
}
}
changeTheme() {
const checkTheme = localStorage.getItem('qortalTheme')
if (checkTheme === 'dark') {
this.theme = 'dark';
} else {
this.theme = 'light';
}
document.querySelector('html').setAttribute('theme', this.theme);
}
changeLanguage() {
const checkLanguage = localStorage.getItem('qortalLanguage')
if (checkLanguage === null || checkLanguage.length === 0) {
localStorage.setItem('qortalLanguage', 'us')
use('us')
} else {
use(checkLanguage)
}
}
// Navigation
goBack() {
window.history.back();
}
renderUploadField() {
if (this.uploadType === "file") {
return html`
<p>
<input style="width: 100%; background: var(--white); color: var(--black)" id="file" type="file">
</p>
`
}
else if (this.uploadType === "zip") {
return html`
<p>
<span class="upload-text">${translate("publishpage.pchange12")}:</span><br />
<input style="color: var(--black)" id="file" type="file" accept=".zip">
</p>
`
}
else {
return html`
<p>
<mwc-textfield style="width:100%;" label="${translate("publishpage.pchange13")}" id="path" type="text" value="${this.path}"></mwc-textfield>
</p>
`
}
}
doPublish(e) {
let registeredName = this.shadowRoot.getElementById('registeredName').value
let service = this.shadowRoot.getElementById('service').value
let identifier = this.shadowRoot.getElementById('identifier').value
// If name is hidden, use the value passed in via the name parameter
if (!this.showName) {
registeredName = this.name
}
let file;
let path;
if (this.uploadType === "file" || this.uploadType === "zip") {
file = this.shadowRoot.getElementById('file').files[0]
}
else if (this.uploadType === "path") {
path = this.shadowRoot.getElementById('path').value
}
this.generalMessage = ''
this.successMessage = ''
this.errorMessage = ''
if (registeredName === '') {
this.showName = true
let err1string = get("publishpage.pchange14")
parentEpml.request('showSnackBar', `${err1string}`)
}
else if (this.uploadType === "file" && file == null) {
let err2string = get("publishpage.pchange15")
parentEpml.request('showSnackBar', `${err2string}`)
}
else if (this.uploadType === "zip" && file == null) {
let err3string = get("publishpage.pchange16")
parentEpml.request('showSnackBar', `${err3string}`)
}
else if (this.uploadType === "path" && path === '') {
let err4string = get("publishpage.pchange17")
parentEpml.request('showSnackBar', `${err4string}`)
}
else if (service === '') {
let err5string = get("publishpage.pchange18")
parentEpml.request('showSnackBar', `${err5string}`)
}
else {
this.publishData(registeredName, path, file, service, identifier)
}
}
async publishData(registeredName, path, file, service, identifier) {
this.loading = true
this.btnDisable = true
const validateName = async (receiverName) => {
let nameRes = await parentEpml.request('apiCall', {
type: 'api',
url: `/names/${receiverName}`,
})
return nameRes
}
const showError = async (errorMessage) => {
this.loading = false
this.btnDisable = false
this.generalMessage = ''
this.successMessage = ''
console.error(errorMessage)
}
const validate = async () => {
let validNameRes = await validateName(registeredName)
if (validNameRes.error) {
this.errorMessage = "Error: " + validNameRes.message
showError(this.errorMessage)
throw new Error(this.errorMessage);
}
let err6string = get("publishpage.pchange19")
this.generalMessage = `${err6string}`
let transactionBytes = await uploadData(registeredName, path, file)
if (transactionBytes.error) {
let err7string = get("publishpage.pchange20")
this.errorMessage = `${err7string}` + transactionBytes.message
showError(this.errorMessage)
throw new Error(this.errorMessage);
}
else if (transactionBytes.includes("Error 500 Internal Server Error")) {
let err8string = get("publishpage.pchange21")
this.errorMessage = `${err8string}`
showError(this.errorMessage)
throw new Error(this.errorMessage);
}
let err9string = get("publishpage.pchange22")
this.generalMessage = `${err9string}`
let signAndProcessRes = await signAndProcess(transactionBytes)
if (signAndProcessRes.error) {
let err10string = get("publishpage.pchange20")
this.errorMessage = `${err10string}` + signAndProcessRes.message
showError(this.errorMessage)
throw new Error(this.errorMessage);
}
let err11string = get("publishpage.pchange23")
this.btnDisable = false
this.loading = false
this.errorMessage = ''
this.generalMessage = ''
this.successMessage = `${err11string}`
}
const uploadData = async (registeredName, path, file) => {
let postBody = path
let urlSuffix = ""
if (file != null) {
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API
if (this.uploadType === "zip") {
urlSuffix = "/zip"
}
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API
else if (this.uploadType === "file") {
urlSuffix = "/base64"
}
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays
let fileBuffer = new Uint8Array(await file.arrayBuffer())
postBody = Buffer.from(fileBuffer).toString('base64');
}
// Optional metadata
let title = encodeURIComponent(this.shadowRoot.getElementById('title').value);
let description = encodeURIComponent(this.shadowRoot.getElementById('description').value);
let category = encodeURIComponent(this.shadowRoot.getElementById('category').value);
let tag1 = encodeURIComponent(this.shadowRoot.getElementById('tag1').value);
let tag2 = encodeURIComponent(this.shadowRoot.getElementById('tag2').value);
let tag3 = encodeURIComponent(this.shadowRoot.getElementById('tag3').value);
let tag4 = encodeURIComponent(this.shadowRoot.getElementById('tag4').value);
let tag5 = encodeURIComponent(this.shadowRoot.getElementById('tag5').value);
let metadataQueryString = `title=${title}&description=${description}&category=${category}&tags=${tag1}&tags=${tag2}&tags=${tag3}&tags=${tag4}&tags=${tag5}`
let uploadDataUrl = `/arbitrary/${this.service}/${registeredName}${urlSuffix}?${metadataQueryString}&apiKey=${this.getApiKey()}`
if (identifier != null && identifier.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${this.identifier}${urlSuffix}?${metadataQueryString}&apiKey=${this.getApiKey()}`
}
let uploadDataRes = await parentEpml.request('apiCall', {
type: 'api',
method: 'POST',
url: `${uploadDataUrl}`,
body: `${postBody}`,
})
return uploadDataRes
}
const convertBytesForSigning = async (transactionBytesBase58) => {
let convertedBytes = await parentEpml.request('apiCall', {
type: 'api',
method: 'POST',
url: `/transactions/convert`,
body: `${transactionBytesBase58}`,
})
return convertedBytes
}
const signAndProcess = async (transactionBytesBase58) => {
let convertedBytesBase58 = await convertBytesForSigning(transactionBytesBase58)
if (convertedBytesBase58.error) {
let err12string = get("publishpage.pchange20")
this.errorMessage = `${err12string}` + convertedBytesBase58.message
showError(this.errorMessage)
throw new Error(this.errorMessage);
}
const convertedBytes = window.parent.Base58.decode(convertedBytesBase58);
const _convertedBytesArray = Object.keys(convertedBytes).map(function (key) { return convertedBytes[key]; });
const convertedBytesArray = new Uint8Array(_convertedBytesArray)
const convertedBytesHash = new window.parent.Sha256().process(convertedBytesArray).finish().result
const hashPtr = window.parent.sbrk(32, window.parent.heap);
const hashAry = new Uint8Array(window.parent.memory.buffer, hashPtr, 32);
hashAry.set(convertedBytesHash);
const difficulty = 14;
const workBufferLength = 8 * 1024 * 1024;
const workBufferPtr = window.parent.sbrk(workBufferLength, window.parent.heap);
this.errorMessage = '';
this.successMessage = '';
let nonce = window.parent.computePow(hashPtr, workBufferPtr, workBufferLength, difficulty)
let response = await parentEpml.request('sign_arbitrary', {
nonce: this.selectedAddress.nonce,
arbitraryBytesBase58: transactionBytesBase58,
arbitraryBytesForSigningBase58: convertedBytesBase58,
arbitraryNonce: nonce
})
let myResponse = { error: '' }
if (response === false) {
let err13string = get("publishpage.pchange24")
myResponse.error = `${err13string}`
}
else {
myResponse = response
}
return myResponse
}
validate()
}
_textMenu(event) {
const getSelectedText = () => {
var text = ''
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString()
} else if (typeof this.shadowRoot.selection != 'undefined' && this.shadowRoot.selection.type == 'Text') {
text = this.shadowRoot.selection.createRange().text
}
return text
}
const checkSelectedTextAndShowMenu = () => {
let selectedText = getSelectedText()
if (selectedText && typeof selectedText === 'string') {
let _eve = { pageX: event.pageX, pageY: event.pageY, clientX: event.clientX, clientY: event.clientY }
let textMenuObject = { selectedText: selectedText, eventObject: _eve, isFrame: true }
parentEpml.request('openCopyTextMenu', textMenuObject)
}
}
checkSelectedTextAndShowMenu()
}
fetchResourceMetadata() {
let identifier = this.identifier != null ? this.identifier : "default";
parentEpml.request('apiCall', {
url: `/arbitrary/metadata/${this.service}/${this.name}/${identifier}?apiKey=${this.getApiKey()}`
}).then(res => {
setTimeout(() => {
this.metadata = res
if (this.metadata != null && this.metadata.category != null) {
this.shadowRoot.getElementById('category').value = this.metadata.category;
}
else {
this.shadowRoot.getElementById('category').value = "";
}
}, 1)
})
}
selectName(e) {
let name = this.shadowRoot.getElementById('registeredName')
this.selectedName = (name.value)
// Update the current name if one has been selected
if (name.value.length > 0) {
this.name = (name.value)
}
this.fetchResourceMetadata();
}
getApiKey() {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
clearSelection() {
window.getSelection().removeAllRanges()
window.parent.getSelection().removeAllRanges()
}
}
window.customElements.define('publish-app', PublishApp)

1041
qortal-ui-plugins/plugins/core/q-app/q-apps.src.js

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save