Commit everything.

This commit is contained in:
FireMasterK
2021-02-24 15:05:41 +05:30
parent bb2888343a
commit 69e6118dc5
11 changed files with 394 additions and 458 deletions

View File

@@ -64,15 +64,11 @@ export default {
return;
}
fetch(
this.fetchJson(
Constants.BASE_URL +
"/suggestions?query=" +
encodeURI(this.searchText + e.key)
)
.then(resp => resp.json())
.then(json => {
this.searchSuggestions = json;
});
);
}
}
};
@@ -104,7 +100,7 @@ export default {
background-color: #0b0e0f;
}
* {
scrollbar-color: #15191a #444a4e;
}
</style>
* {
scrollbar-color: #15191a #444a4e;
}
</style>

View File

@@ -7,6 +7,7 @@
v-if="channel.bannerUrl"
v-bind:src="channel.bannerUrl"
style="width: 100%"
loading="lazy"
/>
<p v-html="this.channel.description.replaceAll('\n', '<br>')"></p>
@@ -22,17 +23,23 @@
class="uk-link-muted uk-text-justify"
v-bind:to="item.url || '/'"
>
<img style="width: 100%" v-bind:src="item.thumbnail" />
<img
style="width: 100%"
v-bind:src="item.thumbnail"
loading="lazy"
/>
<a>{{ item.title }}</a>
</router-link>
<br />
<div>
<b class="uk-text-small uk-align-left">
{{ timeFormat(item.duration) }}
</b>
<b class="uk-text-small uk-align-right">
<font-awesome-icon icon="eye"></font-awesome-icon>
{{ item.views }} views
<br />
{{ item.uploadedDate }}
</b>
<b class="uk-text-small uk-align-right">
{{ timeFormat(item.duration) }}
</b>
</div>
</div>
@@ -58,13 +65,9 @@ export default {
},
methods: {
async fetchChannel() {
return await (
await fetch(
Constants.BASE_URL +
"/channels/" +
this.$route.params.channelId
)
).json();
return await this.fetchJson(
Constants.BASE_URL + "/channels/" + this.$route.params.channelId
);
},
async getChannelData() {
this.fetchChannel()
@@ -78,22 +81,20 @@ export default {
document.body.offsetHeight - window.innerHeight
) {
this.loading = true;
fetch(
this.fetchJson(
Constants.BASE_URL +
"/nextpage/channels/" +
this.$route.params.channelId +
"?url=" +
encodeURIComponent(this.channel.nextpage)
)
.then(body => body.json())
.then(json => {
this.channel.relatedStreams.concat(json.relatedStreams);
this.channel.nextpage = json.nextpage;
this.loading = false;
json.relatedStreams.map(stream =>
this.channel.relatedStreams.push(stream)
);
});
).then(json => {
this.channel.relatedStreams.concat(json.relatedStreams);
this.channel.nextpage = json.nextpage;
this.loading = false;
json.relatedStreams.map(stream =>
this.channel.relatedStreams.push(stream)
);
});
}
}
}

View File

@@ -1,7 +1,8 @@
<template>
<div v-if="playlist">
<h1 class="uk-text-center">
<img v-bind:src="playlist.avatarUrl" />{{ playlist.name }}
<img v-bind:src="playlist.avatarUrl" loading="lazy" />
{{ playlist.name }}
</h1>
<b
@@ -9,7 +10,7 @@
class="uk-text-justify"
v-bind:to="playlist.uploaderUrl || '/'"
>
<img v-bind:src="playlist.uploaderAvatar" />
<img v-bind:src="playlist.uploaderAvatar" loading="lazy" />
{{ playlist.uploader }}</router-link
></b
>
@@ -28,7 +29,11 @@
class="uk-link-muted uk-text-justify"
v-bind:to="item.url || '/'"
>
<img style="width: 100%" v-bind:src="item.thumbnail" />
<img
style="width: 100%"
v-bind:src="item.thumbnail"
loading="lazy"
/>
<a>{{ item.title }}</a>
</router-link>
<br />
@@ -67,11 +72,9 @@ export default {
},
methods: {
async fetchPlaylist() {
return await (
await fetch(
Constants.BASE_URL + "/playlists/" + this.$route.query.list
)
).json();
return await await this.fetchJson(
Constants.BASE_URL + "/playlists/" + this.$route.query.list
);
},
async getPlaylistData() {
this.fetchPlaylist()
@@ -86,24 +89,20 @@ export default {
document.body.offsetHeight - window.innerHeight
) {
this.loading = true;
fetch(
this.fetchJson(
Constants.BASE_URL +
"/nextpage/playlists/" +
this.$route.query.list +
"?url=" +
encodeURIComponent(this.playlist.nextpage)
)
.then(body => body.json())
.then(json => {
this.playlist.relatedStreams.concat(
json.relatedStreams
);
this.playlist.nextpage = json.nextpage;
this.loading = false;
json.relatedStreams.map(stream =>
this.playlist.relatedStreams.push(stream)
);
});
).then(json => {
this.playlist.relatedStreams.concat(json.relatedStreams);
this.playlist.nextpage = json.nextpage;
this.loading = false;
json.relatedStreams.map(stream =>
this.playlist.relatedStreams.push(stream)
);
});
}
}
}

View File

@@ -15,7 +15,11 @@
class="uk-text-emphasis"
v-bind:to="result.url || '/'"
>
<img style="width: 100%" v-bind:src="result.thumbnail" />
<img
style="width: 100%"
v-bind:src="result.thumbnail"
loading="lazy"
/>
<p>{{ result.name }}</p>
</router-link>
<router-link
@@ -25,6 +29,8 @@
<p>{{ result.uploaderName }}</p>
</router-link>
{{ result.duration ? timeFormat(result.duration) : "" }}
<br />
{{ "1/1/2020" }}
<b v-if="result.views" class="uk-text-small uk-align-right">
<font-awesome-icon icon="eye"></font-awesome-icon>
{{ result.views }} views
@@ -57,13 +63,11 @@ export default {
},
methods: {
async fetchResults() {
return await (
await fetch(
Constants.BASE_URL +
"/search?q=" +
encodeURIComponent(this.$route.query.search_query)
)
).json();
return await await this.fetchJson(
Constants.BASE_URL +
"/search?q=" +
encodeURIComponent(this.$route.query.search_query)
);
},
async updateResults() {
document.title = this.$route.query.search_query + " - Piped";
@@ -78,7 +82,7 @@ export default {
document.body.offsetHeight - window.innerHeight
) {
this.loading = true;
fetch(
this.fetchJson(
Constants.BASE_URL +
"/nextpage/search" +
"?url=" +
@@ -87,16 +91,12 @@ export default {
encodeURIComponent(this.results.id) +
"&q=" +
encodeURIComponent(this.$route.query.search_query)
)
.then(body => body.json())
.then(json => {
this.results.nextpage = json.nextpage;
this.results.id = json.id;
this.loading = false;
json.items.map(stream =>
this.results.items.push(stream)
);
});
).then(json => {
this.results.nextpage = json.nextpage;
this.results.id = json.id;
this.loading = false;
json.items.map(stream => this.results.items.push(stream));
});
}
}
}

View File

@@ -15,7 +15,11 @@
class="uk-text-emphasis"
v-bind:to="video.url || '/'"
>
<img style="width: 100%" v-bind:src="video.thumbnail" />
<img
style="width: 100%"
v-bind:src="video.thumbnail"
loading="lazy"
/>
<p>{{ video.title }}</p>
</router-link>
<router-link
@@ -24,10 +28,14 @@
>
<p>{{ video.uploaderName }}</p>
</router-link>
{{ timeFormat(video.duration) }}
<b class="uk-text-small uk-align-right">
<b class="uk-text-small uk-align-left">
<font-awesome-icon icon="eye"></font-awesome-icon>
{{ video.views }} views
<br />
{{ video.uploadedDate }}
</b>
<b class="uk-text-small uk-align-right">
{{ timeFormat(video.duration) }}
</b>
</div>
</div>
@@ -50,7 +58,7 @@ export default {
},
methods: {
async fetchTrending() {
return await (await fetch(Constants.BASE_URL + "/trending")).json();
return await this.fetchJson(Constants.BASE_URL + "/trending");
}
}
};

View File

@@ -1,13 +1,9 @@
<template>
<div class="uk-container uk-container-xlarge">
<video
controls
ref="player"
class="video-js preview-player-dimensions"
></video>
<video controls ref="player"></video>
<h1 class="uk-text-bold">{{ video.title }}</h1>
<img :src="video.uploaderAvatar" />
<img :src="video.uploaderAvatar" loading="lazy" />
<router-link class="uk-text-bold" v-bind:to="video.uploaderUrl || '/'">
<a>{{ video.uploader }}</a>
</router-link>
@@ -56,7 +52,11 @@
>
<router-link class="uk-link-muted" v-bind:to="related.url">
<p class="uk-text-emphasis">{{ related.title }}</p>
<img style="width: 100%" v-bind:src="related.thumbnail" />
<img
style="width: 100%"
v-bind:src="related.thumbnail"
loading="lazy"
/>
</router-link>
<p>
<router-link
@@ -73,14 +73,8 @@
</template>
<script>
import("video.js/dist/video-js.css");
import("@silvermine/videojs-quality-selector/dist/css/quality-selector.css");
import videojs from "video.js";
import("videojs-hotkeys");
const shaka = import("shaka-player/dist/shaka-player.compiled.js");
import Constants from "@/Constants.js";
import("@silvermine/videojs-quality-selector").then(module => {
module.default(videojs);
});
export default {
name: "App",
@@ -119,22 +113,18 @@ export default {
}
},
methods: {
async fetchVideo() {
return await (
await fetch(
Constants.BASE_URL + "/streams/" + this.$route.query.v
)
).json();
fetchVideo() {
return this.fetchJson(
Constants.BASE_URL + "/streams/" + this.$route.query.v
);
},
async fetchSponsors() {
return await (
await fetch(
Constants.BASE_URL +
"/sponsors/" +
this.$route.query.v +
'?category=["sponsor","interaction","selfpromo","music_offtopic"]'
)
).json();
await this.fetchJson(
Constants.BASE_URL +
"/sponsors/" +
this.$route.query.v +
'?category=["sponsor","interaction","selfpromo","music_offtopic"]'
);
},
onChange() {
if (localStorage)
@@ -142,7 +132,9 @@ export default {
},
async getVideoData() {
this.fetchVideo()
.then(data => (this.video = data))
.then(data => {
this.video = data;
})
.then(() => {
document.title = this.video.title + " - Piped";
@@ -151,96 +143,63 @@ export default {
.replaceAll("https://www.youtube.com", "")
.replaceAll("\n", "<br>");
const options = {
autoplay: false,
controlBar: {
children: [
"playToggle",
"currentTimeDisplay",
"progressControl",
"volumePanel",
"qualitySelector",
"captionsButton",
"fullscreenToggle"
]
},
responsive: false,
aspectRatio: "16:9"
};
const noPrevPlayer = !this.player;
var streams = [];
streams.push(...this.video.audioStreams);
streams.push(...this.video.videoStreams);
const dash = require("../utils/DashUtils.js").default.generate_dash_file_from_formats(
streams,
this.video.duration
);
if (noPrevPlayer) {
this.player = videojs(this.$refs.player, options);
if (localStorage)
this.player.volume(
localStorage.getItem("volume") || 1
);
setTimeout(function() {
shaka
.then(shaka => shaka.default)
.then(shaka => {
console.log(shaka);
shaka.polyfill.installAll();
this.player = new shaka.Player(
document.querySelector("video")
);
this.player.load(
"data:application/dash+xml;charset=utf-8;base64," +
btoa(dash)
);
});
}, 0);
// if (localStorage)
// this.player.volume(
// localStorage.getItem("volume") || 1
// );
}
shaka;
console.log(this.player);
if (this.$route.query.t)
this.player.currentTime(this.$route.query.t);
this.player.hotkeys({
volumeStep: 0.1,
seekStep: 5,
enableModifiersForNumbers: false,
enableHoverScroll: true
});
// this.player.poster(this.video.thumbnailUrl);
this.player.poster(this.video.thumbnailUrl);
var src = [];
// src.push({
// src:
// "data:application/dash+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPE1QRCB0eXBlPSJzdGF0aWMiIHhtbG5zPSJ1cm46bXBlZzpkYXNoOnNjaGVtYTptcGQ6MjAxMSIgbWluQnVmZmVyVGltZT0iUFQxLjVTIiBtZWRpYVByZXNlbnRhdGlvbkR1cmF0aW9uPSJQVDIyMC43NjIyMDgzMzMzMzMzMlMiIHByb2ZpbGVzPSJ1cm46bXBlZzpkYXNoOnByb2ZpbGU6aXNvZmYtbWFpbjoyMDExIj4KICA8UGVyaW9kIHN0YXJ0PSJQVDBTIj4KICAgIDxBZGFwdGF0aW9uU2V0PgogICAgICA8UmVwcmVzZW50YXRpb24gaWQ9InZpZGVvMDEiIG1pbWVUeXBlPSJ2aWRlby9tcDQiIGNvZGVjcz0iYXZjMS42NDAwMjgiIGJhbmR3aWR0aD0iMjgxNjM0Ij4KICAgICAgICAgIDxCYXNlVVJMPmh0dHBzOi8vcGlwZWRwcm94eS5rYXZpbi5yb2Nrcy92aWRlb3BsYXliYWNrP2V4cGlyZT0xNjA1NjkxNTMzJmVpPUxaUzBYLVA5RG9TLWh3YU95Sl9vRHcmaXA9MjA5LjE0MS40Ni4zOCZpZD0wN2FmZTI0MmY2ODg4ZDdjJml0YWc9MjQ4JmFpdGFncz0xMzMlMkMxMzQlMkMxMzUlMkMxMzYlMkMxMzclMkMxNjAlMkMyNDIlMkMyNDMlMkMyNDQlMkMyNDclMkMyNDglMkMyNzgmc291cmNlPXlvdXR1YmUmcmVxdWlyZXNzbD15ZXMmbWg9U0EmbW09MzElMkMyOSZtbj1zbi1uNHY3c243cyUyQ3NuLW40djdrbmxrJm1zPWF1JTJDcmR1Jm12PW0mbXZpPTUmcGw9MjMmZ2NyPXVzJmluaXRjd25kYnBzPTExMTI1MCZ2cHJ2PTEmbWltZT12aWRlbyUyRndlYm0mbnM9dllDakpkUFdQTWVjeHhrS3NlXzF4QUFGJmdpcj15ZXMmY2xlbj01MDE0MDM4NyZkdXI9MjIwLjc2MiZsbXQ9MTYwNTY0ODY5MjQyNjI0NSZtdD0xNjA1NjY5ODg1JmZ2aXA9NSZrZWVwYWxpdmU9eWVzJmM9V0VCJnR4cD01NDMyNDM0Jm49blYweDdYZXlodTV4R2ZIJnNwYXJhbXM9ZXhwaXJlJTJDZWklMkNpcCUyQ2lkJTJDYWl0YWdzJTJDc291cmNlJTJDcmVxdWlyZXNzbCUyQ2djciUyQ3ZwcnYlMkNtaW1lJTJDbnMlMkNnaXIlMkNjbGVuJTJDZHVyJTJDbG10JmxzcGFyYW1zPW1oJTJDbW0lMkNtbiUyQ21zJTJDbXYlMkNtdmklMkNwbCUyQ2luaXRjd25kYnBzJmxzaWc9QUczQ194QXdSQUlnUGh1ZklrTzBfZFBSdnFNRFhvRVZsYV9Dbzk1ZkpOYXdwbEM4QWE4eDJCd0NJRVhlOHdnTFJKeUFvZ2xNZmVPak1YTTF0d2hkcnRVWEV3eWowRVZOajFXTSZzaWc9QU9xMFFKOHdSUUloQVA5VDNQNXBCemJpZ3FoaXd2OXVlZjJDMlVoWFlmOHNfbDU2RzFla1VjV25BaUFCU0pSNFdLRlMxS05nUkhjRkUtVGJFRWFiWUtSYlA4YnItcVlzRTczVFFnPT0maG9zdD1yNS0tLXNuLW40djdzbjdzLmdvb2dsZXZpZGVvLmNvbTwvQmFzZVVSTD4KICAgICAgICA8U2VnbWVudEJhc2UgaW5kZXhSYW5nZT0iNzQwLTYyMTc0ODMxIj4KICAgICAgICAgIDxJbml0aWFsaXphdGlvbiByYW5nZT0iMC03NDAiLz4KICAgICAgICA8L1NlZ21lbnRCYXNlPgogICAgICAgIDwvUmVwcmVzZW50YXRpb24+CiAgICA8L0FkYXB0YXRpb25TZXQ+CiAgPC9QZXJpb2Q+CjwvTVBEPgo=",
// type: "application/dash+xml",
// label: "DASH"
// this.video.subtitles.map(subtitle => {
// this.player.addRemoteTextTrack({
// kind: "captions",
// src: subtitle.url.replace("fmt=ttml", "fmt=vtt"),
// label: "Track",
// language: "en",
// type: "captions/captions.vtt"
// });
// });
this.video.videoStreams.map(stream =>
src.push({
src: stream.url,
type: stream.mimeType,
label: stream.quality,
videoOnly: stream.videoOnly
})
);
this.video.audioStreams.map(stream =>
src.push({
src: stream.url,
type: stream.mimeType,
label: stream.quality
})
);
this.video.subtitles.map(subtitle => {
this.player.addRemoteTextTrack({
kind: "captions",
src: subtitle.url.replace("fmt=ttml", "fmt=vtt"),
label: "Track",
language: "en",
type: "captions/captions.vtt"
});
});
this.player.src(src);
const currentSrc = src.filter(
src => src.src == this.player.currentSrc()
)[0];
if (currentSrc.videoOnly)
if (!this.audioplayer)
this.audioplayer = new Audio(
this.video.audioStreams.slice(-1)[0].url
);
else
this.audioplayer.src = this.video.audioStreams.slice(
-1
)[0].url;
// this.player.src(src);
if (noPrevPlayer) {
this.player.on("timeupdate", () => {
@@ -262,78 +221,29 @@ export default {
}
});
}
if (this.audioplayer) {
const delay =
this.audioplayer.currentTime -
this.player.currentTime(),
absdelay = Math.abs(delay);
console.log(delay);
if (absdelay > 0.05) {
this.audioplayer.currentTime =
absdelay > 0.2
? this.player.currentTime()
: this.player.currentTime() - delay;
}
}
});
this.player.on("play", () => {
if (this.audioplayer) this.audioplayer.play();
});
// this.player.on("volumechange", () => {
// if (this.audioplayer)
// this.audioplayer.volume = this.player.volume();
// if (localStorage)
// localStorage.setItem(
// "volume",
// this.player.volume()
// );
// });
this.player.on("pause", () => {
if (this.audioplayer) {
this.audioplayer.currentTime = this.player.currentTime();
this.audioplayer.pause();
}
});
this.player.on("volumechange", () => {
if (this.audioplayer)
this.audioplayer.volume = this.player.volume();
if (localStorage)
localStorage.setItem(
"volume",
this.player.volume()
);
});
this.player.on("ended", () => {
if (
this.selectedAutoPlay &&
this.video.relatedStreams.length > 0
)
this.$router.push(
this.video.relatedStreams[0].url
);
});
// this.player.on("ended", () => {
// if (
// this.selectedAutoPlay &&
// this.video.relatedStreams.length > 0
// )
// this.$router.push(
// this.video.relatedStreams[0].url
// );
// });
}
if (!noPrevPlayer)
this.player
.remoteTextTracks()
.map(track =>
this.player.removeRemoteTextTrack(track)
);
this.video.subtitles.map(subtitle => {
this.player.addRemoteTextTrack(
{
kind: "captions",
src: subtitle.url.replace(
"fmt=ttml",
"fmt=vtt"
),
label: "Track",
type: "captions/captions.vtt"
},
false
).mode = "showing";
});
//const parent = this.player.el().querySelector(".vjs-progress-holder")
//TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
});

View File

@@ -33,6 +33,12 @@ const mixin = {
return str;
},
fetchJson: function (url, options) {
return fetch(url, options)
.then(response => {
return response.json();
})
}
}
}

189
src/utils/DashUtils.js Normal file
View File

@@ -0,0 +1,189 @@
// Based of https://github.com/GilgusMaximus/yt-dash-manifest-generator/blob/master/src/DashGenerator.js
const xml = require('xml-js')
const DashUtils = {
generate_dash_file_from_formats(VideoFormats, VideoLength) {
const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength)
return xml.json2xml(generatedJSON)
},
generate_xmljs_json_from_data(VideoFormatArray, VideoLength) {
const convertJSON = {
"declaration": {
"attributes": {
"version": "1.0",
"encoding": "utf-8"
}
},
"elements": [
{
"type": "element",
"name": "MPD",
"attributes": {
"xmlns": "urn:mpeg:dash:schema:mpd:2011",
"profiles": "urn:mpeg:dash:profile:full:2011",
"minBufferTime": "PT1.5S",
"type": "static",
"mediaPresentationDuration": `PT${VideoLength}S`
},
"elements": [
{
"type": "element",
"name": "Period",
"elements": this.generate_adaptation_set(VideoFormatArray)
}
]
}
]
}
return convertJSON
},
generate_adaptation_set(VideoFormatArray) {
const adaptationSets = []
const mimeTypes = []
const mimeObjects = [[]]
// sort the formats by mime types
VideoFormatArray.forEach((videoFormat) => {
// the dual formats should not be used
if (videoFormat.mimeType.indexOf("video") != -1 && !videoFormat.videoOnly) {
return
}
// if these properties are not available, then we skip it because we cannot set these properties
//if (!(videoFormat.hasOwnProperty('initRange') && videoFormat.hasOwnProperty('indexRange'))) {
// return
//}
const mimeType = videoFormat.mimeType
const mimeTypeIndex = mimeTypes.indexOf(mimeType)
if (mimeTypeIndex > -1) {
mimeObjects[mimeTypeIndex].push(videoFormat)
} else {
mimeTypes.push(mimeType)
mimeObjects.push([])
mimeObjects[mimeTypes.length - 1].push(videoFormat)
}
})
// for each MimeType generate a new Adaptation set with Representations as sub elements
for (let i = 0; i < mimeTypes.length; i++) {
let isVideoFormat = false
const adapSet = {
"type": "element",
"name": "AdaptationSet",
"attributes": {
"id": i,
"mimeType": mimeTypes[i],
"startWithSAP": "1",
"subsegmentAlignment": "true"
},
"elements": []
}
if (!mimeTypes[i].includes("audio")) {
adapSet.attributes.scanType = "progressive"
isVideoFormat = true
}
mimeObjects[i].forEach((format) => {
if (isVideoFormat) {
adapSet.elements.push(this.generate_representation_video(format))
} else {
adapSet.elements.push(this.generate_representation_audio(format))
}
})
adaptationSets.push(adapSet)
}
return adaptationSets
}, generate_representation_audio(Format) {
const representation =
{
"type": "element",
"name": "Representation",
"attributes": {
"id": Format.itag,
"codecs": Format.codec,
"bandwidth": Format.bitrate
},
"elements": [
{
"type": "element",
"name": "AudioChannelConfiguration",
"attributes": {
"schemeIdUri": "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
"value": "2"
},
},
{
"type": "element",
"name": "BaseURL",
"elements": [
{
"type": "text",
"text": Format.url
}
]
},
{
"type": "element",
"name": "SegmentBase",
"attributes": {
"indexRange": `${Format.indexStart}-${Format.indexEnd}`
},
"elements": [
{
"type": "element",
"name": "Initialization",
"attributes": {
"range": `${Format.initStart}-${Format.initEnd}`
}
}
]
}
]
}
return representation
},
generate_representation_video(Format) {
const representation =
{
"type": "element",
"name": "Representation",
"attributes": {
"id": Format.itag,
"codecs": Format.codec,
"bandwidth": Format.bitrate,
"width": Format.width,
"height": Format.height,
"maxPlayoutRate": "1",
"frameRate": Format.fps
},
"elements": [
{
"type": "element",
"name": "BaseURL",
"elements": [
{
"type": "text",
"text": Format.url
}
]
},
{
"type": "element",
"name": "SegmentBase",
"attributes": {
"indexRange": `${Format.indexStart}-${Format.indexEnd}`
},
"elements": [
{
"type": "element",
"name": "Initialization",
"attributes": {
"range": `${Format.initStart}-${Format.initEnd}`
}
}
]
}
]
}
return representation
}
}
export default DashUtils;