library refactor
This commit is contained in:
parent
9d8a81ebc8
commit
181c988649
9 changed files with 118 additions and 74 deletions
63
app/app.css
63
app/app.css
|
@ -12,7 +12,7 @@ body {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
text-shadow: 0 1px 1px #000;
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -137,14 +137,14 @@ nav ul li.active {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.component .grid li {
|
.component li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.component .grid h2 {
|
.component h2 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -152,15 +152,15 @@ nav ul li.active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.component .grid h2 .icon {
|
.component h2 .icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
.component .grid li:nth-child(odd) {
|
.component li:nth-child(odd) {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
.component .grid .icon {
|
.component .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,14 +176,14 @@ nav ul li.active {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#queue .grid li {
|
#queue li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#queue .grid h2 {
|
#queue h2 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -191,15 +191,15 @@ nav ul li.active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#queue .grid h2 .icon {
|
#queue h2 .icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
#queue .grid li:nth-child(odd) {
|
#queue li:nth-child(odd) {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
#queue .grid .icon {
|
#queue .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,14 +218,14 @@ nav ul li.active {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#library .grid li {
|
#library li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#library .grid h2 {
|
#library h2 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -233,26 +233,33 @@ nav ul li.active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#library .grid h2 .icon {
|
#library h2 .icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
#library .grid li:nth-child(odd) {
|
#library li:nth-child(odd) {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
#library .grid .icon {
|
#library .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#library .tiles {
|
#library .tiles {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
grid-gap: 2px;
|
||||||
}
|
}
|
||||||
#library .tiles li {
|
#library .tiles li {
|
||||||
border: 3px solid lime;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
#library .tiles li h2 {
|
||||||
|
font-size: 150%;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
#fs {
|
#fs {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -265,14 +272,14 @@ nav ul li.active {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#fs .grid li {
|
#fs li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#fs .grid h2 {
|
#fs h2 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -280,15 +287,15 @@ nav ul li.active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#fs .grid h2 .icon {
|
#fs h2 .icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
#fs .grid li:nth-child(odd) {
|
#fs li:nth-child(odd) {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
#fs .grid .icon {
|
#fs .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,14 +314,14 @@ nav ul li.active {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#playlists .grid li {
|
#playlists li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
#playlists .grid h2 {
|
#playlists h2 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -322,15 +329,15 @@ nav ul li.active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#playlists .grid h2 .icon {
|
#playlists h2 .icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
#playlists .grid li:nth-child(odd) {
|
#playlists li:nth-child(odd) {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
#playlists .grid .icon {
|
#playlists .icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ body {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
text-shadow: 0 1px 1px #000;
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -11,35 +11,33 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
li {
|
||||||
li {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: row;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
align-items: center;
|
padding: 0 4px;
|
||||||
padding: 0 4px;
|
white-space: nowrap;
|
||||||
white-space: nowrap;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li:nth-child(odd) {
|
|
||||||
background-color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
.icon { width: 32px; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li:nth-child(odd) {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.icon { width: 32px; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,19 @@
|
||||||
|
|
||||||
.tiles {
|
.tiles {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
grid-gap: 2px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
border: 3px solid lime;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 150%;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,10 @@
|
||||||
<button class="clear" data-icon="close" title="Clear the queue"></button>
|
<button class="clear" data-icon="close" title="Clear the queue"></button>
|
||||||
<button class="save" data-icon="content-save" title="Save the queue"></button>
|
<button class="save" data-icon="content-save" title="Save the queue"></button>
|
||||||
</header>
|
</header>
|
||||||
<ul class="grid"></ul>
|
<ul></ul>
|
||||||
</section>
|
</section>
|
||||||
<section id="playlists">
|
<section id="playlists">
|
||||||
<ul class="grid"></ul>
|
<ul></ul>
|
||||||
</section>
|
</section>
|
||||||
<section id="library">
|
<section id="library">
|
||||||
<header></header>
|
<header></header>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</section>
|
</section>
|
||||||
<section id="fs">
|
<section id="fs">
|
||||||
<header></header>
|
<header></header>
|
||||||
<ul class="grid"></ul>
|
<ul></ul>
|
||||||
</section>
|
</section>
|
||||||
<section id="yt">
|
<section id="yt">
|
||||||
<button class="go" data-icon="download">Go!</button>
|
<button class="go" data-icon="download">Go!</button>
|
||||||
|
|
|
@ -11,6 +11,7 @@ function onMessage(e) {
|
||||||
if (last.startsWith("OK")) {
|
if (last.startsWith("OK")) {
|
||||||
current.resolve(lines);
|
current.resolve(lines);
|
||||||
} else {
|
} else {
|
||||||
|
console.warn(last);
|
||||||
current.reject(last);
|
current.reject(last);
|
||||||
}
|
}
|
||||||
current = null;
|
current = null;
|
||||||
|
@ -108,12 +109,13 @@ export async function listTags(tag, filter = null) {
|
||||||
}
|
}
|
||||||
let lines = await command(tokens.join(" "));
|
let lines = await command(tokens.join(" "));
|
||||||
let parsed = parser.linesToStruct(lines);
|
let parsed = parser.linesToStruct(lines);
|
||||||
return [].concat(parsed[tag] || []);
|
return [].concat(tag in parsed ? parsed[tag] : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listSongs(filter) {
|
export async function listSongs(filter, window = null) {
|
||||||
let tokens = ["find"];
|
let tokens = ["find"];
|
||||||
tokens.push(serializeFilter(filter));
|
tokens.push(serializeFilter(filter));
|
||||||
|
if (window) { tokens.push("window", window.join(":")); }
|
||||||
let lines = await command(tokens.join(" "));
|
let lines = await command(tokens.join(" "));
|
||||||
return parser.songList(lines);
|
return parser.songList(lines);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as mpd from "./mpd.js";
|
||||||
import * as html from "./html.js";
|
import * as html from "./html.js";
|
||||||
import * as pubsub from "./pubsub.js";
|
import * as pubsub from "./pubsub.js";
|
||||||
import * as format from "./format.js";
|
import * as format from "./format.js";
|
||||||
|
import * as art from "./art.js";
|
||||||
import * as player from "../player.js";
|
import * as player from "../player.js";
|
||||||
|
|
||||||
export const CTX_FS = 1;
|
export const CTX_FS = 1;
|
||||||
|
@ -15,7 +16,7 @@ const TYPE_PLAYLIST = 4;
|
||||||
|
|
||||||
const SORT = "-Track";
|
const SORT = "-Track";
|
||||||
|
|
||||||
function enqueue(type, what) {
|
async function enqueue(type, what) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPE_URL: return mpd.command(`add "${mpd.escape(what)}"`); break;
|
case TYPE_URL: return mpd.command(`add "${mpd.escape(what)}"`); break;
|
||||||
case TYPE_FILTER: return mpd.enqueueByFilter(what, SORT); break;
|
case TYPE_FILTER: return mpd.enqueueByFilter(what, SORT); break;
|
||||||
|
@ -23,6 +24,28 @@ function enqueue(type, what) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fillArt(parent, filter) {
|
||||||
|
let artist = filter["Artist"];
|
||||||
|
let album = filter["Album"];
|
||||||
|
let src = null;
|
||||||
|
|
||||||
|
if (artist && album) {
|
||||||
|
src = await art.get(artist, album);
|
||||||
|
if (!src) {
|
||||||
|
let songs = await mpd.listSongs(filter, [0,1]);
|
||||||
|
if (songs.length) {
|
||||||
|
src = await art.get(artist, album, songs[0]["file"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src) {
|
||||||
|
html.node("img", {src}, "", parent);
|
||||||
|
} else {
|
||||||
|
html.icon("music", parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fileName(data) {
|
function fileName(data) {
|
||||||
return data["file"].split("/").pop();
|
return data["file"].split("/").pop();
|
||||||
}
|
}
|
||||||
|
@ -106,7 +129,6 @@ function addButton(type, what, parent) {
|
||||||
export function song(ctx, data, parent) {
|
export function song(ctx, data, parent) {
|
||||||
let node = html.node("li", {className:"song"}, "", parent);
|
let node = html.node("li", {className:"song"}, "", parent);
|
||||||
|
|
||||||
|
|
||||||
let title = formatTitle(ctx, data);
|
let title = formatTitle(ctx, data);
|
||||||
let h2 = html.node("h2", {}, "", node);
|
let h2 = html.node("h2", {}, "", node);
|
||||||
if (ctx == CTX_FS || ctx == CTX_LIBRARY) { html.icon("music", h2); }
|
if (ctx == CTX_FS || ctx == CTX_LIBRARY) { html.icon("music", h2); }
|
||||||
|
@ -136,10 +158,16 @@ export function song(ctx, data, parent) {
|
||||||
export function group(ctx, label, urlOrFilter, parent) {
|
export function group(ctx, label, urlOrFilter, parent) {
|
||||||
let node = html.node("li", {className:"group"}, "", parent);
|
let node = html.node("li", {className:"group"}, "", parent);
|
||||||
|
|
||||||
|
if (ctx == CTX_LIBRARY) {
|
||||||
|
let art = html.node("span", {className:"art"}, "", node);
|
||||||
|
fillArt(art, urlOrFilter);
|
||||||
|
}
|
||||||
|
|
||||||
let h2 = html.node("h2", {}, "", node);
|
let h2 = html.node("h2", {}, "", node);
|
||||||
if (ctx == CTX_FS) { html.icon("folder", h2); }
|
if (ctx == CTX_FS) { html.icon("folder", h2); }
|
||||||
html.text(label, h2);
|
html.text(label, h2);
|
||||||
|
|
||||||
|
|
||||||
let type = (ctx == CTX_FS ? TYPE_URL : TYPE_FILTER);
|
let type = (ctx == CTX_FS ? TYPE_URL : TYPE_FILTER);
|
||||||
|
|
||||||
playButton(type, urlOrFilter, node);
|
playButton(type, urlOrFilter, node);
|
||||||
|
|
|
@ -44,8 +44,6 @@ function buildArtist(artist, filter, parent) {
|
||||||
function buildSongs(songs, filter) {
|
function buildSongs(songs, filter) {
|
||||||
let ul = node.querySelector("ul");
|
let ul = node.querySelector("ul");
|
||||||
html.clear(ul);
|
html.clear(ul);
|
||||||
ul.classList.remove("tiles");
|
|
||||||
ul.classList.add("grid");
|
|
||||||
|
|
||||||
songs.map(song => ui.song(ui.CTX_LIBRARY, song, ul));
|
songs.map(song => ui.song(ui.CTX_LIBRARY, song, ul));
|
||||||
}
|
}
|
||||||
|
@ -53,8 +51,6 @@ function buildSongs(songs, filter) {
|
||||||
function buildAlbums(albums, filter) {
|
function buildAlbums(albums, filter) {
|
||||||
let ul = node.querySelector("ul");
|
let ul = node.querySelector("ul");
|
||||||
html.clear(ul);
|
html.clear(ul);
|
||||||
ul.classList.add("tiles");
|
|
||||||
ul.classList.remove("grid");
|
|
||||||
|
|
||||||
albums.map(album => buildAlbum(album, filter, ul));
|
albums.map(album => buildAlbum(album, filter, ul));
|
||||||
}
|
}
|
||||||
|
@ -62,8 +58,6 @@ function buildAlbums(albums, filter) {
|
||||||
function buildArtists(artists, filter) {
|
function buildArtists(artists, filter) {
|
||||||
let ul = node.querySelector("ul");
|
let ul = node.querySelector("ul");
|
||||||
html.clear(ul);
|
html.clear(ul);
|
||||||
ul.classList.add("tiles");
|
|
||||||
ul.classList.remove("grid");
|
|
||||||
|
|
||||||
artists.map(artist => buildArtist(artist, filter, ul));
|
artists.map(artist => buildArtist(artist, filter, ul));
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,16 @@ function sync(data) {
|
||||||
DOM.elapsed.textContent = format.time(Number(data["elapsed"] || 0)); // changed time
|
DOM.elapsed.textContent = format.time(Number(data["elapsed"] || 0)); // changed time
|
||||||
|
|
||||||
if (data["file"] != current["file"]) { // changed song
|
if (data["file"] != current["file"]) { // changed song
|
||||||
DOM.duration.textContent = format.time(Number(data["duration"] || 0));
|
if (data["file"]) { // playing at all?
|
||||||
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
DOM.duration.textContent = format.time(Number(data["duration"] || 0));
|
||||||
DOM["artist-album"].textContent = format.artistAlbum(data["Artist"], data["Album"]);
|
DOM.title.textContent = data["Title"] || data["file"].split("/").pop();
|
||||||
|
DOM["artist-album"].textContent = format.artistAlbum(data["Artist"], data["Album"]);
|
||||||
|
} else {
|
||||||
|
DOM.duration.textContent = "";
|
||||||
|
DOM.title.textContent = "";
|
||||||
|
DOM["artist-album"].textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
pubsub.publish("song-change", null, data);
|
pubsub.publish("song-change", null, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue