Menu

Examples

Coming Soon...

This page is a work in progress.

PlayerWidget

Waiting for audio...
00:00
00:00
<script lang="ts">
  import {
    LoadingSpinner,
    Pause,
    Play,
    SkipBack,
    SkipForward,
  } from '$src/components/icon';
  import { AudioPlayer, user_preferences } from 'svelte-podcast';

  export let skip_back = 30;
  export let skip_forward = 10;
  export let playback_rate_values = [1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4];
</script>

<svelte:head>
  <style>
    :root {
      --svpod-surface-darker: rgb(0, 0, 0);
      --svpod-surface-base: rgb(40, 40, 40);
      --svpod-surface-lighter: rgb(60, 60, 60);

      --svpod-content-darker: rgb(150, 150, 150);
      --svpod-content-base: rgb(200, 200, 200);
      --svpod-content-lighter: rgb(255, 255, 255);

      --svpod-accent-darker: rgb(75, 15, 0);
      --svpod-accent-base: rgb(180, 40, 0);
      --svpod-accent-lighter: rgb(255, 55, 25);
    }
  </style>
</svelte:head>

<AudioPlayer
  let:PlayerProgress
  let:skip_by
  let:play
  let:preference
  let:attributes
>
  <div
    class="svpod-container {$$restProps.class}"
    data-loaded={attributes.is_loaded ? 'true' : 'false'}
  >
    <!-- Player Action: Skip back -->
    <button
      on:click={() => skip_by(skip_back, 'backward')}
      class="svpod-skip"
      type="button"
      aria-label="Skip back {skip_back} seconds"
    >
      <div class="svpod-skip-label">
        <span>
          {skip_back}
        </span>
      </div>
      <div class="svpod-skip-icon">
        <SkipBack />
      </div>
    </button>

    <!-- Player Action: Toggle play / pause -->
    {#if attributes.is_loaded}
      <button
        on:click={() => play('toggle')}
        class="svpod-toggle"
        type="button"
        aria-label={attributes.is_paused ? 'Play' : 'Pause'}
      >
        <svelte:component this={attributes.is_paused ? Play : Pause} />
      </button>
    {:else}
      <div class="svpod-toggle text-white">
        <LoadingSpinner />
        <span class="svpod--a11y">Waiting for audio...</span>
      </div>
    {/if}

    <!-- Player Action: Skip forward -->
    <button
      on:click={() => skip_by(skip_forward, 'forward')}
      class="svpod-skip"
      type="button"
      aria-label="Skip forward {skip_forward} seconds"
    >
      <div class="svpod-skip-label">
        <span>
          {skip_forward}
        </span>
      </div>
      <div class="svpod-skip-icon">
        <SkipForward />
      </div>
    </button>

    <!-- Player Data: Current timestamp -->
    <div class="svpod-timestamp" class:active={!attributes.is_paused}>
      <span style="width:{attributes.timestamp_end.length}ch">
        {attributes.timestamp_current}
      </span>
    </div>

    <!-- Player Component: Timeline -->
    <div class="svpod--timeline">
      <PlayerProgress />
    </div>

    <!-- Player Data: Total duration timestamp -->
    <div class="svpod-timestamp">
      <span style="width:{attributes.timestamp_end.length}ch">
        {attributes.timestamp_end}
      </span>
    </div>

    <!-- Player Action: Select playback speed -->

    <select
      class="svpod-playback"
      value={$user_preferences.playback_rate}
      on:change={(e) => {
        const value = parseFloat(e.currentTarget.value);
        preference.set_playback_rate(value);
      }}
    >
      {#each playback_rate_values as value}
        <option {value}>
          {#if Number.isInteger(value)}
            {value}.0
          {:else}
            {value}
          {/if}
        </option>
      {/each}
    </select>
  </div>
</AudioPlayer>

<style lang="postcss">
  * {
    /* css-reset */
    text-transform: none;
    font-style: normal;
    text-indent: 0;
    text-shadow: none;
    text-align: left;
    margin: 0;
    padding: 0;
  }
  .svpod--a11y {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .svpod-container {
    --size: 48px;
    --column: var(--size);
    --row: var(--size);
    --text: calc(var(--size) / 3);

    --padding: 2px;
    --radius: 12px;
    --inner-radius: calc(var(--radius) - var(--padding));

    --bg: var(--svpod-surface-darker);
    --fg: var(--svpod-content-base);

    display: grid;
    grid-template-columns:
      var(--column)
      var(--column)
      var(--column)
      max-content
      auto
      max-content
      var(--column);
    grid-template-rows: var(--column);
    user-select: none;
    background-color: var(--bg);

    padding: var(--padding);
    gap: var(--padding);
    border-radius: var(--radius);
  }

  button,
  select {
    background-color: var(--bg);
    color: var(--fg);
    border-radius: var(--inner-radius);
    overflow: hidden;
    font-size: 0.8em;
    font-weight: 500;

    &:hover {
      --fg: var(--svpod-content-lighter);
      --bg: var(--svpod-surface-base);
    }

    &:focus {
      outline: 2px solid var(--svpod-accent-lighter);
      box-shadow: none;
    }
  }

  .svpod-skip {
    position: relative;
    display: grid;
    align-content: stretch;
    justify-content: stretch;

    .svpod-skip-icon {
      position: absolute;
      inset: 4px;
      z-index: 10;
      display: flex;
      align-items: center;
      justify-content: center;

      :global(svg) {
        width: 100%;
        height: 100%;
      }
    }

    .svpod-skip-label {
      width: 100%;
      height: 100%;
      display: grid;
      place-content: center;
      z-index: 30;
      background: linear-gradient(0deg, var(--bg), transparent);

      span {
        line-height: 1em;
      }
    }
  }

  .svpod-toggle {
    --fg: var(--svpod-content-lighter);
    display: grid;
    place-content: center;

    &:hover {
      --fg: var(--svpod-accent-lighter);
      --bg: var(--svpod-accent-darker);
    }
  }

  .svpod-playback {
    appearance: none;
    background-image: none;
    margin: 0;
    text-align: center;
    text-align-last: center;
    padding: 0 0.5em;
    letter-spacing: 0.05em;
    border: none;

    &::before,
    &::after {
      content: none;
    }
  }

  .svpod-timestamp {
    display: grid;
    place-content: center;
    letter-spacing: 0.05em;
    font-size: 0.8em;
    text-align: center;
    padding: 0.25em 1em;
    color: var(--fg);
    background-color: var(--bg);
    border-radius: var(--inner-radius);
    place-self: center;

    span {
      display: inline-block;
      text-align: center;
    }

    &.active {
      --fg: var(--svpod-accent-lighter);
      --bg: var(--svpod-accent-darker);
    }
  }

  .svpod--timeline {
    display: flex;
    align-items: center;
    justify-content: stretch;
    flex-grow: 1;
    flex-shrink: 1;
    font-size: 1em;
    background: none;

    --progress-height: var(--size);

    --progress-fg: var(--svpod-content-base);
    --progress-bg: var(--svpod-surface-base);
    --progress-border: var(--svpod-surface-base);

    --progress-active-fg: var(--svpod-accent-lighter);
    --progress-active-bg: var(--svpod-accent-darker);
    --progress-active-border: var(--svpod-accent-base);
  }
</style>