/* v1.playback-should-work-better-on-mobile-now, get a blank slate at:
   https://pirahxcx.neocities.org/pira-player/pira-player.js

   Check https://pirahxcx.neocities.org/pira-player for more info
   Send suggestions, bug reports and nudes to pirahxcx@proton.me  */

class PiraPlayer extends HTMLElement {
  constructor() {
    super();

    ///////////////////////////////////////////////
    //                                           //
    //               CUSTOMIZATION               //
    //                                           //
    ///////////////////////////////////////////////


    // ========================== //
    //     YOUR PLAYLIST HERE     //
    // ========================== //
                                                                                      
    this.playlist = {
      playlistURL: "media/Music/Playlist.json", // Check https://pirahxcx.neocities.org/pira-player
      shuffle: true,                                                                // to learn how to make one
    };

    this.dummyfile = "https://www.dropbox.com/scl/fi/0wkecpwwh2qrjjnedz7do/dummy.mp3?rlkey=eswotymil8footlaao5b9epsw&st=bw9j1gv4&raw=1";
    // The dummy is a silent playback to keep audio alive on mobile, here I'm linking my own 1h of silence file created in ffmpeg
    // no problem hotlinking mine, but you can also create and host your own, if you want do it with ffmpeg try the command-line (3600 = 1h)
    // ffmpeg -f lavfi -i anullsrc=r=8000:cl=mono -t 3600 -c:a libmp3lame -b:a 4k dummy.mp3

    // ==================== //
    //     CUSTOM FONTS     //
    // ==================== //

    this.customfonts = [
      {
        name: "Roboto Mono",                                                        // If you are going to use custom fonts
        url: "https://pirahxcx.neocities.org/assets/fonts/RobotoMono-Regular.ttf",  // name and place them here
      },                                                                            // Add as many as you wish
      {
        name: "Moms Typerwritter",
        url: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/Mom%C2%ABt___.ttf",
      },
      {
        name: "VT323",
        url: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/VT323-Regular.ttf",
      },
    ];


    // ============================ //
    //     PLAYER TITLE OPTIONS     //
    // ============================ //

    /* You can set either text or image
       Image will replace text
       Their height is capped at 48px   */

    this.playertitle = { 
      text: "",            // leave these empty if you
      image: "",           // want them to rotate
      animation: "",
      hovertext: "",
      hoverimg: "",        // Swap image on hover
      hoverbrightness: "", // Add hover effects if you wish 
      hoverhue: "",
    };

    this.titlesettings = {
      randomtitle: false,          // Title will be picked at random
      randomtitleanimation: false, // Title animation will be picked at random
      titlerotation: true,         // Titles will change with time
      titletimer: 10,              // Set how many seconds before change
    };

    this.titlepool = [             // Create as many items you wish
      {
        text: "",
        image: "https://pirahxcx.neocities.org/pira-player/asset-gallery/title-akira.avif",
        animation: "blink 5s infinite linear",
        hovertext: "",
        hoverimg: "",
        hoverbrightness: "",
        hoverhue: "",
      },
      {
        text: "",
        image: "https://pirahxcx.neocities.org/pira-player/asset-gallery/title-evangelion.avif",
        animation: "blink 5s infinite linear",
        hovertext: "",
        hoverimg: "",
        hoverbrightness: "1.5",
        hoverhue: "",
      },
      {
        text: "pira player",
        image: "",
        animation: "shake 1s infinite linear",
        hovertext: "pÍ‘Ì‰Ì£ÌºÌ§iÌ„Ì†Ì†ÌÌ½rÍ‡Ì¢aÍƒÍ’Í‚Ì²Ìµ pÍ‘ÌÌ½ÍlÍ¬ÍÍ‘Ì“Ì¦ÌºÍ™Í…Ì§Ì¨aÍ¤Ì«Í–Í€yÍ…ÍˆeÍ–Í‡Í™Ì´ÌµrÍ†ÌŸÌ´Í˜Íœ",
        hoverimg: "",
        hoverbrightness: "",
        hoverhue: "",
      },
    ];

    /* If you are using text for title
       you can configure the CSS here  */

    this.titlecss = {
      fontfamily: "Moms Typerwritter",  // Leave all fields empty 
      fontsize: "26px",                 // to use the pool
      color: "white",
      outlinecolor: "black",
      outlinesize: "2px",
      textshadowcolor: "purple",        // These are for
      blurradius: "6px",                // the hover effect
    };

    this.titlecsssettings = {  // Here's if you want to use multiple styles
      randomtitlecss: false,   // Style will be picked at random
      csstitlerotation: false, // Style will change with time
      csstitletimer: 10,       // Set how many seconds before change
    };

    this.csstitlepool = [      // Create as many items you wish
      {
        fontfamily: "",
        fontsize: "",
        color: "",
        outlinecolor: "",
        outlinesize: "",
        textshadowcolor: "",
        blurradius: "",
      },
    ];

    /* If you are using img for title
       you can set a hover drop shadow here */

    this.titledropshadow = {
      dropshadowcolor: "",         // Leave empty to use the pool
      dropshadowradius: "", 
    };

    this.titledropshadowsettings = {
      randomdropshadow: false,     // Hover shadow will be picked at random
      dropshadowrotation: true,    // Hover shadow will change with time
      dropshadowtimer: 10,         // Set how many seconds before change
    };

    this.dropshadowpool = [        // Create as many items you wish
      {
        dropshadowcolor: "silver", // Of course you can use color hex codes
        dropshadowradius: "12px",
      },
      {
        dropshadowcolor: "",
        dropshadowradius: "",
      },
      {
        dropshadowcolor: "",
        dropshadowradius: "",
      },
    ];


    // ============================ //
    //     CONTROL ICON OPTIONS     //
    // ============================ //

    /* Icons are defaulted to 24x24px */

    this.icons = {
      play: "",             // Fill these to use fixed icons
      pause: "",            // Leave empty to use the pool
      next: "",
      volumehigh: "",
      volumelow: "",
      volumemute: "",
      hoverplay: "",        // For image swap on hover
      hoverpause: "",
      hovernext: "",
      hovervolumehigh: "",
      hovervolumelow: "",
      hovervolumemute: "",
      brightness: "",       // Add hover effects if you wish
      huechange: "",
      dropshadowcolor: "",
      dropshadowradius: "",
    };

    this.iconsettings = {
      randomicons: false,      // Icon set will be picked at random
      totalrandomicons: false, // Icon sets will be mixed
      iconrotation: true,      // Icon set will change with time
      icontimer: 10,           // Set how many seconds before change
    };

    this.iconpool = [     // Create as many items you wish
      {
        play: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set01-play.avif",
        pause: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set01-pause.avif",
        next: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set01-next.avif",
        volumehigh: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set01-volumehigh.avif",
        volumelow: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set01-volumelow.avif",
        volumemute: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set01-volumemute.avif",
        hoverplay: "",
        hoverpause: "",
        hovernext: "",
        hovervolumehigh: "",
        hovervolumelow: "",
        hovervolumemute: "",
        brightness: "",
        huechange: "",
        dropshadowcolor: "white",
        dropshadowradius: "4px",
      },
      {
        play: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set02-play.avif",
        pause: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set02-pause.avif",
        next: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set02-next.avif",
        volumehigh: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set02-volumehigh.avif",
        volumelow: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set02-volumelow.avif",
        volumemute: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set02-volumemute.avif",
        hoverplay: "",
        hoverpause: "",
        hovernext: "",
        hovervolumehigh: "",
        hovervolumelow: "",
        hovervolumemute: "",
        brightness: "1.5",
        huechange: "",
        dropshadowcolor: "#4488d4",
        dropshadowradius: "2px",
      },
      {
        play: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-play.avif",
        pause: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-pause.avif",
        next: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-next.avif",
        volumehigh: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-volumehigh.avif",
        volumelow: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-volumelow.avif",
        volumemute: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-volumemute.avif",
        hoverplay: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-play.gif",
        hoverpause: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-pause.gif",
        hovernext: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-next.gif",
        hovervolumehigh: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-volumehigh.gif",
        hovervolumelow: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-volumelow.gif",
        hovervolumemute: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/set03-volumemute.gif",
        brightness: "",
        huechange: "",
        dropshadowcolor: "",
        dropshadowradius: "",
      },
    ];

    this.iconrandomization = {
      randomcolor: false,    // This will give random colors to your buttons
      randomangle: false,    // And also random angles, for a cartoony effect

      huejump: 90,           // Choose the hue rotate for buttons, best set to a divisor of 360

      allowzeroangle: false, // Set to false if you want your buttons always tilted
      anglerange: 10,        // A 10 values means angles can go from -10 to 10 degrees

      colorrotation: false,  // Set these to true if you want color and
      anglerotation: false,  // angle changing with time
      colortimer: 10,        // And here you set their timers
      angletimer: 10,
    };


    // ===================== //
    //     SLIDER COLORS     //
    // ===================== //

    /* You can set colors or images for
       the sliders, images replace color,
       the bars will tile, thumb will fill */

    this.slider = {
      barcolor: "",          // Leave these empty to use rotation
      barimg: "",
      fillcolor: "",
      fillimg: "",
      thumbcolor: "",
      thumbborder: "",
      thumbimg: "",
      thumbbrightness: "",   // Add hover effects if you wish
      thumbhue: "",
      thumbshadowcolor: "",
      thumbshadowradius: "",
    };

    this.slidersettings = {
      randomslider: false,   // Slider will be picked at random
      sliderrotation: true,  // Slider will change with time
      slidertimer: 10,       // Set how many seconds before change
    };

    this.sliderpool = [      // Create as many items you wish
      {
        barcolor: "#2F2F2F",
        barimg: "",
        fillcolor: "#ffd33b",
        fillimg: "",
        thumbcolor: "#ab313b",
        thumbborder: "1px silver outset",
        thumbimg: "",
        thumbbrightness: "",
        thumbhue: "",
        thumbshadowcolor: "white",
        thumbshadowradius: "2px",
      },
      {
        barcolor: "#cbcfce",
        barimg: "",
        fillcolor: "#22446a",
        fillimg: "",
        thumbcolor: "",
        thumbborder: "",
        thumbimg: "https://pirahxcx.neocities.org/pira-player/asset-gallery/thumb-ramiel.avif",
        thumbbrightness: "1.5",
        thumbhue: "",
        thumbshadowcolor: "#4488d4",
        thumbshadowradius: "2px",
      },
      {
        barcolor: "#5b5992",
        barimg: "",
        fillcolor: "",
        fillimg: "https://pirahxcx.neocities.org/pira-player/asset-gallery/slider-shock.gif",
        thumbcolor: "",
        thumbborder: "",
        thumbimg: "https://pirahxcx.neocities.org/pira-player/asset-gallery/thumb-coplandos.gif",
        thumbbrightness: "",
        thumbhue: "",
        thumbshadowcolor: "purple",
        thumbshadowradius: "2px",
      },
    ];


    // =========================== //
    //     FILE DISPLAY OPTIONS    //
    // =========================== //

    this.filecolors = {
      fontfamily: "",           // Leave these empty to use rotation
      color: "",
      outlinecolor: "",
      outlinesize: "",
      hovercolor: "",
      scrolltext: false,        // Set to true to apply text scrolling
      typingtext: false,        // Set to true to apply typing effect (don't look good with scrolling)
      backgroundcolor: "",      // If your background makes it difficult to read song 
      backgroundimg: "",        // names, you can set a background directly on them
      backgroundblur: false,    // Set to true if you prefer to just blur the background
      backgroundbluramount: "", // the background, and by how much
    };

    this.filecolorsettings = {
      randomfilecolor: false,   // File name settings will be picked at random
      filecolorrotation: true,  // File name settings will change with time
      filecolortimer: 10,       // Set how many seconds before change
    };

    this.filecolorpool = [      // Create as many items you wish
      {
        fontfamily: "monospace",
        color: "silver",
        outlinecolor: "black",
        outlinesize: "1px",
        hovercolor: "white",
        scrolltext: false,
        typingtext: false,
        backgroundcolor: "",
        backgroundimg: "https://pirahxcx.neocities.org/pira-player/asset-gallery/display-hazardstripes.avif",
        backgroundblur: false,
        backgroundbluramount: "",
      },
      {
        fontfamily: "monospace",
        color: "#875b8c",
        outlinecolor: "#604666",
        outlinesize: "1px",
        hovercolor: "#db98f7",
        scrolltext: true,
        typingtext: false,
        backgroundcolor: "",
        backgroundimg: "",
        backgroundblur: true,
        backgroundbluramount: "",
      },
      {
        fontfamily: "VT323",
        color: "#00A000",
        outlinecolor: "",
        outlinesize: "",
        hovercolor: "#00ff00",
        scrolltext: false,
        typingtext: true,
        backgroundcolor: "black",
        backgroundimg: "",
        backgroundblur: false,
        backgroundbluramount: "4px",
      },
    ];


    // ========================= //
    //     CUSTOM BACKGROUND     //
    // ========================= //

    /* You can set either color or image
       as background, default size is 400x148px
       Color is actually a free field, so if you
       want gradients and other effects, or even
       edit better url("") you can do on it      */

    this.background = {
      color: "",   // Leave empty to use the pool
      image: "",
      tile: false,
    };

    this.backgroundsettings = {
      randombackground: false,  // Background will be picked at random
      backgroundrotation: true, // Background will change with time
      backgroundtimer: 10,      // Set how many seconds before change
    };

    this.backgroundpool = [     // Create as many items you wish
      {
        color: "#2F2F2F",
        image: "",
        tile: false,
      },
      {
        color: "",
        image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/evangelionbackground.gif",
        tile: false,
      },
      {
        color: "",
        image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/lainbackground.gif",
        tile: false,
      },
    ];

    /* Depending on your background it may become difficult to
       read the song names, here you can set another background
       only for the main UI, or just set a blur                 */

    this.controlbackground = {
      color: "",      // Leave empty to use the pool
      image: "",
      tile: false,
      blur: false,    // You can set just blur if you wish, or add it
      bluramount: "", // Set how blurry you want
    };

    this.controlbackgroundsettings = {
      randomcontrolbackground: false,  // UI background will be picked at random
      controlbackgroundrotation: true, // UI background will change with time
      controlbackgroundtimer: 10,      // Set how many seconds before change
    };

    this.controlbackgroundpool = [     // Create as many items you wish
      {
        color: "",
        image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/akirabackground.avif",
        tile: false,
        blur: false,
        bluramount: "",
      },
      {
        color: "",
        image: "",
        tile: false,
        blur: false,
        bluramount: "",
      },
      {
        color: "",
        image: "",
        tile: false,
        blur: true,
        bluramount: "6px",
      },
    ];


    // ===================== //
    //     DECOR OPTIONS     //
    // ===================== //

    /* 4 slots, width defaulted to 80px
       but you can set manually for each img */

    this.decorations = {  // Any slot that you fill here will
      topleft: {          // become fixed - you can still
        image: "",        // rotate and randomize on the others
        animation: "",    // Animation pool at the end of the section
        width: "",
      },
      bottomleft: {
        image: "",
        animation: "",
        width: "",
      },
      topright: {
        image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/demologo.avif",
        animation: "",
        width: "40px",
      },
      bottomright: {
        image: "",
        animation: "",
        width: "",
      },
    };

    this.decorrotations = { // If you want rotation,
      topleft: {            // each slot will pick from their own pool
        enabled: true,      // Control what slots are enabled
        random: false,      // Pick their pool in order or not
        timer: 10,          // Set how many seconds before change
      },
      bottomleft: {
        enabled: true,
        random: false,
        timer: 10,
      },
      topright: {
        enabled: false,
        random: false,
        timer: 10,
      },
      bottomright: {
        enabled: true,
        random: false,
        timer: 10,
      },
    };

    this.randomdecorations = {
      totalrandom: false,       // If true it will pick images from the random pool
      randomslots: 2,           // You enable slots individually, you can control how many are filled at once
      randomanimations: false,  // Apply random animations to image
      mirrorrandomright: false, // If picking from the random pool, you can flip horizontally the ones that go
    };                          // to the right slots

    this.decorpools = {         // Create as many items you wish
      topleft: [
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/akira02.avif",
          animation: "spin 8s infinite linear",
          width: "",
        },
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/evangelion02.avif",
          animation: "pulse 2s infinite ease-in-out",
          width: "",
        },
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/lain02.gif",
          animation: "fade 2s infinite ease-in-out",
          width: "",
        },
      ],
      bottomleft: [
        {
          image: "",
          animation: "",
          width: "",
        },
        {
          image: "",
          animation: "",
          width: "",
        },
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/fauuxhueedit.gif",
          animation: "",
          width: "",
        },
      ],
      topright: [
        {
          image: "",
          animation: "",
          width: "",
        },
      ],
      bottomright: [
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/akira01.avif",
          animation: "jiggle 2s infinite ease-in-out",
          width: "100px",
        },
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/evangelion01.avif",
          animation: "blink 2s infinite linear",
          width: "100px",
        },
        {
          image: "https://pirahxcx.neocities.org/pira-player/demo-generic-assets/lain01.gif",
          animation: "ghost 4s infinite ease-in-out",
          width: "100px",
        },
      ],
      random: [
        {
          image: "",
          animation: "",
          width: "",
        },
      ],
    };


    // ===================== //
    //     CUSTOM CURSOR     //
    // ===================== //

    this.cursors = {
      default: "", // Leave empty to use the pool
      pointer: "",
    };

    this.cursorsettings = {
      randomcursor: false,   // Cursors will be picked at random
      cursorrotation: true,  // Cursors will change with time 
      cursortimer: 10,       // Set how many seconds before change
    };

    this.cursorpool = [      // Create as many items you wish
      {
        default: "https://pirahxcx.neocities.org/pira-player/asset-gallery/cursor-kaneda01.avif",
        pointer: "https://pirahxcx.neocities.org/pira-player/asset-gallery/cursor-kaneda02.avif",
      },
      {
        default: "https://pirahxcx.neocities.org/pira-player/asset-gallery/cursor-sachiel01.avif",
        pointer: "https://pirahxcx.neocities.org/pira-player/asset-gallery/cursor-sachiel02.avif",
      },
      {
        default: "https://pirahxcx.neocities.org/pira-player/asset-gallery/cursor-lain01.avif",
        pointer: "https://pirahxcx.neocities.org/pira-player/asset-gallery/cursor-lain02.avif",
      },
    ];


    // ====================== //
    //     BORDER CONTROL     //
    // ====================== //

    this.bordercontrol = {    // Change the border style here
      outerborder: "2px outset silver",
      outerborderradius: "20px",

      playerborder: "2px outset silver",
      playerborderradius: "20px",

      displayborder: "1px inset silver",
      displayborderradius: "20px",
    };

    // ============= //
    //     SCALE     //
    // ============= //

    /* I designed this on 1920x1080 but with
    150% scaling, so if you think the player is
    too small - or even too big, change it here */

    this.hostsettings = {
      scale: "1",
    };

    // ================== //
    //     ANIMATIONS     //
    // ================== //

    this.animations = [
      "blink 2s infinite linear",       // You can change duration, timing, iteration 
      "fade 2s infinite ease-in-out",   // These here are just some default values to use
      "ghost 4s infinite ease-in-out",
      "jiggle 2s infinite linear",
      "pulse 2s infinite ease-in-out",
      "shake 1s infinite linear",
      "spin 4s infinite linear",
    ];


    /////////////////////////////////////////////
    //                                         //
    //               DON'T TOUCH               //
    //   UNLESS YOU KNOW WHAT YOU ARE DOING    //
    //                                         //
    /////////////////////////////////////////////

    this.attachShadow({ mode: "open" });

    this.shadowRoot.innerHTML = `
      <style>
        :host, * {
          box-sizing: border-box;
          margin: 0;
        }

        :host {
          position: relative;
          display: flex;
          width: 400px;
          height: 148px;
          padding: 12px !important;
          border: var(--outer-border, 2px outset silver);
          border-radius: var(--outer-border-radius, 20px);
          overflow: hidden;
        }

        button {
          appearance: none;
          -webkit-appearance: none;
          background: none;
          border: none;
          padding: 0;
          max-width: 24px;
          max-height: 24px;
          display: flex;
          align-items: center;
          justify-content: center;
        }

        button:focus-visible, input:focus-visible {
          outline: auto;
        }

        #player {
          display: flex;
          flex-direction: column;
          align-items: center;
          width: 100%;
          max-height: 120px;
          padding: 6px;
          border: var(--player-border, 2px outset silver);
          border-radius: var(--player-border-radius, 20px);
          backdrop-filter: var(--control-background-blur, none);
          -webkit-backdrop-filter: var(--control-background-blur, none);
        }

        a, button, input {
          cursor: pointer;
        }

        a {
          text-decoration: none;
        }

        .player-title {
          display: flex;
          margin: auto;
        }

        .player-title a {
          text-shadow:
            var(--title-outline-size, 0px) var(--title-outline-size, 0px) 0 var(--title-outline-color, transparent),
            calc(var(--title-outline-size, 0px) * -1) var(--title-outline-size, 0px) 0 var(--title-outline-color, transparent),
            var(--title-outline-size, 0px) calc(var(--title-outline-size, 0px) * -1) 0 var(--title-outline-color, transparent),
            calc(var(--title-outline-size, 0px) * -1) calc(var(--title-outline-size, 0px) * -1) 0 var(--title-outline-color, transparent),
            var(--title-outline-size, 0px) 0 0 var(--title-outline-color, transparent),
            calc(var(--title-outline-size, 0px) * -1) 0 0 var(--title-outline-color, transparent),
            0 var(--title-outline-size, 0px) 0 var(--title-outline-color, transparent),
            0 calc(var(--title-outline-size, 0px) * -1) 0 var(--title-outline-color, transparent);
          transition: text-shadow 0.5s ease;
        }

        .player-title a:hover {
          text-shadow:
            var(--title-outline-size, 0px) var(--title-outline-size, 0px) 0 var(--title-outline-color, transparent),
            calc(var(--title-outline-size, 0px) * -1) var(--title-outline-size, 0px) 0 var(--title-outline-color, transparent),
            var(--title-outline-size, 0px) calc(var(--title-outline-size, 0px) * -1) 0 var(--title-outline-color, transparent),
            calc(var(--title-outline-size, 0px) * -1) calc(var(--title-outline-size, 0px) * -1) 0 var(--title-outline-color, transparent),
            var(--title-outline-size, 0px) 0 0 var(--title-outline-color, transparent),
            calc(var(--title-outline-size, 0px) * -1) 0 0 var(--title-outline-color, transparent),
            0 var(--title-outline-size, 0px) 0 var(--title-outline-color, transparent),
            0 calc(var(--title-outline-size, 0px) * -1) 0 var(--title-outline-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent),
            0 0 var(--title-blur-radius, 0px) var(--title-shadow-color, transparent);
        }

        .player-title a img {
          display: block;
          border-radius: 10px;
          transition: filter 0.5s ease;
        }

        .player-title a img:hover {
          filter: drop-shadow(0 0
            var(--title-image-dropshadow-radius, 0)
            var(--title-image-dropshadow-color, transparent));
        }

        .player-controls {
          display: flex;
          align-items: center;
          gap: 6px;
          width: 70%;
          margin: auto 0 6px 0;
          position: relative;
        }

        .player-icon {
          width: 24px;
        }

        #seekbar {
          flex: 1;
          max-width: 80%;
          min-width: 0;
        }

        #seekbar,
        #volume-slider {
          -webkit-appearance: none;
          appearance: none;
          height: 4px;
          border-radius: 2px;
          background-color: var(--slider-bar-color, #dddddd);
          background-image: var(--slider-bar-image, none);
        }

        #seekbar::-webkit-slider-runnable-track,
        #volume-slider::-webkit-slider-runnable-track,
        #seekbar::-moz-range-track,
        #volume-slider::-moz-range-track {
          height: 4px;
          border-radius: 2px;
          background-color: var(--slider-bar-color, #dddddd);
          background-image: var(--slider-bar-image, none);
        }

        #seekbar::-moz-range-progress,
        #volume-slider::-moz-range-progress {
          height: 4px;
          border-radius: 2px;
          background-color: var(--slider-fill-color, #333333);
          background-image: var(--slider-fill-image, none);
        }

        #seekbar::-webkit-slider-thumb,
        #volume-slider::-webkit-slider-thumb {
          -webkit-appearance: none;
          appearance: none;
          width: 10px;
          height: 10px;
          margin-top: -3px;
          border: var(--slider-thumb-border, transparent);
          border-radius: 50%;
          background-color: var(--slider-thumb-color, #333333);
          background-image: var(--slider-thumb-image, none);
          background-size: cover;
          background-position: center;
          transition: filter 0.5s ease;
        }

        #seekbar:hover::-webkit-slider-thumb,
        #volume-slider:hover::-webkit-slider-thumb {
          filter: brightness(var(--slider-thumb-hover-brightness, 1))
            hue-rotate(var(--slider-thumb-hover-hue, 0deg))
            drop-shadow(0 0 var(--slider-thumb-shadow-radius, 0)
            var(--slider-thumb-shadow-color, transparent));
        }

        #seekbar::-moz-range-thumb,
        #volume-slider::-moz-range-thumb {
          width: 10px;
          height: 10px;
          border: var(--slider-thumb-border, transparent);
          border-radius: 50%;
          background-color: var(--slider-thumb-color, #333333);
          background-image: var(--slider-thumb-image, none);
          background-size: cover;
          background-position: center;
          transition: filter 0.5s ease;
        }

        #seekbar:hover::-moz-range-thumb,
        #volume-slider:hover::-moz-range-thumb {
          filter: brightness(var(--slider-thumb-hover-brightness, 1))
            hue-rotate(var(--slider-thumb-hover-hue, 0deg))
            drop-shadow(0 0 var(--slider-thumb-shadow-radius, 0)
            var(--slider-thumb-shadow-color, transparent));
        }

        .volume-wrapper {
          position: relative;
          display: flex;
          align-items: center;
          justify-content: center;
        }

        #volume-slider {
          position: absolute;
          bottom: 28px;
          left: 50%;
          transform: translateX(-50%);
          max-width: 80px;
          opacity: 0;
          pointer-events: none;
          transition: opacity 0.5s ease;
          z-index: 2;
        }

        .volume-wrapper.show #volume-slider {
          opacity: 1;
          pointer-events: auto;
        }

        #filedisplay {
          position: relative;
          display: block;
          width: 70%;
          padding-left: 1ch;
          border: var(--display-border, 1px inset silver);
          border-radius: var(--display-border-radius, 20px);
          white-space: nowrap;
          overflow: hidden;
          background-color: var(--file-background-color, transparent);
          background-image: var(--file-background-image, none);
          background-repeat: repeat-x;
          background-size: auto 100%;
          backdrop-filter: var(--control-background-blur, none);
          -webkit-backdrop-filter: var(--control-background-blur, none);
        }

        #filename {
          display: inline-block;
          width: auto;
          white-space: nowrap;
          color: var(--file-color, white);
          text-shadow:
            var(--file-outline-size, 0px) var(--file-outline-size, 0px) 0 var(--file-outline-color, transparent),
            calc(var(--file-outline-size, 0px) * -1) var(--file-outline-size, 0px) 0 var(--file-outline-color, transparent),
            var(--file-outline-size, 0px) calc(var(--file-outline-size, 0px) * -1) 0 var(--file-outline-color, transparent),
            calc(var(--file-outline-size, 0px) * -1) calc(var(--file-outline-size, 0px) * -1) 0 var(--file-outline-color, transparent),
            var(--file-outline-size, 0px) 0 0 var(--file-outline-color, transparent),
            calc(var(--file-outline-size, 0px) * -1) 0 0 var(--file-outline-color, transparent),
            0 var(--file-outline-size, 0px) 0 var(--file-outline-color, transparent),
            0 calc(var(--file-outline-size, 0px) * -1) 0 var(--file-outline-color, transparent);
          transition: color 0.5s ease;
        }

        #filename:hover {
          color: var(--file-hover-color, none);
        }

        .scrolling-text {
          position: relative;
          left: 100%;
          animation: textscroll 12s linear infinite;
        }

        .scrolling-text:hover {
          animation-play-state: paused;
        }

        .typing-text {
          white-space: nowrap;
        }

        .typing-text::after {
          content: "â–ˆ";
          animation: typing 1s steps(1) infinite;
        }

        #buffering {
          position: absolute;
          right: 10px;
          color: silver;
          font-size: 10px;
        }

        .decor-img {
          position: absolute;
          display: block;
          width: 80px;
          transform: var(--decor-transform,);
          pointer-events: none;
        }

        #topleft-img, #topright-img {
          top: 0;
          z-index: 2;
        }

        #bottomleft-img, #bottomright-img {
          bottom: 0;
          z-index: 1;
        }

        #topleft-img,
        #bottomleft-img {
          left: 0;
        }
        
        #topright-img,
        #bottomright-img {
          right: 0;
        }

        @keyframes textscroll {
            0% { transform: translateX(0); }
          100% { transform: translateX(calc(-100% - 260px)); }
        }

        @keyframes typing {
          50% { opacity: 0; }
        }

        @keyframes blink {
           94% { opacity: 1; transform: var(--decor-transform,) translateX(0px); }
           95% { opacity: 0; transform: var(--decor-transform,) translateX(3px); }
           96% { opacity: 1; transform: var(--decor-transform,) translateX(0); }
           99% { opacity: 0; transform: var(--decor-transform,) translateX(0); }
          100% { opacity: 1; transform: var(--decor-transform,) translateX(3px); }
        }

        @keyframes fade {
            0% { opacity: 1; }
           50% { opacity: 0.1; }
          100% { opacity: 1; }
        }

        @keyframes ghost {
           0%,  30% { opacity: 0; }
          33%,  60% { opacity: 1; }
          66%, 100% { opacity: 0; }
        }

        @keyframes jiggle {
            0% { transform: var(--decor-transform,) translateY(-2px); }
           50% { transform: var(--decor-transform,) translateY(2px); }
          100% { transform: var(--decor-transform,) translateY(-2px); }
        }

        @keyframes pulse {
            0% { filter: brightness(1); }
           50% { filter: brightness(2); }
          100% { filter: brightness(1); }
        }

        @keyframes shake {
          0%   { transform: var(--decor-transform,) translate(0, 0); }
          10%  { transform: var(--decor-transform,) translate(-1px, -1px); }
          20%  { transform: var(--decor-transform,) translate(1px, 1px); }
          30%  { transform: var(--decor-transform,) translate(-1px, 1px); }
          40%  { transform: var(--decor-transform,) translate(1px, -1px); }
          50%  { transform: var(--decor-transform,) translate(-1px, 1px); }
          60%  { transform: var(--decor-transform,) translate(1px, -1px); }
          70%  { transform: var(--decor-transform,) translate(-1px, 1px); }
          80%  { transform: var(--decor-transform,) translate(1px, -1px); }
          90%  { transform: var(--decor-transform,) translate(-1px, -1px); }
          100% { transform: var(--decor-transform,) translate(0, 0); }
        }

        @keyframes spin {
            0% { transform: rotate(0deg) var(--decor-transform,); }
          100% { transform: rotate(360deg) var(--decor-transform,); }
        }
      </style>

      <div id="player">
        <div class="player-title" title="Pira HxCx Player">
          <a href="https://pirahxcx.neocities.org/pira-player" target="_blank"></a>
        </div>

        <div class="player-controls">
          <button id="playpause" title="Play" aria-label="Play">
            <img class="player-icon" id="playpause-icon" src="" alt="play">
          </button>

          <button id="next" title="Next Song" aria-label="Next Song">
            <img class="player-icon" src="" alt="next">
          </button>

          <div class="volume-wrapper" id="volume-wrapper">
            <button id="volume" title="Volume" aria-label="Volume">
              <img class="player-icon" id="volume-icon" src="" alt="volume">
            </button>

            <input type="range" id="volume-slider" min="0" max="1" step="0.01" value="1" aria-label="Volume Slider">
          </div>

          <input type="range" id="seekbar" value="0" min="0" max="100" step="0.1" aria-label="Seek Bar">
        </div>

        <div id="filedisplay">
          <a id="filename" href="#" target="_blank" title="Download" aria-label="Download"></a>

          <span id="buffering" hidden>loading</span>
        </div>
      </div>
      
      <img id="topleft-img" class="decor-img">
      <img id="bottomleft-img" class="decor-img">
      <img id="topright-img" class="decor-img">
      <img id="bottomright-img" class="decor-img">
    `;


    //////////////////////////////////
    //                              //
    //        DANGER ZONE!!!        //
    //     PROCEED WITH CAUTION     //
    //                              //
    //////////////////////////////////

    this.titleinterval = null;
    this.csstitleinterval = null;

    this.titlestate = {
      remaining: [],
      currentindex: 0,
    };

    this.csstitlestate = {
      remaining: [],
      currentindex: 0,
    };

    this.dropshadowinterval = null;

    this.dropshadowstate = {
      remaining: [],
      currentindex: 0,
    };

    this.iconinterval = null;
    this.iconstyleinterval = null;

    this.iconstate = {
      remaining: [],
      currentindex: 0,
    };

    this.slidercolorinterval = null;

    this.slidercolorstate = {
      remaining: [],
      currentindex: 0,
    };

    this.filecolorinterval = null;

    this.filecolorstate = {
      remaining: [],
      currentindex: 0,
    };

    this.backgroundinterval = null;

    this.backgroundstate = {
      remaining: [],
      currentindex: 0,
    };

    this.controlbackgroundinterval = null;

    this.controlbackgroundstate = {
      remaining: [],
      currentindex: 0,
    };

    this.decorintervals = {};

    this.decorstates = {
      topleft: {
        remaining: [],
        currentindex: 0,
      },
      bottomleft: {
        remaining: [],
        currentindex: 0,
      },
      topright: {
        remaining: [],
        currentindex: 0,
      },
      bottomright: {
        remaining: [],
        currentindex: 0,
      },
      random: {
        remaining: [],
        currentindex: 0,
      },
    };

    this.cursorinterval = null;

    this.cursorstate = {
      remaining: [],
      currentindex: 0,
    };

    this.songs = [];
    this.currentindex = -1;

    this.dummyaudio = new Audio();

    this.dummyaudio.preload = "auto";
    this.dummyaudio.loop = true;
    this.dummyaudio.src = this.dummyfile;
    this.dummyaudio.volume = 1;

    this.audio = new Audio();
    this.audio.preload = "auto";

    this.preloaded = null;
    this.playbacktimeout = null;
    this.imagereinterval = null;

    this.imagestatepool = {
      remaining: [],
    };

    this.randomstate = this.createrandomiconstate();

    this.playpausebtn = this.shadowRoot.getElementById("playpause");
    this.nextbtn = this.shadowRoot.getElementById("next");

    this.playpauseicon = this.shadowRoot.getElementById("playpause-icon");
    this.volumeicon = this.shadowRoot.getElementById("volume-icon");

    this.seekbar = this.shadowRoot.getElementById("seekbar");

    this.volumebtn = this.shadowRoot.getElementById("volume");
    this.volumeslider = this.shadowRoot.getElementById("volume-slider");
    this.volumewrapper = this.shadowRoot.getElementById("volume-wrapper");

    this.filename = this.shadowRoot.getElementById("filename");
    this.buffering = this.shadowRoot.getElementById("buffering");

    this.remainingtracks = [];

    this.handleDocumentClick = (e) => {
      if (!this.contains(e.target)) {
        this.volumewrapper.classList.remove("show");
      }
    };

    this.preloadedassets = new Set();

    this.assetpreloadqueue = [];

    this.assetpreloadrunning = false;
  }

  // ========================== //
  //     CUSTOM FONT LOADER     //
  // ========================== //

  async loadcustomfonts() {
    if (!Array.isArray(this.customfonts)) {
      return;
    }

    for (const font of this.customfonts) {
      if (!font) {
        continue;
      }

      if (!font.name || typeof font.name !== "string") {
        continue;
      }

      if (!font.url || typeof font.url !== "string") {
        continue;
      }

      const styleid = `custom-font-${font.name}`;

      const preloadid = `custom-font-preload-${font.name}`;

      const existingstyle = document.getElementById(styleid);

      if (!existingstyle) {
        const style =
          document.createElement("style");

        style.id = styleid;

        style.textContent = `
          @font-face {
            font-family: "${font.name}";
            src: url("${font.url}");
          }
        `;

        document.head.appendChild(style);
      }

      const existingpreload = document.getElementById(preloadid);

      if (!existingpreload) {
        const preload =
          document.createElement("link");

        preload.id = preloadid;

        preload.rel = "preload";
        preload.as = "font";
        preload.href = font.url;
        preload.crossOrigin = "anonymous";

        document.head.appendChild(preload);
      }

      try {
        await document.fonts.load(`16px "${font.name}"`);
      }
      catch (error) {
        console.warn(`Failed loading font: ${font.name}`, error);
      }
    }
  }

  // =================== //
  //     TITLE LOGIC     //
  // =================== //

  applytitle(root) {

    const eltitle = root.querySelector(".player-title a");

    if (!eltitle) {
      return;
    }

    eltitle.innerHTML = "";

    eltitle.style.animation = "";
    eltitle.textContent = "";

    eltitle.onmouseenter = null;
    eltitle.onmouseleave = null;

    eltitle.style.filter = "";

    const hasimage = this.playertitle.image && this.playertitle.image.trim() !== "";

    const hastext = this.playertitle.text && this.playertitle.text.trim() !== "";

    let animation = this.playertitle.animation || "";

    if (this.titlesettings.randomtitleanimation && this.animations.length) {
      animation = this.animations[Math.floor(Math.random() * this.animations.length)];
    }

    const hovertext = this.playertitle.hovertext || "";

    const hoverimg = this.playertitle.hoverimg || "";

    const hoverbrightness = this.playertitle.hoverbrightness || "";

    const hoverhue = this.playertitle.hoverhue || "";

    const buildfilter = () => {

      const filters = [];

      if (hoverbrightness && hoverbrightness.trim() !== "") {
        filters.push(`brightness(${hoverbrightness})`);
      }

      if (hoverhue && hoverhue.trim() !== "") {
        filters.push(`hue-rotate(${hoverhue}deg)`);
      }

      return filters.join(" ");
    };

    if (hasimage) {

      const imgtitle = document.createElement("img");

      imgtitle.src = this.playertitle.image;

      imgtitle.alt = "player title";

      imgtitle.style.animation = animation || "";

      eltitle.appendChild(imgtitle);

      eltitle.onmouseenter = () => {

        if (hoverimg && hoverimg.trim() !== "") {
          imgtitle.src = hoverimg;
        }

        const filter = buildfilter();

        imgtitle.style.filter = filter;
      };

      eltitle.onmouseleave = () => {

        imgtitle.src = this.playertitle.image;

        imgtitle.style.filter = "";
      };

      return;
    }

    eltitle.textContent = hastext
        ? this.playertitle.text
        : "";

    eltitle.style.animation = animation || "";

    eltitle.onmouseenter = () => {

      if (hovertext && hovertext.trim() !== "") {
        eltitle.textContent = hovertext;
      }

      const filter = buildfilter();

      eltitle.style.filter = filter;
    };

    eltitle.onmouseleave = () => {

      eltitle.textContent = hastext
          ? this.playertitle.text
          : "";

      eltitle.style.filter = "";
    };
  }

  buildtitlepool() {
    return this.titlepool.filter(title => {
      if (!title) {
        return false;
      }

      const hastext = title.text && title.text.trim() !== "";

      const hasimage = title.image && title.image.trim() !== "";

      return hastext || hasimage;
    });
  }

  refilltitlepool() {
    this.titlestate.remaining = [...this.buildtitlepool()];
  }

  pickrandomtitle() {
    if (!this.titlestate.remaining.length) {
      this.refilltitlepool();
    }

    if (!this.titlestate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.titlestate.remaining.length);

    const pickedtitle = this.titlestate.remaining[pickindex];

    this.titlestate.remaining.splice(pickindex, 1);

    return pickedtitle;
  }

  picksequentialtitle() {
    const titles = this.buildtitlepool();

    if (!titles.length) {
      return null;
    }

    const pickedtitle = titles[this.titlestate.currentindex];

    this.titlestate.currentindex++;

    if (this.titlestate.currentindex >= titles.length) {
      this.titlestate.currentindex = 0;
    }

    return pickedtitle;
  }

  getnexttitle() {
    if (this.titlesettings.randomtitle) {
      return this.pickrandomtitle();
    }

    return this.picksequentialtitle();
  }

  applycustomtitle(root, titledata) {
    if (!titledata) {
      return;
    }

    this.playertitle = {
      text: titledata.text || "",
      image: titledata.image || "",
      animation: titledata.animation || "",
      hovertext: titledata.hovertext || "",
      hoverimg: titledata.hoverimg || "",
      hoverbrightness: titledata.hoverbrightness || "",
      hoverhue: titledata.hoverhue || "",
    };

    this.applytitle(root);

    this.applytitledropshadow(
      root,
      this.currentdropshadow || this.titledropshadow
    );
  }

  starttitlerotation(root) {
    clearInterval(this.titleinterval);

    this.titleinterval = null;

    this.titlestate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualtitle = (this.playertitle.text && this.playertitle.text.trim() !== "") || (this.playertitle.image && this.playertitle.image.trim() !== "");

    if (hasmanualtitle) {
      this.applytitle(root);
      return;
    }

    const titles = this.buildtitlepool();

    if (!titles.length) {
      this.applytitle(root);
      return;
    }

    const runtitlechange = () => {
      const nexttitle = this.getnexttitle();

      this.applycustomtitle(root, nexttitle);
    };

    runtitlechange();

    if (!this.titlesettings.titlerotation) {
      return;
    }

    this.titleinterval = setInterval(() => {
      runtitlechange();
    },
    Math.max(1, this.titlesettings.titletimer || 1) * 1000);
  }

  buildcsstitlepool() {
    return this.csstitlepool.filter(style => {
      return !!style;
    });
  }

  pickrandomcsstitle() {
    if (!this.csstitlestate.remaining.length) {
      this.csstitlestate.remaining =
        [...this.buildcsstitlepool()];
    }

    if (!this.csstitlestate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.csstitlestate.remaining.length);

    const pickedstyle = this.csstitlestate.remaining[pickindex];

    this.csstitlestate.remaining.splice(pickindex, 1);

    return pickedstyle;
  }

  picksequentialcsstitle() {
    const styles = this.buildcsstitlepool();

    if (!styles.length) {
      return null;
    }

    const pickedstyle = styles[this.csstitlestate.currentindex];

    this.csstitlestate.currentindex++;

    if (this.csstitlestate.currentindex >= styles.length) {
      this.csstitlestate.currentindex = 0;
    }

    return pickedstyle;
  }

  getnexttitlecss() {
    if (this.titlecsssettings.randomtitlecss) {
      return this.pickrandomcsstitle();
    }

    return this.picksequentialcsstitle();
  }

  applytitlecss(root, styledata) {
    const eltitle = root.querySelector(".player-title a");

    if (!eltitle || !styledata) {
      return;
    }

    if (styledata.fontfamily && styledata.fontfamily.trim() !== "") {
      eltitle.style.fontFamily = styledata.fontfamily;
    }
    else {
      eltitle.style.fontFamily = "";
    }

    if (styledata.fontsize && styledata.fontsize.trim() !== "") {
      eltitle.style.fontSize = styledata.fontsize;
    }
    else {
      eltitle.style.fontSize = "";
    }

    if (styledata.color && styledata.color.trim() !== "") {
      eltitle.style.color = styledata.color;
    }
    else {
      eltitle.style.color = "";
    }

    if (styledata.textshadowcolor && styledata.textshadowcolor.trim() !== "") {
      eltitle.style.setProperty("--title-shadow-color", styledata.textshadowcolor);
    }
    else {
      eltitle.style.removeProperty("--title-shadow-color");
    }

    if (styledata.outlinecolor && styledata.outlinecolor.trim() !== "") {
      eltitle.style.setProperty("--title-outline-color", styledata.outlinecolor);
    }
    else {
      eltitle.style.removeProperty("--title-outline-color");
    }

    if (styledata.outlinesize && styledata.outlinesize.trim() !== "") {
      eltitle.style.setProperty("--title-outline-size", styledata.outlinesize);
    }
    else {
      eltitle.style.removeProperty("--title-outline-size");
    }

    if (styledata.blurradius && styledata.blurradius.trim() !== "") {
      eltitle.style.setProperty("--title-blur-radius", styledata.blurradius);
    }
    else {
      eltitle.style.removeProperty("--title-blur-radius");
    }
  }

  starttitlecssrotation(root) {
    clearInterval(this.csstitleinterval);

    this.csstitleinterval = null;

    this.csstitlestate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualstyle = Object.values(this.titlecss).some(value => typeof value === "string" && value.trim() !== "");

    if (hasmanualstyle) {
      this.applytitlecss(root, this.titlecss);
      return;
    }

    const styles = this.buildcsstitlepool();

    if (!styles.length) {
      return;
    }

    const runstylechange = () => {
      const nextstyle = this.getnexttitlecss();

      this.applytitlecss(root, nextstyle);
    };

    runstylechange();

    if (!this.titlecsssettings.csstitlerotation) {
      return;
    }

    this.csstitleinterval = setInterval(() => {
      runstylechange();
    },
    Math.max(1, this.titlecsssettings.csstitletimer || 1) * 1000);
  }

  stoptitlerotation() {
    clearInterval(this.titleinterval);

    this.titleinterval = null;
  }

  stoptitlecssrotation() {
    clearInterval(this.csstitleinterval);

    this.csstitleinterval = null;
  }

  builddropshadowpool() {
    return this.dropshadowpool.filter(shadow => {
      return !!shadow;
    });
  }

  pickrandomdropshadow() {
    if (!this.dropshadowstate.remaining.length) {
      this.dropshadowstate.remaining = [...this.builddropshadowpool()];
    }

    if (!this.dropshadowstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.dropshadowstate.remaining.length);

    const pickedshadow = this.dropshadowstate.remaining[pickindex];

    this.dropshadowstate.remaining.splice(pickindex, 1);

    return pickedshadow;
  }

  picksequentialdropshadow() {
    const shadows = this.builddropshadowpool();

    if (!shadows.length) {
      return null;
    }

    const pickedshadow = shadows[this.dropshadowstate.currentindex];

    this.dropshadowstate.currentindex++;

    if (this.dropshadowstate.currentindex >= shadows.length) {
      this.dropshadowstate.currentindex = 0;
    }

    return pickedshadow;
  }

  getnextdropshadow() {
    if (this.titledropshadowsettings.randomdropshadow) {
      return this.pickrandomdropshadow();
    }

    return this.picksequentialdropshadow();
  }

  applytitledropshadow(root, shadowdata) {
    const img = root.querySelector(".player-title a img");

    if (!img || !shadowdata) {
      return;
    }

    if (shadowdata.dropshadowcolor && shadowdata.dropshadowcolor.trim() !== "") {
      img.style.setProperty("--title-image-dropshadow-color", shadowdata.dropshadowcolor);
    }
    else {
      img.style.removeProperty("--title-image-dropshadow-color");
    }

    if (shadowdata.dropshadowradius && shadowdata.dropshadowradius.trim() !== "") {
      img.style.setProperty("--title-image-dropshadow-radius", shadowdata.dropshadowradius);
    }
    else {
      img.style.removeProperty("--title-image-dropshadow-radius");
    }
  }

  starttitledropshadowrotation(root) {
    clearInterval(this.dropshadowinterval);

    this.dropshadowinterval = null;

    this.dropshadowstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualshadow = Object.values(this.titledropshadow).some(value => typeof value === "string" && value.trim() !== "");

    if (hasmanualshadow) {
      this.applytitledropshadow(root, this.titledropshadow);

      return;
    }

    const shadows = this.builddropshadowpool();

    if (!shadows.length) {
      return;
    }

    const runshadowchange = () => {
      const nextshadow = this.getnextdropshadow();

      this.currentdropshadow = nextshadow;

      this.applytitledropshadow(root, nextshadow);
    };

    runshadowchange();

    if (!this.titledropshadowsettings.dropshadowrotation) {
      return;
    }

    this.dropshadowinterval = setInterval(() => {
      runshadowchange();
    },
    Math.max(1, this.titledropshadowsettings.dropshadowtimer || 1) * 1000);
  }

  stoptitledropshadowrotation() {
    clearInterval(this.dropshadowinterval);

    this.dropshadowinterval = null;
  }

  // ================== //
  //     ICON LOGIC     //
  // ================== //

  buildhuepool() {
    const jump = Math.max(1, this.iconrandomization.huejump || 90);

    const hues = [];

    for (let i = 0; i < 360; i += jump) {
      hues.push(i);
    }

    return hues;
  }

  pickrandomhue(exclude = []) {
    const hues = this.buildhuepool();

    const availablehues = hues.filter(hue => !exclude.includes(hue));

    if (!availablehues.length) {
      return 0;
    }

    return availablehues[
      Math.floor(Math.random() * availablehues.length)
    ];
  }

  pickrandomangle(exclude = []) {
    const range = Math.max(0, this.iconrandomization.anglerange || 0);

    const allowzero = this.iconrandomization.allowzeroangle;

    const angles = [];

    for (let i = -range; i <= range; i++) {
      if (i === 0 && !allowzero) {
        continue;
      }

      angles.push(i);
    }

    const availableangles = angles.filter(angle => !exclude.includes(angle));

    if (!availableangles.length) {
      return 0;
    }

    return availableangles[
      Math.floor(Math.random() * availableangles.length)
    ];
  }

  createrandomiconstate() {
    const state = {
      play: {},
      pause: {},
      next: {},
      volumehigh: {},
      volumelow: {},
      volumemute: {},
    };

    state.play.hue = this.pickrandomhue([]);

    state.next.hue = this.pickrandomhue([state.play.hue,]);

    state.volumehigh.hue = this.pickrandomhue([state.play.hue, state.next.hue,]);

    state.pause.hue = this.pickrandomhue([state.play.hue,]);

    state.volumelow.hue = this.pickrandomhue([state.volumehigh.hue,]);

    state.volumemute.hue = this.pickrandomhue([state.volumehigh.hue,state.volumelow.hue,]);

    state.play.angle = this.pickrandomangle([]);

    state.next.angle = this.pickrandomangle([state.play.angle,]);

    state.volumehigh.angle = this.pickrandomangle([state.play.angle,state.next.angle,]);

    state.pause.angle = this.pickrandomangle([state.play.angle,]);

    state.volumelow.angle = this.pickrandomangle([state.volumehigh.angle,]);

    state.volumemute.angle = this.pickrandomangle([state.volumehigh.angle,state.volumelow.angle,]);

    return state;
  }

  applyiconstate(el, state, shadow = "", hoverfilter = "") {
    if (!el || !state) {
      return;
    }

    const filters = [];

    if (this.iconrandomization.randomcolor) {
      filters.push(`hue-rotate(${state.hue}deg)`);
    }

    if (hoverfilter) {
      filters.push(hoverfilter);
    }

    if (shadow) {
      filters.push(shadow);
    }

    el.style.filter = filters.join(" ");

    if (this.iconrandomization.randomangle) {
      el.style.setProperty("--icon-rotate", `${state.angle}deg`);

      el.style.transform = `rotate(var(--icon-rotate, 0deg))`;
    }

    else {
      el.style.removeProperty("--icon-rotate");

      el.style.transform = "";
    }
  }

  buildiconpool() {
    return this.iconpool.filter(iconset => {
      return !!iconset;
    });
  }

  refilliconpool() {
    this.iconstate.remaining =
      [...this.buildiconpool()];
  }

  pickrandomiconset() {
    if (!this.iconstate.remaining.length) {
      this.refilliconpool();
    }

    if (!this.iconstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.iconstate.remaining.length);

    const pickedicons = this.iconstate.remaining[pickindex];

    this.iconstate.remaining.splice(pickindex, 1);

    return pickedicons;
  }

  picksequentialiconset() {
    const iconsets = this.buildiconpool();

    if (!iconsets.length) {
      return null;
    }

    const pickedicons = iconsets[this.iconstate.currentindex];

    this.iconstate.currentindex++;

    if (this.iconstate.currentindex >= iconsets.length) {
      this.iconstate.currentindex = 0;
    }

    return pickedicons;
  }

  getrandomiconfrompool(type) {
  const valid = this.iconpool.filter(set => {
    return set && set[type];
  });

  if (!valid.length) {
    return null;
  }

  return valid[
    Math.floor(Math.random() * valid.length)
  ];
}

  buildtotalrandomiconset() {

    const playset = this.getrandomiconfrompool("play");

    const nextset = this.getrandomiconfrompool("next");

    const volumeset = this.getrandomiconfrompool("volumehigh");

    return {

      play: playset?.play || "",
      pause: playset?.pause || "",

      hoverplay: playset?.hoverplay || "",
      hoverpause: playset?.hoverpause || "",

      next: nextset?.next || "",

      hovernext: nextset?.hovernext || "",

      volumehigh: volumeset?.volumehigh || "",
      volumelow: volumeset?.volumelow || "",
      volumemute: volumeset?.volumemute || "",

      hovervolumehigh: volumeset?.hovervolumehigh || "",
      hovervolumelow: volumeset?.hovervolumelow || "",
      hovervolumemute: volumeset?.hovervolumemute || "",

      brightness: "",
      huechange: "",

      dropshadowcolor: "",
      dropshadowradius: "",
    };
  }

  getnexticonset() {

    if (this.iconsettings.totalrandomicons) {
      return this.buildtotalrandomiconset();
    }

    if (this.iconsettings.randomicons) {
      return this.pickrandomiconset();
    }

    return this.picksequentialiconset();
  }

  applyicons(root, iconset) {
    if (!iconset) {
      return;
    }

    this.icons = {

      play: iconset.play || "",
      pause: iconset.pause || "",

      next: iconset.next || "",

      volumehigh: iconset.volumehigh || "",
      volumelow: iconset.volumelow || "",
      volumemute: iconset.volumemute || "",

      hoverplay: iconset.hoverplay || "",
      hoverpause: iconset.hoverpause || "",

      hovernext: iconset.hovernext || "",

      hovervolumehigh: iconset.hovervolumehigh || "",
      hovervolumelow: iconset.hovervolumelow || "",
      hovervolumemute: iconset.hovervolumemute || "",

      brightness: iconset.brightness || "",
      huechange: iconset.huechange || "",

      dropshadowcolor: iconset.dropshadowcolor || "",
      dropshadowradius: iconset.dropshadowradius || "",
    };

    const playicon = root.getElementById("playpause-icon");

    const nexticon = root.getElementById("next")?.querySelector("img");

    const volumeicon = root.getElementById("volume-icon");

    if (playicon) {
      playicon.src = this.audio.paused
        ? this.icons.play
        : this.icons.pause;
    }

    if (nexticon) {
      nexticon.src = this.icons.next;
    }

    if (volumeicon) {

      let volumesrc = this.icons.volumehigh;

      if (this.audio.volume === 0) {
        volumesrc = this.icons.volumemute;
      }

      else if (this.audio.volume < 0.5) {
        volumesrc = this.icons.volumelow;
      }

      volumeicon.src = volumesrc;
    }

    this.updateplaybutton();

    this.applyiconhovereffects(root, iconset);
  }

  applyrandomiconstyles(root) {
    const playicon = root.getElementById("playpause-icon");

    const nexticon = root.getElementById("next").querySelector("img");

    const volumeicon = root.getElementById("volume-icon");

    this.randomstate = this.createrandomiconstate();

    const randomstate = this.randomstate;

    this.applyiconstate(playicon, randomstate.play);

    this.applyiconstate(nexticon, randomstate.next);

    let volumestate = randomstate.volumehigh;

    if (this.audio.volume === 0) {
      volumestate = randomstate.volumemute;
    }
    else if (this.audio.volume < 0.5) {
      volumestate = randomstate.volumelow;
    }

    this.applyiconstate(volumeicon, volumestate);
  }

  startrandomiconstyles(root) {
    clearInterval(this.iconstyleinterval);

    this.iconstyleinterval = null;

    const shouldrotatecolors = this.iconrandomization.randomcolor && this.iconrandomization.colorrotation;

    const shouldrotateangles = this.iconrandomization.randomangle && this.iconrandomization.anglerotation;

    this.applyrandomiconstyles(root);

    if (!shouldrotatecolors && !shouldrotateangles) {
      return;
    }

    const timer = Math.max(1, Math.min(this.iconrandomization.colortimer || 10, this.iconrandomization.angletimer || 10)
    );

    this.iconstyleinterval = setInterval(() => {
      const playicon = root.getElementById("playpause-icon");

      const nexticon = root.getElementById("next").querySelector("img");

      const volumeicon = root.getElementById("volume-icon");

      const randomstate = this.createrandomiconstate();

      this.randomstate = randomstate;

      if (shouldrotatecolors) {
        const shadow = this.buildiconshadow(this.icons);
      
        if (playicon) {
          this.applyiconstate(playicon, randomstate.play, playicon.matches(":hover") ? shadow : "");
        }
      
        if (nexticon) {
          this.applyiconstate(nexticon, randomstate.next, nexticon.matches(":hover") ? shadow : "");
        }
      
        if (volumeicon) {
          let volumestate = randomstate.volumehigh;
      
          if (this.audio.volume === 0) {
            volumestate = randomstate.volumemute;
          }
          else if (this.audio.volume < 0.5) {
            volumestate = randomstate.volumelow;
          }
      
          this.applyiconstate(volumeicon, volumestate, volumeicon.matches(":hover") ? shadow : "");
        }
      }

      if (shouldrotateangles) {
        if (playicon) {
          playicon.style.setProperty(
            "--icon-rotate",
            `${randomstate.play.angle}deg`
          );
        }

        if (nexticon) {
          nexticon.style.setProperty("--icon-rotate", `${randomstate.next.angle}deg`);
        }

        if (volumeicon) {
          let volumeangle = randomstate.volumehigh.angle;

          if (this.audio.volume === 0) {
            volumeangle = randomstate.volumemute.angle;
          }
          else if (this.audio.volume < 0.5) {
            volumeangle = randomstate.volumelow.angle;
          }

          volumeicon.style.setProperty("--icon-rotate", `${volumeangle}deg`);
        }
      }

    }, timer * 1000);
  }

  stoprandomiconstyles() {
    clearInterval(this.iconstyleinterval);

    this.iconstyleinterval = null;
  }

  starticonrotation(root) {
    clearInterval(this.iconinterval);

    this.iconinterval = null;

    this.iconstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualicons = Object.values(this.icons).some(value => typeof value === "string" && value.trim() !== "");

    if (hasmanualicons) {
      this.applyicons(root, this.icons);
      return;
    }

    const iconsets = this.buildiconpool();

    if (!iconsets.length) {
      return;
    }

    const runiconchange = () => {
      const nexticons = this.getnexticonset();

      this.applyicons(root, nexticons);
    };

    runiconchange();

    if (!this.iconsettings.iconrotation) {
      return;
    }

    this.iconinterval = setInterval(() => {
      runiconchange();
    },
    Math.max(1, this.iconsettings.icontimer || 1) * 1000);
  }

  stopiconrotation() {
    clearInterval(this.iconinterval);

    this.iconinterval = null;
  }

  buildiconshadow(iconset) {
    if (!iconset) {
      return "";
    }

    const color = typeof iconset.dropshadowcolor === "string"
      ? iconset.dropshadowcolor.trim()
      : "";

    const radius = typeof iconset.dropshadowradius === "string"
      ? iconset.dropshadowradius.trim()
      : String(iconset.dropshadowradius || "").trim();

    if (!color || !radius) {
      return "";
    }

    return `drop-shadow(0 0 ${radius} ${color})`;
  }

  applyiconhovereffects(root, iconset) {

    const shadow = this.buildiconshadow(iconset);

    const playicon = root.getElementById("playpause-icon");

    const nexticon = root.getElementById("next")?.querySelector("img");

    const volumeicon = root.getElementById("volume-icon");

    const gethoverfilter = () => {

      const filters = [];

      if (iconset.brightness && String(iconset.brightness).trim() !== "") {
        filters.push(`brightness(${iconset.brightness})`);
      }
  
      if (iconset.huechange && String(iconset.huechange).trim() !== "") {
        filters.push(`hue-rotate(var(--icon-huechange))`);
      }
  
      return filters.join(" ");
    };
  
    const hoverfilter = gethoverfilter();
  
    if (iconset.huechange && String(iconset.huechange).trim() !== "") {
      root.host.style.setProperty("--icon-huechange", `${iconset.huechange}deg`);
    }
  
    else {
      root.host.style.removeProperty("--icon-huechange");
    }
  
    const applyleave = (icon, state) => {
      this.applyiconstate(icon, state || {});
    };
  
    const applyenter = (
      icon,
      state,
      normalsrc,
      hoversrc
    ) => {
  
      if (hoversrc) {
        icon.src = hoversrc;
      }
  
      this.applyiconstate(
        icon,
        state || {},
        shadow,
        hoverfilter
      );
    };
  
    if (playicon) {
  
      playicon.onmouseenter = () => {
  
        const paused = this.audio.paused;
  
        applyenter(
          playicon,
          paused
            ? this.randomstate?.play
            : this.randomstate?.pause,
  
          paused
            ? this.icons.play
            : this.icons.pause,
  
          paused
            ? this.icons.hoverplay
            : this.icons.hoverpause
        );
      };
  
      playicon.onmouseleave = () => {
  
        playicon.src = this.audio.paused
          ? this.icons.play
          : this.icons.pause;
  
        applyleave(
          playicon,
          this.audio.paused
            ? this.randomstate?.play
            : this.randomstate?.pause
        );
      };
    }
  
    if (nexticon) {
  
      nexticon.onmouseenter = () => {
  
        applyenter(
          nexticon,
          this.randomstate?.next,
          this.icons.next,
          this.icons.hovernext
        );
      };
  
      nexticon.onmouseleave = () => {
  
        nexticon.src = this.icons.next;
  
        applyleave(
          nexticon,
          this.randomstate?.next
        );
      };
    }
  
    if (volumeicon) {
  
      const getvolumesrc = () => {
  
        if (this.audio.volume === 0) {
          return {
            normal: this.icons.volumemute,
            hover: this.icons.hovervolumemute,
            state: this.randomstate?.volumemute,
          };
        }
  
        if (this.audio.volume < 0.5) {
          return {
            normal: this.icons.volumelow,
            hover: this.icons.hovervolumelow,
            state: this.randomstate?.volumelow,
          };
        }
  
        return {
          normal: this.icons.volumehigh,
          hover: this.icons.hovervolumehigh,
          state: this.randomstate?.volumehigh,
        };
      };
  
      volumeicon.onmouseenter = () => {
  
        const current = getvolumesrc();
  
        applyenter(volumeicon, current.state, current.normal, current.hover);
      };
  
      volumeicon.onmouseleave = () => {
  
        const current = getvolumesrc();
  
        volumeicon.src = current.normal;
  
        applyleave(volumeicon, current.state);
      };
    }
  }

  // ==================== //
  //     SLIDER LOGIC     //
  // ==================== //

  buildslidercolorpool() {
    return this.sliderpool.filter(style => {
      return !!style;
    });
  }

  refillslidercolorpool() {
    this.slidercolorstate.remaining = [...this.buildslidercolorpool()];
  }

  pickrandomslidercolors() {
    if (!this.slidercolorstate.remaining.length) {
      this.refillslidercolorpool();
    }

    if (!this.slidercolorstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.slidercolorstate.remaining.length);

    const pickedstyle = this.slidercolorstate.remaining[pickindex];

    this.slidercolorstate.remaining.splice(pickindex, 1);

    return pickedstyle;
  }

  picksequentialslidercolors() {
    const styles = this.buildslidercolorpool();

    if (!styles.length) {
      return null;
    }

    const pickedstyle = styles[this.slidercolorstate.currentindex];

    this.slidercolorstate.currentindex++;

    if (this.slidercolorstate.currentindex >= styles.length) {
      this.slidercolorstate.currentindex = 0;
    }

    return pickedstyle;
  }

  getnextslidercolors() {
    if (this.slidersettings.randomslider) {
      return this.pickrandomslidercolors();
    }

    return this.picksequentialslidercolors();
  }

  applyslider(root, styledata) {
    if (!styledata) {
      return;
    }

    const seekbar = root.getElementById("seekbar");

    const volumeslider = root.getElementById("volume-slider");

    const sliders = [seekbar, volumeslider];

    sliders.forEach(slider => {
      if (!slider) {
        return;
      }

      if (styledata.barcolor && styledata.barcolor.trim() !== "") {
        slider.style.setProperty("--slider-bar-color", styledata.barcolor);
      }
      else {
        slider.style.removeProperty("--slider-bar-color");
      }

      if (styledata.barimg && styledata.barimg.trim() !== "") {
        slider.style.setProperty("--slider-bar-image", `url("${styledata.barimg}")`);
      }
      else {
        slider.style.removeProperty("--slider-bar-image");
      }

      if (styledata.fillcolor && styledata.fillcolor.trim() !== "") {
        slider.style.setProperty("--slider-fill-color", styledata.fillcolor);
      }
      else {
        slider.style.removeProperty("--slider-fill-color");
      }

      if (styledata.fillimg && styledata.fillimg.trim() !== "") {
        slider.style.setProperty("--slider-fill-image", `url("${styledata.fillimg}")`);
      }
      else {
        slider.style.removeProperty("--slider-fill-image");
      }

      if (styledata.thumbcolor !== undefined) {
        if (typeof styledata.thumbcolor === "string" && styledata.thumbcolor.trim() !== "") {
          slider.style.setProperty("--slider-thumb-color", styledata.thumbcolor);
        }
        else {
          slider.style.setProperty("--slider-thumb-color", "transparent");
        }
      }

      if (styledata.thumbimg !== undefined) {
        if (typeof styledata.thumbimg === "string" && styledata.thumbimg.trim() !== "") {
          slider.style.setProperty("--slider-thumb-image", `url("${styledata.thumbimg}")`);
        }
        else {
          slider.style.setProperty("--slider-thumb-image", "none");
        }
      }

      if (styledata.thumbborder !== undefined) {
        if (typeof styledata.thumbborder === "string" && styledata.thumbborder.trim() !== "") {
          slider.style.setProperty("--slider-thumb-border", styledata.thumbborder);
        }
        else {
          slider.style.setProperty("--slider-thumb-border", "none");
        }
      }

      if (styledata.thumbbrightness !== undefined) {
        if (typeof styledata.thumbbrightness === "string" && styledata.thumbbrightness.trim() !== "") {
          slider.style.setProperty("--slider-thumb-hover-brightness", styledata.thumbbrightness);
        }
        else {
          slider.style.removeProperty("--slider-thumb-hover-brightness");
        }
      }

      if (styledata.thumbhue !== undefined) {
        if (typeof styledata.thumbhue === "string" && styledata.thumbhue.trim() !== "") {
          slider.style.setProperty("--slider-thumb-hover-hue", `${styledata.thumbhue}deg`);
        }
        else {
          slider.style.removeProperty("--slider-thumb-hover-hue");
        }
      }

      if (styledata.thumbshadowcolor !== undefined) {
        if (typeof styledata.thumbshadowcolor === "string" && styledata.thumbshadowcolor.trim() !== "") {
          slider.style.setProperty("--slider-thumb-shadow-color", styledata.thumbshadowcolor);
        }
        else {
          slider.style.setProperty("--slider-thumb-shadow-color", "transparent");
        }
      }

      if (styledata.thumbshadowradius !== undefined) {
        if (typeof styledata.thumbshadowradius === "string" && styledata.thumbshadowradius.trim() !== "") {
          slider.style.setProperty("--slider-thumb-shadow-radius", styledata.thumbshadowradius);
        }
        else {
          slider.style.setProperty("--slider-thumb-shadow-radius", "0px");
        }
      }
    });
  }

  startslidercolorrotation(root) {
    clearInterval(this.slidercolorinterval);

    this.slidercolorinterval = null;

    this.slidercolorstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualcolors = Object.values(this.slider).some(value => typeof value === "string" && value.trim() !== "");

    if (hasmanualcolors) {
      this.applyslider(root, this.slider);

      return;
    }

    const styles = this.buildslidercolorpool();

    if (!styles.length) {
      return;
    }

    const runsliderchange = () => {
      const nextstyle = this.getnextslidercolors();

      this.applyslider(root, nextstyle);
    };

    runsliderchange();

    if (!this.slidersettings.sliderrotation) {
      return;
    }

    this.slidercolorinterval = setInterval(() => {
        runsliderchange();
      },
      Math.max(1, this.slidersettings.slidertimer || 1) * 1000);
  }

  stopslidercolorrotation() {
    clearInterval(this.slidercolorinterval);

    this.slidercolorinterval = null;
  }

  // ========================== //
  //     FILE DISPLAY LOGIC     //
  // ========================== //

  buildfilecolorpool() {
    return this.filecolorpool.filter(style => {
      if (!style) {
        return false;
      }

      return Object.values(style).some(value => {
        if (typeof value === "string") {
          return value.trim() !== "";
        }

        if (typeof value === "boolean") {
          return value === true;
        }

        return false;
      });
    });
  }

  refillfilecolorpool() {
    this.filecolorstate.remaining = [...this.buildfilecolorpool()];
  }

  pickrandomfilecolor() {
    if (!this.filecolorstate.remaining.length) {
      this.refillfilecolorpool();
    }

    if (!this.filecolorstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.filecolorstate.remaining.length);

    const pickedstyle = this.filecolorstate.remaining[pickindex];

    this.filecolorstate.remaining.splice(pickindex, 1);

    return pickedstyle;
  }

  picksequentialfilecolor() {
    const styles = this.buildfilecolorpool();

    if (!styles.length) {
      return null;
    }

    const pickedstyle = styles[this.filecolorstate.currentindex];

    this.filecolorstate.currentindex++;

    if (this.filecolorstate.currentindex >= styles.length) {
      this.filecolorstate.currentindex = 0;
    }

    return pickedstyle;
  }

  getnextfilecolor() {
    if (this.filecolorsettings.randomfilecolor) {
      return this.pickrandomfilecolor();
    }

    return this.picksequentialfilecolor();
  }

  cleartypingeffect(filelink) {
    if (!filelink) {
      return;
    }

    clearInterval(filelink.typinginterval);

    filelink.typinginterval = null;

    filelink.classList.remove("typing-text");

    filelink.classList.remove("scrolling-text");

    if (filelink.dataset.fulltext) {
      filelink.textContent = filelink.dataset.fulltext;
    }
  }

  starttypingeffect(filelink, filedisplay, enablescroll = false) {
    if (!filelink) {
      return;
    }

    const fulltext = filelink.dataset.fulltext || "";

    this.cleartypingeffect(filelink);

    if (!fulltext.length) {
      filelink.textContent = "";
      return;
    }

    filelink.classList.add("typing-text");

    let currentindex = 0;

    filelink.textContent = "";

    filelink.typinginterval = setInterval(() => {

      currentindex++;

      filelink.textContent = fulltext.slice(0, currentindex);

      const overflowing =
        filelink.scrollWidth > filedisplay.clientWidth;

      if (overflowing && enablescroll) {
        filelink.classList.add("scrolling-text");
      }

      if (currentindex >= fulltext.length) {

        clearInterval(filelink.typinginterval);

        filelink.typinginterval = null;

        filelink.textContent = fulltext;

        if (enablescroll) {
          filelink.classList.add("scrolling-text");
        }
      }

    }, 200);
  }

  applyfilecolor(root, styledata) {
    this.currentfilecolor = styledata;

    const filelink = root.getElementById("filename");

    const filedisplay = root.getElementById("filedisplay");

    if (!filelink || !filedisplay || !styledata) {
      return;
    }

    if (styledata.color !== undefined) {
      filelink.style.setProperty("--file-color", styledata.color || "inherit");
    }

    if (styledata.hovercolor !== undefined) {
      filelink.style.setProperty("--file-hover-color", styledata.hovercolor || "inherit");
    }

    if (styledata.fontfamily !== undefined) {
      filelink.style.fontFamily = styledata.fontfamily || "";
    }

    if (styledata.backgroundcolor !== undefined) {
      filedisplay.style.setProperty("--file-background-color", styledata.backgroundcolor || "transparent");
    }

    if (styledata.backgroundimg !== undefined) {
      if (typeof styledata.backgroundimg === "string" && styledata.backgroundimg.trim() !== "") {
        filedisplay.style.setProperty("--file-background-image", `url("${styledata.backgroundimg}")`);
      }
      else {
        filedisplay.style.setProperty("--file-background-image", "none");
      }
    }

    if (styledata.backgroundblur) {
      const bluramount = styledata.backgroundbluramount || "4px";

      filedisplay.style.backdropFilter = `blur(${bluramount})`;

      filedisplay.style.webkitBackdropFilter = `blur(${bluramount})`;
    }
    else {
      filedisplay.style.backdropFilter = "none";

      filedisplay.style.webkitBackdropFilter = "none";
    }

    if (styledata.outlinecolor !== undefined) {
      filelink.style.setProperty("--file-outline-color", styledata.outlinecolor || "transparent");
    }

    if (styledata.outlinesize !== undefined) {
      filelink.style.setProperty("--file-outline-size", styledata.outlinesize || "0px");
    }

    const shouldoverflow = filelink.scrollWidth > filedisplay.clientWidth;

    const enablescroll = styledata.scrolltext || shouldoverflow;

    if (enablescroll) {
      filelink.classList.add("scrolling-text");
    }
    else {
      filelink.classList.remove("scrolling-text");
    }

    if (styledata.typingtext) {

      const currenttext = filelink.dataset.fulltext || filelink.textContent;

      const typingdisabled = !filelink.classList.contains("typing-text") && !filelink.typinginterval;
    
      if (filelink.dataset.typingtext !== currenttext || typingdisabled) {

        filelink.dataset.typingtext = currenttext;

        this.starttypingeffect(
          filelink,
          filedisplay,
          enablescroll
        );
      }
    }
    else {

      delete filelink.dataset.typingtext;

      this.cleartypingeffect(filelink);

      if (enablescroll) {
        filelink.classList.add("scrolling-text");
      }
    }
  }

  startfilecolorrotation(root) {
    clearInterval(this.filecolorinterval);

    this.filecolorinterval = null;

    this.filecolorstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualcolors = Object.values(this.filecolors).some(value => value !== "" && value !== false && value !== undefined && value !== null);

    if (hasmanualcolors) {
      this.applyfilecolor(root, this.filecolors);

      return;
    }

    const styles = this.buildfilecolorpool();

    if (!styles.length) {
      return;
    }

    const runfilecolorchange = () => {const nextstyle = this.getnextfilecolor();

      this.applyfilecolor(root, nextstyle);
    };

    runfilecolorchange();

    if (!this.filecolorsettings.filecolorrotation) {
      return;
    }

    this.filecolorinterval = setInterval(() => {
      runfilecolorchange();
    },
    Math.max(1, this.filecolorsettings.filecolortimer || 1) * 1000);
  }

  stopfilecolorrotation() {
    clearInterval(this.filecolorinterval);

    this.filecolorinterval = null;
  }

  // ======================== //
  //     BACKGROUND LOGIC     //
  // ======================== //

  buildbackgroundpool() {
    return this.backgroundpool.filter(style => {
      if (!style) {
        return false;
      }

      return ((style.color && style.color.trim() !== "") || (style.image && style.image.trim() !== ""));
    });
  }

  refillbackgroundpool() {
    this.backgroundstate.remaining = [...this.buildbackgroundpool()];
  }

  pickrandombackground() {
    if (!this.backgroundstate.remaining.length) {
      this.refillbackgroundpool();
    }

    if (!this.backgroundstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.backgroundstate.remaining.length);

    const pickedbackground = this.backgroundstate.remaining[pickindex];

    this.backgroundstate.remaining.splice(pickindex, 1);

    return pickedbackground;
  }

  picksequentialbackground() {
    const backgrounds = this.buildbackgroundpool();

    if (!backgrounds.length) {
      return null;
    }

    const pickedbackground = backgrounds[this.backgroundstate.currentindex];

    this.backgroundstate.currentindex++;

    if (this.backgroundstate.currentindex >= backgrounds.length) {
      this.backgroundstate.currentindex = 0;
    }

    return pickedbackground;
  }

  getnextbackground() {
    if (this.backgroundsettings.randombackground) {
      return this.pickrandombackground();
    }

    return this.picksequentialbackground();
  }

  applybackground(styledata) {
    if (!styledata) {
      return;
    }

    const layers = [];

    if (typeof styledata.color === "string" && styledata.color.trim() !== "") {
      layers.push(styledata.color);
    }

    if (typeof styledata.image === "string" && styledata.image.trim() !== "") {
      let imagestring = `url("${styledata.image}")`;

      if (styledata.tile) {
        imagestring += " top left / auto repeat";
      }
      else {
        imagestring += " center / cover no-repeat";
      }

      layers.push(imagestring);
    }

    if (!layers.length) {
      this.style.background = "transparent";
      return;
    }

    this.style.background = layers.join(", ");
  }

  startbackgroundrotation() {
    clearInterval(this.backgroundinterval);

    this.backgroundinterval = null;

    this.backgroundstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualbackground = (this.background.color && this.background.color.trim() !== "") || (this.background.image && this.background.image.trim() !== "");

    if (hasmanualbackground) {
      this.applybackground(this.background);

      return;
    }

    const backgrounds = this.buildbackgroundpool();

    if (!backgrounds.length) {
      return;
    }

    const runbackgroundchange = () => {
      const nextbackground = this.getnextbackground();

      this.applybackground(nextbackground);
    };

    runbackgroundchange();

    if (!this.backgroundsettings.backgroundrotation) {
      return;
    }

    this.backgroundinterval = setInterval(() => {
        runbackgroundchange();
      },
      Math.max(1, this.backgroundsettings.backgroundtimer || 1) * 1000);
  }

  stopbackgroundrotation() {
    clearInterval(this.backgroundinterval);

    this.backgroundinterval = null;
  }

  buildcontrolbackgroundpool() {
    return this.controlbackgroundpool.filter(style => {
      return !!style;
    });
  }

  refillcontrolbackgroundpool() {
    this.controlbackgroundstate.remaining = [...this.buildcontrolbackgroundpool()];
  }

  pickrandomcontrolbackground() {
    if (!this.controlbackgroundstate.remaining.length) {
      this.refillcontrolbackgroundpool();
    }

    if (!this.controlbackgroundstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.controlbackgroundstate.remaining.length);

    const pickedbackground = this.controlbackgroundstate.remaining[pickindex];

    this.controlbackgroundstate.remaining.splice(pickindex, 1);

    return pickedbackground;
  }

  picksequentialcontrolbackground() {
    const backgrounds = this.buildcontrolbackgroundpool();

    if (!backgrounds.length) {
      return null;
    }

    const pickedbackground = backgrounds[this.controlbackgroundstate.currentindex];

    this.controlbackgroundstate.currentindex++;

    if (this.controlbackgroundstate.currentindex >= backgrounds.length) {
      this.controlbackgroundstate.currentindex = 0;
    }

    return pickedbackground;
  }

  getnextcontrolbackground() {
    if (this.controlbackgroundsettings.randomcontrolbackground) {
      return this.pickrandomcontrolbackground();
    }

    return this.picksequentialcontrolbackground();
  }

  applycontrolbackground(root, styledata) {
    const player = root.getElementById("player");

    if (!player || !styledata) {
      return;
    }

    const layers = [];

    if (typeof styledata.color === "string" && styledata.color.trim() !== "") {
      layers.push(styledata.color);
    }

    if (typeof styledata.image === "string" && styledata.image.trim() !== "") {
      let imagestring = `url("${styledata.image}")`;

      if (styledata.tile) {
        imagestring += " top left / auto repeat";
      }
      else {
        imagestring += " center / cover no-repeat";
      }

      layers.push(imagestring);
    }

    if (!layers.length) {
      player.style.background = "transparent";
    }
    else {
      player.style.background = layers.join(", ");
    }

    if (styledata.blur) {
      const bluramount = styledata.bluramount || "6px";

      player.style.backdropFilter = `blur(${bluramount})`;

      player.style.webkitBackdropFilter = `blur(${bluramount})`;
    }
    else {
      player.style.backdropFilter = "none";

      player.style.webkitBackdropFilter = "none";
    }
  }

  startcontrolbackgroundrotation(root) {
    clearInterval(this.controlbackgroundinterval);

    this.controlbackgroundinterval = null;

    this.controlbackgroundstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualbackground = (this.controlbackground.color && this.controlbackground.color.trim() !== "") || (this.controlbackground.image && this.controlbackground.image.trim() !== "");

    if (hasmanualbackground) {
      this.applycontrolbackground(root, this.controlbackground);

      return;
    }

    const backgrounds =this.buildcontrolbackgroundpool();

    if (!backgrounds.length) {
      return;
    }

    const runbackgroundchange = () => {
      const nextbackground = this.getnextcontrolbackground();

      this.applycontrolbackground(root, nextbackground);
    };

    runbackgroundchange();

    if (!this.controlbackgroundsettings.controlbackgroundrotation) {
      return;
    }

    this.controlbackgroundinterval = setInterval(() => {
        runbackgroundchange();
      },
      Math.max(1, this.controlbackgroundsettings.controlbackgroundtimer || 1) * 1000);
  }

  stopcontrolbackgroundrotation() {
    clearInterval(this.controlbackgroundinterval);

    this.controlbackgroundinterval = null;
  }

  // =================== //
  //     DECOR LOGIC     //
  // =================== //

  builddecorpool(poolname) {
    const pool = this.decorpools[poolname];

    if (!Array.isArray(pool)) {
      return [];
    }

    return pool.filter(item => !!item);
  }

  refilldecorpool(poolname) {
    this.decorstates[poolname].remaining = [...this.builddecorpool(poolname)];
  }

  pickrandomdecor(poolname) {
    if (!this.decorstates[poolname].remaining.length) {
      this.refilldecorpool(poolname);
    }

    if (!this.decorstates[poolname].remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.decorstates[poolname].remaining.length);

    const picked = this.decorstates[poolname].remaining[pickindex];

    this.decorstates[poolname].remaining.splice(pickindex,1);

    return picked;
  }

  picksequentialdecor(poolname) {
    const pool = this.builddecorpool(poolname);

    if (!pool.length) {
      return null;
    }

    const picked = pool[this.decorstates[poolname].currentindex];

    this.decorstates[poolname].currentindex++;

    if (this.decorstates[poolname].currentindex >= pool.length) {
      this.decorstates[poolname].currentindex = 0;
    }

    return picked;
  }

  getnextdecor(poolname, random) {
    if (random) {
      return this.pickrandomdecor(poolname);
    }

    return this.picksequentialdecor(poolname);
  }

  applydecor(root, slot, data) {
    const img = root.getElementById(`${slot}-img`);

    if (!img) {
      return;
    }

    img.removeAttribute("src");

    img.style.display = "none";

    img.style.animation = "";

    img.style.width = "";

    img.style.removeProperty("--decor-transform");

    if (!data) {
      return;
    }

    const hasimage = typeof data.image === "string" && data.image.trim() !== "";

    if (!hasimage) {
      return;
    }

    img.src = data.image;

    img.style.display = "";

    if (data.width && data.width.trim() !== "") {
      img.style.width = data.width;
    }

    let animation = data.animation || "";

    if (this.randomdecorations.randomanimations && this.animations.length) {
      animation = this.animations[Math.floor(Math.random() * this.animations.length)];
    }
  
    img.style.animation = animation;
  
    if (this.randomdecorations.mirrorrandomright && this.randomdecorations.totalrandom && (slot === "topright" || slot === "bottomright")) {
      img.style.setProperty("--decor-transform", "scaleX(-1)");
    }
  }

  startdecorations(root) {
    this.stopdecorations();

    const slots = [
      "topleft",
      "bottomleft",
      "topright",
      "bottomright",
    ];

    slots.forEach(slot => {
      const data = this.decorations[slot];

      if (data.image && data.image.trim() !== "") {
        this.applydecor(root, slot, data);
      }
    });

    this.startrandomslotdecor(root);
  }

  startrandomslotdecor(root) {
    const enabledslots = [];

    [
      "topleft",
      "bottomleft",
      "topright",
      "bottomright",
    ].forEach(slot => {

      const fixed = this.decorations[slot];

      if (
        fixed.image &&
        fixed.image.trim() !== ""
      ) {
        return;
      }

      if (this.decorrotations[slot].enabled) {
        enabledslots.push(slot);
      }
    });

    if (!enabledslots.length) {
      return;
    }

    if (this.randomdecorations.totalrandom) {
      const runrandomdecorchange = () => {
        const leftslots = enabledslots.filter(slot => slot.includes("left"));

        const rightslots = enabledslots.filter(slot => slot.includes("right"));

        const usedslots = [];

        if (this.randomdecorations.randomslots >= 2 && leftslots.length && rightslots.length) {
          usedslots.push( leftslots[Math.floor(Math.random() * leftslots.length)]);

          usedslots.push( rightslots[Math.floor(Math.random() * rightslots.length)]);
        }

        while (usedslots.length < Math.min(this.randomdecorations.randomslots, enabledslots.length)) {
          const slot = enabledslots[Math.floor(Math.random() * enabledslots.length)];

          if (!usedslots.includes(slot)) {
            usedslots.push(slot);
          }
        }

        enabledslots.forEach(slot => {
          if (!usedslots.includes(slot)) {
            this.applydecor(root, slot, null);

            return;
          }

          const picked = this.pickrandomdecor("random");

          this.applydecor(root, slot, picked);
        });
      };

      runrandomdecorchange();

      const timers = enabledslots.map(slot => Math.max(1, this.decorrotations[slot].timer || 1));

      const randomtimer = Math.min(...timers);

      this.decorintervals.totalrandom = setInterval(() => {
        runrandomdecorchange();
      }, randomtimer * 1000);

      return;
    }

    enabledslots.forEach(slot => {
      const settings = this.decorrotations[slot];

      const runchange = () => {
        const picked = this.getnextdecor(slot, settings.random);

        this.applydecor(root, slot, picked);
      };

      runchange();

      clearInterval(this.decorintervals[slot]);

      this.decorintervals[slot] = setInterval(() => {
          runchange();
        },
        Math.max(1, settings.timer || 1) * 1000);
    });
  }

  stopdecorations() {
    Object.values(this.decorintervals).forEach(interval => {
      clearInterval(interval);
    });

    this.decorintervals = {};
  }

  // ==================== //
  //     CURSOR LOGIC     //
  // ==================== //

  buildcursorpool() {
    return this.cursorpool.filter(style => !!style);
  }

  refillcursorpool() {
    this.cursorstate.remaining = [...this.buildcursorpool()];
  }

  pickrandomcursor() {
    if (!this.cursorstate.remaining.length) {
      this.refillcursorpool();
    }

    if (!this.cursorstate.remaining.length) {
      return null;
    }

    const pickindex = Math.floor(Math.random() * this.cursorstate.remaining.length);

    const pickedstyle = this.cursorstate.remaining[pickindex];

    this.cursorstate.remaining.splice(pickindex, 1);

    return pickedstyle;
  }

  picksequentialcursor() {
    const styles = this.buildcursorpool();

    if (!styles.length) {
      return null;
    }

    const pickedstyle = styles[this.cursorstate.currentindex];

    this.cursorstate.currentindex++;

    if (this.cursorstate.currentindex >= styles.length) {
      this.cursorstate.currentindex = 0;
    }

    return pickedstyle;
  }

  getnextcursor() {
    if (this.cursorsettings.randomcursor) {
      return this.pickrandomcursor();
    }

    return this.picksequentialcursor();
  }

  applycursors(root, styledata) {
    if (!styledata) {
      return;
    }

    if (styledata.default && styledata.default.trim() !== "") {
      this.style.cursor = `url("${styledata.default}"), auto`;
    }
    else {
      this.style.cursor = "auto";
    }

    const interactive = root.querySelectorAll("a, button, input");

    interactive.forEach(el => {
      if (styledata.pointer && styledata.pointer.trim() !== "") {
        el.style.cursor = `url("${styledata.pointer}"), pointer`;
      }
      else {
        el.style.cursor = "pointer";
      }
    });
  }

  startcursorrotation(root) {
    clearInterval(this.cursorinterval);

    this.cursorinterval = null;

    this.cursorstate = {
      remaining: [],
      currentindex: 0,
    };

    const hasmanualcursor = Object.values(this.cursors).some(value => typeof value === "string" && value.trim() !== "");

    if (hasmanualcursor) {
      this.applycursors(root, this.cursors);

      return;
    }

    const styles = this.buildcursorpool();

    if (!styles.length) {
      return;
    }

    const runcursorchange = () => {
      const nextstyle = this.getnextcursor();

      this.applycursors(root, nextstyle);
    };

    runcursorchange();

    if (!this.cursorsettings.cursorrotation) {
      return;
    }

    this.cursorinterval = setInterval(() => {
        runcursorchange();
      },
      Math.max(1, this.cursorsettings.cursortimer || 1 )* 1000);
  }

  stopcursorrotation() {
    clearInterval(this.cursorinterval);

    this.cursorinterval = null;
  }

  // ====================== //
  //     BORDER CONTROL     //
  // ====================== //

  applybordercontrol() {
    this.style.setProperty("--outer-border", this.bordercontrol.outerborder || "");

    this.style.setProperty("--outer-border-radius", this.bordercontrol.outerborderradius || "");

    this.style.setProperty("--player-border", this.bordercontrol.playerborder || "");

    this.style.setProperty("--player-border-radius", this.bordercontrol.playerborderradius || "");

    this.style.setProperty("--display-border", this.bordercontrol.displayborder || "");

    this.style.setProperty("--display-border-radius", this.bordercontrol.displayborderradius || "");
  }

  // ====================== //
  //     ZOOM ZOOM ZOOM     //
  // ====================== //

  applyhostscale() {
    const hostscale = typeof this.hostsettings.scale === "string" && this.hostsettings.scale.trim() !== ""
        ? this.hostsettings.scale
        : "1";

    this.style.transform = `scale(${hostscale})`;
    this.style.transformOrigin = "top left";
  }

  // ============================================================ //
  //     THE CYCLE OF LIFE AND DEATH, AND ALSO PLAYBACK LOGIC     //
  // ============================================================ //

  async connectedCallback() {
    try {
      await this.loadcustomfonts();

      try {
        const response = await fetch(this.playlist.playlistURL);

        if (!response.ok) {
          throw new Error(`playlist fetch failed: ${response.status}`);
        }

        const data = await response.json();

        this.songs = Array.isArray(data)
          ? data
          : [];
      }

      catch (err) {
        console.warn("playlist unavailable:", err);

        this.songs = [];
      }

      if (this.filename) {
        this.filename.textContent = this.songs.length
          ? "press play"
          : "no playlist";
      }

      this.preloaded = null;

      this.nextsong = null;

      this.failedsongs = new Set();

      this.lastplaybacktime = 0;

      this.lastplaybackstamp = 0;

      console.log("startup complete");

      this.starttitlerotation(this.shadowRoot);
      this.starttitlecssrotation(this.shadowRoot);
      this.starttitledropshadowrotation(this.shadowRoot);

      this.starticonrotation(this.shadowRoot);

      this.startrandomiconstyles(this.shadowRoot);

      this.startslidercolorrotation(this.shadowRoot);

      this.startfilecolorrotation(this.shadowRoot);

      this.startbackgroundrotation();

      this.startcontrolbackgroundrotation(this.shadowRoot);

      this.startdecorations(this.shadowRoot);

      this.startcursorrotation(this.shadowRoot);

      this.applybordercontrol();

      this.applyhostscale();

      if (this.songs.length) {
        this.preloadnext();
      }
      else {
        this.startassetpreloading();
      }
    }

    catch (err) {
      console.error("connectedCallback failed:", err);
    }

    this.playpausebtn.onclick = async () => {
      if (!this.audio.src) {

        if (this.preloaded && this.preloaded.song && this.preloaded.audio) {
          this.usepreloadedsong();

          return;
        }

        this.playrandomsong();

        return;
      }

      if (this.audio.paused) {
        try {
          await this.dummyaudio.play();
          await this.audio.play();
        }

        catch (err) {
          if (err.name === "AbortError" || err.name === "NotAllowedError") {
            return;
          }

          console.error(err);
        }
      }

      else {
        this.audio.pause();
        this.dummyaudio.pause();
      }

      this.updateplaybutton();
    };

    this.nextbtn.onclick = () => {
      this.clearplaybackwatchdog();

      if (this.preloaded && this.preloaded.song && this.preloaded.audio) {
        this.usepreloadedsong();

        return;
      }

      this.playrandomsong();
    };

    this.seekbar.addEventListener("input", () => {
      if (!this.audio.duration) {
        return;
      }

      this.audio.currentTime = (this.seekbar.value / 100) * this.audio.duration;
    });

    this.volumebtn.onclick = (e) => {
      e.stopPropagation();

      this.volumewrapper.classList.toggle("show");
    };

    this.volumeslider.addEventListener("input", () => {
      const volume = parseFloat(this.volumeslider.value);

      this.audio.volume = volume;

      let icon = this.icons.volumehigh;

      let state = this.randomstate
        ? this.randomstate.volumehigh
        : null;

      if (volume === 0) {
        icon = this.icons.volumemute;

        state = this.randomstate
          ? this.randomstate.volumemute
          : null;
      }

      else if (volume < 0.5) {
        icon = this.icons.volumelow;

        state = this.randomstate
          ? this.randomstate.volumelow
          : null;
      }

      this.volumeicon.src = icon;

      this.applyiconstate(this.volumeicon, state);
    });

    document.addEventListener("click", this.handleDocumentClick);

    this.audio.ontimeupdate = () => {
      if (!this.audio.duration) {
        return;
      }

      this.seekbar.value = (this.audio.currentTime / this.audio.duration) * 100;

      if (this.audio.currentTime !== this.lastplaybacktime) {
        this.lastplaybacktime = this.audio.currentTime;

        this.lastplaybackstamp = Date.now();

        this.clearplaybackwatchdog();
      }
    };

    this.audio.onended = () => {
      this.clearplaybackwatchdog();

      this.handlesongend();
    };

    this.audio.onplay = () => {
      this.lastplaybacktime = this.audio.currentTime;

      this.lastplaybackstamp = Date.now();

      this.startplaybackwatchdog();

      this.updateplaybutton();
    };

    this.audio.onpause = () => {
      this.dummyaudio.pause();

      this.clearplaybackwatchdog();

      this.updateplaybutton();
    };

    this.audio.onerror = () => {
      console.error("audio error");

      this.clearplaybackwatchdog();

      if (this.audio.src) {
        this.failedsongs.add(this.audio.src);
      }

      this.handlesongend();
    };

    this.audio.onwaiting = () => {
      this.buffering.hidden = false;

      this.startplaybackwatchdog();
    };

    this.audio.onplaying = () => {
      this.buffering.hidden = true;

      this.clearplaybackwatchdog();
    };
  }

  disconnectedCallback() {

    this.stoptitlerotation();
    this.stoptitlecssrotation();
    this.stoptitledropshadowrotation();

    this.stopiconrotation();
    this.stoprandomiconstyles();

    this.stopslidercolorrotation();

    this.stopfilecolorrotation();

    this.stopbackgroundrotation();
    this.stopcontrolbackgroundrotation();

    this.stopdecorations();

    this.stopcursorrotation();

    document.removeEventListener("click", this.handleDocumentClick);

    this.clearplaybackwatchdog();

    this.dummyaudio.pause();
    this.dummyaudio.src = "";
    
    this.assetpreloadqueue = [];
    this.assetpreloadrunning = false;

    clearInterval(this.imagereinterval);
  }

  startplaybackwatchdog(timeout = 30000) {
    this.clearplaybackwatchdog();

    this.playbacktimeout = setTimeout(() => {

      if (this.audio.paused) {
        return;
      }

      const stalled = Date.now() - this.lastplaybackstamp >= timeout;

      if (!stalled) {
        return;
      }

      console.warn("playback stalled, skipping");

      this.audio.pause();

      this.handlesongend();

    }, timeout);
  }

  clearplaybackwatchdog() {
    clearTimeout(this.playbacktimeout);
  }

  updateplaybutton() {
    if (!this.randomstate) {
      return;
    }

    const state = this.audio.paused
      ? this.randomstate.play
      : this.randomstate.pause;

    this.playpauseicon.src = this.audio.paused
      ? this.icons.play
      : this.icons.pause;

    this.applyiconstate(this.playpauseicon, state);
  }

  pickrandom() {
    if (!this.playlist.shuffle) {

      if (!this.songs.length) {
        return -1;
      }

      return this.currentindex >= this.songs.length - 1
        ? 0
        : this.currentindex + 1;
    }
  
    if (!this.remainingtracks.length) {

      this.remainingtracks = [...Array(this.songs.length).keys()];

      if (this.songs.length > 1 && this.currentindex !== -1) {
        this.remainingtracks = this.remainingtracks.filter(idx => idx !== this.currentindex);
      }

      for (let i = this.remainingtracks.length - 1; i > 0; i--) {

        const j = Math.floor(Math.random() * (i + 1));

        [this.remainingtracks[i], this.remainingtracks[j]] = [this.remainingtracks[j], this.remainingtracks[i]];
      }
    }

    return this.remainingtracks.pop();
  }

  preloadnext() {
    if (!this.songs.length) {
      return;
    }

    let idx = null;

    let attempts = 0;

    while (attempts < this.songs.length) {
      const candidate = this.pickrandom();

      const song = this.songs[candidate];

      if (song && song.url && !this.failedsongs.has(song.url)) {
        idx = candidate;
 
        break;
      }
 
      attempts++;
    }
 
    if (idx === null) {
      console.warn("no valid songs left to preload");
 
      return;
    }
 
    const song = this.songs[idx];
 
    const preloadAudio = new Audio();
 
    preloadAudio.preload = "auto";
 
    preloadAudio.volume = this.audio.volume;
 
    preloadAudio.src = song.url;
 
    preloadAudio.load();
 
    preloadAudio.oncanplaythrough = () => {
 
      this.preloaded = {
        index: idx,
        song,
        audio: preloadAudio,
      };
 
      this.nextsong = song;
 
      if (!this.assetpreloadrunning) {
        this.startassetpreloading();
      }
    };
 
    preloadAudio.onerror = () => {
      this.failedsongs.add(song.url);
 
      this.preloaded = null;
    };
  }

  usepreloadedsong() {
    if (!this.preloaded || !this.preloaded.song) {
      return;
    }

    const preload = this.preloaded;

    this.preloaded = null;

    this.currentindex = preload.index;

    this.audio.pause();

    this.audio.src = preload.song.url;

    this.filename.dataset.fulltext = preload.song.name;

    this.filename.textContent = preload.song.name;

    this.applyfilecolor(this.shadowRoot, this.currentfilecolor || this.filecolors);

    this.filename.href = preload.song.url;

    document.title = preload.song.name + " - Pira HxCx Player";

    this.seekbar.value = 0;

    Promise.all([
      this.dummyaudio.play(),
      this.audio.play()
    ]).catch(err => {
      if (err.name === "AbortError" || err.name === "NotAllowedError") {
        return;
      }

      console.error(err);

      this.handlesongend();
    });

    this.updateplaybutton();

    this.preloadnext();
  }

  playrandomsong() {
    if (!this.songs.length) {
      return;
    }

    if (this.preloaded && this.preloaded.song) {
      this.usepreloadedsong();

      return;
    }

    const idx = this.pickrandom();

    this.loadsong(this.songs[idx], idx);
  }

  handlesongend() {

    if (this.preloaded && this.preloaded.song && this.preloaded.audio && this.preloaded.audio.readyState >= 3) {
      this.usepreloadedsong();

      return;
    }

    this.playrandomsong();
  }

  loadsong(song, idx) {
    if (!song) {
      return;
    }

    this.currentindex = idx;

    this.audio.pause();

    this.audio.src = song.url;

    this.audio.load();

    this.filename.dataset.fulltext = song.name;

    this.filename.textContent = song.name;

    this.applyfilecolor(this.shadowRoot, this.currentfilecolor || this.filecolors);

    this.filename.href = song.url;

    document.title = song.name + " - Pira HxCx Player";

    this.seekbar.value = 0;

    Promise.all([
      this.dummyaudio.play(),
      this.audio.play()
    ]).catch(err => {
      if (err.name === "AbortError" || err.name === "NotAllowedError") {
        return;
      }

      console.error(err);

      this.handlesongend();
    });

    this.updateplaybutton();

    this.preloadnext();
  }

  collectassets() {
    const assets = [];

    const collect = value => {
      if (typeof value === "string" && value.trim() !== "" && /^https?:/i.test(value)) {
        assets.push(value);
      }
    };
 
    const scan = obj => {
      if (!obj) {
        return;
      }
 
      if (Array.isArray(obj)) {
        obj.forEach(scan);
        return;
      }
 
      if (typeof obj === "object") {
        Object.values(obj).forEach(scan);
        return;
      }
 
      collect(obj);
    };
 
    scan(this.iconpool);
 
    scan(this.backgroundpool);
 
    scan(this.controlbackgroundpool);
 
    scan(this.titlepool);
 
    scan(this.decorpools);
 
    scan(this.sliderpool);
 
    scan(this.filecolorpool);
 
    scan(this.cursorpool);
 
    return [...new Set(assets)];
  }
 
  preloadasset(url) {
    if (!url || this.preloadedassets.has(url)) {
      return;
    }
 
    this.preloadedassets.add(url);
 
    const img = new Image();
 
    img.decoding = "async";
 
    img.loading = "eager";
 
    img.src = url;
  }
 
  startassetpreloading() {
    if (this.assetpreloadrunning) {
      return;
    }
 
    this.assetpreloadrunning = true;
 
    this.assetpreloadqueue = this.collectassets();
 
    const processqueue = () => {
      if (!this.assetpreloadqueue.length) {
        this.assetpreloadrunning = false;
        return;
      }
 
      const next = this.assetpreloadqueue.shift();
 
      this.preloadasset(next);
 
      if ("requestIdleCallback" in window) {
        requestIdleCallback(processqueue, {
          timeout: 2000
        });
      }
      else {
        setTimeout(processqueue, 150);
      }
    };
  
    processqueue();
  }
}

customElements.define("pira-player", PiraPlayer);