mirror of
				https://github.com/TeamPiped/Piped.git
				synced 2024-08-14 23:57:27 +00:00 
			
		
		
		
	Commit everything.
This commit is contained in:
		
							parent
							
								
									bb2888343a
								
							
						
					
					
						commit
						69e6118dc5
					
				
					 11 changed files with 394 additions and 458 deletions
				
			
		
							
								
								
									
										16
									
								
								src/App.vue
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								src/App.vue
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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));
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
                });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
									
								
							
							
						
						
									
										189
									
								
								src/utils/DashUtils.js
									
										
									
									
									
										Normal 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;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue