feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
180
f_scripts/playlist_enhancements.js
Normal file
180
f_scripts/playlist_enhancements.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* EasyStream Playlist Enhancements
|
||||
* Adds shuffle, loop, and autoplay features to playlists
|
||||
*/
|
||||
|
||||
class PlaylistEnhancer {
|
||||
constructor(playlistId, options = {}) {
|
||||
this.playlistId = playlistId;
|
||||
this.currentIndex = 0;
|
||||
this.items = [];
|
||||
this.options = {
|
||||
shuffle: options.shuffle || false,
|
||||
loop: options.loop || false,
|
||||
autoplay: options.autoplay !== false,
|
||||
...options
|
||||
};
|
||||
this.originalOrder = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadPlaylistItems();
|
||||
this.setupControls();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadPlaylistItems() {
|
||||
try {
|
||||
const response = await fetch(`/api/playlists/${this.playlistId}/items`);
|
||||
const data = await response.json();
|
||||
this.items = data.items || [];
|
||||
this.originalOrder = [...this.items];
|
||||
} catch (error) {
|
||||
console.error('Error loading playlist:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupControls() {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'playlist-controls';
|
||||
controls.innerHTML = `
|
||||
<button id="playlistShuffle" class="${this.options.shuffle ? 'active' : ''}" title="Shuffle">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M15 4v2h-2.5c-.9 0-1.7.3-2.4.9l-5.2 5.2c-.6.6-1.5.9-2.4.9H0v2h2.5c1.5 0 2.9-.6 4-1.6l5.1-5.1c.3-.3.7-.4 1.1-.4H15v2l4-3-4-3zm0 12v-2h-2.5c-.4 0-.8-.1-1.1-.4L11 13.2l-1.4 1.4 1.4 1.4c1.1 1 2.5 1.6 4 1.6H15v2l4-3-4-3zM6.4 11.1l1.4-1.4L6.4 8.3 5 9.7l1.4 1.4z"/></svg>
|
||||
</button>
|
||||
<button id="playlistLoop" class="${this.options.loop ? 'active' : ''}" title="Loop">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M14 7l-5 5-5-5H8V0h3v7h3zm1 6v3H3V9H0l5-5 5 5H7v4h7v-1h3z"/></svg>
|
||||
</button>
|
||||
<button id="playlistAutoplay" class="${this.options.autoplay ? 'active' : ''}" title="Autoplay">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M5 4l10 6-10 6V4z"/></svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const playlistContainer = document.querySelector('.playlist-container');
|
||||
if (playlistContainer) {
|
||||
playlistContainer.insertBefore(controls, playlistContainer.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('playlistShuffle')?.addEventListener('click', () => this.toggleShuffle());
|
||||
document.getElementById('playlistLoop')?.addEventListener('click', () => this.toggleLoop());
|
||||
document.getElementById('playlistAutoplay')?.addEventListener('click', () => this.toggleAutoplay());
|
||||
|
||||
// Listen for video end event
|
||||
const video = document.querySelector('video');
|
||||
if (video) {
|
||||
video.addEventListener('ended', () => this.onVideoEnded());
|
||||
}
|
||||
}
|
||||
|
||||
toggleShuffle() {
|
||||
this.options.shuffle = !this.options.shuffle;
|
||||
document.getElementById('playlistShuffle').classList.toggle('active');
|
||||
|
||||
if (this.options.shuffle) {
|
||||
this.shufflePlaylist();
|
||||
} else {
|
||||
this.items = [...this.originalOrder];
|
||||
}
|
||||
|
||||
this.savePreferences();
|
||||
}
|
||||
|
||||
toggleLoop() {
|
||||
this.options.loop = !this.options.loop;
|
||||
document.getElementById('playlistLoop').classList.toggle('active');
|
||||
this.savePreferences();
|
||||
}
|
||||
|
||||
toggleAutoplay() {
|
||||
this.options.autoplay = !this.options.autoplay;
|
||||
document.getElementById('playlistAutoplay').classList.toggle('active');
|
||||
this.savePreferences();
|
||||
}
|
||||
|
||||
shufflePlaylist() {
|
||||
const currentItem = this.items[this.currentIndex];
|
||||
|
||||
// Fisher-Yates shuffle
|
||||
for (let i = this.items.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[this.items[i], this.items[j]] = [this.items[j], this.items[i]];
|
||||
}
|
||||
|
||||
// Keep current item at current position
|
||||
if (currentItem) {
|
||||
const newIndex = this.items.indexOf(currentItem);
|
||||
[this.items[this.currentIndex], this.items[newIndex]] = [this.items[newIndex], this.items[this.currentIndex]];
|
||||
}
|
||||
|
||||
this.updatePlaylistUI();
|
||||
}
|
||||
|
||||
playNext() {
|
||||
if (this.currentIndex < this.items.length - 1) {
|
||||
this.currentIndex++;
|
||||
} else if (this.options.loop) {
|
||||
this.currentIndex = 0;
|
||||
} else {
|
||||
return; // End of playlist
|
||||
}
|
||||
|
||||
this.playItem(this.currentIndex);
|
||||
}
|
||||
|
||||
playPrevious() {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
} else if (this.options.loop) {
|
||||
this.currentIndex = this.items.length - 1;
|
||||
} else {
|
||||
return; // Start of playlist
|
||||
}
|
||||
|
||||
this.playItem(this.currentIndex);
|
||||
}
|
||||
|
||||
playItem(index) {
|
||||
if (index < 0 || index >= this.items.length) return;
|
||||
|
||||
this.currentIndex = index;
|
||||
const item = this.items[index];
|
||||
|
||||
// Navigate to video
|
||||
window.location.href = `/watch?v=${item.file_key}&list=${this.playlistId}`;
|
||||
}
|
||||
|
||||
onVideoEnded() {
|
||||
if (this.options.autoplay) {
|
||||
setTimeout(() => this.playNext(), 1000); // 1 second delay
|
||||
}
|
||||
}
|
||||
|
||||
updatePlaylistUI() {
|
||||
const playlistItems = document.querySelectorAll('.playlist-item');
|
||||
playlistItems.forEach((item, index) => {
|
||||
item.dataset.index = index;
|
||||
item.classList.toggle('active', index === this.currentIndex);
|
||||
});
|
||||
}
|
||||
|
||||
savePreferences() {
|
||||
localStorage.setItem('playlistPreferences', JSON.stringify({
|
||||
shuffle: this.options.shuffle,
|
||||
loop: this.options.loop,
|
||||
autoplay: this.options.autoplay
|
||||
}));
|
||||
}
|
||||
|
||||
loadPreferences() {
|
||||
const prefs = localStorage.getItem('playlistPreferences');
|
||||
if (prefs) {
|
||||
const parsed = JSON.parse(prefs);
|
||||
this.options = { ...this.options, ...parsed };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for global use
|
||||
window.PlaylistEnhancer = PlaylistEnhancer;
|
||||
Reference in New Issue
Block a user