diff --git a/src/AudioPlayerStyles.js b/src/AudioPlayerStyles.js index 88c5adb..8df9511 100644 --- a/src/AudioPlayerStyles.js +++ b/src/AudioPlayerStyles.js @@ -1,113 +1,117 @@ const defaultClasses = { - playerHeader: '', - playerContainer: 'w-full mx-auto max-w-7xl', - bibleListContainer: '', - chapterListContainer: 'relative w-full max-w-4xl mx-auto', - bibleListGrid: 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3', + playerHeader: '', + playerContainer: 'w-full mx-auto max-w-7xl', + bibleListContainer: '', + chapterListContainer: 'relative w-full max-w-4xl mx-auto', + bibleListGrid: 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3', - bibleBlockInfoWrap: 'flex flex-row justify-between gap-4 max-w-md w-full mx-auto bg-stone-100 border border-stone-200 mt-12 p-3', - bibleBlock: 'flex flex-col justify-between text-sm text-center md:text-base lg:text-lg font-semibold text-black', - bibleBlockTitleGroup: 'flex-shrink truncate', - bibleBlockVernacular: 'truncate text-sm mb-1', - bibleBlockTitle: 'truncate text-2xl font-semibold', - bibleBlockLanguageGroup: '', - bibleBlockIso: 'text-sm', - bibleBlockButtonGroup: 'flex flex-col justify-between', + bibleBlockInfoWrap: 'flex flex-row justify-between gap-4 max-w-md w-full mx-auto bg-stone-100 border border-stone-200 mt-12 p-3', + bibleBlock: 'flex flex-col justify-between text-sm text-center md:text-base lg:text-lg font-semibold text-black', + bibleBlockTitleGroup: 'flex-shrink truncate', + bibleBlockVernacular: 'truncate text-sm mb-1', + bibleBlockTitle: 'truncate text-2xl font-semibold', + bibleBlockLanguageGroup: '', + bibleBlockIso: 'text-sm', + bibleBlockButtonGroup: 'flex flex-col justify-between', - container: 'audio-player-container mx-auto max-w-7xl w-full', - audio: '', - controlsContainer: 'controls-container flex flex-col', - navRow: 'flex flex-row items-center justify-center gap-4 py-4 bg-stone-100 border border-stone-200 w-full max-w-md mx-auto', - prevBookButton: 'prev-book-button flex justify-center items-center', - prevChapterButton: 'prev-chapter-button flex justify-center items-center', - playPauseButton: 'play-pause-button flex justify-center items-center', - playPauseIcon: 'icon-play', - nextChapterButton: 'next-chapter-button flex justify-center items-center', - nextBookButton: 'next-book-button flex justify-center items-center', - controlRow2: 'grid grid-cols-5 mb-1', - leftControls: 'flex flex-row justify-center items-center col-span-2 space-x-6', + container: 'audio-player-container mx-auto max-w-7xl w-full', + audio: '', + controlsContainer: 'controls-container flex flex-col', + navRow: 'flex flex-row items-center justify-center gap-4 py-4 bg-stone-100 border border-stone-200 w-full max-w-md mx-auto', + prevBookButton: 'prev-book-button flex justify-center items-center', + prevChapterButton: 'prev-chapter-button flex justify-center items-center', + playPauseButton: 'play-pause-button flex justify-center items-center', + playPauseIcon: 'icon-play', + nextChapterButton: 'next-chapter-button flex justify-center items-center', + nextBookButton: 'next-book-button flex justify-center items-center', + controlRow2: 'grid grid-cols-5 mb-1', + leftControls: 'flex flex-row justify-center items-center col-span-2 space-x-6', - playbackRate: { - wrapper: 'flex mx-auto w-full max-w-md justify-center px-8 py-2 bg-stone-100 text-xs', - display: 'mx-1 text-stone-400 text-sm', - increase: 'w-6 h-6 border-2 rounded-2xl text-stone-500', - decrease: 'w-6 h-6 border-2 rounded-2xl text-stone-500', - disabled: 'opacity-50' - }, + playbackRate: { + wrapper: 'flex mx-auto w-full max-w-md justify-center px-8 py-2 bg-stone-100 text-xs', + display: 'mx-1 text-stone-400 text-sm', + increase: 'w-6 h-6 border-2 rounded-2xl text-stone-500', + decrease: 'w-6 h-6 border-2 rounded-2xl text-stone-500', + disabled: 'opacity-50' + }, - rightControls: 'flex flex-row justify-center items-center col-span-2 space-x-6', - playSpeedControl: 'flex flex-row justify-center items-center text-xs', - decrementIcon: 'size-6', - incrementIcon: 'size-6', - decreaseSpeedButton: 'w-6 h-6 border-2 rounded-2xl hover:border-blue-600 dark:hover:border-white text-stone-500 dark:hover:text-white', - increaseSpeedButton: 'w-6 h-6 border-2 rounded-2xl hover:border-blue-600 dark:hover:border-white text-stone-500 dark:hover:text-white', - prevSkipButton: 'flex justify-center items-center text-stone-400 hover:text-blue-600 dark:text-white dark:hover:text-blue-400', - nextSkipButton: 'flex justify-center items-center text-stone-400 hover:text-blue-600 dark:text-white dark:hover:text-blue-400', + rightControls: 'flex flex-row justify-center items-center col-span-2 space-x-6', + playSpeedControl: 'flex flex-row justify-center items-center text-xs', + decrementIcon: 'size-6', + incrementIcon: 'size-6', + decreaseSpeedButton: 'w-6 h-6 border-2 rounded-2xl hover:border-blue-600 dark:hover:border-white text-stone-500 dark:hover:text-white', + increaseSpeedButton: 'w-6 h-6 border-2 rounded-2xl hover:border-blue-600 dark:hover:border-white text-stone-500 dark:hover:text-white', + prevSkipButton: 'flex justify-center items-center text-stone-400 hover:text-blue-600 dark:text-white dark:hover:text-blue-400', + nextSkipButton: 'flex justify-center items-center text-stone-400 hover:text-blue-600 dark:text-white dark:hover:text-blue-400', - progress: { - container: 'flex mx-auto w-full max-w-md justify-around px-1 pt-4 bg-stone-100 text-xs', - wrapper: 'mx-auto min-w-80 mx-3 w-3/4 sm:w-5/6 h-3 rounded-lg relative border bg-gradient-to-br shadow-inner border-stone-300 from-white to-stone-200 dark:from-neutral-50 dark:to-stone-400 border-stone-500', - barInner: 'h-3 w-0 bg-blue-600 rounded-md', - circleTip: 'absolute top-1/2 transform -translate-y-1/2 -translate-x-1/2 w-5 h-5 bg-blue-600 rounded-full border border-blue-500', - currentTimeDisplay: 'text-black', - durationDisplay: 'text-black', - tick: 'w-[1.5px] absolute top-0 h-full bg-stone-400' - }, - - mediaPlayerWrap: 'w-full flex flex-col justify-center', - mediaPlayerHeader: '', - mediaPlayerBody: '', - mediaPlayerNavRow: 'grid grid-cols-5 px-2 text-stone-700 dark:text-stone-300 divide-x divide-stone-500 dark:divide-stone-600', - bibleListNavButton: '', - bookListNavButton: '', - chapterListNavButton: '', + progress: { + container: 'flex flex-col mx-auto w-full max-w-md justify-around px-1 pt-4 bg-stone-100 text-xs', + barContainer: 'flex flex-row justify-between', + barWrapper: 'mx-auto min-w-80 mx-3 w-3/4 sm:w-5/6 h-3 rounded-lg relative border bg-gradient-to-br shadow-inner border-stone-300 from-white to-stone-200 dark:from-neutral-50 dark:to-stone-400 border-stone-500', + barInner: 'h-3 w-0 bg-blue-600 rounded-md', + circleTip: 'absolute top-1/2 transform -translate-y-1/2 -translate-x-1/2 w-5 h-5 bg-blue-600 rounded-full border border-blue-500', + currentTimeDisplay: 'text-black', + durationDisplay: 'text-black', + timestamps: 'flex w-full h-4 relative block my-4', + tick: 'absolute top-0 h-full bg-stone-400', + tickWrapper: 'group relative h-full', + tickLabel: 'verse-label hidden group-hover:block absolute left-1/2 -translate-x-1/2 top-full mt-1 px-2 py-1 text-xs bg-gray-200 text-gray-900 rounded-lg shadow border whitespace-nowrap' + }, + + mediaPlayerWrap: 'w-full flex flex-col justify-center', + mediaPlayerHeader: '', + mediaPlayerBody: '', + mediaPlayerNavRow: 'grid grid-cols-5 px-2 text-stone-700 dark:text-stone-300 divide-x divide-stone-500 dark:divide-stone-600', + bibleListNavButton: '', + bookListNavButton: '', + chapterListNavButton: '', - volumeRow: 'flex mx-auto w-full max-w-md justify-center items-center px-8 bg-stone-100 text-xs', - volumeControl: 'volume-control w-full', - volumeInput: 'w-full', - volumeLabel: 'flex flex-row pl-2 mx-4 w-1/2', + volumeRow: 'flex mx-auto w-full max-w-md justify-center items-center px-8 bg-stone-100 text-xs', + volumeControl: 'volume-control w-full', + volumeInput: 'w-full', + volumeLabel: 'flex flex-row pl-2 mx-4 w-1/2', - selectBookChapterWrap: 'relative bg-stone-100 flex flex-row gap-2 w-full max-w-md mx-auto', - selectBook: 'inline-flex items-center gap-x-1.5 bg-stone-100 hover:bg-stone-200 px-3 py-2 w-full', - selectChapter: 'inline-flex items-center text-center bg-stone-100 w-12 hover:bg-stone-200 border-none appearance-none', - selectVerseSeparator: 'inline-flex items-center bg-stone-100', - selectVerse: "inline-flex items-center text-center bg-stone-100 w-12 hover:bg-stone-200 border-none appearance-none", + selectBookChapterWrap: 'relative bg-stone-100 flex flex-row gap-2 w-full max-w-md mx-auto', + selectBook: 'inline-flex items-center gap-x-1.5 bg-stone-100 hover:bg-stone-200 px-3 py-2 w-full', + selectChapter: 'inline-flex items-center text-center bg-stone-100 w-12 hover:bg-stone-200 border-none appearance-none', + selectVerseSeparator: 'inline-flex items-center bg-stone-100', + selectVerse: "inline-flex items-center text-center bg-stone-100 w-12 hover:bg-stone-200 border-none appearance-none", - bibleButton: { - wrapper: 'relative bg-stone-100 border border-stone-200 rounded min-h-20', - button: 'flex flex-row bg-stone-100 h-full w-full hover:bg-stone-200', - languageWrap: 'py-1 bg-stone-200 h-full w-24 flex flex-col justify-center items-center', - language: 'text-sm font-medium text-stone-900', - iso: 'truncate font-mono mt-1 text-sm text-stone-500', - titleWrap: 'py-1 h-full w-full flex flex-col justify-center items-center', - title: 'line-clamp-2 text-center text-sm font-medium text-stone-900', - vernacular: 'text-sm text-stone-500 max-w-64', - download: 'absolute text-stone-500 right-0 bottom-0 rounded-tl size-8 flex justify-center items-center bg-stone-200 hover:bg-stone-300' - }, + bibleButton: { + wrapper: 'relative bg-stone-100 border border-stone-200 rounded min-h-20', + button: 'flex flex-row bg-stone-100 h-full w-full hover:bg-stone-200', + languageWrap: 'py-1 bg-stone-200 h-full w-24 flex flex-col justify-center items-center', + language: 'text-sm font-medium text-stone-900', + iso: 'truncate font-mono mt-1 text-sm text-stone-500', + titleWrap: 'py-1 h-full w-full flex flex-col justify-center items-center', + title: 'line-clamp-2 text-center text-sm font-medium text-stone-900', + vernacular: 'text-sm text-stone-500 max-w-64', + download: 'absolute text-stone-500 right-0 bottom-0 rounded-tl size-8 flex justify-center items-center bg-stone-200 hover:bg-stone-300' + }, - bibleDownloadDialog: { - wrapper: 'p-4 shadow-lg text-center', - button_download: 'px-5 py-2 mx-1 bg-blue-500 text-white rounded cursor-pointer', - cancel: 'px-5 py-2 mx-1 bg-red-600 text-white rounded cursor-pointer', - audio_copyright: 'block text-stone-600', - text_copyright: 'block text-stone-600 text-sm', - }, + bibleDownloadDialog: { + wrapper: 'p-4 shadow-lg text-center', + button_download: 'px-5 py-2 mx-1 bg-blue-500 text-white rounded cursor-pointer', + cancel: 'px-5 py-2 mx-1 bg-red-600 text-white rounded cursor-pointer', + audio_copyright: 'block text-stone-600', + text_copyright: 'block text-stone-600 text-sm', + }, - sleepTimerButton: 'sleep-timer-button group block py-1.5 px-1', - sleepTimerDuration: 'text-xs pl-1 pt-2 inline-block timer-display', - sleepTimerWrap: 'flex flex-row justify-center align-start text-stone-400 hover:text-blue-600', - searchWrapper: 'mx-auto flex items-center w-1/3 my-3', - searchInput: 'px-4 py-2 w-full max-w-7xl mx-auto bg-white border border-stone-200', - - searchInputContainer: 'flex w-full max-w-xl mx-auto my-4', - bookListContainer: 'w-full', - bookListGrid: 'grid grid-cols-3 md:grid-cols-4 gap-2', - bookListButton: 'bg-stone-100 border rounded md:rounded-lg', - bookListButtonActive: 'bg-stone-100 border border-stone-300', - bookListTitle: 'font-medium text-sm lg:text-base', - bookListId: 'w-full flex justify-end opacity-60 text-sm mr-8', - chapterButton: 'bg-stone-100/90 w-12 h-12 m-2 border border-stone-700 rounded text-lg hover:bg-stone-300', - chapterButtonActive: 'bg-stone-500 text-stone-100' + sleepTimerButton: 'sleep-timer-button group block py-1.5 px-1', + sleepTimerDuration: 'text-xs pl-1 pt-2 inline-block timer-display', + sleepTimerWrap: 'flex flex-row justify-center align-start text-stone-400 hover:text-blue-600', + searchWrapper: 'mx-auto flex items-center w-1/3 my-3', + searchInput: 'px-4 py-2 w-full max-w-7xl mx-auto bg-white border border-stone-200', + + searchInputContainer: 'flex w-full max-w-xl mx-auto my-4', + bookListContainer: 'w-full', + bookListGrid: 'grid grid-cols-3 md:grid-cols-4 gap-2', + bookListButton: 'bg-stone-100 border rounded md:rounded-lg', + bookListButtonActive: 'bg-stone-100 border border-stone-300', + bookListTitle: 'font-medium text-sm lg:text-base', + bookListId: 'w-full flex justify-end opacity-60 text-sm mr-8', + chapterButton: 'bg-stone-100/90 w-12 h-12 m-2 border border-stone-700 rounded text-lg hover:bg-stone-300', + chapterButtonActive: 'bg-stone-500 text-stone-100' }; /** @@ -116,54 +120,54 @@ const defaultClasses = { * Keys without an underscore will simply merge by replacing the existing default with the custom class. * * @param {Object} customClasses - An object containing custom class definitions. Keys starting with - * an underscore indicate classes that should overwrite default classes. + * an underscore indicate classes that should overwrite default classes. * @returns {Object} An object containing the merged classes. */ export const mergeClasses = (customClasses = {}) => { - const mergedClasses = { ...defaultClasses }; - for (const key of Object.keys(customClasses)) { - if (key.startsWith('_')) { - mergedClasses[key.substring(1)] = customClasses[key]; - } else { - mergedClasses[key] = customClasses[key]; - } + const mergedClasses = { ...defaultClasses }; + for (const key of Object.keys(customClasses)) { + if (key.startsWith('_')) { + mergedClasses[key.substring(1)] = customClasses[key]; + } else { + mergedClasses[key] = customClasses[key]; } + } - return mergedClasses; + return mergedClasses; }; const defaultIcons = { - speedSlow: ``, - speedFast: ``, - sleepTimer: ``, - chapters: ``, - bibles: ``, - books: ``, - prevBook: ``, - nextBook: ``, - prevChapter: ``, - nextChapter: ``, - prevSkip: ``, - nextSkip: ``, - play: ``, - pause: ``, - volumeIcon: ``, - download: `` + speedSlow: ``, + speedFast: ``, + sleepTimer: ``, + chapters: ``, + bibles: ``, + books: ``, + prevBook: ``, + nextBook: ``, + prevChapter: ``, + nextChapter: ``, + prevSkip: ``, + nextSkip: ``, + play: ``, + pause: ``, + volumeIcon: ``, + download: `` } export const mergeIcons = (customIcons = {}) => { - const mergedIcons = { ...defaultIcons }; - for (const key of Object.keys(customIcons)) { - mergedIcons[key] = customIcons[key]; - } - return mergedIcons; + const mergedIcons = { ...defaultIcons }; + for (const key of Object.keys(customIcons)) { + mergedIcons[key] = customIcons[key]; + } + return mergedIcons; }; const defaultArt = {} export const mergeArt = (customArt = {}) => { - const mergedArt = { ...defaultArt }; - for (const key of Object.keys(customArt)) { - mergedArt[key] = customArt[key]; - } - return mergedArt; + const mergedArt = { ...defaultArt }; + for (const key of Object.keys(customArt)) { + mergedArt[key] = customArt[key]; + } + return mergedArt; }; \ No newline at end of file diff --git a/src/MediaProgressBar.js b/src/MediaProgressBar.js index 93cb562..576536f 100644 --- a/src/MediaProgressBar.js +++ b/src/MediaProgressBar.js @@ -14,8 +14,10 @@ export function createProgressBar(ctx) { textContent: formatTime(ctx.audio.currentTime) }); - const progressWrapper = elem('div', { id: 'progress-wrapper', className: ctx.class?.progress?.wrapper }); + const progressBarContainer = elem('div', { id: 'progress-barContainer', className: ctx.class?.progress?.barContainer }); + const progressWrapper = elem('div', { id: 'progress-barWrapper', className: ctx.class?.progress?.barWrapper }); const progressBarInner = elem('div', { id: 'progress-bar', className: ctx.class?.progress?.barInner, style: { width: '0%' } }); + const timestampWrapper = elem('div', {id: 'timestamps-wrapper', className:ctx.class?.progress?.timestamps}) const circleTip = elem('div', { @@ -65,26 +67,38 @@ export function createProgressBar(ctx) { ctx.audio.addEventListener('loadedmetadata', () => { timestampWrapper.innerHTML = ''; durationDisplay.textContent = formatTime(ctx.audio.duration); - + if (ctx.currentChapter.timestamps && Array.isArray(ctx.currentChapter.timestamps)) { + const sortedTimestamps = ctx.currentChapter.timestamps.map(parseTimestampToSeconds).filter(ts => ts >= 0 && ts <= ctx.audio.duration).sort((a, b) => a - b); + const margin = .25; + + for (let i = 0; i < sortedTimestamps.length - 1; i++) { + const current = sortedTimestamps[i]; + const next = sortedTimestamps[i + 1]; + const gapPercent = ((next - current) / ctx.audio.duration) * 100; + const adjustedWidth = Math.max(0, gapPercent - margin); + const adjustedLeft = ((current / ctx.audio.duration) * 100) + margin / 2; + const tickWrapper = elem('div',{className: ctx.class.tickWrapper,style:{position: 'absolute', left: `${adjustedLeft}%`, width: `${adjustedWidth}%`}}); + + const tick = elem('div', {className: `progress-tick ${ctx?.class?.progress?.tick} w-full h-full`}); + tick.addEventListener('click', () => {ctx.audio.currentTime = current;}); + + const verseLabel = elem('div', {className: ctx.class.progress.tickLabel}); + verseLabel.textContent = `${i + 1}`; + + tickWrapper.appendChild(tick); + tickWrapper.appendChild(verseLabel); + timestampWrapper.appendChild(tickWrapper); + } + } + }); + - ctx.currentChapter.timestamps.forEach(tsStr => { - const tsSeconds = parseTimestampToSeconds(tsStr); - if (tsSeconds <= ctx.audio.duration && tsSeconds >= 0) { - const tickPercent = (tsSeconds / ctx.audio.duration) * 100; - const tick = elem('div', { - className: `progress-tick ${ctx?.class?.progress?.tick}`, - style: { left: `${tickPercent}%` } - }); - timestampWrapper.appendChild(tick); - } - }); - } - }); - - progressWrapper.append(timestampWrapper) - progressContainer.append(currentTimeDisplay, progressWrapper, durationDisplay); + progressBarContainer.append(currentTimeDisplay) + progressBarContainer.append(progressWrapper) + progressBarContainer.append(durationDisplay) + progressContainer.append(progressBarContainer, timestampWrapper); return progressContainer; }