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
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "bardic-inspiration",
"title": "Bardic Inspiration",
"description": "Synchronized YouTube DJ for Foundry VTT - Play YouTube videos in sync across all players",
"version": "1.0.2",
"version": "1.0.3",
"compatibility": {
"minimum": "12",
"verified": "13",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bardic-inspiration",
"version": "1.0.2",
"version": "1.0.3",
"description": "A Foundry VTT module",
"type": "module",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ Hooks.once('ready', async () => {
const savedQueuesManager = new SavedQueuesManager(store, queueManager);
logger.info('YouTube DJ SavedQueuesManager initialized globally');

// Connect QueueManager to SavedQueuesManager for queue modification tracking
queueManager.setSavedQueuesManager(savedQueuesManager);

// Store global references for access across components
(globalThis as any).youtubeDJSocketManager = socketManager;
(globalThis as any).youtubeDJSessionManager = sessionManager;
Expand Down
13 changes: 10 additions & 3 deletions src/services/PlayerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,11 +775,18 @@ export class PlayerManager {
return; // DJ doesn't sync to their own heartbeat
}

// Skip sync if player is still initializing
const playerState = this.store.getPlayerState();
if (playerState.isInitializing) {
logger.debug('🎵 YouTube DJ | Skipping heartbeat sync - player still initializing');
return;
}

// Only sync if we have a different video or significant time difference
const currentVideo = this.store.getPlayerState().currentVideo;
const currentVideo = playerState.currentVideo;

// Check if video changed
if (currentVideo?.videoId !== heartbeat.videoId) {
// Check if video changed (only if heartbeat has a video)
if (heartbeat.videoId && currentVideo?.videoId !== heartbeat.videoId) {
logger.debug('🎵 YouTube DJ | Syncing to new video from heartbeat:', heartbeat.videoId);
// Send load command to widget
Hooks.callAll('youtubeDJ.playerCommand', {
Expand Down
31 changes: 30 additions & 1 deletion src/services/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface YouTubeDJMessage {

export class QueueManager {
private store: SessionStore;
private savedQueuesManager?: any; // Will be set after initialization

constructor(store: SessionStore) {
this.store = store;
Expand All @@ -36,6 +37,22 @@ export class QueueManager {
Hooks.on('youtubeDJ.playlistEmbedError', this.onPlaylistEmbedError.bind(this));
}

/**
* Set reference to SavedQueuesManager (called after initialization)
*/
setSavedQueuesManager(savedQueuesManager: any): void {
this.savedQueuesManager = savedQueuesManager;
}

/**
* Mark queue as modified if there's a currently loaded saved queue
*/
private markQueueAsModified(): void {
if (this.savedQueuesManager) {
this.savedQueuesManager.markQueueAsModified();
}
}

/**
* Check if user can add videos to queue
*/
Expand Down Expand Up @@ -215,6 +232,9 @@ export class QueueManager {
}
});

// Mark queue as modified if there's a loaded saved queue
this.markQueueAsModified();

// Broadcast queue update
this.broadcastMessage({
type: 'QUEUE_ADD',
Expand Down Expand Up @@ -279,6 +299,9 @@ export class QueueManager {
}
});

// Mark queue as modified if there's a loaded saved queue
this.markQueueAsModified();

// Broadcast queue update
this.broadcastMessage({
type: 'QUEUE_REMOVE',
Expand Down Expand Up @@ -338,6 +361,9 @@ export class QueueManager {
}
});

// Mark queue as modified if there's a loaded saved queue
this.markQueueAsModified();

// Broadcast queue update
this.broadcastMessage({
type: 'QUEUE_UPDATE',
Expand Down Expand Up @@ -611,7 +637,10 @@ export class QueueManager {
items: [],
currentIndex: -1,
mode: 'single-dj',
djUserId: this.store.getSessionState().djUserId
djUserId: this.store.getSessionState().djUserId,
savedQueues: this.store.getQueueState().savedQueues,
currentlyLoadedQueueId: null, // Clear loaded queue tracking
isModifiedFromSaved: false
},
player: {
...this.store.getPlayerState(),
Expand Down
144 changes: 142 additions & 2 deletions src/services/SavedQueuesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export class SavedQueuesManager {
// Save to world settings
await game.settings.set('core', 'youtubeDJ.savedQueues', updatedQueues);

// Update queue state to track this as the currently loaded queue
this.store.updateState({
queue: {
...currentQueue,
currentlyLoadedQueueId: savedQueue.id,
isModifiedFromSaved: false
}
});

// Broadcast save event
this.broadcastMessage({
type: 'QUEUE_SAVED',
Expand Down Expand Up @@ -160,12 +169,15 @@ export class SavedQueuesManager {
newItems = [...currentQueue.items, ...savedQueue.items];
}

// Update queue state
// Update queue state with tracking
this.store.updateState({
queue: {
...currentQueue,
items: newItems,
currentIndex: replace && newItems.length > 0 ? 0 : currentQueue.currentIndex
currentIndex: replace && newItems.length > 0 ? 0 : currentQueue.currentIndex,
// Track the loaded queue only if we replaced (not if we appended)
currentlyLoadedQueueId: replace ? queueId : null,
isModifiedFromSaved: false
}
});

Expand Down Expand Up @@ -324,6 +336,134 @@ export class SavedQueuesManager {
ui.notifications?.success(`Queue renamed to "${trimmedName}"`);
}

/**
* Save changes to the currently loaded saved queue
*/
async saveChangesToCurrentQueue(): Promise<SavedQueue | null> {
if (!this.store.isDJ()) {
throw new Error('Only the DJ can save queue changes');
}

const currentQueue = this.store.getQueueState();

// Check if there's a currently loaded saved queue
if (!currentQueue.currentlyLoadedQueueId) {
throw new Error('No saved queue is currently loaded. Use "Save Queue" to create a new saved queue.');
}

// Check if the queue has been modified
if (!currentQueue.isModifiedFromSaved) {
ui.notifications?.info('No changes to save - queue is already up to date');
return null;
}

const savedQueue = this.getSavedQueue(currentQueue.currentlyLoadedQueueId);
if (!savedQueue) {
throw new Error('Currently loaded saved queue not found');
}

if (currentQueue.items.length === 0) {
throw new Error('Cannot save an empty queue');
}

logger.debug('🎵 YouTube DJ | Saving changes to currently loaded queue:', savedQueue.name);

// Create updated saved queue
const updatedQueue: SavedQueue = {
...savedQueue,
items: [...currentQueue.items], // Deep copy current items
updatedAt: Date.now()
};

// Update saved queues list
const savedQueues = this.getSavedQueues();
const updatedQueues = savedQueues.map(q =>
q.id === currentQueue.currentlyLoadedQueueId ? updatedQueue : q
);

// Save to world settings
await game.settings.set('core', 'youtubeDJ.savedQueues', updatedQueues);

// Update queue state to reflect saved changes
this.store.updateState({
queue: {
...currentQueue,
isModifiedFromSaved: false
}
});

// Broadcast save event
this.broadcastMessage({
type: 'QUEUE_SAVED',
userId: game.user?.id || '',
timestamp: Date.now(),
data: {
savedQueue: updatedQueue,
isOverwrite: true,
isCurrentQueueUpdate: true
}
});

logger.info('🎵 YouTube DJ | Changes saved to queue:', savedQueue.name);
ui.notifications?.success(`Changes saved to "${savedQueue.name}"`);

return updatedQueue;
}

/**
* Get information about the currently loaded saved queue
*/
getCurrentlyLoadedQueue(): { savedQueue: SavedQueue | null; hasChanges: boolean } {
const currentQueue = this.store.getQueueState();

if (!currentQueue.currentlyLoadedQueueId) {
return { savedQueue: null, hasChanges: false };
}

const savedQueue = this.getSavedQueue(currentQueue.currentlyLoadedQueueId);
return {
savedQueue,
hasChanges: currentQueue.isModifiedFromSaved
};
}

/**
* Mark the current queue as modified from the saved version
*/
markQueueAsModified(): void {
const currentQueue = this.store.getQueueState();

if (currentQueue.currentlyLoadedQueueId && !currentQueue.isModifiedFromSaved) {
this.store.updateState({
queue: {
...currentQueue,
isModifiedFromSaved: true
}
});

logger.debug('🎵 YouTube DJ | Queue marked as modified from saved version');
}
}

/**
* Clear the currently loaded queue tracking (when creating a new queue from scratch)
*/
clearCurrentlyLoadedQueue(): void {
const currentQueue = this.store.getQueueState();

if (currentQueue.currentlyLoadedQueueId || currentQueue.isModifiedFromSaved) {
this.store.updateState({
queue: {
...currentQueue,
currentlyLoadedQueueId: null,
isModifiedFromSaved: false
}
});

logger.debug('🎵 YouTube DJ | Cleared currently loaded queue tracking');
}
}

/**
* Export a saved queue to JSON
*/
Expand Down
7 changes: 6 additions & 1 deletion src/state/StateTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ export interface QueueState {
mode: 'single-dj' | 'collaborative';
djUserId: string | null;
savedQueues: SavedQueue[];
// Track the currently loaded saved queue (if any)
currentlyLoadedQueueId: string | null;
isModifiedFromSaved: boolean; // Track if current queue differs from the loaded saved queue
}

export interface UIState {
Expand Down Expand Up @@ -171,7 +174,9 @@ export function createDefaultQueueState(): QueueState {
currentIndex: -1,
mode: 'single-dj',
djUserId: null,
savedQueues: []
savedQueues: [],
currentlyLoadedQueueId: null,
isModifiedFromSaved: false
};
}

Expand Down
Loading
Loading