\n if (!this.preserveMultipleSlashes) {\n for (let i = 1; i < parts.length - 1; i++) {\n const p = parts[i];\n // don't squeeze out UNC patterns\n if (i === 1 && p === '' && parts[0] === '')\n continue;\n if (p === '.' || p === '') {\n didSomething = true;\n parts.splice(i, 1);\n i--;\n }\n }\n if (parts[0] === '.' &&\n parts.length === 2 &&\n (parts[1] === '.' || parts[1] === '')) {\n didSomething = true;\n parts.pop();\n }\n }\n // //../ -> /\n let dd = 0;\n while (-1 !== (dd = parts.indexOf('..', dd + 1))) {\n const p = parts[dd - 1];\n if (p && p !== '.' && p !== '..' && p !== '**') {\n didSomething = true;\n parts.splice(dd - 1, 2);\n dd -= 2;\n }\n }\n } while (didSomething);\n return parts.length === 0 ? [''] : parts;\n }\n // First phase: single-pattern processing\n // is 1 or more portions\n // is 1 or more portions\n // is any portion other than ., .., '', or **\n // is . or ''\n //\n // **/.. is *brutal* for filesystem walking performance, because\n // it effectively resets the recursive walk each time it occurs,\n // and ** cannot be reduced out by a .. pattern part like a regexp\n // or most strings (other than .., ., and '') can be.\n //\n // /**/..//
/ -> {/..//
/,/**//
/}\n // // -> /\n // //../ -> /\n // **/**/ -> **/\n //\n // **/*/ -> */**/ <== not valid because ** doesn't follow\n // this WOULD be allowed if ** did follow symlinks, or * didn't\n firstPhasePreProcess(globParts) {\n let didSomething = false;\n do {\n didSomething = false;\n // /**/..//
/ -> {/..//
/,/**//
/}\n for (let parts of globParts) {\n let gs = -1;\n while (-1 !== (gs = parts.indexOf('**', gs + 1))) {\n let gss = gs;\n while (parts[gss + 1] === '**') {\n // /**/**/ -> /**/\n gss++;\n }\n // eg, if gs is 2 and gss is 4, that means we have 3 **\n // parts, and can remove 2 of them.\n if (gss > gs) {\n parts.splice(gs + 1, gss - gs);\n }\n let next = parts[gs + 1];\n const p = parts[gs + 2];\n const p2 = parts[gs + 3];\n if (next !== '..')\n continue;\n if (!p ||\n p === '.' ||\n p === '..' ||\n !p2 ||\n p2 === '.' ||\n p2 === '..') {\n continue;\n }\n didSomething = true;\n // edit parts in place, and push the new one\n parts.splice(gs, 1);\n const other = parts.slice(0);\n other[gs] = '**';\n globParts.push(other);\n gs--;\n }\n // // -> /\n if (!this.preserveMultipleSlashes) {\n for (let i = 1; i < parts.length - 1; i++) {\n const p = parts[i];\n // don't squeeze out UNC patterns\n if (i === 1 && p === '' && parts[0] === '')\n continue;\n if (p === '.' || p === '') {\n didSomething = true;\n parts.splice(i, 1);\n i--;\n }\n }\n if (parts[0] === '.' &&\n parts.length === 2 &&\n (parts[1] === '.' || parts[1] === '')) {\n didSomething = true;\n parts.pop();\n }\n }\n // //../ -> /\n let dd = 0;\n while (-1 !== (dd = parts.indexOf('..', dd + 1))) {\n const p = parts[dd - 1];\n if (p && p !== '.' && p !== '..' && p !== '**') {\n didSomething = true;\n const needDot = dd === 1 && parts[dd + 1] === '**';\n const splin = needDot ? ['.'] : [];\n parts.splice(dd - 1, 2, ...splin);\n if (parts.length === 0)\n parts.push('');\n dd -= 2;\n }\n }\n }\n } while (didSomething);\n return globParts;\n }\n // second phase: multi-pattern dedupes\n // {/*/,//} -> /*/\n // {/,/} -> /\n // {/**/,/} -> /**/\n //\n // {/**/,/**//} -> /**/\n // ^-- not valid because ** doens't follow symlinks\n secondPhasePreProcess(globParts) {\n for (let i = 0; i < globParts.length - 1; i++) {\n for (let j = i + 1; j < globParts.length; j++) {\n const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes);\n if (!matched)\n continue;\n globParts[i] = matched;\n globParts[j] = [];\n }\n }\n return globParts.filter(gs => gs.length);\n }\n partsMatch(a, b, emptyGSMatch = false) {\n let ai = 0;\n let bi = 0;\n let result = [];\n let which = '';\n while (ai < a.length && bi < b.length) {\n if (a[ai] === b[bi]) {\n result.push(which === 'b' ? b[bi] : a[ai]);\n ai++;\n bi++;\n }\n else if (emptyGSMatch && a[ai] === '**' && b[bi] === a[ai + 1]) {\n result.push(a[ai]);\n ai++;\n }\n else if (emptyGSMatch && b[bi] === '**' && a[ai] === b[bi + 1]) {\n result.push(b[bi]);\n bi++;\n }\n else if (a[ai] === '*' &&\n b[bi] &&\n (this.options.dot || !b[bi].startsWith('.')) &&\n b[bi] !== '**') {\n if (which === 'b')\n return false;\n which = 'a';\n result.push(a[ai]);\n ai++;\n bi++;\n }\n else if (b[bi] === '*' &&\n a[ai] &&\n (this.options.dot || !a[ai].startsWith('.')) &&\n a[ai] !== '**') {\n if (which === 'a')\n return false;\n which = 'b';\n result.push(b[bi]);\n ai++;\n bi++;\n }\n else {\n return false;\n }\n }\n // if we fall out of the loop, it means they two are identical\n // as long as their lengths match\n return a.length === b.length && result;\n }\n parseNegate() {\n if (this.nonegate)\n return;\n const pattern = this.pattern;\n let negate = false;\n let negateOffset = 0;\n for (let i = 0; i < pattern.length && pattern.charAt(i) === '!'; i++) {\n negate = !negate;\n negateOffset++;\n }\n if (negateOffset)\n this.pattern = pattern.slice(negateOffset);\n this.negate = negate;\n }\n // set partial to true to test if, for example,\n // \"/a/b\" matches the start of \"/*/b/*/d\"\n // Partial means, if you run out of file before you run\n // out of pattern, then that's fine, as long as all\n // the parts match.\n matchOne(file, pattern, partial = false) {\n const options = this.options;\n // UNC paths like //?/X:/... can match X:/... and vice versa\n // Drive letters in absolute drive or unc paths are always compared\n // case-insensitively.\n if (this.isWindows) {\n const fileDrive = typeof file[0] === 'string' && /^[a-z]:$/i.test(file[0]);\n const fileUNC = !fileDrive &&\n file[0] === '' &&\n file[1] === '' &&\n file[2] === '?' &&\n /^[a-z]:$/i.test(file[3]);\n const patternDrive = typeof pattern[0] === 'string' && /^[a-z]:$/i.test(pattern[0]);\n const patternUNC = !patternDrive &&\n pattern[0] === '' &&\n pattern[1] === '' &&\n pattern[2] === '?' &&\n typeof pattern[3] === 'string' &&\n /^[a-z]:$/i.test(pattern[3]);\n const fdi = fileUNC ? 3 : fileDrive ? 0 : undefined;\n const pdi = patternUNC ? 3 : patternDrive ? 0 : undefined;\n if (typeof fdi === 'number' && typeof pdi === 'number') {\n const [fd, pd] = [file[fdi], pattern[pdi]];\n if (fd.toLowerCase() === pd.toLowerCase()) {\n pattern[pdi] = fd;\n if (pdi > fdi) {\n pattern = pattern.slice(pdi);\n }\n else if (fdi > pdi) {\n file = file.slice(fdi);\n }\n }\n }\n }\n // resolve and reduce . and .. portions in the file as well.\n // dont' need to do the second phase, because it's only one string[]\n const { optimizationLevel = 1 } = this.options;\n if (optimizationLevel >= 2) {\n file = this.levelTwoFileOptimize(file);\n }\n this.debug('matchOne', this, { file, pattern });\n this.debug('matchOne', file.length, pattern.length);\n for (var fi = 0, pi = 0, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {\n this.debug('matchOne loop');\n var p = pattern[pi];\n var f = file[fi];\n this.debug(pattern, p, f);\n // should be impossible.\n // some invalid regexp stuff in the set.\n /* c8 ignore start */\n if (p === false) {\n return false;\n }\n /* c8 ignore stop */\n if (p === GLOBSTAR) {\n this.debug('GLOBSTAR', [pattern, p, f]);\n // \"**\"\n // a/**/b/**/c would match the following:\n // a/b/x/y/z/c\n // a/x/y/z/b/c\n // a/b/x/b/x/c\n // a/b/c\n // To do this, take the rest of the pattern after\n // the **, and see if it would match the file remainder.\n // If so, return success.\n // If not, the ** \"swallows\" a segment, and try again.\n // This is recursively awful.\n //\n // a/**/b/**/c matching a/b/x/y/z/c\n // - a matches a\n // - doublestar\n // - matchOne(b/x/y/z/c, b/**/c)\n // - b matches b\n // - doublestar\n // - matchOne(x/y/z/c, c) -> no\n // - matchOne(y/z/c, c) -> no\n // - matchOne(z/c, c) -> no\n // - matchOne(c, c) yes, hit\n var fr = fi;\n var pr = pi + 1;\n if (pr === pl) {\n this.debug('** at the end');\n // a ** at the end will just swallow the rest.\n // We have found a match.\n // however, it will not swallow /.x, unless\n // options.dot is set.\n // . and .. are *never* matched by **, for explosively\n // exponential reasons.\n for (; fi < fl; fi++) {\n if (file[fi] === '.' ||\n file[fi] === '..' ||\n (!options.dot && file[fi].charAt(0) === '.'))\n return false;\n }\n return true;\n }\n // ok, let's see if we can swallow whatever we can.\n while (fr < fl) {\n var swallowee = file[fr];\n this.debug('\\nglobstar while', file, fr, pattern, pr, swallowee);\n // XXX remove this slice. Just pass the start index.\n if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {\n this.debug('globstar found match!', fr, fl, swallowee);\n // found a match.\n return true;\n }\n else {\n // can't swallow \".\" or \"..\" ever.\n // can only swallow \".foo\" when explicitly asked.\n if (swallowee === '.' ||\n swallowee === '..' ||\n (!options.dot && swallowee.charAt(0) === '.')) {\n this.debug('dot detected!', file, fr, pattern, pr);\n break;\n }\n // ** swallows a segment, and continue.\n this.debug('globstar swallow a segment, and continue');\n fr++;\n }\n }\n // no match was found.\n // However, in partial mode, we can't say this is necessarily over.\n /* c8 ignore start */\n if (partial) {\n // ran out of file\n this.debug('\\n>>> no match, partial?', file, fr, pattern, pr);\n if (fr === fl) {\n return true;\n }\n }\n /* c8 ignore stop */\n return false;\n }\n // something other than **\n // non-magic patterns just have to match exactly\n // patterns with magic have been turned into regexps.\n let hit;\n if (typeof p === 'string') {\n hit = f === p;\n this.debug('string match', p, f, hit);\n }\n else {\n hit = p.test(f);\n this.debug('pattern match', p, f, hit);\n }\n if (!hit)\n return false;\n }\n // Note: ending in / means that we'll get a final \"\"\n // at the end of the pattern. This can only match a\n // corresponding \"\" at the end of the file.\n // If the file ends in /, then it can only match a\n // a pattern that ends in /, unless the pattern just\n // doesn't have any more for it. But, a/b/ should *not*\n // match \"a/b/*\", even though \"\" matches against the\n // [^/]*? pattern, except in partial mode, where it might\n // simply not be reached yet.\n // However, a/b/ should still satisfy a/*\n // now either we fell off the end of the pattern, or we're done.\n if (fi === fl && pi === pl) {\n // ran out of pattern and filename at the same time.\n // an exact hit!\n return true;\n }\n else if (fi === fl) {\n // ran out of file, but still had pattern left.\n // this is ok if we're doing the match as part of\n // a glob fs traversal.\n return partial;\n }\n else if (pi === pl) {\n // ran out of pattern, still have file left.\n // this is only acceptable if we're on the very last\n // empty segment of a file with a trailing slash.\n // a/* should match a/b/\n return fi === fl - 1 && file[fi] === '';\n /* c8 ignore start */\n }\n else {\n // should be unreachable.\n throw new Error('wtf?');\n }\n /* c8 ignore stop */\n }\n braceExpand() {\n return braceExpand(this.pattern, this.options);\n }\n parse(pattern) {\n assertValidPattern(pattern);\n const options = this.options;\n // shortcuts\n if (pattern === '**')\n return GLOBSTAR;\n if (pattern === '')\n return '';\n // far and away, the most common glob pattern parts are\n // *, *.*, and *. Add a fast check method for those.\n let m;\n let fastTest = null;\n if ((m = pattern.match(starRE))) {\n fastTest = options.dot ? starTestDot : starTest;\n }\n else if ((m = pattern.match(starDotExtRE))) {\n fastTest = (options.nocase\n ? options.dot\n ? starDotExtTestNocaseDot\n : starDotExtTestNocase\n : options.dot\n ? starDotExtTestDot\n : starDotExtTest)(m[1]);\n }\n else if ((m = pattern.match(qmarksRE))) {\n fastTest = (options.nocase\n ? options.dot\n ? qmarksTestNocaseDot\n : qmarksTestNocase\n : options.dot\n ? qmarksTestDot\n : qmarksTest)(m);\n }\n else if ((m = pattern.match(starDotStarRE))) {\n fastTest = options.dot ? starDotStarTestDot : starDotStarTest;\n }\n else if ((m = pattern.match(dotStarRE))) {\n fastTest = dotStarTest;\n }\n const re = AST.fromGlob(pattern, this.options).toMMPattern();\n return fastTest ? Object.assign(re, { test: fastTest }) : re;\n }\n makeRe() {\n if (this.regexp || this.regexp === false)\n return this.regexp;\n // at this point, this.set is a 2d array of partial\n // pattern strings, or \"**\".\n //\n // It's better to use .match(). This function shouldn't\n // be used, really, but it's pretty convenient sometimes,\n // when you just want to work with a regex.\n const set = this.set;\n if (!set.length) {\n this.regexp = false;\n return this.regexp;\n }\n const options = this.options;\n const twoStar = options.noglobstar\n ? star\n : options.dot\n ? twoStarDot\n : twoStarNoDot;\n const flags = new Set(options.nocase ? ['i'] : []);\n // regexpify non-globstar patterns\n // if ** is only item, then we just do one twoStar\n // if ** is first, and there are more, prepend (\\/|twoStar\\/)? to next\n // if ** is last, append (\\/twoStar|) to previous\n // if ** is in the middle, append (\\/|\\/twoStar\\/) to previous\n // then filter out GLOBSTAR symbols\n let re = set\n .map(pattern => {\n const pp = pattern.map(p => {\n if (p instanceof RegExp) {\n for (const f of p.flags.split(''))\n flags.add(f);\n }\n return typeof p === 'string'\n ? regExpEscape(p)\n : p === GLOBSTAR\n ? GLOBSTAR\n : p._src;\n });\n pp.forEach((p, i) => {\n const next = pp[i + 1];\n const prev = pp[i - 1];\n if (p !== GLOBSTAR || prev === GLOBSTAR) {\n return;\n }\n if (prev === undefined) {\n if (next !== undefined && next !== GLOBSTAR) {\n pp[i + 1] = '(?:\\\\/|' + twoStar + '\\\\/)?' + next;\n }\n else {\n pp[i] = twoStar;\n }\n }\n else if (next === undefined) {\n pp[i - 1] = prev + '(?:\\\\/|' + twoStar + ')?';\n }\n else if (next !== GLOBSTAR) {\n pp[i - 1] = prev + '(?:\\\\/|\\\\/' + twoStar + '\\\\/)' + next;\n pp[i + 1] = GLOBSTAR;\n }\n });\n return pp.filter(p => p !== GLOBSTAR).join('/');\n })\n .join('|');\n // need to wrap in parens if we had more than one thing with |,\n // otherwise only the first will be anchored to ^ and the last to $\n const [open, close] = set.length > 1 ? ['(?:', ')'] : ['', ''];\n // must match entire pattern\n // ending in a * or ** will make it less strict.\n re = '^' + open + re + close + '$';\n // can match anything, as long as it's not this.\n if (this.negate)\n re = '^(?!' + re + ').+$';\n try {\n this.regexp = new RegExp(re, [...flags].join(''));\n /* c8 ignore start */\n }\n catch (ex) {\n // should be impossible\n this.regexp = false;\n }\n /* c8 ignore stop */\n return this.regexp;\n }\n slashSplit(p) {\n // if p starts with // on windows, we preserve that\n // so that UNC paths aren't broken. Otherwise, any number of\n // / characters are coalesced into one, unless\n // preserveMultipleSlashes is set to true.\n if (this.preserveMultipleSlashes) {\n return p.split('/');\n }\n else if (this.isWindows && /^\\/\\/[^\\/]+/.test(p)) {\n // add an extra '' for the one we lose\n return ['', ...p.split(/\\/+/)];\n }\n else {\n return p.split(/\\/+/);\n }\n }\n match(f, partial = this.partial) {\n this.debug('match', f, this.pattern);\n // short-circuit in the case of busted things.\n // comments, etc.\n if (this.comment) {\n return false;\n }\n if (this.empty) {\n return f === '';\n }\n if (f === '/' && partial) {\n return true;\n }\n const options = this.options;\n // windows: need to use /, not \\\n if (this.isWindows) {\n f = f.split('\\\\').join('/');\n }\n // treat the test path as a set of pathparts.\n const ff = this.slashSplit(f);\n this.debug(this.pattern, 'split', ff);\n // just ONE of the pattern sets in this.set needs to match\n // in order for it to be valid. If negating, then just one\n // match means that we have failed.\n // Either way, return on the first hit.\n const set = this.set;\n this.debug(this.pattern, 'set', set);\n // Find the basename of the path by looking for the last non-empty segment\n let filename = ff[ff.length - 1];\n if (!filename) {\n for (let i = ff.length - 2; !filename && i >= 0; i--) {\n filename = ff[i];\n }\n }\n for (let i = 0; i < set.length; i++) {\n const pattern = set[i];\n let file = ff;\n if (options.matchBase && pattern.length === 1) {\n file = [filename];\n }\n const hit = this.matchOne(file, pattern, partial);\n if (hit) {\n if (options.flipNegate) {\n return true;\n }\n return !this.negate;\n }\n }\n // didn't get any hits. this is success if it's a negative\n // pattern, failure otherwise.\n if (options.flipNegate) {\n return false;\n }\n return this.negate;\n }\n static defaults(def) {\n return minimatch.defaults(def).Minimatch;\n }\n}\n/* c8 ignore start */\nexport { AST } from './ast.js';\nexport { escape } from './escape.js';\nexport { unescape } from './unescape.js';\n/* c8 ignore stop */\nminimatch.AST = AST;\nminimatch.Minimatch = Minimatch;\nminimatch.escape = escape;\nminimatch.unescape = unescape;\n//# sourceMappingURL=index.js.map","import { Minimatch } from 'minimatch';\n\n/**\n * Router class for managing client-side navigation in a Single Page Application (SPA).\n * This class enables the creation of a simple client-side router to handle navigation within an SPA,\n * allowing developers to define routes and associated actions.\n *\n * ## Usage:\n * 1. Import the router in your project:\n * ```javascript\n * import router from './path/to/Router';\n * ```\n * 2. Define routes by using the `addRoute` method:\n * ```javascript\n * router.addRoute('/', 'home-component', () => {\n * // Function to execute when navigating to the root path\n * console.log('Navigated to the root path');\n * }, () => {\n * // Function to execute when navigating away from the root path\n * console.log('Navigated away from the root path');\n * });\n * ```\n * 3. Integrate with an `` tag using the `href` attribute:\n * ```html\n * Home\n * About\n * ```\n * Clicking on these links will trigger the router to navigate to the specified paths.\n * 4. Define a route outlet where the component associated with the route will be rendered :\n * ```html\n * \n * ``\n * @class\n */\nclass Router extends EventTarget {\n /**\n * The fallback route to be used in case no matching route is found.\n * @private\n * @type {string|null}\n */\n #defaultPath = null;\n\n /**\n * The currently active route.\n * @private\n * @type {{ path: Minimatch, start: function, destroy: function }|null}\n */\n #currentRoute = null;\n\n /**\n * The query parameters associated with the current route.\n * @private\n * @type {object}\n */\n #currentQueryParams = {};\n\n /**\n * An array containing registered route objects with their patterns and associated actions.\n * @private\n * @type {Array<{ path: Minimatch, start: function, destroy: function }>}\n */\n #routes = [];\n\n constructor() {\n super();\n\n // Event listener for the popstate event\n window.addEventListener('popstate', () => {\n const entries = new URL(window.location.href).searchParams.entries();\n const queryParams = Object.fromEntries(entries);\n\n this.#handleRouteChange(window.location.pathname, queryParams, true);\n });\n }\n\n /**\n * Adds a route to the router.\n *\n * @param {string} pattern - The path of the route, can be a glob pattern.\n * @param {string} component - The component to be rendered when the route is navigated to.\n * @param {function} start - The function to be called when the route is navigated to.\n * @param {function} destroy - The function to be called when the route is navigated away from.\n */\n addRoute(pattern, component = null, start = () => {\n }, destroy = () => {\n }) {\n const path = new Minimatch(pattern, { matchBase: true });\n\n this.#routes.push({\n path,\n start,\n component,\n destroy\n });\n }\n\n /**\n * Checks if the given path is the current active route.\n *\n * @param {string} path - The path to check.\n * @returns {boolean} - True if the given path is the current active route, false otherwise.\n */\n isActiveRoute(path) {\n return this.#currentRoute &&\n this.#currentRoute.path === this.findRoute(path)?.path;\n }\n\n /**\n * Finds a route based on the given path.\n *\n * @param {string} path - The path to find the route for.\n * @returns {object} - The route object if found, otherwise undefined.\n */\n findRoute(path) {\n return this.#routes.find(r => r.path.match(path));\n }\n\n /**\n * Navigates to the specified path.\n *\n * @param {string} path - The path to navigate to.\n * @param {object} [queryParams={}] - (Optional) query parameters to be associated with the route.\n */\n navigateTo(path, queryParams = {}) {\n const url = new URL(window.location.href);\n const normalizedPath = '/' + path.trim().replace(/^\\/+/, '');\n\n url.pathname = normalizedPath.startsWith(this.base)\n ? normalizedPath\n : this.base + normalizedPath;\n url.search = new URLSearchParams(queryParams).toString();\n\n window.history.pushState({}, '', url.href);\n this.#handleRouteChange(path, queryParams);\n }\n\n /**\n * Updates the state by navigating to the current path with the provided query parameters.\n *\n * @param {Object} queryParams - The new query parameters to be merged with the current ones.\n */\n updateState(queryParams, keysToRemove = []) {\n const filteredParams = Object.fromEntries(\n Object.entries(this.#currentQueryParams)\n .filter(([key]) => !keysToRemove.includes(key))\n );\n\n this.navigateTo(\n window.location.pathname,\n { ...filteredParams, ...queryParams }\n );\n }\n\n /**\n * Replaces the current state by navigating to the current path with the provided query parameters,\n * discarding the current query parameters.\n *\n * @param {Object} newParams - The new query parameters to replace the current ones.\n */\n replaceState(newParams) {\n this.navigateTo(window.location.pathname, newParams);\n }\n\n /**\n * Handles a change in the route.\n *\n * @private\n * @param {string} path - The path of the new route.\n * @param {object} [queryParams={}] - (Optional) The query parameters associated with the route.\n * @param {boolean} [popstate=false] - (Optional) if a popstate is at the origin of this route change.\n */\n #handleRouteChange(path, queryParams = {}, popstate = false) {\n if (this.isActiveRoute(path)) {\n if (!this.#matchCurrentParams(queryParams)) {\n this.#currentQueryParams = queryParams;\n this.dispatchEvent(new CustomEvent('queryparams', {\n detail: {\n route: this.#currentRoute,\n popstate,\n queryParams\n }\n }));\n }\n\n return;\n }\n\n const route = this.findRoute(path);\n\n if (route) {\n this.#updateCurrentRoute(route, queryParams, popstate);\n } else if (this.#defaultPath) {\n this.#handleRouteChange(this.#defaultPath, queryParams, popstate);\n } else {\n throw Error(`No route found for '${path}'`);\n }\n }\n\n /**\n * Checks if the given query parameters match the current query parameters.\n *\n * @private\n * @param {object} params - The query parameters to compare.\n * @returns {boolean} - True if the given parameters match the current query parameters, false otherwise.\n */\n #matchCurrentParams(params) {\n const paramsKeys = Object.keys(params);\n\n if (paramsKeys.length !== Object.keys(this.#currentQueryParams).length) {\n return false;\n }\n\n return paramsKeys.every(k => this.#currentQueryParams[k] === params[k]);\n }\n\n /**\n * Updates the current route and dispatches the 'routechanged' event.\n *\n * @private\n * @param {object} route - The route object.\n * @param {object} queryParams - The query parameters associated with the route.\n * @param {boolean} [popstate=false] - (Optional) if a popstate is at the origin of this route change.\n */\n #updateCurrentRoute(route, queryParams, popstate = false) {\n this.#currentRoute?.destroy();\n this.#currentRoute = route;\n this.#currentQueryParams = queryParams;\n route.start(queryParams);\n this.dispatchEvent(new CustomEvent('routechanged', {\n detail: {\n route,\n popstate,\n queryParams\n }\n }));\n }\n\n /**\n * Initiates the router based on the current window location.\n *\n * @param defaultPath - The fallback path when no path is found during route resolving.\n */\n start({ defaultPath }) {\n const url = new URL(window.location.href);\n const path = url.pathname;\n const queryParams = Object.fromEntries(url.searchParams.entries());\n\n this.#defaultPath = defaultPath;\n this.base = this.findRoute(path)\n ? path.replace(/\\/[^/]+\\/?$/, '/')\n : path;\n\n this.base = this.base.replace(/\\/+$/, '');\n\n this.#handleRouteChange(path, queryParams);\n }\n\n get queryParams() {\n return this.#currentQueryParams;\n }\n\n get currentRoute() {\n return this.#currentRoute;\n }\n}\n\n// Export a singleton instance of the router\nexport default new Router();\n","/**\n * Initialized the demo player.\n *\n * @module\n */\nimport '../dialog/demo-dialog-component';\nimport PreferencesProvider\n from '../../layout/content/settings/preferences-provider';\nimport router from '../../router/router';\nimport Pillarbox from '@srgssr/pillarbox-web';\n\nconst DEMO_PLAYER_ID = 'player';\nconst DEFAULT_OPTIONS = {\n fill: true,\n restoreEl: true\n};\n\n/**\n * Creates and configures a Pillarbox player.\n *\n * @param {Object} options - (Optional) options to customize the player behaviour.\n *\n * @returns {Object} The configured Pillarbox player instance.\n */\nconst createPlayer = (options = {}) => {\n const preferences = PreferencesProvider.loadPreferences();\n\n window.player = new Pillarbox(DEMO_PLAYER_ID, {\n ...DEFAULT_OPTIONS,\n ...{\n muted: preferences.muted ?? true,\n autoplay: preferences.autoplay ?? false,\n debug: preferences.debug ?? false\n },\n ...options\n });\n\n return window.player;\n};\n\n/**\n * Disposes of the Pillarbox video player instance.\n */\nconst destroyPlayer = () => {\n Pillarbox.getPlayer(DEMO_PLAYER_ID).dispose();\n window.player = null;\n};\n\n// Expose Pillarbox and player in the window object for debugging\nwindow.pillarbox = Pillarbox;\n// Configure the dialog\nconst playerDialog = document.querySelector('demo-dialog');\n\nconst toParams = (keySystems) => {\n const vendor = Object.keys(keySystems ?? {})[0];\n\n if (!vendor) {\n return {};\n }\n\n return {\n vendor,\n ...keySystems[vendor]\n };\n};\n\nconst toKeySystem = (params) => {\n if (!params.vendor) {\n return undefined;\n }\n\n const keySystem = {};\n const { certificateUrl, licenseUrl } = params;\n\n keySystem[params.vendor] = { certificateUrl, licenseUrl };\n\n return keySystem;\n};\n\nexport const asQueryParams = ({ src, type, keySystems }) => {\n return new URLSearchParams({ src, type, ...toParams(keySystems) }).toString();\n};\n\nplayerDialog.addEventListener('close', () => {\n destroyPlayer();\n router.updateState({}, ['src', 'type', 'vendor', 'certificateUrl', 'licenseUrl']);\n});\n\nconst loadPlayerFromRouter = (e) => {\n const params = e.detail.queryParams;\n\n if ('src' in params) {\n const { src, type } = params;\n const keySystems = toKeySystem(params);\n\n openPlayerModal({ src, type, keySystems });\n }\n};\n\nrouter.addEventListener('routechanged', loadPlayerFromRouter);\nrouter.addEventListener('queryparams', loadPlayerFromRouter);\n\n/**\n * Opens a modal containing a video player with specified source and type. Can only\n * load URN if the type 'srgssr/urn`is explicitly provided, otherwise the created\n * Pillarbox player tries to guess the type.\n *\n * @param {object} options - An object containing the source and type of the video to be played.\n * @param {string} options.src - The source URL of the video.\n * @param {string} [options.type] - (Optional) The type/format of the video (e.g., 'video/mp4').\n * @param {object} [options.keySystems] - (Optional) The DRM configuration for DRM protected sources.\n * @param {object} [options.playerOptions] - (Optional) Additional configuration for the player.\n * @param {Boolean} shouldUpdateRouter - Whether the router should be updated or not (Default: true).\n *\n * @returns {Object} The configured Pillarbox player instance.\n */\nexport const openPlayerModal = (\n { src, type, keySystems, playerOptions },\n shouldUpdateRouter = true\n) => {\n const player = createPlayer(playerOptions ?? {});\n\n playerDialog.open = true;\n player.src({ src, type, keySystems });\n\n if (shouldUpdateRouter) {\n router.updateState({ src, type, ...toParams(keySystems) });\n }\n\n return player;\n};\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}export{i as Directive,t as PartType,e as directive};\n//# sourceMappingURL=directive.js.map\n","import{noChange as t}from\"../lit-html.js\";import{directive as s,Directive as i,PartType as r}from\"../directive.js\";\n/**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const e=s(class extends i{constructor(t){if(super(t),t.type!==r.ATTRIBUTE||\"class\"!==t.name||t.strings?.length>2)throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\")}render(t){return\" \"+Object.keys(t).filter((s=>t[s])).join(\" \")+\" \"}update(s,[i]){if(void 0===this.st){this.st=new Set,void 0!==s.strings&&(this.nt=new Set(s.strings.join(\" \").split(/\\s/).filter((t=>\"\"!==t))));for(const t in i)i[t]&&!this.nt?.has(t)&&this.st.add(t);return this.render(i)}const r=s.element.classList;for(const t of this.st)t in i||(r.remove(t),this.st.delete(t));for(const t in i){const s=!!i[t];s===this.st.has(t)||this.nt?.has(t)||(s?(r.add(t),this.st.add(t)):(r.remove(t),this.st.delete(t)))}return t}});export{e as classMap};\n//# sourceMappingURL=class-map.js.map\n","import { html, LitElement, unsafeCSS } from 'lit';\nimport { animations, theme } from '../../../theme/theme';\nimport componentCSS from './load-media-form-component.scss?inline';\nimport { classMap } from 'lit/directives/class-map.js';\n\n/**\n * LoadMediaFormComponent is a LitElement that provides a user interface for loading media content.\n *\n * @element load-media-form\n *\n * @fires LoadMediaFormComponent#submit-media - Dispatched when the user submits media with the specified details.\n *\n * @prop {String} src - The URL or URN of the media content to be loaded.\n * @prop {{vendor: String, certificateUrl: String, licenseUrl: String}} drmSettings - DRM settings for the loaded media.\n */\nexport class LoadMediaFormComponent extends LitElement {\n static properties = {\n src: { type: String },\n drmSettings: { type: Object },\n drmSettingsShown: { state: true, type: Boolean }\n };\n\n static styles = [theme, animations, unsafeCSS(componentCSS)];\n\n constructor() {\n super();\n\n this.src = '';\n this.#initDrmSettings();\n }\n\n #initDrmSettings() {\n this.drmSettings = {\n vendor: '',\n certificateUrl: '',\n licenseUrl: ''\n };\n }\n\n #submitMedia() {\n const src = this.src;\n const type = src.startsWith('urn:') ? 'srgssr/urn' : undefined;\n const keySystems = this.#keySystems;\n\n /**\n * Custom event dispatched by LoadMediaFormComponent when the user submits media.\n *\n * @event LoadMediaFormComponent#submit-media\n * @type {CustomEvent}\n * @property {Object} detail - The event detail object.\n * @property {string} detail.src - The URL or URN of the media content to be loaded.\n * @property {string | undefined} detail.type - The type of media. Undefined if the type cannot be determined.\n * @property {Object | undefined} detail.keySystems - DRM key systems for the loaded media.\n */\n this.dispatchEvent(new CustomEvent('submit-media', {\n detail: { src, type, keySystems }\n }));\n }\n\n #handleLoadBarKeyUp(event) {\n this.src = event.target.value;\n\n if (event.key === 'Enter' && this.src) {\n this.#submitMedia();\n }\n }\n\n get #keySystems() {\n if (!this.drmSettings?.vendor) {\n return undefined;\n }\n\n const certificateUrl = this.drmSettings.certificateUrl;\n const licenseUrl = this.drmSettings.licenseUrl;\n\n if (this.drmSettings.vendor === 'com.apple.fps.1_0') {\n return { [this.drmSettings.vendor]: { certificateUrl, licenseUrl } };\n }\n\n return { [this.drmSettings.vendor]: { licenseUrl } };\n }\n\n render() {\n const btnSettingsClassMap = {\n spin: this.drmSettingsShown === true,\n 'spin-back': this.drmSettingsShown === false\n };\n\n return html`\n e.target.classList.remove('fade-in')}\">\n
\n insert_link\n \n \n
\n \n ${this.#drmSettingsTemplate()}\n\n
\n
\n `;\n }\n\n updated(_changedProperties) {\n super.updated(_changedProperties);\n\n if (_changedProperties.has('drmSettingsShown') && this.drmSettingsShown) {\n this.shadowRoot.querySelector('.drm-settings-container').classList.add('active');\n }\n }\n\n #onFormAnimationEnd(e) {\n if (e.target.classList.contains('shrink')) {\n e.target.classList.remove('active');\n }\n\n e.target.classList.remove('fade-in-grow', 'shrink');\n }\n\n #formAnimationClassMap() {\n return {\n 'fade-in-grow': this.drmSettingsShown === true,\n shrink: this.drmSettingsShown === false\n };\n }\n\n #drmSettingsTemplate() {\n return html`\n \n `;\n }\n}\n\ncustomElements.define('load-media-form', LoadMediaFormComponent);\n","import router from '../../router/router';\nimport { html, LitElement, unsafeCSS } from 'lit';\nimport componentCSS from './content-link-component.scss?inline';\n\n/**\n * A component for rendering a content link.\n *\n * @prop {string} href - The URL to navigate to.\n * @prop {string} title - The title attribute for the link.\n *\n * @csspart a - The anchor element.\n * @csspart title - The title span within the anchor.\n * @csspart description - The slot for additional description content within the anchor.\n *\n * @example\n * \n * Additional Description Content\n * \n */\nexport class ContentLinkComponent extends LitElement {\n static properties = {\n href: {}\n };\n\n static styles = unsafeCSS(componentCSS);\n\n #onClick = (event) => {\n event.preventDefault();\n\n const url = new URL(`${window.location.origin}/${this.href}`);\n const queryParams = Object.fromEntries(url.searchParams.entries());\n\n router.navigateTo(url.pathname, queryParams);\n };\n\n connectedCallback() {\n super.connectedCallback();\n this.addEventListener('click', this.#onClick);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener('click', this.#onClick);\n }\n\n render() {\n return html`\n \n ${this.title}\n \n \n `;\n }\n}\n\ncustomElements.define('content-link', ContentLinkComponent);\n","const EXAMPLES = {\n SRGSSR: [\n {\n title: 'Horizontal video',\n src: 'urn:rts:video:6820736',\n type: 'srgssr/urn'\n },\n {\n title: 'Square video',\n src: 'urn:rts:video:8393241',\n type: 'srgssr/urn'\n },\n {\n title: 'Vertical video',\n src: 'urn:rts:video:13444390',\n type: 'srgssr/urn'\n },\n {\n title: 'Token-protected video',\n description: 'Ski alpin, Slalom Messieurs',\n src: 'urn:swisstxt:video:rts:c56ea781-99ad-40c3-8d9b-444cc5ac3aea',\n type: 'srgssr/urn'\n },\n {\n title: 'Superfluously token-protected video',\n description: 'Telegiornale flash',\n src: 'urn:rsi:video:15916771',\n type: 'srgssr/urn'\n },\n {\n title: 'DRM-protected video',\n description: 'Top Models 8870',\n src: 'urn:rts:video:13639837',\n type: 'srgssr/urn'\n },\n {\n title: 'Live video',\n description: 'SRF 1',\n src: 'urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651',\n type: 'srgssr/urn'\n },\n {\n title: 'DVR video livestream',\n description: 'RTS 1',\n src: 'urn:rts:video:3608506',\n type: 'srgssr/urn'\n },\n {\n title: 'DVR audio livestream',\n description: 'Couleur 3 (DVR)',\n src: 'urn:rts:audio:3262363',\n type: 'srgssr/urn'\n },\n {\n title: 'On-demand audio stream',\n description: 'Il lavoro di TerraProject per una fotografia documentaria',\n src: 'urn:rsi:audio:8833144',\n type: 'srgssr/urn'\n },\n {\n title: 'Expired URN',\n description: 'Content that is not available anymore',\n src: 'urn:rts:video:13382911',\n type: 'srgssr/urn'\n },\n {\n title: 'Unknown URN',\n description: 'Content that does not exist',\n src: 'urn:srf:video:unknown',\n type: 'srgssr/urn'\n }\n ],\n HLS: [\n {\n title: 'VOD - HLS',\n description: 'Switzerland says sorry! The fondue invasion',\n src: 'https://swi-vod.akamaized.net/videoJson/47603186/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'VOD - HLS (short)',\n description: 'Des violents orages ont touché Ajaccio, chef-lieu de la Corse, jeudi',\n src: 'https://rts-vod-amd.akamaized.net/ww/13317145/f1d49f18-f302-37ce-866c-1c1c9b76a824/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Brain Farm Skate Phantom Flex',\n description: '4K video',\n src: 'https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Video livestream - HLS',\n description: 'Couleur 3 en vidéo (live)',\n src: 'https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Video livestream with DVR - HLS',\n description: 'Couleur 3 en vidéo (DVR)',\n src: 'https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Video livestream with DVR and timestamps - HLS',\n description: 'Tageschau',\n src: 'https://tagesschau.akamaized.net/hls/live/2020115/tagesschau/tagesschau_1/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Audio livestream - HLS',\n description: 'Couleur 3 (DVR)',\n src: 'https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple Basic 4:3',\n description: '4x3 aspect ratio, H.264 @ 30Hz',\n src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple Basic 16:9',\n description: '16x9 aspect ratio, H.264 @ 30Hz',\n src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple Advanced 16:9 (TS)',\n description: '16x9 aspect ratio, H.264 @ 30Hz and 60Hz, Transport stream',\n src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple Advanced 16:9 (fMP4)',\n description: '16x9 aspect ratio, H.264 @ 30Hz and 60Hz, Fragmented MP4',\n src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple Advanced 16:9 (HEVC/H.264)',\n description: '16x9 aspect ratio, H.264 and HEVC @ 30Hz and 60Hz',\n src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple Atmos',\n src: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple WWDC Keynote 2023',\n src: 'https://events-delivery.apple.com/0105cftwpxxsfrpdwklppzjhjocakrsk/m3u8/vod_index-PQsoJoECcKHTYzphNkXohHsQWACugmET.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Apple tv trailer',\n description: 'Lot of audios and subtitles choices',\n src: 'https://play-edge.itunes.apple.com/WebObjects/MZPlayLocal.woa/hls/subscription/playlist.m3u8?cc=CH&svcId=tvs.vds.4021&a=1522121579&isExternal=true&brandId=tvs.sbd.4000&id=518077009&l=en-GB&aec=UHD\\n',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Multiple subtitles and audio tracks',\n description: 'On some devices codec may crash',\n src: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: '4K, HEVC',\n src: 'https://cdn.bitmovin.com/content/encoding_test_dash_hls/4k/hls/4k_profile/master.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'VoD, single audio track',\n src: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'AES-128',\n src: 'https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'HLS - Fragmented MP4',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'HLS - Alternate audio language',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'HLS - Audio only',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8?filter=(type!=%22video%22)',\n type: 'application/x-mpegURL'\n },\n {\n title: 'HLS - Trickplay',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Limiting bandwidth use',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?max_bitrate=800000',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Dynamic Track Selection',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?filter=%28type%3D%3D%22audio%22%26%26systemBitrate%3C100000%29%7C%7C%28type%3D%3D%22video%22%26%26systemBitrate%3C1024000%29',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Pure live',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Timeshift (5 minutes)',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?time_shift=300',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Live audio',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?filter=(type!=%22video%22)',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Pure live (scte35)',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'fMP4, clear',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-fmp4.ism/.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'fMP4, HEVC 4K',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hevc.ism/.m3u8',\n type: 'application/x-mpegURL'\n },\n {\n title: 'Test1',\n description: 'Forced subtitles',\n src: 'https://prd.vod-srgssr.ch/origin/1053457/fr/master.m3u8?complexSubs=true',\n type: 'application/x-mpegURL'\n }\n ],\n DASH: [\n {\n title: 'VoD - Dash (H264)',\n src: 'https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'VoD - Dash Widewine cenc (H264)',\n src: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd',\n type: 'application/dash+xml',\n keySystems: {\n 'com.widevine.alpha': 'https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test'\n }\n },\n {\n title: 'VoD - Dash (H265)',\n src: 'https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'VoD - Dash widewine cenc (H265)',\n src: 'https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd',\n type: 'application/dash+xml',\n keySystems: {\n 'com.widevine.alpha': 'https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test'\n }\n },\n {\n title: 'VoD - Dash - MP4',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Fragmented MP4',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - TrickPlay',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Tiled thumbnails (live/timeline)',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Accessibility - hard of hearing',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hoh-subs.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Single - fragmented TTML',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-en.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Multiple - RFC 5646 language tags',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-rfc5646.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Multiple - fragmented TTML',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-ttml.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Audio only',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd?filter=(type!=%22video%22)',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Multiple audio codecs',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-codec.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Alternate audio language',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Accessibility - audio description',\n src: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-desc-aud.ism/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Pure live',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - Timeshift (5 minutes)',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd?time_shift=300',\n type: 'application/dash+xml'\n },\n {\n title: 'Dash - DVB DASH low latency',\n src: 'https://demo.unified-streaming.com/k8s/live/stable/live-low-latency.isml/.mpd',\n type: 'application/dash+xml'\n }\n ],\n MP4: [\n {\n title: 'VOD - MP4',\n description: 'The dig',\n src: 'https://media.swissinfo.ch/media/video/dddaff93-c2cd-4b6e-bdad-55f75a519480/rendition/154a844b-de1d-4854-93c1-5c61cd07e98c.mp4',\n type: 'video/mp4'\n },\n {\n title: 'AVC Progressive',\n src: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/MI201109210084_mpeg-4_hd_high_1080p25_10mbits.mp4',\n type: 'video/mp4'\n }\n ],\n AOD: [\n {\n title: 'Audio HLS',\n description: 'Content with PTS rollover',\n src: 'https://cdn.rts.ch/audio-sample/playlist.m3u8',\n type: 'application/x-mpegURL'\n }\n ]\n};\n\nexport default EXAMPLES;\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nfunction*o(o,f){if(void 0!==o){let i=0;for(const t of o)yield f(t,i++)}}export{o as map};\n//# sourceMappingURL=map.js.map\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nfunction n(n,r,t){return n?r(n):t?.(n)}export{n as when};\n//# sourceMappingURL=when.js.map\n","import router from '../../../router/router';\nimport { css, html, LitElement } from 'lit';\nimport { animations, theme } from '../../../theme/theme';\nimport './load-media-form-component';\nimport '../../../components/content-link/content-link-component';\nimport Examples from './examples';\nimport { map } from 'lit/directives/map.js';\nimport { when } from 'lit/directives/when.js';\nimport { asQueryParams, openPlayerModal } from '../../../components/player/player';\n\n/**\n * A web component that represents the examples page.\n *\n * @element examples-page\n */\nexport class ExamplesPage extends LitElement {\n static styles = [\n theme, animations, css`\n .example-section p {\n margin-bottom: 0;\n color: var(--color-5);\n font-size: var(--size-3);\n text-align: left;\n }`\n ];\n\n render() {\n return html`\n openPlayerModal(e.detail)}\">\n \n\n \n e.target.classList.remove('fade-in')}\">\n ${map(Object.entries(Examples), ([section, examples]) => html`\n \n ${section}
\n ${map(examples, example => html`\n \n ${when(example.description, () => html`\n ${example.title}\n `)}\n \n `)}\n \n `)}\n
\n `;\n }\n}\n\ncustomElements.define('examples-page', ExamplesPage);\nrouter.addRoute('examples', 'examples-page');\n","import { html, LitElement, unsafeCSS } from 'lit';\nimport spinnerCss from './spinner-component.scss?inline';\nimport { animations } from '../../theme/theme';\n\n/**\n * A spinner component.\n *\n * @element loading-spinner\n */\nexport class SpinnerComponent extends LitElement {\n static properties = {\n loading: { type: Boolean, reflect: true }\n };\n\n static styles = [animations, unsafeCSS(spinnerCss)];\n\n constructor() {\n super();\n this.loading = false;\n }\n\n render() {\n return html`\n \n `;\n }\n}\n\ncustomElements.define('loading-spinner', SpinnerComponent);\n","import { html, LitElement } from 'lit';\nimport { animations } from '../../theme/theme';\n\nconst DEFAULT_INT_OPTIONS = {\n root: null,\n rootMargin: '0px',\n threshold: 0.1\n};\n\n/**\n * Attach an Intersection Observer to a target element and execute a callback when it becomes visible.\n *\n * @param {Element} target - The target element to observe.\n * @param {Function} callback - The callback function to execute when the target is intersecting.\n * @param {Object} options - (Optional) Options to configure the Intersection Observer.\n * @param {Element} [options.root=null] - The element that is used as the viewport for checking visibility.\n * @param {string} [options.rootMargin='0px'] - Margin around the root. Can have values similar to CSS margin property.\n * @param {number} [options.threshold=0.1] - The threshold at which the callback will be triggered.\n */\nconst onIntersecting = (target, callback, options = DEFAULT_INT_OPTIONS) => {\n new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n callback();\n }\n });\n }, options).observe(target);\n};\n\n/**\n * This web component acts as a sentinel for an Intersection Observer,\n * dispatching an 'intersecting' event when the observed element intersects\n * with the view.\n *\n * @element intersection-observer\n * @csspart sentinel - the sentinel element.\n *\n * @fires IntersectionObserverComponent#intersecting - Dispatched when the observed element intersects with the view.\n */\nexport class IntersectionObserverComponent extends LitElement {\n static styles = [\n animations\n ];\n\n firstUpdated(_changedProperties) {\n super.firstUpdated(_changedProperties);\n onIntersecting(\n this.renderRoot.querySelector('div'),\n () => {\n /**\n * Custom event dispatched by IntersectionObserverComponent when the observed element intersects\n * with the view.\n *\n * @event IntersectionObserverComponent#intersecting\n * @type {CustomEvent}\n * @property {Object} detail - The event detail object.\n * @property {string} detail.src - The URL or URN of the media content to be loaded.\n * @property {string | undefined} detail.type - The type of media. Undefined if the type cannot be determined.\n * @property {Object | undefined} detail.keySystems - DRM key systems for the loaded media.\n */\n this.dispatchEvent(new CustomEvent('intersecting'));\n }\n );\n }\n\n render() {\n return html`\n \n `;\n }\n}\n\ncustomElements.define('intersection-observer', IntersectionObserverComponent);\n","import { css, html, LitElement } from 'lit';\nimport { animations, theme } from '../../theme/theme';\n\n/**\n * A custom web component that provides a button to scroll to the top of the page.\n *\n * @element scroll-to-top-button\n */\nexport class ScrollToTopComponent extends LitElement {\n static styles = [\n theme,\n animations,\n css`\n .scroll-to-top-button {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--size-7);\n height: var(--size-7);\n padding: 0;\n border: none;\n border-radius: var(--radius-round);\n }\n\n .scroll-to-top-button i {\n font-size: var(--size-8);\n }\n `];\n\n render() {\n return html`\n \n `;\n }\n}\n\ncustomElements.define('scroll-to-top-button', ScrollToTopComponent);\n","/**\n * Converts a given string to kebab case, spaces are replaced with hyphens\n * and all letters are converted to lowercase.\n *\n * @param {string} str - The input string to be converted to kebab case.\n * @returns {string} - The input string converted to kebab case.\n *\n * @example\n * const result = toKebabCase(\"Hello World\");\n * console.log(result); // Output: \"hello-world\"\n *\n * @example\n * const result = toKebabCase(\"CamelCase Example\");\n * console.log(result); // Output: \"camelcase-example\"\n */\nexport const toKebabCase = (str) => str.replace(/\\s+/g, '-').toLowerCase();\n","import { toKebabCase } from '../../../utils/string-utils';\n\n/**\n * Manages the state of a lists page, allowing navigation and retrieval of section data.\n *\n * @class\n */\nclass ListsPageStateManager {\n /**\n * Creates an instance of ListsPageStateManager.\n *\n * @constructor\n * @param {Array} root - The root level of the lists page.\n */\n constructor(root) {\n /**\n * Stack to keep track of the traversal steps for navigation.\n *\n * @type {Array<{level: Array, sectionIndex: number, nodeIndex: number}>}\n */\n this.stack = [];\n /**\n * The current level of the content tree.\n *\n * @type {Array}\n */\n this.level = root;\n }\n\n /**\n * Initializes the state manager with the provided section, business unit, and nodes.\n *\n * @async\n * @param {string} section - The section to initialize.\n * @param {string} bu - The business unit associated with the section.\n * @param {string} nodes - A comma-separated string of nodes representing the initial state.\n * @returns {Promise} - A promise that resolves when initialization is complete.\n *\n * @example\n * // Example Usage:\n * await stateManager.initialize(\"radio-shows\", \"rts\", \"a9e7621504c6959e35c3ecbe7f6bed0446cdf8da,urn:rts:show:radio:9801398\");\n */\n async initialize(section, bu, nodes) {\n if (!section || !bu) {\n return;\n }\n\n const sectionIndex = this.#findSectionIndex(section);\n const nodeIndex = this.#findNodeIndex(this.level[sectionIndex].nodes, bu);\n\n await this.fetchNextState(sectionIndex, nodeIndex);\n\n for (const nodeStr of (nodes?.split(',') || [])) {\n const nodeIndex = this.#findNodeIndex(this.level[0].nodes, nodeStr);\n\n await this.fetchNextState(0, nodeIndex);\n }\n }\n\n /**\n * Fetches the next state based on the provided section index and node index.\n *\n * @param {number} sectionIndex - The index of the section.\n * @param {number} nodeIndex - The index of the node.\n * @returns {Promise} - A promise that resolves when the state is fetched.\n */\n async fetchNextState(sectionIndex, nodeIndex) {\n const section = this.level[sectionIndex];\n\n this.stack.push({ level: this.level, sectionIndex, nodeIndex });\n this.level = [await section.resolve(section.nodes[nodeIndex])];\n }\n\n /**\n * Fetches the previous state based on the provided stack index.\n *\n * @param {number} stackIndex - The index in the stack.\n */\n fetchPreviousState(stackIndex) {\n this.level = this.stack[stackIndex].level;\n this.stack.splice(stackIndex);\n }\n\n /**\n * Checks if the specified section at the given index is a leaf section.\n *\n * @param {number} sectionIndex - The index of the section.\n *\n * @returns {boolean} - True if the section is a leaf section, false otherwise.\n */\n isLeafSection(sectionIndex) {\n return this.level[sectionIndex]?.isLeaf();\n }\n\n /**\n * Retrieves the node at the specified section and node indices.\n *\n * @param {number} sectionIndex - The index of the section.\n * @param {number} nodeIndex - The index of the node.\n *\n * @returns {any} - The retrieved node.\n */\n retrieveNode(sectionIndex, nodeIndex) {\n return this.level[sectionIndex]?.nodes[nodeIndex];\n }\n\n /**\n * Gets the root level of the content tree.\n *\n * @returns {Array} - The root level of the content tree.\n */\n get root() {\n return this.stack[0]?.level || this.level;\n }\n\n /**\n * Return the current state of this manager as query params that are parsable\n * by {@link #initialize}.\n *\n * @returns {{}|{bu: string, section: string, nodes?: string}} The current state as query params.\n */\n get params() {\n return ListsPageStateManager.#params(this.stack);\n }\n\n static #params(stack) {\n if (stack.length === 0) {\n return {};\n }\n\n const root = stack[0];\n const rootSection = root.level[root.sectionIndex];\n const nodes = stack.slice(1).map(n => {\n const node = n.level[n.sectionIndex].nodes[n.nodeIndex];\n\n return node.id || node.urn;\n });\n const params = {\n section: toKebabCase(rootSection.title),\n bu: rootSection.nodes[root.nodeIndex].toLowerCase()\n };\n\n if (nodes && nodes.length) {\n params.nodes = nodes.join(',');\n }\n\n return params;\n }\n\n paramsAt(sectionIndex, nodeIndex) {\n return ListsPageStateManager.#params(\n [...this.stack, { level: this.level, sectionIndex, nodeIndex }]\n );\n }\n\n /**\n * Finds the index of a section based on its title in kebab case.\n *\n * @private\n * @param {string} sectionStr - The section title to find.\n * @returns {number} - The index of the section.\n *\n * @example\n * const index = stateManager.#findSectionIndex(\"Products\");\n */\n #findSectionIndex(sectionStr) {\n const normalizedSectionStr = toKebabCase(sectionStr).toLowerCase();\n\n return this.level\n .map(s => toKebabCase(s.title).toLowerCase())\n .findIndex(title => title === normalizedSectionStr);\n }\n\n /**\n * Finds the index of a node based on its string representation.\n *\n * @private\n * @param {Array} nodes - The array of nodes to search.\n * @param {string} str - The string representation of the node to find.\n *\n * @returns {number} - The index of the node.\n */\n #findNodeIndex(nodes, str) {\n const normalizedStr = str.toLowerCase();\n\n return nodes\n .map(n => (n.urn || n.id || n.toString()).toLowerCase())\n .findIndex(n => n === normalizedStr);\n }\n}\n\nexport default ListsPageStateManager;\n","const IL_DEFAULT_HOST = 'il.srgssr.ch';\nconst DEFAULT_QUERY_PARAMS = {\n vector: 'srgplay'\n};\nconst DEFAULT_SEARCH_PARAMS = {\n includeAggregations: false,\n includeSuggestions: false,\n sortBy: 'default',\n sortDir: 'desc',\n pageSize: 50,\n ...DEFAULT_QUERY_PARAMS\n};\nconst DEFAULT_SHOWLIST_PARAMS = {\n onlyActiveShows: true,\n ...DEFAULT_QUERY_PARAMS\n};\n\nconst toMedia = ({ title, urn, mediaType, date, duration }) => ({\n title, urn, mediaType, date, duration\n});\n\n/**\n * Class representing a provider for the integration layer.\n *\n * @class\n */\nclass ILProvider {\n /**\n * Creates an instance of ILProvider.\n *\n * @param {string} [hostName='il.srgssr.ch'] - The hostname for the integration layer (without the protocol).\n */\n constructor(hostName = IL_DEFAULT_HOST) {\n this.baseUrl = `${hostName}/integrationlayer/2.0`;\n }\n\n /**\n * Performs a search for media content based on the provided business unit and query.\n *\n * @param {string} bu - The business unit for which the search is performed (rsi, rtr, rts, srf or swi).\n * @param {string} query - The search query.\n * @param {AbortSignal} [signal=undefined] - (Optional) An abort signal,\n * allows to abort the query through an abort controller.\n *\n * @returns {Promise<{ results: Array<{ title: string, urn: string }>, next: function }>} - A promise\n * that resolves to an object containing :\n * - `results`: An array of objects containing the title, URN, media type, date, and duration of the search results.\n * - `next`: A function that, when called, retrieves the next set of data and returns a new object with updated results and the next function.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async search(bu, query, signal = undefined) {\n const data = await this.#fetch(\n `/${bu.toLowerCase()}/searchResultMediaList`,\n { ...DEFAULT_SEARCH_PARAMS, q: query },\n signal\n );\n const toResults = (data) => data.searchResultMediaList.map(toMedia);\n\n return {\n results: toResults(data),\n next: data.next ? this.#nextProvider(data.next, toResults) : undefined\n };\n }\n\n /**\n * Retrieves a list of topics for the specified business unit and transmission type.\n *\n * @param {string} bu - The business unit for which to retrieve topics (rsi, rtr, rts, srf, or swi).\n * @param {string} [transmission='tv'] - The transmission type ('tv' or 'radio').\n *\n * @returns {Promise>} - A promise that resolves to an array\n * of objects containing the title and URN of the topics.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async topics(bu, transmission = 'tv') {\n const data = await this.#fetch(`/${bu.toLowerCase()}/topicList/${transmission}`);\n\n return data.topicList.map(\n ({ title, urn }) => ({ title, urn })\n );\n }\n\n /**\n * Retrieves the latest media content for a specific topic.\n *\n * @param {string} topicUrn - The URN (Unique Resource Name) of the topic.\n * @param {number} [pageSize=30] - The maximum number of episodes to retrieve.\n *\n * @returns {Promise<{ results: Array<{ title: string, urn: string }>, next: function }>} - A promise\n * that resolves to an object containing :\n * - `results`: An array of objects containing the title, URN, media type, date, and duration of the medias.\n * - `next`: A function that, when called, retrieves the next set of data and returns a new object with updated results and the next function.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async latestByTopic(topicUrn, pageSize = 30) {\n const data = await this.#fetch(`/mediaList/latest/byTopicUrn/${topicUrn}`, { pageSize });\n\n const toResults = (data) => data.mediaList.map(toMedia);\n\n return {\n results: toResults(data),\n next: data.next ? this.#nextProvider(data.next, toResults) : undefined\n };\n }\n\n /**\n * Retrieves a list of shows for the specified business unit, transmission type, and ordering.\n *\n * @param {string} bu - The business unit for which to retrieve shows (rsi, rtr, rts, srf, or swi).\n * @param {string} [pageSize='unlimited'] - The maximum number of shows to retrieve. Use 'unlimited' for all shows.\n * @param {string} [transmission='tv'] - The transmission type ('tv' or 'radio').\n *\n * @returns {Promise>} - A promise that resolves to an array\n * of objects containing the title and URN of the shows.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async shows(bu, pageSize = 'unlimited', transmission = 'tv') {\n const data = await this.#fetch(\n `/${bu.toLowerCase()}/showList/${transmission}/alphabetical`,\n { ...DEFAULT_SHOWLIST_PARAMS, pageSize }\n );\n\n return data.showList.map(\n ({ title, urn }) => ({ title, urn })\n );\n }\n\n /**\n * Retrieves the latest media content for a specific show.\n *\n * @param {string} showUrn - The URN (Unique Resource Name) of the show.\n * @param {number} [pageSize=30] - The maximum number of episodes to retrieve.\n *\n * @returns {Promise<{ results: Array<{ title: string, urn: string }>, next: function }>} - A promise\n * that resolves to an object containing :\n * - `results`: An array of objects containing the title, URN, media type, date, and duration of the medias.\n * - `next`: A function that, when called, retrieves the next set of data and returns a new object with updated results and the next function.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async latestByShow(showUrn, pageSize = 30) {\n const data = await this.#fetch(\n `/episodeComposition/latestByShow/byUrn/${showUrn}`,\n { ...DEFAULT_QUERY_PARAMS, pageSize }\n );\n\n const toResults = (data) => data.episodeList\n .map(({ mediaList }) => mediaList[0])\n .map(toMedia);\n\n return {\n results: toResults(data),\n next: data.next ? this.#nextProvider(data.next, toResults) : undefined\n };\n }\n\n /**\n * Retrieves editorial media content for the specified business unit.\n *\n * @param {string} bu - The business unit for which to retrieve editorial media (rsi, rtr, rts, srf, or swi).\n * @param {number} [pageSize=30] - The maximum number of editorial media items to retrieve.\n *\n * @returns {Promise<{ results: Array<{ title: string, urn: string }>, next: function }>} - A promise\n * that resolves to an object containing :\n * - `results`: An array of objects containing the title, URN, media type, date, and duration of the medias.\n * - `next`: A function that, when called, retrieves the next set of data and returns a new object with updated results and the next function.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async editorial(bu, pageSize = 30) {\n const data = await this.#fetch(\n `/${bu.toLowerCase()}/mediaList/video/editorial`,\n { ...DEFAULT_QUERY_PARAMS, pageSize }\n );\n\n const toResults = (data) => data.mediaList.map(toMedia);\n\n return {\n results: toResults(data),\n next: data.next ? this.#nextProvider(data.next, toResults) : undefined\n };\n }\n\n /**\n * Retrieves livestream media content for the specified business unit and media type.\n *\n * @param {string} bu - The business unit for which to retrieve livestreams (rsi, rtr, rts, srf, or swi).\n * @param {string} [mediaType='video'] - The media type ('video' or 'audio').\n *\n * @returns {Promise>} - A promise that resolves to an array\n * of objects containing the title and URN of the livestream media content.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async livestreams(bu, mediaType = 'video') {\n const data = await this.#fetch(`/${bu.toLowerCase()}/mediaList/${mediaType}/livestreams`);\n\n return data.mediaList.map(toMedia);\n }\n\n /**\n * Retrieves scheduled livestream media content for the specified business unit.\n *\n * @param {string} bu - The business unit for which to retrieve scheduled livestreams (rsi, rtr, rts, srf, or swi).\n * @param {number} [pageSize=10] - The maximum number of scheduled livestreams to retrieve.\n *\n * @returns {Promise<{ results: Array<{ title: string, urn: string }>, next: function }>} - A promise\n * that resolves to an object containing :\n * - `results`: An array of objects containing the title, URN, media type, date, and duration of the medias.\n * - `next`: A function that, when called, retrieves the next set of data and returns a new object with updated results and the next function.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async scheduledLivestream(bu, pageSize = 10) {\n const data = await this.#fetch(\n `/${bu.toLowerCase()}/mediaList/video/scheduledLivestreams`,\n { ...DEFAULT_QUERY_PARAMS, pageSize }\n );\n const toResults = (data) => data.mediaList.map(toMedia);\n\n return {\n results: toResults(data),\n next: data.next ? this.#nextProvider(data.next, toResults) : undefined\n };\n }\n\n /**\n * Retrieves media content for the livecenter for the specified business unit.\n *\n * @param {string} bu - The business unit for which to retrieve livecenter media (rsi, rtr, rts, srf, or swi).\n * @param {number} [pageSize=10] - The maximum number of livecenter media items to retrieve.\n *\n * @returns {Promise<{ results: Array<{ title: string, urn: string }>, next: function }>} - A promise\n * that resolves to an object containing :\n * - `results`: An array of objects containing the title, URN, media type, date, and duration of the medias.\n * - `next`: A function that, when called, retrieves the next set of data and returns a new object with updated results and the next function.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async livecenter(bu, pageSize = 10) {\n const data = await this.#fetch(\n `/${bu.toLowerCase()}/mediaList/video/scheduledLivestreams/livecenter`,\n { ...DEFAULT_QUERY_PARAMS, pageSize }\n );\n const toResults = (data) => data.mediaList.map(toMedia);\n\n return {\n results: toResults(data),\n next: data.next ? this.#nextProvider(data.next, toResults) : undefined\n };\n }\n\n /**\n * Retrieves a list of channels for the specified business unit and transmission type.\n *\n * @param {string} bu - The business unit for which to retrieve channels (rsi, rtr, rts, srf, or swi).\n * @param {string} [transmission='radio'] - The transmission type ('tv' or 'radio').\n *\n * @returns {Promise>} - A promise that resolves to an array\n * of objects containing the title and ID of the channels.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async channels(bu, transmission = 'radio') {\n const data = await this.#fetch(`/${bu.toLowerCase()}/channelList/${transmission}`);\n\n return data.channelList.map(\n ({ title, id }) => ({ title, id })\n );\n }\n\n /**\n * Retrieves radio shows for the specified business unit and channel.\n *\n * @param {string} bu - The business unit for which to retrieve radio shows (rsi, rtr, rts, srf, or swi).\n * @param {string} channelId - The ID of the channel.\n * @param {string} [pageSize='unlimited'] - The maximum number of radio shows to retrieve. Use 'unlimited' for all shows.\n *\n * @returns {Promise>} - A promise that resolves to an array\n * of objects containing the title and URN of the radio shows.\n *\n * @throws {Promise} - A rejected promise with the response object if the fetch request fails.\n */\n async radioShowByChannel(bu, channelId, pageSize = 'unlimited') {\n const data = await this.#fetch(\n `/${bu.toLowerCase()}/showList/radio/alphabeticalByChannel/${channelId}`,\n { ...DEFAULT_QUERY_PARAMS, pageSize }\n );\n\n return data.showList.map(\n ({ title, urn }) => ({ title, urn })\n );\n }\n\n /**\n * Asynchronously fetches data from the IL for the specified path and parameters.\n *\n * @private\n * @param {string} path - The path to fetch data from.\n * @param {Object} [params=DEFAULT_QUERY_PARAMS] - (Optional) parameters for the request.\n * @param {AbortSignal} [signal=undefined] - (Optional) AbortSignal to abort the request.\n *\n * @returns {Promise<*>} A Promise that resolves to the JSON response data.\n *\n * @throws {Response} If the HTTP response is not ok (status code other than 2xx).\n * @throws {Error} If the fetch operation fails for any other reason.\n */\n async #fetch(path, params = DEFAULT_QUERY_PARAMS, signal = undefined) {\n const queryParams = new URLSearchParams(params).toString();\n const url = `https://${this.baseUrl}/${path.replace(/^\\/+/, '')}?${queryParams}`;\n\n return fetch(url, { signal }).then(response => {\n if (!response.ok) {\n return Promise.reject(response);\n }\n\n return response.json();\n }).catch((reason) => {\n return Promise.reject(reason);\n });\n }\n\n /**\n * Generates a function that, when called, retrieves the next set of data and\n * returns a new object with updated results and the next function.\n *\n * @private\n * @template T - The type of data returned by the resultMapper function.\n *\n * @param {string} nextUrl - The URL for fetching the next set of data.\n * @param {(data: any) => T} resultMapper - A function to map the raw data to the desired format.\n *\n * @returns {(signal?: AbortSignal) => Promise<{ results: T, next: function }>} - A function that,\n * when called, retrieves the next set of data and returns a new object with updated results and the next function.\n */\n #nextProvider(nextUrl, resultMapper) {\n return async(signal = undefined) => {\n const nextData = await fetch(nextUrl, { signal }).then(response => {\n if (!response.ok) {\n return Promise.reject(response);\n }\n\n return response.json();\n }).catch((reason) => {\n return Promise.reject(reason);\n });\n\n const nextResults = resultMapper(nextData);\n\n return {\n results: nextResults,\n next: this.#nextProvider(nextData.next, resultMapper)\n };\n };\n }\n}\n\nexport default new ILProvider();\n","import ilProvider from '../../../utils/il-provider';\n\n/**\n * A section within the content hierarchy.\n *\n * @property {string} title - The title of the section node.\n * @property {string[]} values - An array of values associated with the section.\n * @property {function} resolve - A function that, when defined, resolves the next level\n * of the hierarchy asynchronously.\n */\nclass Section {\n /**\n * Creates an instance of Section.\n *\n * @param {Object} options - The options for creating the section.\n * @param {string} options.title - The title of the section.\n * @param {string[]} options.nodes - An array of nodes associated with the section.\n * @param {function} [options.resolve] - (Optional) A function that, when defined, resolves the next level\n * of the hierarchy asynchronously.\n * @param {function} [options.next] - (Optional) A function that, when defined, resolves the next level\n * of the hierarchy asynchronously.\n */\n constructor({\n title,\n nodes,\n resolve = undefined,\n next = undefined\n }) {\n this.title = title;\n this.nodes = nodes;\n this.resolve = resolve;\n this.next = next;\n }\n\n /**\n * Checks if the node is a leaf node (i.e., has no further levels to resolve).\n *\n * @returns {boolean} True if the node is a leaf node, false otherwise.\n */\n isLeaf() {\n return !this.resolve;\n }\n\n /**\n * Fetch more nodes from the next function if available.\n * @param signal\n * @returns {Promise<*>}\n */\n async fetchNext(signal = undefined) {\n if (!this.next) return;\n const data = await this.next(signal);\n\n this.next = data.next;\n this.nodes.push(...data.results);\n\n return data.results;\n }\n}\n\nconst toNodesAndNext = (data) => ({\n nodes: data.results,\n next: data.next\n});\n\n/**\n * An asynchronous tree-like structure that allows traversing the SRG SSR content\n * by category in a hierarchical fashion.\n *\n * @type {Section[]}\n */\nexport const listsSections = [\n new Section({\n title: 'TV Topics',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF', 'SWI'],\n resolve: async(bu) => new Section({\n title: `${bu} TV Topics`,\n nodes: await ilProvider.topics(bu),\n resolve: async(topic) => new Section({\n title: topic.title,\n ...toNodesAndNext(await ilProvider.latestByTopic(topic.urn))\n })\n })\n }),\n new Section({\n title: 'TV Shows',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF', 'SWI'],\n resolve: async(bu) => new Section({\n title: `${bu} TV Shows`,\n nodes: await ilProvider.shows(bu),\n resolve: async(show) => new Section({\n title: show.title,\n ...toNodesAndNext(await ilProvider.latestByShow(show.urn))\n })\n })\n }),\n new Section({\n title: 'TV Latest Videos',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF'],\n resolve: async(bu) => new Section({\n title: `${bu} TV Latest Videos`,\n ...toNodesAndNext(await ilProvider.editorial(bu))\n })\n }),\n new Section({\n title: 'TV Livestreams',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF'],\n resolve: async(bu) => new Section({\n title: `${bu} TV Livestreams`,\n nodes: await ilProvider.livestreams(bu)\n })\n }),\n new Section({\n title: 'Live web',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF'],\n resolve: async(bu) => new Section({\n title: `${bu} Live web`,\n ...toNodesAndNext(await ilProvider.scheduledLivestream(bu))\n })\n }),\n new Section({\n title: 'Live center',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF'],\n resolve: async(bu) => new Section({\n title: `${bu} Live center`,\n nodes: await ilProvider.livecenter(bu)\n })\n }),\n new Section({\n title: 'Radio Shows',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF'],\n resolve: async(bu) => new Section({\n title: `${bu} Radio Channels`,\n nodes: await ilProvider.channels(bu),\n resolve: async(channel) => new Section({\n title: `${channel.title} Radio shows`,\n nodes: await ilProvider.radioShowByChannel(bu, channel.id),\n resolve: async(show) => new Section({\n title: show.title,\n ...toNodesAndNext(await ilProvider.latestByShow(show.urn))\n })\n })\n })\n }),\n new Section({\n title: 'Radio Livestreams',\n nodes: ['RSI', 'RTR', 'RTS', 'SRF'],\n resolve: async(bu) => new Section({\n title: `${bu} Radio Livestreams`,\n nodes: await ilProvider.livestreams(bu, 'audio')\n })\n })\n];\n","import { html, LitElement, unsafeCSS } from 'lit';\nimport { animations, theme } from '../../../theme/theme';\nimport router from '../../../router/router';\nimport componentCSS from './lists-page.scss?inline';\nimport '../../../components/spinner/spinner-component';\nimport '../../../components/intersection-observer/intersection-observer-component';\nimport '../../../components/scroll-to-top/scroll-to-top-component';\nimport { map } from 'lit/directives/map.js';\nimport { when } from 'lit/directives/when.js';\nimport ListsPageStateManager from './lists-page-state-manager';\nimport { listsSections } from './lists-sections';\nimport Pillarbox from 'video.js';\n\nexport class ListsPage extends LitElement {\n static properties = {\n loading: { state: true, type: Boolean },\n stack: { state: true, type: Array },\n level: { state: true, type: Object },\n nextPage: { state: true, type: Function }\n };\n\n static styles = [\n theme, animations, unsafeCSS(componentCSS)\n ];\n\n /**\n * The abort controller for handling search cancellation.\n *\n * @private\n * @type {AbortController}\n */\n #abortController = new AbortController();\n /**\n * Keeps track of the state of the list page : the current level in display, as\n * well as the traversal stack.\n *\n * @private\n * @type {ListsPageStateManager}\n */\n #stateManager;\n /**\n * The reference to the query params changed event handler.\n *\n * @private\n * @type {Function}\n */\n #onQueryParamsChanged;\n\n constructor() {\n super();\n\n this.loading = false;\n this.#stateManager = new ListsPageStateManager(listsSections);\n this.#updateState();\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.#onQueryParamsChanged = async(event) => {\n if (!event.detail.popstate) return;\n\n this.abortFetch();\n const manager = new ListsPageStateManager(this.#stateManager.root);\n const { section, bu, nodes } = event.detail.queryParams;\n\n this.loading = true;\n try {\n await manager.initialize(section, bu, nodes);\n this.#stateManager = manager;\n this.#updateState();\n } finally {\n this.loading = false;\n }\n };\n router.addEventListener('queryparams', this.#onQueryParamsChanged);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.abortFetch();\n router.removeEventListener('queryparams', this.#onQueryParamsChanged);\n }\n\n firstUpdated(_changedProperties) {\n super.firstUpdated(_changedProperties);\n this.#onQueryParamsChanged(\n { detail: { popstate: true, queryParams: router.queryParams } }\n );\n }\n\n #updateState() {\n this.stack = [...this.#stateManager.stack];\n this.level = [...this.#stateManager.level];\n }\n\n /**\n * Navigates to the specified section and node in the content tree.\n *\n * @param {number} sectionIndex - The index of the section.\n * @param {number} nodeIndex - The index of the node.\n */\n async navigateTo(sectionIndex, nodeIndex) {\n if (!this.#stateManager.isLeafSection(sectionIndex)) {\n this.abortFetch();\n this.loading = true;\n try {\n await this.#stateManager.fetchNextState(sectionIndex, nodeIndex);\n this.#updateState();\n } finally {\n this.loading = false;\n }\n }\n }\n\n /**\n * Aborts the previous fetch by cancelling the associated abort signal and\n * creates a new abort controller for the next fetch.\n *\n * @returns {AbortSignal} - The abort signal associated with the new fetch.\n */\n abortFetch() {\n this.#abortController?.abort('New search launched');\n this.#abortController = new AbortController();\n\n return this.#abortController.signal;\n }\n\n #toMediaButtonParams(node) {\n return new URLSearchParams({ ...router.queryParams, src: node.urn, type: 'srgssr/urn' }).toString();\n }\n\n #renderMediaButton(node) {\n const date = new Intl.DateTimeFormat('fr-CH').format(new Date(node.date));\n const duration = Pillarbox.formatTime(node.duration / 1000);\n\n return html`\n \n \n ${node.mediaType === 'VIDEO' ? 'movie' : 'audiotrack'}\n | ${date} | ${duration}\n
\n \n `;\n }\n\n #toLevelParams(sectionIdx, nodeIdx) {\n const params = this.#stateManager.paramsAt(sectionIdx, nodeIdx);\n\n return new URLSearchParams(params).toString();\n }\n\n #renderLevelButton(node, sectionIdx, nodeIdx) {\n return html`\n \n \n `;\n }\n\n async #nextPage(section) {\n const signal = this.abortFetch();\n\n await section.fetchNext(signal);\n this.#updateState();\n }\n\n #renderNodes(nodes, sectionIdx) {\n const firstSection = this.level[0];\n const hasIntesectionObserver = this.level.length === 1 && firstSection.next;\n\n return html`\n ${map(nodes, (node, idx) => html`\n ${when(node.mediaType, () => this.#renderMediaButton(node, idx), () => this.#renderLevelButton(node, sectionIdx, idx))}\n `)}\n ${when(hasIntesectionObserver, () => html`\n this.#nextPage(firstSection)}\">\n \n `)}\n `;\n }\n\n async #onSectionsClicked(e) {\n const button = e.target.closest('content-link');\n\n if (this.loading || !('nodeIdx' in button.dataset)) return;\n\n const sectionIndex = button.dataset.sectionIdx;\n const nodeIndex = button.dataset.nodeIdx;\n\n await this.navigateTo(sectionIndex, nodeIndex);\n }\n\n #renderResults() {\n return html`\n e.target.classList.remove('fade-in')}\"\n @click=\"${this.#onSectionsClicked.bind(this)}\">\n ${map(this.level, (section, idx) => html`\n \n ${section.title}
\n ${this.#renderNodes(section.nodes, idx)}\n \n `)}\n
\n `;\n }\n\n #renderSpinner() {\n return html`\n e.target.classList.remove('slide-up-fade-in')}\">\n \n `;\n }\n\n #renderScrollToTopBtn() {\n return html`\n `;\n }\n\n #onNavigationClicked(e) {\n if (e.target.tagName.toLowerCase() !== 'button') return;\n this.abortFetch();\n this.#stateManager.fetchPreviousState(e.target.dataset.navigationIdx);\n this.#updateState();\n router.updateState(this.#stateManager.params, ['section', 'bu', 'nodes']);\n }\n\n #renderNavigation() {\n return html`\n \n ${when(this.stack.length > 0, () => html`\n \n `)}\n ${map(this.stack.slice(1), (step, idx) => html`\n chevron_right\n \n `)}\n
\n `;\n }\n\n render() {\n const renderScrollBtn = this.level.length === 1 && this.level[0].next;\n\n return html`\n ${this.#renderNavigation()}\n ${when(this.loading, this.#renderSpinner.bind(this), this.#renderResults.bind(this))}\n ${when(renderScrollBtn, this.#renderScrollToTopBtn.bind(this))}\n `;\n }\n}\n\ncustomElements.define('lists-page', ListsPage);\nrouter.addRoute('lists', 'lists-page');\n","import { html, LitElement, unsafeCSS } from 'lit';\nimport { animations, theme } from '../../../theme/theme';\nimport componentCSS from './search-bar-component.scss?inline';\nimport Pillarbox from 'video.js';\n\nconst DEFAULT_BU = 'rsi';\n\n/**\n * A search bar component for filtering content based on a query and business unit.\n *\n * @element search-bar\n *\n * @fires SearchBarComponent#change - Dispatched when the value of the search bar changes.\n */\nexport class SearchBarComponent extends LitElement {\n static properties = {\n bu: { type: String },\n query: { type: String }\n };\n\n static styles = [theme, animations, unsafeCSS(componentCSS)];\n\n constructor() {\n super();\n this.bu = DEFAULT_BU;\n this.query = '';\n }\n\n #handleSearchBarKeyUp() {\n this.query = this.renderRoot.querySelector('input').value;\n }\n\n #handleSelectChange(e) {\n this.bu = e.target.value;\n }\n\n updated(_changedProperties) {\n super.updated(_changedProperties);\n\n if (['bu', 'query'].some(property => _changedProperties.has(property))) {\n const query = this.query ?? '';\n const bu = this.bu ?? DEFAULT_BU;\n\n /**\n * Custom event dispatched by SearchBarComponent when the value of the bar changes.\n *\n * @event SearchBarComponent#change\n * @type {CustomEvent}\n * @property {Object} detail - The event detail object.\n * @property {string} detail.query - The query on the search bar.a\n * @property {string} detail.bu - The selected bu.\n */\n this.dispatchEvent(new CustomEvent('change', {\n detail: { query, bu }\n }));\n }\n }\n\n #clearSearchBar() {\n this.query = '';\n this.renderRoot.querySelector('input').value = '';\n }\n\n render() {\n return html`\n e.target.classList.remove('fade-in')}\">\n \n \n \n
\n `;\n }\n}\n\ncustomElements.define('search-bar', SearchBarComponent);\n","import { html, LitElement, unsafeCSS } from 'lit';\nimport { animations, theme } from '../../../theme/theme';\nimport router from '../../../router/router';\nimport componentCSS from './search-page.scss?inline';\nimport './search-bar-component';\nimport ilProvider from '../../../utils/il-provider';\nimport '../../../components/spinner/spinner-component';\nimport '../../../components/intersection-observer/intersection-observer-component';\nimport '../../../components/scroll-to-top/scroll-to-top-component';\nimport '../../../components/content-link/content-link-component';\nimport { map } from 'lit/directives/map.js';\nimport Pillarbox from 'video.js';\nimport { when } from 'lit/directives/when.js';\nimport { classMap } from 'lit/directives/class-map.js';\n\nexport class SearchPage extends LitElement {\n static properties = {\n loading: {\n state: true,\n type: Boolean\n },\n results: {\n state: true,\n type: Array\n },\n nextPage: {\n state: true,\n type: Function\n }\n };\n\n static styles = [\n theme, animations, unsafeCSS(componentCSS)\n ];\n\n /**\n * The abort controller for handling search cancellation.\n *\n * @private\n * @type {AbortController}\n */\n #abortController = new AbortController();\n /**\n * The reference to the query params changed event handler.\n *\n * @private\n * @type {Function}\n */\n #onQueryParamsChanged;\n\n constructor() {\n super();\n\n this.loading = false;\n this.results = null;\n this.nextPage = null;\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.#onQueryParamsChanged = () => {\n const searchBar = this.renderRoot.querySelector('search-bar');\n\n searchBar.query = router.queryParams.query ?? '';\n searchBar.bu = router.queryParams.bu ?? 'rsi';\n };\n router.addEventListener('queryparams', this.#onQueryParamsChanged);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.abortSearch();\n router.removeEventListener('queryparams', this.#onQueryParamsChanged);\n }\n\n async firstUpdated(_changedProperties) {\n super.firstUpdated(_changedProperties);\n const searchBar = this.renderRoot.querySelector('search-bar');\n\n this.#onQueryParamsChanged();\n await this.#search(searchBar.bu, searchBar.query);\n }\n\n async #onSearchBarChanged(bu, query) {\n router.updateState({ bu, ...(query ? { query } : {}) });\n await this.#search(bu, query);\n }\n\n /**\n * Performs a search based on the specified business unit and query. Performing\n * a new search will abort the previous search if it's ongoing and display a\n * loading spinner for the asynchronous operation.\n *\n * @param {string} bu - The selected business unit.\n * @param {string} query - The search query.\n *\n * @throws {Promise} - A rejected promise with the response object if\n * the fetch request for the search results fails.\n */\n async #search(bu, query) {\n const signal = this.abortSearch();\n\n if (!query) {\n [this.results, this.nextPage] = [null, null];\n\n return;\n }\n\n this.loading = true;\n try {\n const data = await ilProvider.search(bu, query, signal);\n\n [this.results, this.nextPage] = [data.results, data.next];\n } finally {\n this.loading = false;\n }\n }\n\n /**\n * Advances to the next page of search results and updates the UI accordingly.\n *\n * @throws {Promise} - A rejected promise with the response object if\n * the fetch request for the next page fails.\n */\n async #fetchNextPage() {\n const signal = this.abortSearch();\n const data = await this.nextPage(signal);\n\n this.nextPage = data.next;\n this.results = [...this.results, ...data.results];\n }\n\n /**\n * Aborts the previous search by cancelling the associated abort signal and\n * creates a new abort controller for the next search.\n *\n * @returns {AbortSignal} - The abort signal associated with the new search.\n */\n abortSearch() {\n this.#abortController?.abort('New search launched');\n this.#abortController = new AbortController();\n\n return this.#abortController.signal;\n }\n\n #toQueryParams(r) {\n return new URLSearchParams({\n ...router.queryParams,\n src: r.urn,\n type: 'srgssr/urn'\n }).toString();\n }\n\n #renderButton(r) {\n const date = new Intl.DateTimeFormat('fr-CH').format(new Date(r.date));\n const duration = Pillarbox.formatTime(r.duration / 1000);\n\n return html`\n \n \n ${r.mediaType === 'VIDEO' ? 'movie' : 'audiotrack'}\n | ${date} | ${duration}\n
\n \n `;\n }\n\n #renderResults() {\n const resultsClassMap = {\n empty: this.results == null,\n 'no-results': this.results && this.results.length === 0,\n 'material-icons': !this.results || this.results.length === 0\n };\n\n return html`\n e.target.classList.remove('fade-in')}\">\n ${map(this.results ?? [], this.#renderButton.bind(this))}\n ${when(this.nextPage, () => html`\n \n \n `)}\n \n `;\n }\n\n #renderSpinner() {\n return html`\n e.target.classList.remove('slide-up-fade-in')}\">\n \n `;\n }\n\n #renderScrollToTopBtn() {\n return html`\n `;\n }\n\n render() {\n return html`\n this.#onSearchBarChanged(e.detail.bu, e.detail.query)}\">\n \n\n \n ${when(this.loading, this.#renderSpinner.bind(this), this.#renderResults.bind(this))}\n ${when(this.results?.length > 0, this.#renderScrollToTopBtn.bind(this))}\n `;\n }\n}\n\ncustomElements.define('search-page', SearchPage);\nrouter.addRoute('search', 'search-page');\n","import { html, LitElement, unsafeCSS } from 'lit';\nimport { theme } from '../../theme/theme';\nimport componentCSS from './toggle-switch-component.scss?inline';\n\n/**\n * Custom element representing a toggle switch.\n *\n * @element toggle-switch\n *\n * @csspart switch - The container for the toggle switch.\n * @csspart slider - The slider button of the toggle switch.\n *\n * @prop {Boolean} checked - Reflects the current state of the toggle switch.\n * @prop {Boolean} disabled - Indicates whether the toggle switch is disabled.\n *\n * @attribute {String} role - ARIA role for accessibility, set to 'switch'.\n * @attribute {String} tabindex - ARIA tabindex for accessibility, set to '0'.\n *\n * @fires ToggleSwitchComponent#change\n *\n * @example\n * \n *\n * @customElement\n */\nexport class ToggleSwitchComponent extends LitElement {\n static formAssociated = true;\n static properties = {\n checked: { type: Boolean, reflect: true },\n disabled: { type: Boolean }\n };\n\n constructor() {\n super();\n this.checked = false;\n this.disabled = false;\n }\n\n static styles = [\n theme, unsafeCSS(componentCSS)\n ];\n\n #onKeyDown = (e) => {\n if (e.key === ' ') {\n e.preventDefault();\n this.toggle();\n }\n };\n\n #onClick = () => {\n this.toggle();\n };\n\n connectedCallback() {\n super.connectedCallback();\n\n if (!this.hasAttribute('role')) {\n this.setAttribute('role', 'switch');\n }\n\n if (!this.hasAttribute('tabindex')) {\n this.setAttribute('tabindex', '0');\n }\n\n this.addEventListener('click', this.#onClick);\n this.addEventListener('keydown', this.#onKeyDown);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n this.removeEventListener('click', this.#onClick);\n this.removeEventListener('keydown', this.#onKeyDown);\n }\n\n toggle(force) {\n if (!this.disabled) {\n this.checked = force ?? !this.checked;\n }\n }\n\n updated(_changedProperties) {\n super.updated(_changedProperties);\n\n if (_changedProperties.has('checked')) {\n this.setAttribute('aria-checked', this.checked.toString());\n /**\n * Custom event dispatched when the state of the toggle switch changes.\n *\n * @event ToggleSwitchComponent#change\n * @type {CustomEvent}\n * @property {Object} detail - The event detail object.\n * @property {Boolean} detail.checked - The new state of the toggle switch.\n */\n this.dispatchEvent(new CustomEvent('change', { detail: { checked: this.checked } }));\n }\n }\n\n render() {\n return html`\n \n `;\n }\n}\n\ncustomElements.define('toggle-switch', ToggleSwitchComponent);\n","import router from '../../../router/router';\nimport { html, LitElement, unsafeCSS } from 'lit';\nimport { animations, theme } from '../../../theme/theme';\nimport '../../../components/toggle-switch/toggle-switch-component.js';\nimport PreferencesProvider from './preferences-provider';\nimport componentCss from './settings-page.scss?inline';\n\n/**\n * A web component that represents the settings page.\n *\n * @element settings-page\n */\nexport class SettingsPage extends LitElement {\n static properties = {\n autoplay: { type: Boolean, state: true },\n muted: { type: Boolean, state: true },\n debug: { type: Boolean, state: true }\n };\n\n static styles = [theme, animations, unsafeCSS(componentCss)];\n\n constructor() {\n super();\n const preferences = PreferencesProvider.loadPreferences();\n\n this.autoplay = preferences.autoplay ?? false;\n this.muted = preferences.muted ?? true;\n this.debug = preferences.debug ?? false;\n }\n\n updated(_changedProperties) {\n super.updated(_changedProperties);\n\n const preferences = PreferencesProvider.loadPreferences();\n\n [..._changedProperties.keys()]\n .filter(property => ['autoplay', 'muted', 'debug'].includes(property))\n .forEach((property) => { preferences[property] = this[property]; });\n\n PreferencesProvider.savePreferences(preferences);\n\n if (_changedProperties.has('debug')) {\n router.replaceState(this.debug ? { debug: 'true' } : {});\n }\n }\n\n #renderToggle(property, label) {\n return html`\n \n \n { this[property] = e.detail.checked; }}\">\n \n
\n `;\n }\n\n render() {\n return html`\n e.target.classList.remove('fade-in')}\">\n Player Settings
\n ${this.#renderToggle('autoplay', 'Autoplay')}\n ${this.#renderToggle('muted', 'Player starts muted')}\n ${this.#renderToggle('debug', 'Enable debug mode')}\n \n `;\n }\n}\n\ncustomElements.define('settings-page', SettingsPage);\nrouter.addRoute('settings', 'settings-page');\n","import{nothing as t,noChange as i}from\"../lit-html.js\";import{Directive as r,PartType as s,directive as n}from\"../directive.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */class e extends r{constructor(i){if(super(i),this.it=t,i.type!==s.CHILD)throw Error(this.constructor.directiveName+\"() can only be used in child bindings\")}render(r){if(r===t||null==r)return this._t=void 0,this.it=r;if(r===i)return r;if(\"string\"!=typeof r)throw Error(this.constructor.directiveName+\"() called with a non-string value\");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName=\"unsafeHTML\",e.resultType=1;const o=n(e);export{e as UnsafeHTMLDirective,o as unsafeHTML};\n//# sourceMappingURL=unsafe-html.js.map\n","/* eslint-disable no-multi-assign */\n\nfunction deepFreeze(obj) {\n if (obj instanceof Map) {\n obj.clear =\n obj.delete =\n obj.set =\n function () {\n throw new Error('map is read-only');\n };\n } else if (obj instanceof Set) {\n obj.add =\n obj.clear =\n obj.delete =\n function () {\n throw new Error('set is read-only');\n };\n }\n\n // Freeze self\n Object.freeze(obj);\n\n Object.getOwnPropertyNames(obj).forEach((name) => {\n const prop = obj[name];\n const type = typeof prop;\n\n // Freeze prop if it is an object or function and also not already frozen\n if ((type === 'object' || type === 'function') && !Object.isFrozen(prop)) {\n deepFreeze(prop);\n }\n });\n\n return obj;\n}\n\n/** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */\n/** @typedef {import('highlight.js').CompiledMode} CompiledMode */\n/** @implements CallbackResponse */\n\nclass Response {\n /**\n * @param {CompiledMode} mode\n */\n constructor(mode) {\n // eslint-disable-next-line no-undefined\n if (mode.data === undefined) mode.data = {};\n\n this.data = mode.data;\n this.isMatchIgnored = false;\n }\n\n ignoreMatch() {\n this.isMatchIgnored = true;\n }\n}\n\n/**\n * @param {string} value\n * @returns {string}\n */\nfunction escapeHTML(value) {\n return value\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * performs a shallow merge of multiple objects into one\n *\n * @template T\n * @param {T} original\n * @param {Record[]} objects\n * @returns {T} a single new object\n */\nfunction inherit$1(original, ...objects) {\n /** @type Record */\n const result = Object.create(null);\n\n for (const key in original) {\n result[key] = original[key];\n }\n objects.forEach(function(obj) {\n for (const key in obj) {\n result[key] = obj[key];\n }\n });\n return /** @type {T} */ (result);\n}\n\n/**\n * @typedef {object} Renderer\n * @property {(text: string) => void} addText\n * @property {(node: Node) => void} openNode\n * @property {(node: Node) => void} closeNode\n * @property {() => string} value\n */\n\n/** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */\n/** @typedef {{walk: (r: Renderer) => void}} Tree */\n/** */\n\nconst SPAN_CLOSE = '