create new files
This commit is contained in:
parent
773c748b7d
commit
2084654842
2 changed files with 424 additions and 0 deletions
240
src/renderer/views/components/party-fullscreen.ejs
Normal file
240
src/renderer/views/components/party-fullscreen.ejs
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<script type="text/x-template" id="fullscreen-view">
|
||||||
|
<div class="fullscreen-view" tabindex="0">
|
||||||
|
<div class="background">
|
||||||
|
<div class="bgArtworkMaterial">
|
||||||
|
<div class="bg-artwork-container">
|
||||||
|
<img v-if="(app.cfg.visual.bg_artwork_rotation && app.animateBackground)" class="bg-artwork a"
|
||||||
|
:src="(image ?? '').replace('{w}','30').replace('{h}','30')">
|
||||||
|
<img v-if="(app.cfg.visual.bg_artwork_rotation && app.animateBackground)" class="bg-artwork b"
|
||||||
|
:src="(image ?? '').replace('{w}','30').replace('{h}','30')">
|
||||||
|
<img v-if="!(app.cfg.visual.bg_artwork_rotation && app.animateBackground)"
|
||||||
|
class="bg-artwork no-animation" :src="(image ?? '').replace('{w}','30').replace('{h}','30')">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fs-header" v-if="immersiveEnabled">
|
||||||
|
<div class="top-nav-group">
|
||||||
|
<sidebar-library-item @click.native="tabMode = 'catalog'" :name="$root.getLz('home.title')" svg-icon="./assets/feather/home.svg" svg-icon-name="home" page="home">
|
||||||
|
</sidebar-library-item>
|
||||||
|
<sidebar-library-item @click.native="tabMode = 'catalog'" :name="$root.getLz('term.listenNow')" svg-icon="./assets/feather/play-circle.svg" svg-icon-name="listenNow"
|
||||||
|
page="listen_now"></sidebar-library-item>
|
||||||
|
<sidebar-library-item @click.native="tabMode = 'catalog'" :name="$root.getLz('term.browse')" svg-icon="./assets/feather/globe.svg" svg-icon-name="browse" page="browse">
|
||||||
|
</sidebar-library-item>
|
||||||
|
<sidebar-library-item @click.native="tabMode = 'catalog'" :name="$root.getLz('term.radio')" svg-icon="./assets/feather/radio.svg" svg-icon-name="radio" page="radio">
|
||||||
|
</sidebar-library-item>
|
||||||
|
<sidebar-library-item @click.native="tabMode = 'catalog'" :name="$root.getLz('term.library')" svg-icon="./assets/feather/radio.svg" svg-icon-name="library" page="library">
|
||||||
|
</sidebar-library-item>
|
||||||
|
<sidebar-library-item @click.native="tabMode = ''" :name="$root.getLz('term.nowPlaying')" svg-icon="./assets/play.svg" svg-icon-name="nowPlaying" page="nowPlaying">
|
||||||
|
</sidebar-library-item>
|
||||||
|
<sidebar-library-item @click.native="tabMode = 'catalog'" name="" svg-icon="./assets/search.svg" svg-icon-name="search" page="search">
|
||||||
|
</sidebar-library-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row fs-row" v-if="tabMode != 'catalog'">
|
||||||
|
<div class="col artwork-col">
|
||||||
|
<div class="artwork" @click="app.fullscreen(false)">
|
||||||
|
<mediaitem-artwork
|
||||||
|
:size="600"
|
||||||
|
:video="video"
|
||||||
|
:videoPriority="true"
|
||||||
|
:url="(image ?? '').replace('{w}','600').replace('{h}','600')"
|
||||||
|
></mediaitem-artwork>
|
||||||
|
</div>
|
||||||
|
<div class="controls-parents">
|
||||||
|
<template v-if="app.mkReady()">
|
||||||
|
<div class="app-playback-controls" @mouseover="app.chrome.progresshover = true"
|
||||||
|
@mouseleave="app.chrome.progresshover = false" @contextmenu="app.nowPlayingContextMenu">
|
||||||
|
<div class="playback-info">
|
||||||
|
<div class="song-name">
|
||||||
|
{{ app.mk.nowPlayingItem["attributes"]["name"] }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="display: inline-block; -webkit-box-orient: horizontal; white-space: nowrap; margin-top: 0.25vh; overflow: hidden;">
|
||||||
|
<div class="item-navigate song-artist" style="display: inline-block;"
|
||||||
|
@click="app.getNowPlayingItemDetailed(`artist`)">
|
||||||
|
{{ app.mk.nowPlayingItem["attributes"]["artistName"] }}
|
||||||
|
</div>
|
||||||
|
<div class="song-artist item-navigate" style="display: inline-block;"
|
||||||
|
@click="app.getNowPlayingItemDetailed('album')">
|
||||||
|
{{ (app.mk.nowPlayingItem["attributes"]["albumName"]) ? (" — " +
|
||||||
|
app.mk.nowPlayingItem["attributes"]["albumName"]) : "" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="song-progress">
|
||||||
|
<div class="song-duration" style="justify-content: space-between; height: 1px;"
|
||||||
|
:style="[app.chrome.progresshover ? {'display': 'flex'} : {'display' : 'none'} ]">
|
||||||
|
<p style="width: auto">{{ app.convertTime(app.getSongProgress()) }}</p>
|
||||||
|
<p style="width: auto">{{ app.convertTime(app.mk.currentPlaybackDuration) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="range" step="0.01" min="0" :style="app.progressBarStyle()"
|
||||||
|
@input="app.playerLCD.desiredDuration = $event.target.value;app.playerLCD.userInteraction = true"
|
||||||
|
@mouseup="app.mk.seekToTime($event.target.value);app.playerLCD.desiredDuration = 0;app.playerLCD.userInteraction = false"
|
||||||
|
:max="app.mk.currentPlaybackDuration" :value="app.getSongProgress()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<div class="app-chrome-item display--large">
|
||||||
|
<button class="playback-button--small shuffle" v-if="$root.mk.shuffleMode == 0"
|
||||||
|
:class="$root.isDisabled() && 'disabled'"
|
||||||
|
@click="$root.mk.shuffleMode = 1" :title="$root.getLz('term.enableShuffle')"
|
||||||
|
v-b-tooltip.hover></button>
|
||||||
|
<button class="playback-button--small shuffle active" v-else
|
||||||
|
:class="$root.isDisabled() && 'disabled'"
|
||||||
|
@click="$root.mk.shuffleMode = 0"
|
||||||
|
:title="$root.getLz('term.disableShuffle')" v-b-tooltip.hover></button>
|
||||||
|
</div>
|
||||||
|
<div class="app-chrome-item display--large">
|
||||||
|
<button class="playback-button previous" @click="$root.prevButton()"
|
||||||
|
:class="$root.isPrevDisabled() && 'disabled'"
|
||||||
|
:title="$root.getLz('term.previous')" v-b-tooltip.hover></button>
|
||||||
|
</div>
|
||||||
|
<div class="app-chrome-item display--large">
|
||||||
|
<button class="playback-button stop" @click="$root.mk.stop()"
|
||||||
|
v-if="$root.mk.isPlaying && $root.mk.nowPlayingItem.attributes.playParams.kind == 'radioStation'"
|
||||||
|
:title="$root.getLz('term.stop')" v-b-tooltip.hover></button>
|
||||||
|
<button class="playback-button pause" @click="$root.mk.pause()"
|
||||||
|
v-else-if="$root.mk.isPlaying"
|
||||||
|
:title="$root.getLz('term.pause')" v-b-tooltip.hover></button>
|
||||||
|
<button class="playback-button play" @click="$root.mk.play()" v-else
|
||||||
|
:title="$root.getLz('term.play')"
|
||||||
|
v-b-tooltip.hover></button>
|
||||||
|
</div>
|
||||||
|
<div class="app-chrome-item display--large">
|
||||||
|
<button class="playback-button next" @click="$root.skipToNextItem()"
|
||||||
|
:class="$root.isNextDisabled() && 'disabled'"
|
||||||
|
:title="$root.getLz('term.next')" v-b-tooltip.hover></button>
|
||||||
|
</div>
|
||||||
|
<div class="app-chrome-item display--large">
|
||||||
|
<button class="playback-button--small repeat" v-if="$root.mk.repeatMode == 0"
|
||||||
|
:class="$root.isDisabled() && 'disabled'"
|
||||||
|
@click="$root.mk.repeatMode = 1"
|
||||||
|
:title="$root.getLz('term.enableRepeatOne')" v-b-tooltip.hover></button>
|
||||||
|
<button class="playback-button--small repeat repeatOne" @click="mk.repeatMode = 2"
|
||||||
|
:class="$root.isDisabled() && 'disabled'"
|
||||||
|
v-else-if="$root.mk.repeatMode == 1"
|
||||||
|
:title="$root.getLz('term.disableRepeatOne')" v-b-tooltip.hover></button>
|
||||||
|
<button class="playback-button--small repeat active"
|
||||||
|
@click="$root.mk.repeatMode = 0"
|
||||||
|
:class="$root.isDisabled() && 'disabled'"
|
||||||
|
v-else-if="$root.mk.repeatMode == 2"
|
||||||
|
:title="$root.getLz('term.disableRepeat')"
|
||||||
|
v-b-tooltip.hover></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-chrome-item volume display--large">
|
||||||
|
<div class="input-container">
|
||||||
|
<button class="volume-button--small volume" @click="app.muteButtonPressed()"
|
||||||
|
:class="{'active': app.cfg.audio.volume == 0}"
|
||||||
|
:title="app.cfg.audio.muted ? $root.getLz('term.unmute') : $root.getLz('term.mute')"
|
||||||
|
v-b-tooltip.hover></button>
|
||||||
|
<input type="range" class="slider" @wheel="app.volumeWheel"
|
||||||
|
:step="app.cfg.audio.volumeStep" min="0" :max="app.cfg.audio.maxVolume"
|
||||||
|
v-model="app.mk.volume"
|
||||||
|
v-if="typeof app.mk.volume != 'undefined'" @change="app.checkMuteChange()"
|
||||||
|
v-b-tooltip.hover :title="$root.formatVolumeTooltip()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-col">
|
||||||
|
<cider-queue ref="queue"></cider-queue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="col right-col" v-if="tabMode != ''">
|
||||||
|
<!-- <div class="fs-info">
|
||||||
|
<div>Name</div>
|
||||||
|
<div>Name</div>
|
||||||
|
<div>Name</div>
|
||||||
|
</div> -->
|
||||||
|
<div class="lyrics-col" v-if="tabMode == 'lyrics'">
|
||||||
|
<lyrics-view :yoffset="120" :time="time" :lyrics="lyrics"
|
||||||
|
:richlyrics="richlyrics"></lyrics-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-content-container" v-else>
|
||||||
|
<app-content-area></app-content-area>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.component('fullscreen-view', {
|
||||||
|
template: '#fullscreen-view',
|
||||||
|
props: {
|
||||||
|
time: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
lyrics: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
richlyrics: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
app: this.$root,
|
||||||
|
tabMode: "lyrics",
|
||||||
|
video: null,
|
||||||
|
immersiveEnabled: app.cfg.advanced.experiments.includes("immersive-preview")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
if (app.mk.nowPlayingItem._container.type == "albums") {
|
||||||
|
try {
|
||||||
|
const result = (await app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/${app.mk.nowPlayingItem._container.type}/${app.mk.nowPlayingItem._container.id}`, {
|
||||||
|
"fields": "editorialArtwork,editorialVideo",
|
||||||
|
})).data.data[0].attributes?.editorialVideo?.motionDetailSquare?.video
|
||||||
|
if (result) {
|
||||||
|
this.video = result
|
||||||
|
} else {
|
||||||
|
this.video = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.video = null
|
||||||
|
e = null
|
||||||
|
}
|
||||||
|
} else if (app.mk.nowPlayingItem._container.type == "library-albums") {
|
||||||
|
try {
|
||||||
|
const result = (await app.mk.api.v3.music(`/v1/me/library/albums/${app.mk.nowPlayingItem._container.id}/catalog`
|
||||||
|
, {"fields": "editorialArtwork,editorialVideo"})).data.data[0].attributes?.editorialVideo?.motionDetailSquare?.video
|
||||||
|
if (result) {
|
||||||
|
this.video = result
|
||||||
|
} else {
|
||||||
|
this.video = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
e = null
|
||||||
|
this.video = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
window.addEventListener('keyup', this.onEscapeKeyUp);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('keyup', this.onEscapeKeyUp)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onEscapeKeyUp(event) {
|
||||||
|
if (event.which === 27) {
|
||||||
|
app.fullscreen(false);
|
||||||
|
console.log('js')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
184
src/renderer/views/components/party-queue.ejs
Normal file
184
src/renderer/views/components/party-queue.ejs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
<script type="text/x-template" id="cider-queue">
|
||||||
|
<div class="queue-panel">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="queue-header-text" v-if="page == 'queue'">{{app.getLz('term.queue')}}</h3>
|
||||||
|
<h3 class="queue-header-text" v-if="page == 'history'">{{app.getLz('term.history')}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto flex-center">
|
||||||
|
<button class="autoplay" :style="{'background': app.mk.autoplayEnabled ? 'var(--keyColor)' : ''}"
|
||||||
|
@click="app.mk.autoplayEnabled = !app.mk.autoplayEnabled"
|
||||||
|
:title="app.getLz('term.autoplay')" v-b-tooltip.hover>
|
||||||
|
<img class="infinity">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-body" v-if="page == 'history'">
|
||||||
|
<mediaitem-list-item :show-library-status="false" v-for="item in history" :item="item"></mediaitem-list-item>
|
||||||
|
</div>
|
||||||
|
<div class="queue-body" v-if="page == 'queue'">
|
||||||
|
<draggable v-model="queueItems" @start="drag=true" @end="drag=false;move()">
|
||||||
|
<template v-for="(queueItem, position) in displayQueueItems">
|
||||||
|
<div class="cd-queue-item"
|
||||||
|
:class="{selected: selectedItems.includes(position)}"
|
||||||
|
@click="select($event, position)"
|
||||||
|
@dblclick="playQueueItem(queueItem.item.id)" :key="position"
|
||||||
|
@contextmenu="selected = position;queueContext($event, queueItem.item, position)">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto flex-center">
|
||||||
|
<div class="artwork">
|
||||||
|
<mediaitem-artwork :url="queueItem.item.attributes.artwork ? queueItem.item.attributes.artwork.url : ''" :size="32"></mediaitem-artwork>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col queue-info">
|
||||||
|
<div class="queue-title text-overflow-elipsis">{{ queueItem.item.attributes.name }}</div>
|
||||||
|
<div class="queue-subtitle text-overflow-elipsis">{{ queueItem.item.attributes.artistName }} — {{ queueItem.item.attributes.albumName }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-explicit-icon flex-center" v-if="queueItem.item.attributes.contentRating == 'explicit'">
|
||||||
|
<div class="explicit-icon"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col queue-duration-info">
|
||||||
|
<div class="queue-duration flex-center">{{convertTimeToString(queueItem.item.attributes.durationInMillis)}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
<div class="queue-footer">
|
||||||
|
<div class="btn-group" style="width:100%;">
|
||||||
|
<button class="md-btn md-btn-small" :class="{'md-btn-primary': (page == 'queue')}" @click="page = 'queue'">{{app.getLz('term.queue')}}</button>
|
||||||
|
<button class="md-btn md-btn-small" :class="{'md-btn-primary': (page == 'history')}" @click="getHistory();page = 'history'">{{app.getLz('term.history')}}</button>
|
||||||
|
</div>
|
||||||
|
<button class="md-btn md-btn-small" style="width:100%;margin-top:6px;" v-if="queueItems.length > 1" @click="app.mk.clearQueue();updateQueue()">{{app.getLz('term.clearAll')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.component('cider-queue', {
|
||||||
|
template: '#cider-queue',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
drag: false,
|
||||||
|
queuePosition: 0,
|
||||||
|
queueItems: [],
|
||||||
|
selected: -1,
|
||||||
|
selectedItems: [],
|
||||||
|
history: [],
|
||||||
|
page: "queue",
|
||||||
|
app: this.$root
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayQueueItems() {
|
||||||
|
const displayLimit = 50;
|
||||||
|
const lastDisplayPosition = Math.min(displayLimit + this.queuePosition, this.queueItems.length);
|
||||||
|
return this.queueItems.slice(this.queuePosition, lastDisplayPosition);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateQueue()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getHistory() {
|
||||||
|
let history = await app.mk.api.v3.music(`/v1/me/recent/played/tracks`, { l : this.$root.mklang})
|
||||||
|
this.history = history.data.data
|
||||||
|
},
|
||||||
|
select(e, position) {
|
||||||
|
if (e.ctrlKey || e.shiftKey) {
|
||||||
|
if (this.selectedItems.indexOf(position) == -1) {
|
||||||
|
this.selectedItems.push(position)
|
||||||
|
} else {
|
||||||
|
this.selectedItems.splice(this.selectedItems.indexOf(position), 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedItems = [position]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
queueContext(event, item, position) {
|
||||||
|
let self = this
|
||||||
|
let useMenu = "single"
|
||||||
|
if (this.selectedItems.length > 1) {
|
||||||
|
useMenu = "multiple"
|
||||||
|
}
|
||||||
|
let menus = {
|
||||||
|
single: {
|
||||||
|
items: [{
|
||||||
|
"name": app.getLz('action.removeFromQueue'),
|
||||||
|
"action": function () {
|
||||||
|
self.queueItems.splice(position, 1)
|
||||||
|
app.mk.queue._queueItems = self.queueItems;
|
||||||
|
app.mk.queue._reindex()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": app.getLz('action.startRadio'),
|
||||||
|
"action": function () {
|
||||||
|
app.mk.setStationQueue({
|
||||||
|
song: item.attributes.playParams.id ?? item.id
|
||||||
|
}).then(() => {
|
||||||
|
app.mk.play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": app.getLz('action.goToArtist'),
|
||||||
|
"action": function () {
|
||||||
|
app.searchAndNavigate(item,'artist')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": app.getLz('action.goToAlbum'),
|
||||||
|
"action": function () {
|
||||||
|
app.searchAndNavigate(item,'album')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
items: [{
|
||||||
|
"name": app.getLz('action.removeTracks').replace('${self.selectedItems.length}', self.selectedItems.length.toString()),
|
||||||
|
"action": function () {
|
||||||
|
// add property to items to be removed
|
||||||
|
self.selectedItems.forEach(function (item) {
|
||||||
|
self.queueItems[item].remove = true
|
||||||
|
})
|
||||||
|
// remove items
|
||||||
|
self.queueItems = self.queueItems.filter(function (item) {
|
||||||
|
return !item.remove
|
||||||
|
})
|
||||||
|
app.mk.queue._reindex()
|
||||||
|
self.selectedItems = []
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.showMenuPanel(menus[useMenu], event);
|
||||||
|
},
|
||||||
|
playQueueItem(id) {
|
||||||
|
app.mk.changeToMediaAtIndex(app.mk.queue._itemIDs.indexOf(id))
|
||||||
|
},
|
||||||
|
updateQueue() {
|
||||||
|
this.selected = -1
|
||||||
|
if (app.mk.queue) {
|
||||||
|
this.queuePosition = app.mk.queue.position;
|
||||||
|
this.queueItems = app.mk.queue._queueItems;
|
||||||
|
} else {
|
||||||
|
this.queuePosition = 0;
|
||||||
|
this.queueItems = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move() {
|
||||||
|
this.selected = -1
|
||||||
|
app.mk.queue._queueItems = this.queueItems;
|
||||||
|
app.mk.queue._reindex()
|
||||||
|
},
|
||||||
|
convertTimeToString(timeInMilliseconds) {
|
||||||
|
var seconds = ((timeInMilliseconds % 60000) / 1000).toFixed(0);
|
||||||
|
return Math.floor(timeInMilliseconds/60000) + ":" + (seconds < 10 ? '0' : '') + seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Add table
Add a link
Reference in a new issue