Skip to main content

Overview

The audio time slice manages playback position tracking for each podcast episode, allowing users to resume from where they left off. Key Features:
  • Persists playback times to localStorage
  • Optional save/disable functionality
  • Individual episode time management
  • Bulk clear operations

State Shape

{
  playbackTimes: {},         // Object mapping episode titles to times (in seconds)
  savePlaybackTime: true     // Whether to save playback times
}

State Structure Example

{
  playbackTimes: {
    "Episode 1 Title": 1234.5,
    "Episode 2 Title": 567.8,
    "Episode 3 Title": 2890.2
  },
  savePlaybackTime: true
}

Initial State

State is loaded from localStorage on initialization:
src/store/slices/audioTimeSlice.js
initialState: {
  playbackTimes: JSON.parse(
    localStorage.getItem("nsnPlaybackTimes") || "{}"
  ),
  savePlaybackTime: JSON.parse(
    localStorage.getItem("nsnSavePlaybackTime") ?? "true"
  )
}

Actions

updatePlaybackTime

Updates the playback time for a specific episode.
action.payload
object
required
title
string
required
The episode title (used as key)
time
number
required
Current playback position in seconds
dispatch(updatePlaybackTime({ 
  title: "Episode Title", 
  time: 1234.5 
}));
Reducer Logic:
src/store/slices/audioTimeSlice.js
updatePlaybackTime: (state, action) => {
  if (!state.savePlaybackTime) return;
  
  const { title, time } = action.payload;
  state.playbackTimes[title] = time;
  localStorage.setItem(
    "nsnPlaybackTimes", 
    JSON.stringify(state.playbackTimes)
  );
}
If savePlaybackTime is false, this action does nothing.
Real Usage in PersistentPlayer:
src/components/PersistentPlayer/PersistentPlayer.jsx
const handleTimeUpdate = (e) => {
  const currentTime = e.target.currentTime;
  const now = Date.now();
  
  // Throttle updates to once per second
  if (now - lastTimeUpdateRef.current >= 1000) {
    dispatch(updatePlaybackTime({ 
      title: currentPodcast.title, 
      time: currentTime 
    }));
    lastTimeUpdateRef.current = now;
  }
};

<AudioPlayer
  onListen={handleTimeUpdate}
  // ...
/>
The app throttles time updates to once per second to avoid excessive localStorage writes.

toggleSavePlaybackTime

Toggles whether playback times should be saved.
dispatch(toggleSavePlaybackTime());
Reducer Logic:
src/store/slices/audioTimeSlice.js
toggleSavePlaybackTime: (state) => {
  state.savePlaybackTime = !state.savePlaybackTime;
  localStorage.setItem(
    "nsnSavePlaybackTime", 
    JSON.stringify(state.savePlaybackTime)
  );
  
  // Clear all times when disabling
  if (!state.savePlaybackTime) {
    state.playbackTimes = {};
    localStorage.removeItem("nsnPlaybackTimes");
  }
}
Disabling this feature clears all saved playback times.
Usage in Settings:
src/components/Settings/Settings.jsx
import { toggleSavePlaybackTime } from '../../store/slices/audioTimeSlice';

const Settings = () => {
  const { savePlaybackTime } = useSelector((state) => state.audioTime);
  
  return (
    <IOSSwitch
      checked={savePlaybackTime}
      onChange={() => dispatch(toggleSavePlaybackTime())}
    />
  );
};

clearPlaybackTimes

Clears all saved playback times.
dispatch(clearPlaybackTimes());
Reducer Logic:
src/store/slices/audioTimeSlice.js
clearPlaybackTimes: (state) => {
  state.playbackTimes = {};
  localStorage.removeItem("nsnPlaybackTimes");
}

removePlaybackTime

Removes the saved playback time for a specific episode.
action.payload
string
required
The episode title whose playback time should be removed
dispatch(removePlaybackTime("Episode Title"));
Reducer Logic:
src/store/slices/audioTimeSlice.js
removePlaybackTime: (state, action) => {
  const episodeId = action.payload;
  delete state.playbackTimes[episodeId];
  localStorage.setItem(
    "nsnPlaybackTimes", 
    JSON.stringify(state.playbackTimes)
  );
}
Usage Example:
src/components/PodcastDetail/PodcastDetail.jsx
const handleRemoveStarted = (song) => {
  showConfirmToast(
    "¿Estás seguro de que quieres eliminar el tiempo de reproducción?",
    () => {
      dispatch(deleteEpisode(song.title));
      dispatch(removePlaybackTime(song.title));
      toast.success("Tiempo de reproducción eliminado");
    }
  );
};

Selectors

Access audio time state in components:
import { useSelector } from 'react-redux';

const MyComponent = () => {
  const { playbackTimes, savePlaybackTime } = useSelector(
    (state) => state.audioTime
  );
  
  // Get playback time for specific episode
  const episodeTime = playbackTimes[episodeTitle] || 0;
};
Real Examples:
src/components/PodcastDetail/PodcastDetail.jsx
const { playbackTimes } = useSelector((state) => state.audioTime);

const playbackTime = playbackTimes[podcast.title] || 0;

const isListened = 
  listenedEpisodes.includes(podcast.title) ||
  (playbackTimes[podcast.title] && playbackTimes[podcast.title] > 0);

Playback Time Persistence

When Times Are Saved

1

During Playback

Time is updated every second during playback via onListen event
2

On Pause

Current position is saved when user pauses
3

On Episode Completion

Time is reset to 0 when episode ends:
const handleEnded = () => {
  dispatch(updatePlaybackTime({ 
    title: currentPodcast.title, 
    time: 0 
  }));
  dispatch(markAsCompleted(currentPodcast.title));
};

When Times Are Cleared

  • User manually removes time for an episode
  • User clears all playback times
  • User disables the save feature
  • Episode is marked as completed

LocalStorage Keys

nsnPlaybackTimes
object
JSON object mapping episode titles to playback positions (in seconds)
{
  "Episode 1": 1234.5,
  "Episode 2": 567.8
}
nsnSavePlaybackTime
boolean
User preference for saving playback times. Defaults to true.

Utility Functions

Format playback times for display:
src/components/PodcastDetail/PodcastDetail.jsx
const formatTime = (seconds) => {
  if (!seconds) return "0:00";
  const minutes = Math.floor(seconds / 60);
  const remainingSeconds = Math.floor(seconds % 60);
  return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
};

// Usage
const playbackTime = playbackTimes[podcast.title] || 0;
const formattedTime = formatTime(playbackTime); // "20:35"

Complete Action Reference

import {
  updatePlaybackTime,      // Update time for an episode
  toggleSavePlaybackTime,  // Toggle save feature
  clearPlaybackTimes,      // Clear all times
  removePlaybackTime       // Remove time for one episode
} from './store/slices/audioTimeSlice';

Performance Considerations

Time updates are throttled to once per second to prevent excessive localStorage writes:
const lastTimeUpdateRef = useRef(0);
const now = Date.now();

if (now - lastTimeUpdateRef.current >= 1000) {
  dispatch(updatePlaybackTime({ title, time }));
  lastTimeUpdateRef.current = now;
}
Times are only saved when savePlaybackTime is true, allowing users to opt out.
Only the episode title and time are stored, minimizing localStorage usage.

Build docs developers (and LLMs) love