Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions en.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@
"AUTH_STATUS_ERROR": "Authentication failed!",
"AUTH_CODE_STATUS": "AUTH CODE: {code}\n\n{compare}",
"AUTH_CODE_COMPARE": "Please compare the code with the one on the YTMDesktop app and confirm the authorization.",
"CUSTOM_LAYOUT_FILE": "Custom Layout File",
"DISPLAY_FORMAT": "Display Format",
"DISPLAY_TITLE": "Title Format",
"SUPPORT_FEEDBACK_TITLE": "Support & Feedback",
"SUPPORT_FEEDBACK_TEXT": "You found a bug? You have a feature request?<br>You can either send them via an <a href=\"https://github.com/XeroxDev/YTMD-StreamDeck/issues/new/choose\" rel=\"noopener\" target=\"_blank\">GitHub Issue</a> or <a href=\"https://x.xeroxdev.de/s/discord\" rel=\"noopener\" target=\"_blank\">Discord</a>!",
"VAR_USAGE": "<p><strong>Available variables:</strong> {current}, {remaining}, {duration}</p><p><strong>Special values:</strong> :S = seconds, :H = human readable time (default)</p><p><strong>Usage:</strong> {remaining:S} to display remaining time in seconds</p>",
"VAR_USAGE_CUSTOMLAYOUT": "<p><strong>Usage:</strong> Path to custom layout in the plugin folder</p><p>Use the following key values on text objects to get information</p><p><strong>Key values:</strong> song, author, album</p>",
"VAR_USAGE_TITLE": "<p><strong>Available variables:</strong> {title}, {author}, {album}</p><p><strong>Usage:</strong> {title} to display song title</p>",
"TOGGLE": "Toggle",
"PAUSE": "Pause",
"PLAY": "Play",
Expand Down
1,496 changes: 1,000 additions & 496 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"homepage": "https://github.com/XeroxDev/YTMD-StreamDeck#readme",
"dependencies": {
"intl-messageformat": "^10.7.10",
"streamdeck-typescript": "^3.2.0",
"streamdeck-typescript": "^3.3.4",
"ytmdesktop-ts-companion": "^1.1.0"
},
"keywords": [
Expand Down
19 changes: 19 additions & 0 deletions property-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ <h3 class="error-title">Something went wrong</h3>
<p><strong>Special values:</strong> :S = seconds, :H = human readable time (default)</p>
<p><strong>Usage:</strong> {remaining:S} to display remaining time in seconds</p>
</div>
<div id="sdplus-settings" style="display: none;">
<div class="sdpi-item">
<div class="sdpi-item-label display-label" data-i18n="DISPLAY_TITLE">Title Format</div>
<textarea class="sdpi-item-value" type="text" id="displayTitleFormat">{title}</textarea>
</div>
<div style="padding: 5px;" class="var-usage-label" data-i18n-html="VAR_USAGE_TITLE">
<p><strong>Available variables:</strong> {title}, {author}, {album}</p>
<p><strong>Usage:</strong> {title} to display song title</p>
</div>
<div class="sdpi-item">
<div class="sdpi-item-label display-label" data-i18n="CUSTOM_LAYOUT_FILE">Custom Layout File</div>
<textarea class="sdpi-item-value" type="text" id="sdplus-customLayout"></textarea>
</div>
<div style="padding: 5px;" class="var-usage-label" data-i18n-html="VAR_USAGE_CUSTOMLAYOUT">
<p><strong>Usage:</strong> Path to custom layout in the plugin folder</p>
<p>Use the following key values on text objects to get information</p>
<p><strong>Key values:</strong> song, author, album</p>
</div>
</div>
<div class="sdpi-item">
<button class="sdpi-item-value save-label" id="save" data-i18n="SAVE">Save</button>
</div>
Expand Down
65 changes: 65 additions & 0 deletions sdplus_customlayout.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"id": "com.remote.ytmd.main",
"items": [
{
"key": "icon",
"type": "pixmap",
"rect": [104, 0, 96, 96],
"enabled": true,
"zOrder": 0
},
{
"key": "song",
"type": "text",
"font": { "size": 20, "weight": 600 },
"rect": [5, 5, 100, 21],
"alignment": "left",
"value": "Song",
"zOrder": 1
},
{
"key": "author",
"type": "text",
"font": { "size": 20, "weight": 500 },
"rect": [5, 28, 100, 21],
"alignment": "left",
"value": "Artist",
"zOrder": 2
},
{
"key": "album",
"type": "text",
"font": { "size": 15, "weight": 400 },
"rect": [5, 50, 100, 21],
"alignment": "left",
"value": "Album",
"zOrder": 6
},
{
"key": "value",
"type": "text",
"rect": [5, 70, 60, 21],
"font": { "size": 18, "weight": 400 },
"alignment": "left",
"zOrder": 3
},
{
"key": "like-icon",
"type": "pixmap",
"rect": [71, 36, 24, 24],
"enabled": true,
"zOrder": 4
},
{
"key": "indicator",
"type": "bar",
"subtype": 0,
"rect": [0, 90, 200, 10],
"border_w": 0,
"bar_fill_c": "0:yellow, 0.9:orange, 1: white",
"value": 0,
"zOrder": 5
}

]
}
97 changes: 76 additions & 21 deletions src/actions/play-pause.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
private currentTitle: string;
private firstTimes = 10;
private contextFormat: { [key: string]: string } = {};
private contextTitleFormat: { [key: string]: string } = {};
private events: {
context: string,
onTick: (state: StateOutput) => void,
Expand Down Expand Up @@ -102,23 +103,34 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
switch (state) {
case SocketState.CONNECTED:
this.plugin.showOk(event.context);
this.plugin.setTitle("", event.context);
this.plugin.setFeedback(event.context, {"icon": this.thumbnail, "value": "00:00", "indicator": { "enabled": true}});
break;
case SocketState.DISCONNECTED:
case SocketState.ERROR:
this.plugin.showAlert(event.context);
this.plugin.setTitle("⚠", event.context);
this.plugin.setFeedback(event.context, {"icon": this.thumbnail, "value": "⚠", "indicator": { "enabled": false}});
break;
default:
break;
}
},
onError: () => this.plugin.showAlert(event.context)
onError: (error: any) => {
if (error.toString() !== "Error: websocket error")
{
this.plugin.showAlert(event.context);
}
}
};

this.events.push(found);

this.socket.addStateListener(found.onTick);
this.socket.addConnectionStateListener(found.onConChange);
this.socket.addErrorListener(found.onError)

let clayout = event.payload.settings.customLayout;
this.plugin.setFeedbackLayout(event.context, clayout == '' ? '$B1' : clayout);
}

@SDOnActionEvent('willDisappear')
Expand Down Expand Up @@ -171,7 +183,7 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
}

handlePlayerData(
{context, payload: {settings}}: WillAppearEvent<PlayPauseSettings>,
{action, context, payload: {settings}}: WillAppearEvent<PlayPauseSettings>,
data: StateOutput
) {
if (Object.keys(data).length === 0) {
Expand All @@ -182,14 +194,32 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
let duration = Math.floor(data.video?.durationSeconds ?? 0);
let remaining = duration - current;

const title = this.formatTitle(current, duration, remaining, context, settings);
const cover = this.getSongCover(data);
const time = this.formatTime(current, duration, remaining, context, settings);
const {title, album, author, cover} = this.getSongData(data);
const formattitle = this.formatTitle(title, album, author, context, settings);

if (this.currentTitle !== title || this.firstTimes >= 1) {
if (this.currentTitle !== time || this.firstTimes >= 1) {
this.firstTimes--;
this.currentTitle = title;
this.currentTitle = time;
this.plugin.setTitle(this.currentTitle, context);
this.plugin.setFeedback(context, {"icon": this.thumbnail, "value": this.currentTitle, "indicator": { "value": current / duration * 100, "enabled": true}});
if (formattitle != "")
{
this.plugin.setFeedback(context, {"title": formattitle});
}
// these 3 below are for custom layout support with more text fields
if (title != "")
{
this.plugin.setFeedback(context, {"song": title});
}
if (author != "")
{
this.plugin.setFeedback(context, {"author": author});
}
if (album != "")
{
this.plugin.setFeedback(context, {"album": album});
}
if (this.currentThumbnail !== cover)
{
this.currentThumbnail = cover;
Expand All @@ -198,15 +228,15 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {

image.onload = () => {
let canvas = document.createElement('canvas');
canvas.width = 48;
canvas.height = 48;
canvas.width = 100;
canvas.height = 100;

let ctx = canvas.getContext('2d');
if (!ctx) {
return;
}

ctx.drawImage(image, 0, 0, 48, 48);
ctx.drawImage(image, 0, 0, 100, 100);

image.onload = null;
(image as any) = null;
Expand All @@ -226,25 +256,47 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
}
}

private getSongCover(data: StateOutput): string {
let cover = "";
private getSongData(data: StateOutput): {
title: string,
album: string,
author: string,
cover: string
} {
let title = '';
let album = '';
let author = '';
let cover = '';

if (!data.player || !data.video) return cover;
if (!data.player || !data.video) return {title, album, author, cover};

const trackState = data.player.trackState;

switch (trackState) {
case TrackState.PLAYING:
cover = data.video.thumbnails[data.video.thumbnails.length - 1].url ?? cover;
break;
default:
break;
title = data.video.title ?? title;
album = data.video.album ?? album;
author = data.video.author ?? author;
cover = data.video.thumbnails[data.video.thumbnails.length - 1].url ?? cover;

return {title, album, author, cover};
}

private formatTitle(title: string, album: string, author: string, context: string, settings: PlayPauseSettings): string {
const varMapping: { [key: string]: string } = {
'title': title,
'album': album,
'author': author,
};

let result = this.contextTitleFormat[context] ?? settings.displayTitleFormat ?? '{title}';

for (let varMappingKey in varMapping) {
const value = varMapping[varMappingKey];
result = result.replace(new RegExp(`\{${varMappingKey}\}`, 'gi'), value);
}

return cover;
return result;
}

private formatTitle(current: number, duration: number, remaining: number, context: string, settings: PlayPauseSettings): string {
private formatTime(current: number, duration: number, remaining: number, context: string, settings: PlayPauseSettings): string {
current = current ?? 0;
duration = duration ?? 0;
remaining = remaining ?? 0;
Expand Down Expand Up @@ -273,6 +325,9 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
@SDOnActionEvent('didReceiveSettings')
private handleSettings(e: DidReceiveSettingsEvent<PlayPauseSettings>) {
this.contextFormat[e.context] = e.payload.settings?.displayFormat ?? this.contextFormat[e.context];
this.contextTitleFormat[e.context] = e.payload.settings?.displayTitleFormat ?? this.contextTitleFormat[e.context];
let clayout = e.payload.settings?.customLayout;
this.plugin.setFeedbackLayout(e.context, clayout == '' ? '$B1' : clayout);
}

@SDOnActionEvent('dialUp')
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/context-settings.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface VolumeSettings {
export interface PlayPauseSettings {
action: 'PLAY' | 'PAUSE' | 'TOGGLE';
displayFormat: string;
displayTitleFormat: string;
customLayout: string;
}

export interface PlaylistSettings {
Expand Down
11 changes: 9 additions & 2 deletions src/pis/features/play-pause.pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@ export class PlayPausePi extends PisAbstract {
public newSettingsReceived({payload: {settings}}: DidReceiveSettingsEvent<PlayPauseSettings>): void {
this.pi.actionElement.value = settings.action ?? "TOGGLE";
this.pi.displayFormatElement.value = settings.displayFormat ?? "{current}";
this.pi.displayTitleFormatElement.value = settings.displayTitleFormat ?? "{title}";
this.pi.customLayoutElement.value = settings.customLayout ?? "";

}

private saveSettings() {
const action = this.pi.actionElement.value,
displayFormat = this.pi.displayFormatElement.value;
displayFormat = this.pi.displayFormatElement.value,
displayTitleFormat = this.pi.displayTitleFormatElement.value,
customLayout = this.pi.customLayoutElement.value;

this.settingsManager.setContextSettingsAttributes(this.context, {
action: action ?? "TOGGLE",
displayFormat: displayFormat ?? "{current}"
displayFormat: displayFormat ?? "{current}",
displayTitleFormat: displayTitleFormat ?? "{title}",
customLayout: customLayout ?? ""
});
}
}
9 changes: 9 additions & 0 deletions src/ytmd-pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class YTMDPi extends StreamDeckPropertyInspectorHandler {
public playPauseSettings: HTMLElement;
public actionElement: HTMLInputElement;
public displayFormatElement: HTMLInputElement;
public displayTitleFormatElement: HTMLInputElement;
public sdplusSettingsElement: HTMLInputElement;
public customLayoutElement: HTMLInputElement;
public saveElement: HTMLButtonElement;
// Global Settings
public globalSettings: HTMLElement;
Expand Down Expand Up @@ -188,12 +191,18 @@ export class YTMDPi extends StreamDeckPropertyInspectorHandler {
@SDOnPiEvent('didReceiveSettings')
private receivedSettings(event: DidReceiveSettingsEvent) {
this.action?.newSettingsReceived(event);
if (event.payload.controller === "Encoder") {
this.sdplusSettingsElement.style.display = "block";
}
}

private setupElements() {
this.playPauseSettings = document.getElementById('playPauseSettings') as HTMLElement;
this.actionElement = document.getElementById('action') as HTMLInputElement;
this.displayFormatElement = document.getElementById('displayFormat') as HTMLInputElement;
this.displayTitleFormatElement = document.getElementById('displayTitleFormat') as HTMLInputElement;
this.sdplusSettingsElement = document.getElementById('sdplus-settings') as HTMLInputElement;
this.customLayoutElement = document.getElementById('sdplus-customLayout') as HTMLInputElement;
this.saveElement = document.getElementById('save') as HTMLButtonElement;
this.globalSettings = document.getElementById('globalSettings') as HTMLElement;
this.globalHostElement = document.getElementById('globalHost') as HTMLInputElement;
Expand Down