diff --git a/bun.lockb b/bun.lockb index 2c1f65a..a422402 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..6bbe5d0 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,5 @@ +[test] +coverage = true + +[define] +"process.env.NODE_ENV" = "'test'" diff --git a/package.json b/package.json index 3530910..ffc9a3f 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,20 @@ "dev": "concurrently \"bun run build:dev\" \"bun run --watch www/bin.ts\"", "build:dev": "vite build --mode=development --watch", "build": "vite build", - "test": "DATABASE_PATH=:memory: bun test --coverage" + "test": "bun test" }, "dependencies": { "@elysiajs/html": "^1.0.2", "@elysiajs/static": "^1.0.2", + "@hotwired/stimulus": "^3.2.2", "axios": "^1.6.8", "axios-retry": "^4.4.1", - "cache-manager": "~4.1.0", - "cache-manager-sqlite": "^0.2.0", + "cache-manager": "^5.7.3", + "cache-manager-bun-sqlite3": "^0.1.0", "cheerio": "^1.0.0-rc.12", "elysia": "^1.0.14", + "howler": "^2.2.4", + "notyf": "^3.10.0", "pino": "^8.20.0", "pino-pretty": "^11.0.0", "puppeteer": "^22.7.0", @@ -38,6 +41,7 @@ "postcss": "^8.4.38", "prettier": "^3.2.5", "rollup-plugin-copy": "^3.5.0", + "stimulus-vite-helpers": "^3.1.0", "tailwindcss": "^3.4.3", "typescript": "^5.4.5", "vite": "^5.2.10" diff --git a/public/assets/app.js b/public/assets/app.js new file mode 100644 index 0000000..9f6e6a7 --- /dev/null +++ b/public/assets/app.js @@ -0,0 +1,36 @@ +class _e{constructor(e,t,n){this.eventTarget=e,this.eventName=t,this.eventOptions=n,this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=ge(e);for(const n of this.bindings){if(t.immediatePropagationStopped)break;n.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort((e,t)=>{const n=e.index,s=t.index;return ns?1:0})}}function ge(o){if("immediatePropagationStopped"in o)return o;{const{stopImmediatePropagation:e}=o;return Object.assign(o,{immediatePropagationStopped:!1,stopImmediatePropagation(){this.immediatePropagationStopped=!0,e.call(this)}})}}class ve{constructor(e){this.application=e,this.eventListenerMaps=new Map,this.started=!1}start(){this.started||(this.started=!0,this.eventListeners.forEach(e=>e.connect()))}stop(){this.started&&(this.started=!1,this.eventListeners.forEach(e=>e.disconnect()))}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce((e,t)=>e.concat(Array.from(t.values())),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=!1){this.fetchEventListenerForBinding(e).bindingDisconnected(e),t&&this.clearEventListenersForBinding(e)}handleError(e,t,n={}){this.application.handleError(e,`Error ${t}`,n)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);t.hasBindings()||(t.disconnect(),this.removeMappedEventListenerFor(e))}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:n,eventOptions:s}=e,l=this.fetchEventListenerMapForEventTarget(t),h=this.cacheKey(n,s);l.delete(h),l.size==0&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:n,eventOptions:s}=e;return this.fetchEventListener(t,n,s)}fetchEventListener(e,t,n){const s=this.fetchEventListenerMapForEventTarget(e),l=this.cacheKey(t,n);let h=s.get(l);return h||(h=this.createEventListener(e,t,n),s.set(l,h)),h}createEventListener(e,t,n){const s=new _e(e,t,n);return this.started&&s.connect(),s}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);return t||(t=new Map,this.eventListenerMaps.set(e,t)),t}cacheKey(e,t){const n=[e];return Object.keys(t).sort().forEach(s=>{n.push(`${t[s]?"":"!"}${s}`)}),n.join(":")}}const ye={stop({event:o,value:e}){return e&&o.stopPropagation(),!0},prevent({event:o,value:e}){return e&&o.preventDefault(),!0},self({event:o,value:e,element:t}){return e?t===o.target:!0}},be=/^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function Ae(o){const t=o.trim().match(be)||[];let n=t[2],s=t[3];return s&&!["keydown","keyup","keypress"].includes(n)&&(n+=`.${s}`,s=""),{eventTarget:we(t[4]),eventName:n,eventOptions:t[7]?Oe(t[7]):{},identifier:t[5],methodName:t[6],keyFilter:t[1]||s}}function we(o){if(o=="window")return window;if(o=="document")return document}function Oe(o){return o.split(":").reduce((e,t)=>Object.assign(e,{[t.replace(/^!/,"")]:!/^!/.test(t)}),{})}function Te(o){if(o==window)return"window";if(o==document)return"document"}function R(o){return o.replace(/(?:[_-])([a-z0-9])/g,(e,t)=>t.toUpperCase())}function j(o){return R(o.replace(/--/g,"-").replace(/__/g,"_"))}function L(o){return o.charAt(0).toUpperCase()+o.slice(1)}function ie(o){return o.replace(/([A-Z])/g,(e,t)=>`-${t.toLowerCase()}`)}function Ee(o){return o.match(/[^\s]+/g)||[]}function W(o){return o!=null}function H(o,e){return Object.prototype.hasOwnProperty.call(o,e)}const Y=["meta","ctrl","alt","shift"];class ke{constructor(e,t,n,s){this.element=e,this.index=t,this.eventTarget=n.eventTarget||e,this.eventName=n.eventName||xe(e)||P("missing event name"),this.eventOptions=n.eventOptions||{},this.identifier=n.identifier||P("missing identifier"),this.methodName=n.methodName||P("missing method name"),this.keyFilter=n.keyFilter||"",this.schema=s}static forToken(e,t){return new this(e.element,e.index,Ae(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:"",t=this.eventTargetName?`@${this.eventTargetName}`:"";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return!1;const t=this.keyFilter.split("+");if(this.keyFilterDissatisfied(e,t))return!0;const n=t.filter(s=>!Y.includes(s))[0];return n?(H(this.keyMappings,n)||P(`contains unknown key filter: ${this.keyFilter}`),this.keyMappings[n].toLowerCase()!==e.key.toLowerCase()):!1}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return!1;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={},t=new RegExp(`^data-${this.identifier}-(.+)-param$`,"i");for(const{name:n,value:s}of Array.from(this.element.attributes)){const l=n.match(t),h=l&&l[1];h&&(e[R(h)]=Me(s))}return e}get eventTargetName(){return Te(this.eventTarget)}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[n,s,l,h]=Y.map(g=>t.includes(g));return e.metaKey!==n||e.ctrlKey!==s||e.altKey!==l||e.shiftKey!==h}}const Z={a:()=>"click",button:()=>"click",form:()=>"submit",details:()=>"toggle",input:o=>o.getAttribute("type")=="submit"?"click":"input",select:()=>"change",textarea:()=>"input"};function xe(o){const e=o.tagName.toLowerCase();if(e in Z)return Z[e](o)}function P(o){throw new Error(o)}function Me(o){try{return JSON.parse(o)}catch{return o}}class Se{constructor(e,t){this.context=e,this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if(typeof e=="function")return e;throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)}applyEventModifiers(e){const{element:t}=this.action,{actionDescriptorFilters:n}=this.context.application,{controller:s}=this.context;let l=!0;for(const[h,g]of Object.entries(this.eventOptions))if(h in n){const m=n[h];l=l&&m({name:h,value:g,event:e,element:t,controller:s})}else continue;return l}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:n}=e;try{this.method.call(this.controller,e),this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:n,action:this.methodName})}catch(s){const{identifier:l,controller:h,element:g,index:m}=this,p={identifier:l,controller:h,element:g,index:m,event:e};this.context.handleError(s,`invoking action "${this.action}"`,p)}}willBeInvokedByEvent(e){const t=e.target;return e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e)||e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e)?!1:this.element===t?!0:t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element)}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class se{constructor(e,t){this.mutationObserverInit={attributes:!0,childList:!0,subtree:!0},this.element=e,this.started=!1,this.delegate=t,this.elements=new Set,this.mutationObserver=new MutationObserver(n=>this.processMutations(n))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,this.mutationObserverInit),this.refresh())}pause(e){this.started&&(this.mutationObserver.disconnect(),this.started=!1),e(),this.started||(this.mutationObserver.observe(this.element,this.mutationObserverInit),this.started=!0)}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){e.type=="attributes"?this.processAttributeChange(e.target,e.attributeName):e.type=="childList"&&(this.processRemovedNodes(e.removedNodes),this.processAddedNodes(e.addedNodes))}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const n=this.elementFromNode(t);n&&this.processTree(n,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const n=this.elementFromNode(t);n&&this.elementIsActive(n)&&this.processTree(n,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const n of this.matchElementsInTree(e))t.call(this,n)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected!=this.element.isConnected?!1:this.element.contains(e)}addElement(e){this.elements.has(e)||this.elementIsActive(e)&&(this.elements.add(e),this.delegate.elementMatched&&this.delegate.elementMatched(e))}removeElement(e){this.elements.has(e)&&(this.elements.delete(e),this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e))}}class oe{constructor(e,t,n){this.attributeName=t,this.delegate=n,this.elementObserver=new se(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[],n=Array.from(e.querySelectorAll(this.selector));return t.concat(n)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function Fe(o,e,t){ae(o,e).add(t)}function Ne(o,e,t){ae(o,e).delete(t),Ce(o,e)}function ae(o,e){let t=o.get(e);return t||(t=new Set,o.set(e,t)),t}function Ce(o,e){const t=o.get(e);t!=null&&t.size==0&&o.delete(e)}class x{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){return Array.from(this.valuesByKey.values()).reduce((t,n)=>t.concat(Array.from(n)),[])}get size(){return Array.from(this.valuesByKey.values()).reduce((t,n)=>t+n.size,0)}add(e,t){Fe(this.valuesByKey,e,t)}delete(e,t){Ne(this.valuesByKey,e,t)}has(e,t){const n=this.valuesByKey.get(e);return n!=null&&n.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){return Array.from(this.valuesByKey.values()).some(n=>n.has(e))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter(([t,n])=>n.has(e)).map(([t,n])=>t)}}class Ie{constructor(e,t,n,s){this._selector=t,this.details=s,this.elementObserver=new se(e,this),this.delegate=n,this.matchesByElement=new x}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e,this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const n=e.matches(t);return this.delegate.selectorMatchElement?n&&this.delegate.selectorMatchElement(e,this.details):n}else return!1}matchElementsInTree(e){const{selector:t}=this;if(t){const n=this.matchElement(e)?[e]:[],s=Array.from(e.querySelectorAll(t)).filter(l=>this.matchElement(l));return n.concat(s)}else return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const n of t)this.selectorUnmatched(e,n)}elementAttributeChanged(e,t){const{selector:n}=this;if(n){const s=this.matchElement(e),l=this.matchesByElement.has(n,e);s&&!l?this.selectorMatched(e,n):!s&&l&&this.selectorUnmatched(e,n)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details),this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details),this.matchesByElement.delete(t,e)}}class Le{constructor(e,t){this.element=e,this.delegate=t,this.started=!1,this.stringMap=new Map,this.mutationObserver=new MutationObserver(n=>this.processMutations(n))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,{attributes:!0,attributeOldValue:!0}),this.refresh())}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const n=this.delegate.getStringMapKeyForAttribute(e);if(n!=null){this.stringMap.has(e)||this.stringMapKeyAdded(n,e);const s=this.element.getAttribute(e);if(this.stringMap.get(e)!=s&&this.stringMapValueChanged(s,n,t),s==null){const l=this.stringMap.get(e);this.stringMap.delete(e),l&&this.stringMapKeyRemoved(n,e,l)}else this.stringMap.set(e,s)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,n){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,n)}stringMapKeyRemoved(e,t,n){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,n)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map(e=>e.name)}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class ce{constructor(e,t,n){this.attributeObserver=new oe(e,t,this),this.delegate=n,this.tokensByElement=new x}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,n]=this.refreshTokensForElement(e);this.tokensUnmatched(t),this.tokensMatched(n)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach(t=>this.tokenMatched(t))}tokensUnmatched(e){e.forEach(t=>this.tokenUnmatched(t))}tokenMatched(e){this.delegate.tokenMatched(e),this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e),this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e),n=this.readTokensForElement(e),s=Pe(t,n).findIndex(([l,h])=>!De(l,h));return s==-1?[[],[]]:[t.slice(s),n.slice(s)]}readTokensForElement(e){const t=this.attributeName,n=e.getAttribute(t)||"";return Be(n,e,t)}}function Be(o,e,t){return o.trim().split(/\s+/).filter(n=>n.length).map((n,s)=>({element:e,attributeName:t,content:n,index:s}))}function Pe(o,e){const t=Math.max(o.length,e.length);return Array.from({length:t},(n,s)=>[o[s],e[s]])}function De(o,e){return o&&e&&o.index==e.index&&o.content==e.content}class le{constructor(e,t,n){this.tokenListObserver=new ce(e,t,this),this.delegate=n,this.parseResultsByToken=new WeakMap,this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e,{value:n}=this.fetchParseResultForToken(e);n&&(this.fetchValuesByTokenForElement(t).set(e,n),this.delegate.elementMatchedValue(t,n))}tokenUnmatched(e){const{element:t}=e,{value:n}=this.fetchParseResultForToken(e);n&&(this.fetchValuesByTokenForElement(t).delete(e),this.delegate.elementUnmatchedValue(t,n))}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);return t||(t=this.parseToken(e),this.parseResultsByToken.set(e,t)),t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);return t||(t=new Map,this.valuesByTokenByElement.set(e,t)),t}parseToken(e){try{return{value:this.delegate.parseValueForToken(e)}}catch(t){return{error:t}}}}class $e{constructor(e,t){this.context=e,this.delegate=t,this.bindingsByAction=new Map}start(){this.valueListObserver||(this.valueListObserver=new le(this.element,this.actionAttribute,this),this.valueListObserver.start())}stop(){this.valueListObserver&&(this.valueListObserver.stop(),delete this.valueListObserver,this.disconnectAllActions())}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new Se(this.context,e);this.bindingsByAction.set(e,t),this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);t&&(this.bindingsByAction.delete(e),this.delegate.bindingDisconnected(t))}disconnectAllActions(){this.bindings.forEach(e=>this.delegate.bindingDisconnected(e,!0)),this.bindingsByAction.clear()}parseValueForToken(e){const t=ke.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class Ve{constructor(e,t){this.context=e,this.receiver=t,this.stringMapObserver=new Le(this.element,this),this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start(),this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const n=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,n.writer(this.receiver[e]),n.writer(n.defaultValue))}stringMapValueChanged(e,t,n){const s=this.valueDescriptorNameMap[t];e!==null&&(n===null&&(n=s.writer(s.defaultValue)),this.invokeChangedCallback(t,e,n))}stringMapKeyRemoved(e,t,n){const s=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,s.writer(this.receiver[e]),n):this.invokeChangedCallback(e,s.writer(s.defaultValue),n)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:n,writer:s}of this.valueDescriptors)n!=null&&!this.controller.data.has(e)&&this.invokeChangedCallback(t,s(n),void 0)}invokeChangedCallback(e,t,n){const s=`${e}Changed`,l=this.receiver[s];if(typeof l=="function"){const h=this.valueDescriptorNameMap[e];try{const g=h.reader(t);let m=n;n&&(m=h.reader(n)),l.call(this.receiver,g,m)}catch(g){throw g instanceof TypeError&&(g.message=`Stimulus Value "${this.context.identifier}.${h.name}" - ${g.message}`),g}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map(t=>e[t])}get valueDescriptorNameMap(){const e={};return Object.keys(this.valueDescriptorMap).forEach(t=>{const n=this.valueDescriptorMap[t];e[n.name]=n}),e}hasValue(e){const t=this.valueDescriptorNameMap[e],n=`has${L(t.name)}`;return this.receiver[n]}}class je{constructor(e,t){this.context=e,this.delegate=t,this.targetsByName=new x}start(){this.tokenListObserver||(this.tokenListObserver=new ce(this.element,this.attributeName,this),this.tokenListObserver.start())}stop(){this.tokenListObserver&&(this.disconnectAllTargets(),this.tokenListObserver.stop(),delete this.tokenListObserver)}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var n;this.targetsByName.has(t,e)||(this.targetsByName.add(t,e),(n=this.tokenListObserver)===null||n===void 0||n.pause(()=>this.delegate.targetConnected(e,t)))}disconnectTarget(e,t){var n;this.targetsByName.has(t,e)&&(this.targetsByName.delete(t,e),(n=this.tokenListObserver)===null||n===void 0||n.pause(()=>this.delegate.targetDisconnected(e,t)))}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function B(o,e){const t=ue(o);return Array.from(t.reduce((n,s)=>(Ke(s,e).forEach(l=>n.add(l)),n),new Set))}function He(o,e){return ue(o).reduce((n,s)=>(n.push(...Re(s,e)),n),[])}function ue(o){const e=[];for(;o;)e.push(o),o=Object.getPrototypeOf(o);return e.reverse()}function Ke(o,e){const t=o[e];return Array.isArray(t)?t:[]}function Re(o,e){const t=o[e];return t?Object.keys(t).map(n=>[n,t[n]]):[]}class Ge{constructor(e,t){this.started=!1,this.context=e,this.delegate=t,this.outletsByName=new x,this.outletElementsByName=new x,this.selectorObserverMap=new Map,this.attributeObserverMap=new Map}start(){this.started||(this.outletDefinitions.forEach(e=>{this.setupSelectorObserverForOutlet(e),this.setupAttributeObserverForOutlet(e)}),this.started=!0,this.dependentContexts.forEach(e=>e.refresh()))}refresh(){this.selectorObserverMap.forEach(e=>e.refresh()),this.attributeObserverMap.forEach(e=>e.refresh())}stop(){this.started&&(this.started=!1,this.disconnectAllOutlets(),this.stopSelectorObservers(),this.stopAttributeObservers())}stopSelectorObservers(){this.selectorObserverMap.size>0&&(this.selectorObserverMap.forEach(e=>e.stop()),this.selectorObserverMap.clear())}stopAttributeObservers(){this.attributeObserverMap.size>0&&(this.attributeObserverMap.forEach(e=>e.stop()),this.attributeObserverMap.clear())}selectorMatched(e,t,{outletName:n}){const s=this.getOutlet(e,n);s&&this.connectOutlet(s,e,n)}selectorUnmatched(e,t,{outletName:n}){const s=this.getOutletFromMap(e,n);s&&this.disconnectOutlet(s,e,n)}selectorMatchElement(e,{outletName:t}){const n=this.selector(t),s=this.hasOutlet(e,t),l=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return n?s&&l&&e.matches(n):!1}elementMatchedAttribute(e,t){const n=this.getOutletNameFromOutletAttributeName(t);n&&this.updateSelectorObserverForOutlet(n)}elementAttributeValueChanged(e,t){const n=this.getOutletNameFromOutletAttributeName(t);n&&this.updateSelectorObserverForOutlet(n)}elementUnmatchedAttribute(e,t){const n=this.getOutletNameFromOutletAttributeName(t);n&&this.updateSelectorObserverForOutlet(n)}connectOutlet(e,t,n){var s;this.outletElementsByName.has(n,t)||(this.outletsByName.add(n,e),this.outletElementsByName.add(n,t),(s=this.selectorObserverMap.get(n))===null||s===void 0||s.pause(()=>this.delegate.outletConnected(e,t,n)))}disconnectOutlet(e,t,n){var s;this.outletElementsByName.has(n,t)&&(this.outletsByName.delete(n,e),this.outletElementsByName.delete(n,t),(s=this.selectorObserverMap.get(n))===null||s===void 0||s.pause(()=>this.delegate.outletDisconnected(e,t,n)))}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const n of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(n,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e),n=new Ie(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,n),n.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e),n=new oe(this.scope.element,t,this);this.attributeObserverMap.set(e,n),n.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find(t=>this.attributeNameForOutletName(t)===e)}get outletDependencies(){const e=new x;return this.router.modules.forEach(t=>{const n=t.definition.controllerConstructor;B(n,"outlets").forEach(l=>e.add(l,t.identifier))}),e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter(t=>e.includes(t.identifier))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find(n=>n.element===e)}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class Ue{constructor(e,t){this.logDebugActivity=(n,s={})=>{const{identifier:l,controller:h,element:g}=this;s=Object.assign({identifier:l,controller:h,element:g},s),this.application.logDebugActivity(this.identifier,n,s)},this.module=e,this.scope=t,this.controller=new e.controllerConstructor(this),this.bindingObserver=new $e(this,this.dispatcher),this.valueObserver=new Ve(this,this.controller),this.targetObserver=new je(this,this),this.outletObserver=new Ge(this,this);try{this.controller.initialize(),this.logDebugActivity("initialize")}catch(n){this.handleError(n,"initializing controller")}}connect(){this.bindingObserver.start(),this.valueObserver.start(),this.targetObserver.start(),this.outletObserver.start();try{this.controller.connect(),this.logDebugActivity("connect")}catch(e){this.handleError(e,"connecting controller")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect(),this.logDebugActivity("disconnect")}catch(e){this.handleError(e,"disconnecting controller")}this.outletObserver.stop(),this.targetObserver.stop(),this.valueObserver.stop(),this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,n={}){const{identifier:s,controller:l,element:h}=this;n=Object.assign({identifier:s,controller:l,element:h},n),this.application.handleError(e,`Error ${t}`,n)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,n){this.invokeControllerMethod(`${j(n)}OutletConnected`,e,t)}outletDisconnected(e,t,n){this.invokeControllerMethod(`${j(n)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const n=this.controller;typeof n[e]=="function"&&n[e](...t)}}function Xe(o){return qe(o,We(o))}function qe(o,e){const t=Je(o),n=Ye(o.prototype,e);return Object.defineProperties(t.prototype,n),t}function We(o){return B(o,"blessings").reduce((t,n)=>{const s=n(o);for(const l in s){const h=t[l]||{};t[l]=Object.assign(h,s[l])}return t},{})}function Ye(o,e){return Qe(e).reduce((t,n)=>{const s=Ze(o,e,n);return s&&Object.assign(t,{[n]:s}),t},{})}function Ze(o,e,t){const n=Object.getOwnPropertyDescriptor(o,t);if(!(n&&"value"in n)){const l=Object.getOwnPropertyDescriptor(e,t).value;return n&&(l.get=n.get||l.get,l.set=n.set||l.set),l}}const Qe=typeof Object.getOwnPropertySymbols=="function"?o=>[...Object.getOwnPropertyNames(o),...Object.getOwnPropertySymbols(o)]:Object.getOwnPropertyNames,Je=(()=>{function o(t){function n(){return Reflect.construct(t,arguments,new.target)}return n.prototype=Object.create(t.prototype,{constructor:{value:n}}),Reflect.setPrototypeOf(n,t),n}function e(){const n=o(function(){this.a.call(this)});return n.prototype.a=function(){},new n}try{return e(),o}catch{return n=>class extends n{}}})();function ze(o){return{identifier:o.identifier,controllerConstructor:Xe(o.controllerConstructor)}}class et{constructor(e,t){this.application=e,this.definition=ze(t),this.contextsByScope=new WeakMap,this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t),t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);t&&(this.connectedContexts.delete(t),t.disconnect())}fetchContextForScope(e){let t=this.contextsByScope.get(e);return t||(t=new Ue(this,e),this.contextsByScope.set(e,t)),t}}class tt{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){const t=this.data.get(this.getDataKey(e))||"";return Ee(t)}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class rt{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const n=this.getAttributeNameForKey(e);return this.element.setAttribute(n,t),this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);return this.element.removeAttribute(t),!0}else return!1}getAttributeNameForKey(e){return`data-${this.identifier}-${ie(e)}`}}class nt{constructor(e){this.warnedKeysByObject=new WeakMap,this.logger=e}warn(e,t,n){let s=this.warnedKeysByObject.get(e);s||(s=new Set,this.warnedKeysByObject.set(e,s)),s.has(t)||(s.add(t),this.logger.warn(n,e))}}function K(o,e){return`[${o}~="${e}"]`}class it{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return this.find(e)!=null}find(...e){return e.reduce((t,n)=>t||this.findTarget(n)||this.findLegacyTarget(n),void 0)}findAll(...e){return e.reduce((t,n)=>[...t,...this.findAllTargets(n),...this.findAllLegacyTargets(n)],[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){const t=this.schema.targetAttributeForScope(this.identifier);return K(t,e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map(n=>this.deprecate(n,e))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return K(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:n}=this,s=this.schema.targetAttribute,l=this.schema.targetAttributeForScope(n);this.guide.warn(e,`target:${t}`,`Please replace ${s}="${n}.${t}" with ${l}="${t}". The ${s} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class st{constructor(e,t){this.scope=e,this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return this.find(e)!=null}find(...e){return e.reduce((t,n)=>t||this.findOutlet(n),void 0)}findAll(...e){return e.reduce((t,n)=>[...t,...this.findAllOutlets(n)],[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){return this.scope.queryElements(e).filter(s=>this.matchesElement(s,e,t))[0]}findAllElements(e,t){return this.scope.queryElements(e).filter(s=>this.matchesElement(s,e,t))}matchesElement(e,t,n){const s=e.getAttribute(this.scope.schema.controllerAttribute)||"";return e.matches(t)&&s.split(" ").includes(n)}}class G{constructor(e,t,n,s){this.targets=new it(this),this.classes=new tt(this),this.data=new rt(this),this.containsElement=l=>l.closest(this.controllerSelector)===this.element,this.schema=e,this.element=t,this.identifier=n,this.guide=new nt(s),this.outlets=new st(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return K(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new G(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class ot{constructor(e,t,n){this.element=e,this.schema=t,this.delegate=n,this.valueListObserver=new le(this.element,this.controllerAttribute,this),this.scopesByIdentifierByElement=new WeakMap,this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:n}=e;return this.parseValueForElementAndIdentifier(t,n)}parseValueForElementAndIdentifier(e,t){const n=this.fetchScopesByIdentifierForElement(e);let s=n.get(t);return s||(s=this.delegate.createScopeForElementAndIdentifier(e,t),n.set(t,s)),s}elementMatchedValue(e,t){const n=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,n),n==1&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const n=this.scopeReferenceCounts.get(t);n&&(this.scopeReferenceCounts.set(t,n-1),n==1&&this.delegate.scopeDisconnected(t))}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);return t||(t=new Map,this.scopesByIdentifierByElement.set(e,t)),t}}class at{constructor(e){this.application=e,this.scopeObserver=new ot(this.element,this.schema,this),this.scopesByIdentifier=new x,this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce((e,t)=>e.concat(t.contexts),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new et(this.application,e);this.connectModule(t);const n=e.controllerConstructor.afterLoad;n&&n.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const n=this.modulesByIdentifier.get(t);if(n)return n.contexts.find(s=>s.element==e)}proposeToConnectScopeForElementAndIdentifier(e,t){const n=this.scopeObserver.parseValueForElementAndIdentifier(e,t);n?this.scopeObserver.elementMatchedValue(n.element,n):console.error(`Couldn't find or create scope for identifier: "${t}" and element:`,e)}handleError(e,t,n){this.application.handleError(e,t,n)}createScopeForElementAndIdentifier(e,t){return new G(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e),this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(n=>e.connectContextForScope(n))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier),this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(n=>e.disconnectContextForScope(n))}}const ct={controllerAttribute:"data-controller",actionAttribute:"data-action",targetAttribute:"data-target",targetAttributeForScope:o=>`data-${o}-target`,outletAttributeForScope:(o,e)=>`data-${o}-${e}-outlet`,keyMappings:Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",page_up:"PageUp",page_down:"PageDown"},Q("abcdefghijklmnopqrstuvwxyz".split("").map(o=>[o,o]))),Q("0123456789".split("").map(o=>[o,o])))};function Q(o){return o.reduce((e,[t,n])=>Object.assign(Object.assign({},e),{[t]:n}),{})}class lt{constructor(e=document.documentElement,t=ct){this.logger=console,this.debug=!1,this.logDebugActivity=(n,s,l={})=>{this.debug&&this.logFormattedMessage(n,s,l)},this.element=e,this.schema=t,this.dispatcher=new ve(this),this.router=new at(this),this.actionDescriptorFilters=Object.assign({},ye)}static start(e,t){const n=new this(e,t);return n.start(),n}async start(){await ut(),this.logDebugActivity("application","starting"),this.dispatcher.start(),this.router.start(),this.logDebugActivity("application","start")}stop(){this.logDebugActivity("application","stopping"),this.dispatcher.stop(),this.router.stop(),this.logDebugActivity("application","stop")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(s=>{s.controllerConstructor.shouldLoad&&this.router.loadDefinition(s)})}unload(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(s=>this.router.unloadIdentifier(s))}get controllers(){return this.router.contexts.map(e=>e.controller)}getControllerForElementAndIdentifier(e,t){const n=this.router.getContextForElementAndIdentifier(e,t);return n?n.controller:null}handleError(e,t,n){var s;this.logger.error(`%s + +%o + +%o`,t,e,n),(s=window.onerror)===null||s===void 0||s.call(window,t,"",0,0,e)}logFormattedMessage(e,t,n={}){n=Object.assign({application:this},n),this.logger.groupCollapsed(`${e} #${t}`),this.logger.log("details:",Object.assign({},n)),this.logger.groupEnd()}}function ut(){return new Promise(o=>{document.readyState=="loading"?document.addEventListener("DOMContentLoaded",()=>o()):o()})}function dt(o){return B(o,"classes").reduce((t,n)=>Object.assign(t,ft(n)),{})}function ft(o){return{[`${o}Class`]:{get(){const{classes:e}=this;if(e.has(o))return e.get(o);{const t=e.getAttributeName(o);throw new Error(`Missing attribute "${t}"`)}}},[`${o}Classes`]:{get(){return this.classes.getAll(o)}},[`has${L(o)}Class`]:{get(){return this.classes.has(o)}}}}function ht(o){return B(o,"outlets").reduce((t,n)=>Object.assign(t,pt(n)),{})}function J(o,e,t){return o.application.getControllerForElementAndIdentifier(e,t)}function z(o,e,t){let n=J(o,e,t);if(n||(o.application.router.proposeToConnectScopeForElementAndIdentifier(e,t),n=J(o,e,t),n))return n}function pt(o){const e=j(o);return{[`${e}Outlet`]:{get(){const t=this.outlets.find(o),n=this.outlets.getSelectorForOutletName(o);if(t){const s=z(this,t,o);if(s)return s;throw new Error(`The provided outlet element is missing an outlet controller "${o}" instance for host controller "${this.identifier}"`)}throw new Error(`Missing outlet element "${o}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${n}".`)}},[`${e}Outlets`]:{get(){const t=this.outlets.findAll(o);return t.length>0?t.map(n=>{const s=z(this,n,o);if(s)return s;console.warn(`The provided outlet element is missing an outlet controller "${o}" instance for host controller "${this.identifier}"`,n)}).filter(n=>n):[]}},[`${e}OutletElement`]:{get(){const t=this.outlets.find(o),n=this.outlets.getSelectorForOutletName(o);if(t)return t;throw new Error(`Missing outlet element "${o}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${n}".`)}},[`${e}OutletElements`]:{get(){return this.outlets.findAll(o)}},[`has${L(e)}Outlet`]:{get(){return this.outlets.has(o)}}}}function mt(o){return B(o,"targets").reduce((t,n)=>Object.assign(t,_t(n)),{})}function _t(o){return{[`${o}Target`]:{get(){const e=this.targets.find(o);if(e)return e;throw new Error(`Missing target element "${o}" for "${this.identifier}" controller`)}},[`${o}Targets`]:{get(){return this.targets.findAll(o)}},[`has${L(o)}Target`]:{get(){return this.targets.has(o)}}}}function gt(o){const e=He(o,"values"),t={valueDescriptorMap:{get(){return e.reduce((n,s)=>{const l=de(s,this.identifier),h=this.data.getAttributeNameForKey(l.key);return Object.assign(n,{[h]:l})},{})}}};return e.reduce((n,s)=>Object.assign(n,vt(s)),t)}function vt(o,e){const t=de(o,e),{key:n,name:s,reader:l,writer:h}=t;return{[s]:{get(){const g=this.data.get(n);return g!==null?l(g):t.defaultValue},set(g){g===void 0?this.data.delete(n):this.data.set(n,h(g))}},[`has${L(s)}`]:{get(){return this.data.has(n)||t.hasCustomDefaultValue}}}}function de([o,e],t){return wt({controller:t,token:o,typeDefinition:e})}function $(o){switch(o){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function I(o){switch(typeof o){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}if(Array.isArray(o))return"array";if(Object.prototype.toString.call(o)==="[object Object]")return"object"}function yt(o){const{controller:e,token:t,typeObject:n}=o,s=W(n.type),l=W(n.default),h=s&&l,g=s&&!l,m=!s&&l,p=$(n.type),b=I(o.typeObject.default);if(g)return p;if(m)return b;if(p!==b){const r=e?`${e}.${t}`:t;throw new Error(`The specified default value for the Stimulus Value "${r}" must match the defined type "${p}". The provided default value of "${n.default}" is of type "${b}".`)}if(h)return p}function bt(o){const{controller:e,token:t,typeDefinition:n}=o,l=yt({controller:e,token:t,typeObject:n}),h=I(n),g=$(n),m=l||h||g;if(m)return m;const p=e?`${e}.${n}`:t;throw new Error(`Unknown value type "${p}" for "${t}" value`)}function At(o){const e=$(o);if(e)return ee[e];const t=H(o,"default"),n=H(o,"type"),s=o;if(t)return s.default;if(n){const{type:l}=s,h=$(l);if(h)return ee[h]}return o}function wt(o){const{token:e,typeDefinition:t}=o,n=`${ie(e)}-value`,s=bt(o);return{type:s,key:n,name:R(n),get defaultValue(){return At(t)},get hasCustomDefaultValue(){return I(t)!==void 0},reader:Ot[s],writer:te[s]||te.default}}const ee={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:""},Ot={array(o){const e=JSON.parse(o);if(!Array.isArray(e))throw new TypeError(`expected value of type "array" but instead got value "${o}" of type "${I(e)}"`);return e},boolean(o){return!(o=="0"||String(o).toLowerCase()=="false")},number(o){return Number(o.replace(/_/g,""))},object(o){const e=JSON.parse(o);if(e===null||typeof e!="object"||Array.isArray(e))throw new TypeError(`expected value of type "object" but instead got value "${o}" of type "${I(e)}"`);return e},string(o){return o}},te={default:Tt,array:re,object:re};function re(o){return JSON.stringify(o)}function Tt(o){return`${o}`}class M{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:n={},prefix:s=this.identifier,bubbles:l=!0,cancelable:h=!0}={}){const g=s?`${s}:${e}`:e,m=new CustomEvent(g,{detail:n,bubbles:l,cancelable:h});return t.dispatchEvent(m),m}}M.blessings=[dt,mt,gt,ht];M.targets=[];M.outlets=[];M.values={};var N=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},fe={};/*! + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */(function(o){(function(){var e=function(){this.init()};e.prototype={init:function(){var r=this||t;return r._counter=1e3,r._html5AudioPool=[],r.html5PoolSize=10,r._codecs={},r._howls=[],r._muted=!1,r._volume=1,r._canPlayEvent="canplaythrough",r._navigator=typeof window<"u"&&window.navigator?window.navigator:null,r.masterGain=null,r.noAudio=!1,r.usingWebAudio=!0,r.autoSuspend=!0,r.ctx=null,r.autoUnlock=!0,r._setup(),r},volume:function(r){var i=this||t;if(r=parseFloat(r),i.ctx||b(),typeof r<"u"&&r>=0&&r<=1){if(i._volume=r,i._muted)return i;i.usingWebAudio&&i.masterGain.gain.setValueAtTime(r,t.ctx.currentTime);for(var a=0;a=0;i--)r._howls[i].unload();return r.usingWebAudio&&r.ctx&&typeof r.ctx.close<"u"&&(r.ctx.close(),r.ctx=null,b()),r},codecs:function(r){return(this||t)._codecs[r.replace(/^x-/,"")]},_setup:function(){var r=this||t;if(r.state=r.ctx&&r.ctx.state||"suspended",r._autoSuspend(),!r.usingWebAudio)if(typeof Audio<"u")try{var i=new Audio;typeof i.oncanplaythrough>"u"&&(r._canPlayEvent="canplay")}catch{r.noAudio=!0}else r.noAudio=!0;try{var i=new Audio;i.muted&&(r.noAudio=!0)}catch{}return r.noAudio||r._setupCodecs(),r},_setupCodecs:function(){var r=this||t,i=null;try{i=typeof Audio<"u"?new Audio:null}catch{return r}if(!i||typeof i.canPlayType!="function")return r;var a=i.canPlayType("audio/mpeg;").replace(/^no$/,""),c=r._navigator?r._navigator.userAgent:"",d=c.match(/OPR\/(\d+)/g),f=d&&parseInt(d[0].split("/")[1],10)<33,u=c.indexOf("Safari")!==-1&&c.indexOf("Chrome")===-1,_=c.match(/Version\/(.*?) /),v=u&&_&&parseInt(_[1],10)<15;return r._codecs={mp3:!!(!f&&(a||i.canPlayType("audio/mp3;").replace(/^no$/,""))),mpeg:!!a,opus:!!i.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!i.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!i.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(i.canPlayType('audio/wav; codecs="1"')||i.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!i.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!i.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(i.canPlayType("audio/x-m4a;")||i.canPlayType("audio/m4a;")||i.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(i.canPlayType("audio/x-m4b;")||i.canPlayType("audio/m4b;")||i.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(i.canPlayType("audio/x-mp4;")||i.canPlayType("audio/mp4;")||i.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!(!v&&i.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!!(!v&&i.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!i.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(i.canPlayType("audio/x-flac;")||i.canPlayType("audio/flac;")).replace(/^no$/,"")},r},_unlockAudio:function(){var r=this||t;if(!(r._audioUnlocked||!r.ctx)){r._audioUnlocked=!1,r.autoUnlock=!1,!r._mobileUnloaded&&r.ctx.sampleRate!==44100&&(r._mobileUnloaded=!0,r.unload()),r._scratchBuffer=r.ctx.createBuffer(1,1,22050);var i=function(a){for(;r._html5AudioPool.length"u"?v.noteOn(0):v.start(0),typeof r.ctx.resume=="function"&&r.ctx.resume(),v.onended=function(){v.disconnect(0),r._audioUnlocked=!0,document.removeEventListener("touchstart",i,!0),document.removeEventListener("touchend",i,!0),document.removeEventListener("click",i,!0),document.removeEventListener("keydown",i,!0);for(var A=0;A"u"||!t.usingWebAudio)){for(var i=0;i"u"||!t.usingWebAudio))return r.state==="running"&&r.ctx.state!=="interrupted"&&r._suspendTimer?(clearTimeout(r._suspendTimer),r._suspendTimer=null):r.state==="suspended"||r.state==="running"&&r.ctx.state==="interrupted"?(r.ctx.resume().then(function(){r.state="running";for(var i=0;i"u"&&(r="__default",!a._playLock)){for(var d=0,f=0;f0?u._seek:a._sprite[r][0]/1e3),A=Math.max(0,(a._sprite[r][0]+a._sprite[r][1])/1e3-v),w=A*1e3/Math.abs(u._rate),O=a._sprite[r][0]/1e3,T=(a._sprite[r][0]+a._sprite[r][1])/1e3;u._sprite=r,u._ended=!1;var V=function(){u._paused=!1,u._seek=v,u._start=O,u._stop=T,u._loop=!!(u._loop||a._sprite[r][2])};if(v>=T){a._ended(u);return}var y=u._node;if(a._webAudio){var U=function(){a._playLock=!1,V(),a._refreshBuffer(u);var S=u._muted||a._muted?0:u._volume;y.gain.setValueAtTime(S,t.ctx.currentTime),u._playStart=t.ctx.currentTime,typeof y.bufferSource.start>"u"?u._loop?y.bufferSource.noteGrainOn(0,v,86400):y.bufferSource.noteGrainOn(0,v,A):u._loop?y.bufferSource.start(0,v,86400):y.bufferSource.start(0,v,A),w!==1/0&&(a._endTimers[u._id]=setTimeout(a._ended.bind(a,u),w)),i||setTimeout(function(){a._emit("play",u._id),a._loadQueue()},0)};t.state==="running"&&t.ctx.state!=="interrupted"?U():(a._playLock=!0,a.once("resume",U),a._clearTimer(u._id))}else{var X=function(){y.currentTime=v,y.muted=u._muted||a._muted||t._muted||y.muted,y.volume=u._volume*t.volume(),y.playbackRate=u._rate;try{var S=y.play();if(S&&typeof Promise<"u"&&(S instanceof Promise||typeof S.then=="function")?(a._playLock=!0,V(),S.then(function(){a._playLock=!1,y._unlocked=!0,i?a._loadQueue():a._emit("play",u._id)}).catch(function(){a._playLock=!1,a._emit("playerror",u._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),u._ended=!0,u._paused=!0})):i||(a._playLock=!1,V(),a._emit("play",u._id)),y.playbackRate=u._rate,y.paused){a._emit("playerror",u._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");return}r!=="__default"||u._loop?a._endTimers[u._id]=setTimeout(a._ended.bind(a,u),w):(a._endTimers[u._id]=function(){a._ended(u),y.removeEventListener("ended",a._endTimers[u._id],!1)},y.addEventListener("ended",a._endTimers[u._id],!1))}catch(me){a._emit("playerror",u._id,me)}};y.src==="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"&&(y.src=a._src,y.load());var pe=window&&window.ejecta||!y.readyState&&t._navigator.isCocoonJS;if(y.readyState>=3||pe)X();else{a._playLock=!0,a._state="loading";var q=function(){a._state="loaded",X(),y.removeEventListener(t._canPlayEvent,q,!1)};y.addEventListener(t._canPlayEvent,q,!1),a._clearTimer(u._id)}}return u._id},pause:function(r){var i=this;if(i._state!=="loaded"||i._playLock)return i._queue.push({event:"pause",action:function(){i.pause(r)}}),i;for(var a=i._getSoundIds(r),c=0;c"u"?d._node.bufferSource.noteOff(0):d._node.bufferSource.stop(0),i._cleanBuffer(d._node)}else(!isNaN(d._node.duration)||d._node.duration===1/0)&&d._node.pause();arguments[1]||i._emit("pause",d?d._id:null)}return i},stop:function(r,i){var a=this;if(a._state!=="loaded"||a._playLock)return a._queue.push({event:"stop",action:function(){a.stop(r)}}),a;for(var c=a._getSoundIds(r),d=0;d"u"?f._node.bufferSource.noteOff(0):f._node.bufferSource.stop(0),a._cleanBuffer(f._node)):(!isNaN(f._node.duration)||f._node.duration===1/0)&&(f._node.currentTime=f._start||0,f._node.pause(),f._node.duration===1/0&&a._clearSound(f._node))),i||a._emit("stop",f._id))}return a},mute:function(r,i){var a=this;if(a._state!=="loaded"||a._playLock)return a._queue.push({event:"mute",action:function(){a.mute(r,i)}}),a;if(typeof i>"u")if(typeof r=="boolean")a._muted=r;else return a._muted;for(var c=a._getSoundIds(i),d=0;d"u"){var d=r._getSoundIds(),f=d.indexOf(i[0]);f>=0?c=parseInt(i[0],10):a=parseFloat(i[0])}else i.length>=2&&(a=parseFloat(i[0]),c=parseInt(i[1],10));var u;if(typeof a<"u"&&a>=0&&a<=1){if(r._state!=="loaded"||r._playLock)return r._queue.push({event:"volume",action:function(){r.volume.apply(r,i)}}),r;typeof c>"u"&&(r._volume=a),c=r._getSoundIds(c);for(var _=0;_"u")}}return d},_startFadeInterval:function(r,i,a,c,d,f){var u=this,_=i,v=a-i,A=Math.abs(v/.01),w=Math.max(4,A>0?c/A:c),O=Date.now();r._fadeTo=a,r._interval=setInterval(function(){var T=(Date.now()-O)/c;O=Date.now(),_+=v*T,_=Math.round(_*100)/100,v<0?_=Math.max(a,_):_=Math.min(a,_),u._webAudio?r._volume=_:u.volume(_,r._id,!0),f&&(u._volume=_),(ai&&_>=a)&&(clearInterval(r._interval),r._interval=null,r._fadeTo=null,u.volume(a,r._id),u._emit("fade",r._id))},w)},_stopFade:function(r){var i=this,a=i._soundById(r);return a&&a._interval&&(i._webAudio&&a._node.gain.cancelScheduledValues(t.ctx.currentTime),clearInterval(a._interval),a._interval=null,i.volume(a._fadeTo,r),a._fadeTo=null,i._emit("fade",r)),i},loop:function(){var r=this,i=arguments,a,c,d;if(i.length===0)return r._loop;if(i.length===1)if(typeof i[0]=="boolean")a=i[0],r._loop=a;else return d=r._soundById(parseInt(i[0],10)),d?d._loop:!1;else i.length===2&&(a=i[0],c=parseInt(i[1],10));for(var f=r._getSoundIds(c),u=0;u=0?c=parseInt(i[0],10):a=parseFloat(i[0])}else i.length===2&&(a=parseFloat(i[0]),c=parseInt(i[1],10));var u;if(typeof a=="number"){if(r._state!=="loaded"||r._playLock)return r._queue.push({event:"rate",action:function(){r.rate.apply(r,i)}}),r;typeof c>"u"&&(r._rate=a),c=r._getSoundIds(c);for(var _=0;_=0?c=parseInt(i[0],10):r._sounds.length&&(c=r._sounds[0]._id,a=parseFloat(i[0]))}else i.length===2&&(a=parseFloat(i[0]),c=parseInt(i[1],10));if(typeof c>"u")return 0;if(typeof a=="number"&&(r._state!=="loaded"||r._playLock))return r._queue.push({event:"seek",action:function(){r.seek.apply(r,i)}}),r;var u=r._soundById(c);if(u)if(typeof a=="number"&&a>=0){var _=r.playing(c);_&&r.pause(c,!0),u._seek=a,u._ended=!1,r._clearTimer(c),!r._webAudio&&u._node&&!isNaN(u._node.duration)&&(u._node.currentTime=a);var v=function(){_&&r.play(c,!0),r._emit("seek",c)};if(_&&!r._webAudio){var A=function(){r._playLock?setTimeout(A,0):v()};setTimeout(A,0)}else v()}else if(r._webAudio){var w=r.playing(c)?t.ctx.currentTime-u._playStart:0,O=u._rateSeek?u._rateSeek-u._seek:0;return u._seek+(O+w*Math.abs(u._rate))}else return u._node.currentTime;return r},playing:function(r){var i=this;if(typeof r=="number"){var a=i._soundById(r);return a?!a._paused:!1}for(var c=0;c=0&&t._howls.splice(c,1);var d=!0;for(a=0;a=0){d=!1;break}return l&&d&&delete l[r._src],t.noAudio=!1,r._state="unloaded",r._sounds=[],r=null,null},on:function(r,i,a,c){var d=this,f=d["_on"+r];return typeof i=="function"&&f.push(c?{id:a,fn:i,once:c}:{id:a,fn:i}),d},off:function(r,i,a){var c=this,d=c["_on"+r],f=0;if(typeof i=="number"&&(a=i,i=null),i||a)for(f=0;f=0;f--)(!d[f].id||d[f].id===i||r==="load")&&(setTimeout(function(u){u.call(this,i,a)}.bind(c,d[f].fn),0),d[f].once&&c.off(r,d[f].fn,d[f].id));return c._loadQueue(r),c},_loadQueue:function(r){var i=this;if(i._queue.length>0){var a=i._queue[0];a.event===r&&(i._queue.shift(),i._loadQueue()),r||a.action()}return i},_ended:function(r){var i=this,a=r._sprite;if(!i._webAudio&&r._node&&!r._node.paused&&!r._node.ended&&r._node.currentTime=0;c--){if(a<=i)return;r._sounds[c]._ended&&(r._webAudio&&r._sounds[c]._node&&r._sounds[c]._node.disconnect(0),r._sounds.splice(c,1),a--)}}},_getSoundIds:function(r){var i=this;if(typeof r>"u"){for(var a=[],c=0;c=0;if(!r.bufferSource)return i;if(t._scratchBuffer&&r.bufferSource&&(r.bufferSource.onended=null,r.bufferSource.disconnect(0),a))try{r.bufferSource.buffer=t._scratchBuffer}catch{}return r.bufferSource=null,i},_clearSound:function(r){var i=/MSIE |Trident\//.test(t._navigator&&t._navigator.userAgent);i||(r.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var s=function(r){this._parent=r,this.init()};s.prototype={init:function(){var r=this,i=r._parent;return r._muted=i._muted,r._loop=i._loop,r._volume=i._volume,r._rate=i._rate,r._seek=0,r._paused=!0,r._ended=!0,r._sprite="__default",r._id=++t._counter,i._sounds.push(r),r.create(),r},create:function(){var r=this,i=r._parent,a=t._muted||r._muted||r._parent._muted?0:r._volume;return i._webAudio?(r._node=typeof t.ctx.createGain>"u"?t.ctx.createGainNode():t.ctx.createGain(),r._node.gain.setValueAtTime(a,t.ctx.currentTime),r._node.paused=!0,r._node.connect(t.masterGain)):t.noAudio||(r._node=t._obtainHtml5Audio(),r._errorFn=r._errorListener.bind(r),r._node.addEventListener("error",r._errorFn,!1),r._loadFn=r._loadListener.bind(r),r._node.addEventListener(t._canPlayEvent,r._loadFn,!1),r._endFn=r._endListener.bind(r),r._node.addEventListener("ended",r._endFn,!1),r._node.src=i._src,r._node.preload=i._preload===!0?"auto":i._preload,r._node.volume=a*t.volume(),r._node.load()),r},reset:function(){var r=this,i=r._parent;return r._muted=i._muted,r._loop=i._loop,r._volume=i._volume,r._rate=i._rate,r._seek=0,r._rateSeek=0,r._paused=!0,r._ended=!0,r._sprite="__default",r._id=++t._counter,r},_errorListener:function(){var r=this;r._parent._emit("loaderror",r._id,r._node.error?r._node.error.code:0),r._node.removeEventListener("error",r._errorFn,!1)},_loadListener:function(){var r=this,i=r._parent;i._duration=Math.ceil(r._node.duration*10)/10,Object.keys(i._sprite).length===0&&(i._sprite={__default:[0,i._duration*1e3]}),i._state!=="loaded"&&(i._state="loaded",i._emit("load"),i._loadQueue()),r._node.removeEventListener(t._canPlayEvent,r._loadFn,!1)},_endListener:function(){var r=this,i=r._parent;i._duration===1/0&&(i._duration=Math.ceil(r._node.duration*10)/10,i._sprite.__default[1]===1/0&&(i._sprite.__default[1]=i._duration*1e3),i._ended(r)),r._node.removeEventListener("ended",r._endFn,!1)}};var l={},h=function(r){var i=r._src;if(l[i]){r._duration=l[i].duration,p(r);return}if(/^data:[^;]+;base64,/.test(i)){for(var a=atob(i.split(",")[1]),c=new Uint8Array(a.length),d=0;d0?(l[i._src]=d,p(i,d)):a()};typeof Promise<"u"&&t.ctx.decodeAudioData.length===1?t.ctx.decodeAudioData(r).then(c).catch(a):t.ctx.decodeAudioData(r,c,a)},p=function(r,i){i&&!r._duration&&(r._duration=i.duration),Object.keys(r._sprite).length===0&&(r._sprite={__default:[0,r._duration*1e3]}),r._state!=="loaded"&&(r._state="loaded",r._emit("load"),r._loadQueue())},b=function(){if(t.usingWebAudio){try{typeof AudioContext<"u"?t.ctx=new AudioContext:typeof webkitAudioContext<"u"?t.ctx=new webkitAudioContext:t.usingWebAudio=!1}catch{t.usingWebAudio=!1}t.ctx||(t.usingWebAudio=!1);var r=/iP(hone|od|ad)/.test(t._navigator&&t._navigator.platform),i=t._navigator&&t._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),a=i?parseInt(i[1],10):null;if(r&&a&&a<9){var c=/safari/.test(t._navigator&&t._navigator.userAgent.toLowerCase());t._navigator&&!c&&(t.usingWebAudio=!1)}t.usingWebAudio&&(t.masterGain=typeof t.ctx.createGain>"u"?t.ctx.createGainNode():t.ctx.createGain(),t.masterGain.gain.setValueAtTime(t._muted?0:t._volume,t.ctx.currentTime),t.masterGain.connect(t.ctx.destination)),t._setup()}};o.Howler=t,o.Howl=n,typeof N<"u"?(N.HowlerGlobal=e,N.Howler=t,N.Howl=n,N.Sound=s):typeof window<"u"&&(window.HowlerGlobal=e,window.Howler=t,window.Howl=n,window.Sound=s)})();/*! + * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. + * + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */(function(){HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(t){var n=this;if(!n.ctx||!n.ctx.listener)return n;for(var s=n._howls.length-1;s>=0;s--)n._howls[s].stereo(t);return n},HowlerGlobal.prototype.pos=function(t,n,s){var l=this;if(!l.ctx||!l.ctx.listener)return l;if(n=typeof n!="number"?l._pos[1]:n,s=typeof s!="number"?l._pos[2]:s,typeof t=="number")l._pos=[t,n,s],typeof l.ctx.listener.positionX<"u"?(l.ctx.listener.positionX.setTargetAtTime(l._pos[0],Howler.ctx.currentTime,.1),l.ctx.listener.positionY.setTargetAtTime(l._pos[1],Howler.ctx.currentTime,.1),l.ctx.listener.positionZ.setTargetAtTime(l._pos[2],Howler.ctx.currentTime,.1)):l.ctx.listener.setPosition(l._pos[0],l._pos[1],l._pos[2]);else return l._pos;return l},HowlerGlobal.prototype.orientation=function(t,n,s,l,h,g){var m=this;if(!m.ctx||!m.ctx.listener)return m;var p=m._orientation;if(n=typeof n!="number"?p[1]:n,s=typeof s!="number"?p[2]:s,l=typeof l!="number"?p[3]:l,h=typeof h!="number"?p[4]:h,g=typeof g!="number"?p[5]:g,typeof t=="number")m._orientation=[t,n,s,l,h,g],typeof m.ctx.listener.forwardX<"u"?(m.ctx.listener.forwardX.setTargetAtTime(t,Howler.ctx.currentTime,.1),m.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),m.ctx.listener.forwardZ.setTargetAtTime(s,Howler.ctx.currentTime,.1),m.ctx.listener.upX.setTargetAtTime(l,Howler.ctx.currentTime,.1),m.ctx.listener.upY.setTargetAtTime(h,Howler.ctx.currentTime,.1),m.ctx.listener.upZ.setTargetAtTime(g,Howler.ctx.currentTime,.1)):m.ctx.listener.setOrientation(t,n,s,l,h,g);else return p;return m},Howl.prototype.init=function(t){return function(n){var s=this;return s._orientation=n.orientation||[1,0,0],s._stereo=n.stereo||null,s._pos=n.pos||null,s._pannerAttr={coneInnerAngle:typeof n.coneInnerAngle<"u"?n.coneInnerAngle:360,coneOuterAngle:typeof n.coneOuterAngle<"u"?n.coneOuterAngle:360,coneOuterGain:typeof n.coneOuterGain<"u"?n.coneOuterGain:0,distanceModel:typeof n.distanceModel<"u"?n.distanceModel:"inverse",maxDistance:typeof n.maxDistance<"u"?n.maxDistance:1e4,panningModel:typeof n.panningModel<"u"?n.panningModel:"HRTF",refDistance:typeof n.refDistance<"u"?n.refDistance:1,rolloffFactor:typeof n.rolloffFactor<"u"?n.rolloffFactor:1},s._onstereo=n.onstereo?[{fn:n.onstereo}]:[],s._onpos=n.onpos?[{fn:n.onpos}]:[],s._onorientation=n.onorientation?[{fn:n.onorientation}]:[],t.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(t,n){var s=this;if(!s._webAudio)return s;if(s._state!=="loaded")return s._queue.push({event:"stereo",action:function(){s.stereo(t,n)}}),s;var l=typeof Howler.ctx.createStereoPanner>"u"?"spatial":"stereo";if(typeof n>"u")if(typeof t=="number")s._stereo=t,s._pos=[t,0,0];else return s._stereo;for(var h=s._getSoundIds(n),g=0;g"u")if(typeof t=="number")h._pos=[t,n,s];else return h._pos;for(var g=h._getSoundIds(l),m=0;m"u")if(typeof t=="number")h._orientation=[t,n,s];else return h._orientation;for(var g=h._getSoundIds(l),m=0;m"u"&&(s.pannerAttr||(s.pannerAttr={coneInnerAngle:s.coneInnerAngle,coneOuterAngle:s.coneOuterAngle,coneOuterGain:s.coneOuterGain,distanceModel:s.distanceModel,maxDistance:s.maxDistance,refDistance:s.refDistance,rolloffFactor:s.rolloffFactor,panningModel:s.panningModel}),t._pannerAttr={coneInnerAngle:typeof s.pannerAttr.coneInnerAngle<"u"?s.pannerAttr.coneInnerAngle:t._coneInnerAngle,coneOuterAngle:typeof s.pannerAttr.coneOuterAngle<"u"?s.pannerAttr.coneOuterAngle:t._coneOuterAngle,coneOuterGain:typeof s.pannerAttr.coneOuterGain<"u"?s.pannerAttr.coneOuterGain:t._coneOuterGain,distanceModel:typeof s.pannerAttr.distanceModel<"u"?s.pannerAttr.distanceModel:t._distanceModel,maxDistance:typeof s.pannerAttr.maxDistance<"u"?s.pannerAttr.maxDistance:t._maxDistance,refDistance:typeof s.pannerAttr.refDistance<"u"?s.pannerAttr.refDistance:t._refDistance,rolloffFactor:typeof s.pannerAttr.rolloffFactor<"u"?s.pannerAttr.rolloffFactor:t._rolloffFactor,panningModel:typeof s.pannerAttr.panningModel<"u"?s.pannerAttr.panningModel:t._panningModel});else return h=t._soundById(parseInt(n[0],10)),h?h._pannerAttr:t._pannerAttr;else n.length===2&&(s=n[0],l=parseInt(n[1],10));for(var g=t._getSoundIds(l),m=0;mD||(D=new Mt({ripple:!1,dismissible:!0,duration:2e3,types:[{type:"success",background:"black"}]}),D),he=async o=>{try{if(navigator.clipboard)await navigator.clipboard.writeText(o);else{const e=document.createElement("textarea");e.value=o,document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}C().success("Link copied to clipboard!")}catch(e){C().error(e.message)}};class St extends M{static values={id:String,universalLink:String,audio:String};static targets=["icon","audioProgress"];async connect(){this.soundPlayer=new fe.Howl({src:[this.audioValue],html5:!0,volume:.7}),this.soundPlayer.on("end",()=>{this.resetProgressBar(),this.updateAudioPreviewIcon(!0)}),this.soundPlayer.on("play",()=>{this.startAudioProgress()}),this.soundPlayer.on("pause",()=>{this.stopProgressUpdate()})}async share(){const e=this.universalLinkValue;if(!e){console.error("Universal link not found");return}try{if(navigator.share){await navigator.share({title:"Share this universal link",url:e});return}await he(e)}catch(t){console.error(t)}}toggleAudio(){const e=this.soundPlayer.playing();e?this.soundPlayer.pause():this.soundPlayer.play(),this.updateAudioPreviewIcon(e)}startAudioProgress(){this.audioProgressInterval=setInterval(()=>{const e=this.soundPlayer.duration(),n=this.soundPlayer.seek()/e*100;this.audioProgressTarget.style.width=`${n}%`},10)}stopProgressUpdate(){clearInterval(this.audioProgressInterval)}resetProgressBar(){clearInterval(this.audioProgressInterval),this.audioProgressTarget.style.width="0%"}updateAudioPreviewIcon(e){const t=this.iconTarget;e?(t.classList.remove("fa-pause"),t.classList.add("fa-play")):(t.classList.remove("fa-play"),t.classList.add("fa-pause"))}}const Ft=Object.freeze(Object.defineProperty({__proto__:null,default:St},Symbol.toStringTag,{value:"Module"})),Nt=/^https:\/\/(open\.spotify\.com\/(track|album|playlist|artist|episode|show)|spotify\.link)\/(\w{11,24})(?:[\?#].*)?$/,Ct=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be|music\.youtube\.com)\/(?:watch\?v=|embed\/|v\/|shorts\/|playlist\?list=|channel\/)?([\w-]{11,})(?:\S+)?(?:&si=\S+)?/;class It extends M{static targets=["form","link"];initialize(){document.addEventListener("htmx:afterOnLoad",()=>{const e=new URLSearchParams(window.location.search),t=this.element.querySelector('[data-controller="search-card"]')?.getAttribute("data-search-card-id-value");t&&(e.set("id",t),window.history.replaceState({},"",`${window.location.pathname}?${e}`))}),document.addEventListener("htmx:timeout",function(){C().error("Something went wrong, please try again later.")})}async submitFromClipboard(){if("clipboard"in navigator){const e=new URLSearchParams(window.location.search);try{(await navigator.clipboard.readText()).match(new RegExp(`${Nt.source}|${Ct.source}`))&&!e.get("id")&&(this.linkTarget.value=link,this.formTarget.submit())}catch{C().error("Clipboard access error")}}else C().error("Feature not supported in your browser")}}const Lt=Object.freeze(Object.defineProperty({__proto__:null,default:It},Symbol.toStringTag,{value:"Module"}));class Bt extends M{static values={url:String};share(){he(this.urlValue)}}const Pt=Object.freeze(Object.defineProperty({__proto__:null,default:Bt},Symbol.toStringTag,{value:"Module"}));var Dt=/^(?:.*?(?:controllers|components)\/|\.?\.\/)?(.+)(?:[/_-]controller\..+?)$/;function $t(o,e){o.load(Vt(e))}function Vt(o){return Object.entries(o).map(jt).filter(e=>e)}function jt([o,e]){var t;const n=Ht(o),s=(t=e.default)!=null?t:e;if(n&&typeof s=="function")return{identifier:n,controllerConstructor:s}}function Ht(o){const e=(o.match(Dt)||[])[1];if(e)return e.replace(/_/g,"-").replace(/\//g,"--")}const Kt=lt.start(),Rt=Object.assign({"./search_card_controller.js":Ft,"./search_controller.js":Lt,"./search_link_controller.js":Pt});$t(Kt,Rt); diff --git a/public/assets/entry.js b/public/assets/entry.js deleted file mode 100644 index fc816ab..0000000 --- a/public/assets/entry.js +++ /dev/null @@ -1 +0,0 @@ -const r=/^https:\/\/(open\.spotify\.com\/(track|album|playlist|artist|episode|show)|spotify\.link)\/(\w{11,24})(?:[\?#].*)?$/,n=/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be|music\.youtube\.com)\/(?:watch\?v=|embed\/|v\/|shorts\/|playlist\?list=|channel\/)?([\w-]{11,})(?:\S+)?(?:&si=\S+)?/,a=new URLSearchParams(window.location.search),c=({link:e})=>{const o=document.getElementById("search-form");o.querySelector("input").value=e,htmx.ajax("POST","/search",{source:"#search-form"})},s=async()=>{if("clipboard"in navigator)try{const e=await navigator.clipboard.readText();e.match(new RegExp(`${r.source}|${n.source}`))&&!a.get("id")&&c({link:e})}catch(e){console.error("Clipboard access error:",e)}else console.error("Clipboard API is not supported.")};document.addEventListener("htmx:afterOnLoad",()=>{const e=document.getElementById("search-card")?.getAttribute("data-id");e&&(a.set("id",e),window.history.replaceState({},"",`${window.location.pathname}?${a}`))});document.addEventListener("htmx:timeout",function(){document.getElementById("search-results").innerHTML='

Something went wrong, try again later.

'});document.addEventListener("DOMContentLoaded",async()=>{await s()});window.shareLink=async e=>{if(!e){console.error("Universal link not found");return}const o=new Notyf({riple:!1,dismissible:!0,duration:2e3,types:[{type:"success",background:"black"}]});try{if(navigator.share){await navigator.share({title:"Share this universal link",url:e});return}if(navigator.clipboard)await navigator.clipboard.writeText(e);else{const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}o.success("Link copied to clipboard!")}catch(t){console.error(t)}}; diff --git a/src/config/env.ts b/src/config/env.ts index 050758d..c1ab968 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -37,7 +37,7 @@ export const ENV = { version: version, }, cache: { - databasePath: Bun.env.DATABASE_PATH!, + databasePath: Bun.env.DATABASE_PATH ?? ':memory:', expTime: 60 * 60 * 24 * 7, // 1 week in seconds }, }; diff --git a/src/routes/page.tsx b/src/routes/page.tsx index da617a9..4a68eb3 100644 --- a/src/routes/page.tsx +++ b/src/routes/page.tsx @@ -30,7 +30,7 @@ export const pageRouter = new Elysia() return ; } - return ; + return ; }) .get( '/', @@ -62,6 +62,7 @@ export const pageRouter = new Elysia() '/search', async ({ body: { link } }) => { const searchResult = await search({ link }); + return ; }, { diff --git a/src/services/cache.ts b/src/services/cache.ts index a8503be..9d0ee25 100644 --- a/src/services/cache.ts +++ b/src/services/cache.ts @@ -1,22 +1,21 @@ -const sqliteStore = require('cache-manager-sqlite'); -const cacheManager = require('cache-manager'); +import { caching } from 'cache-manager'; +import bunSqliteStore from 'cache-manager-bun-sqlite3'; import { ENV } from '~/config/env'; import { SearchMetadata, SearchResultLink } from './search'; -export const cacheStore = cacheManager.caching({ - store: sqliteStore, +export const cacheStore = await caching(bunSqliteStore, { name: 'cache', path: ENV.cache.databasePath, + ttl: ENV.cache.expTime, + serializer: 'json', }); export const cacheSearchResultLink = async ( url: URL, searchResultLink: SearchResultLink ) => { - await cacheStore.set(`search:${url.toString()}`, searchResultLink, { - ttl: ENV.cache.expTime, - }); + await cacheStore.set(`search:${url.toString()}`, searchResultLink); }; export const getCachedSearchResultLink = async (url: URL) => { @@ -26,9 +25,7 @@ export const getCachedSearchResultLink = async (url: URL) => { }; export const cacheSearchMetadata = async (id: string, searchMetadata: SearchMetadata) => { - await cacheStore.set(`metadata:${id}`, searchMetadata, { - ttl: ENV.cache.expTime, - }); + await cacheStore.set(`metadata:${id}`, searchMetadata); }; export const getCachedSearchMetadata = async (id: string) => { @@ -38,9 +35,7 @@ export const getCachedSearchMetadata = async (id: string) => { }; export const cacheSpotifyAccessToken = async (accessToken: string, expTime: number) => { - await cacheStore.set('spotify:accessToken', accessToken, { - ttl: expTime, - }); + await cacheStore.set('spotify:accessToken', accessToken, expTime); }; export const getCachedSpotifyAccessToken = async () => { @@ -48,9 +43,7 @@ export const getCachedSpotifyAccessToken = async () => { }; export const cacheShortenLink = async (link: string, refer: string) => { - await cacheStore.set(`url-shortener:${link}`, refer, { - ttl: ENV.cache.expTime, - }); + await cacheStore.set(`url-shortener:${link}`, refer); }; export const getCachedShortenLink = async (link: string) => { diff --git a/src/services/search.ts b/src/services/search.ts index 9317838..6f5030a 100644 --- a/src/services/search.ts +++ b/src/services/search.ts @@ -103,14 +103,12 @@ export const search = async ({ url: link, isVerified: true, }, - ], + ] as SearchResultLink[], }; } const searchResultsPromise = Promise.all([ - searchAdapters.includes(Adapter.Spotify) && searchParser.type !== Adapter.Spotify - ? getSpotifyLink(query, metadata) - : null, + searchParser.type !== Adapter.Spotify ? getSpotifyLink(query, metadata) : null, searchAdapters.includes(Adapter.YouTube) && searchParser.type !== Adapter.YouTube ? getYouTubeLink(query, metadata) : null, @@ -128,6 +126,20 @@ export const search = async ({ shortenLink(`${ENV.app.url}?id=${id}`), ]); + if (searchParser.type !== Adapter.Spotify) { + const spotifySearchResult = searchResults.find( + searchResult => searchResult?.type === Adapter.Spotify + ); + + if (spotifySearchResult) { + const spotifySearchParser = await getSearchParser(spotifySearchResult.url); + metadata = await getSpotifyMetadata( + spotifySearchParser.id, + spotifySearchResult.url + ); + } + } + const links = searchResults.filter(Boolean); logger.info(`[${search.name}] (results) ${JSON.stringify(links, null, 2)}`); diff --git a/src/views/components/search-bar.tsx b/src/views/components/search-bar.tsx deleted file mode 100644 index 19c4ae7..0000000 --- a/src/views/components/search-bar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -export default function SearchBar({ source }: { source?: string }) { - return ( -
- - - - - ); -} diff --git a/src/views/components/search-card.tsx b/src/views/components/search-card.tsx index 3b93ae8..a2dc588 100644 --- a/src/views/components/search-card.tsx +++ b/src/views/components/search-card.tsx @@ -1,43 +1,122 @@ +import { Adapter } from '~/config/enum'; import { SearchResult } from '~/services/search'; -import SearchLink from './search-link'; +const SEARCH_LINK_DICT = { + [Adapter.Spotify]: { + icon: 'fab fa-spotify', + label: 'Listen on Spotify', + }, + [Adapter.YouTube]: { + icon: 'fab fa-youtube', + label: 'Listen on YouTube Music', + }, + [Adapter.Deezer]: { + icon: 'fab fa-deezer', + label: 'Listen on Deezer', + }, + [Adapter.AppleMusic]: { + icon: 'fab fa-apple', + label: 'Listen on Apple Music', + }, + [Adapter.Tidal]: { + icon: 'fa fa-music', + label: 'Listen on Tidal', + }, + [Adapter.SoundCloud]: { + icon: 'fab fa-soundcloud', + label: 'Listen on SoundCloud', + }, +}; export default function SearchCard(props: { searchResult: SearchResult }) { return (
- -
+
{props.searchResult.title} -
-
-
- {props.searchResult.title} +
+

+ {props.searchResult.title} +

+

{props.searchResult.description}

+
+ + +
-

{props.searchResult.description}

+
+
{props.searchResult.links.length === 0 && ( -

+

Not available on other platforms

)} {props.searchResult.links.length > 0 && ( -
diff --git a/src/views/components/search-link.tsx b/src/views/components/search-link.tsx deleted file mode 100644 index 4a0bfc1..0000000 --- a/src/views/components/search-link.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Adapter } from '~/config/enum'; - -const SEARCH_LINK_DICT = { - [Adapter.Spotify]: { - icon: 'fab fa-spotify', - label: 'Listen on Spotify', - }, - [Adapter.YouTube]: { - icon: 'fab fa-youtube', - label: 'Listen on YouTube Music', - }, - [Adapter.Deezer]: { - icon: 'fab fa-deezer', - label: 'Listen on Deezer', - }, - [Adapter.AppleMusic]: { - icon: 'fab fa-apple', - label: 'Listen on Apple Music', - }, - [Adapter.Tidal]: { - icon: 'fa fa-music', - label: 'Listen on Tidal', - }, - [Adapter.SoundCloud]: { - icon: 'fab fa-soundcloud', - label: 'Listen on SoundCloud', - }, -}; - -export default function SearchLink(props: { - type: Adapter; - url: string; - isVerified?: boolean; -}) { - const searchResult = SEARCH_LINK_DICT[props.type]; - - return ( - - -

{searchResult.label}

- {props.isVerified && ( - - - - )} -
- ); -} diff --git a/src/views/controllers/helpers.js b/src/views/controllers/helpers.js new file mode 100644 index 0000000..17092eb --- /dev/null +++ b/src/views/controllers/helpers.js @@ -0,0 +1,52 @@ +import { Notyf } from 'notyf'; + +let _toast; + +/** + * Initializes and returns a Notyf instance for displaying toast notifications. + * + * @returns {Notyf} The Notyf instance for toast notifications. + */ +export const toast = () => { + if (_toast) return _toast; + + _toast = new Notyf({ + ripple: false, + dismissible: true, + duration: 2000, + types: [ + { + type: 'success', + background: 'black', + }, + ], + }); + + return _toast; +}; + +/** + * Copies the provided link to the clipboard and shows a success toast notification. + * If the clipboard API is not available, it falls back to using a temporary textarea. + * + * @param {string} link - The link to copy to the clipboard. + * @returns {Promise} + */ +export const copyToClipboard = async link => { + try { + if (navigator.clipboard) { + await navigator.clipboard.writeText(link); + } else { + const textArea = document.createElement('textarea'); + textArea.value = link; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } + + toast().success('Link copied to clipboard!'); + } catch (err) { + toast().error(err.message); + } +}; diff --git a/src/views/controllers/index.js b/src/views/controllers/index.js new file mode 100644 index 0000000..e2504f5 --- /dev/null +++ b/src/views/controllers/index.js @@ -0,0 +1,6 @@ +import { Application } from '@hotwired/stimulus'; +import { registerControllers } from 'stimulus-vite-helpers'; + +const application = Application.start(); +const controllers = import.meta.glob('./**/*_controller.js', { eager: true }); +registerControllers(application, controllers); diff --git a/src/views/controllers/search_card_controller.js b/src/views/controllers/search_card_controller.js new file mode 100644 index 0000000..eb049ef --- /dev/null +++ b/src/views/controllers/search_card_controller.js @@ -0,0 +1,110 @@ +import { Controller } from '@hotwired/stimulus'; +import { Howl } from 'howler'; +import { copyToClipboard } from './helpers'; + +export default class extends Controller { + static values = { id: String, universalLink: String, audio: String }; + static targets = ['icon', 'audioProgress']; + + async connect() { + this.soundPlayer = new Howl({ + src: [this.audioValue], + html5: true, + volume: 0.7, + }); + + this.soundPlayer.on('end', () => { + this.resetProgressBar(); + this.updateAudioPreviewIcon(true); + }); + + this.soundPlayer.on('play', () => { + this.startAudioProgress(); + }); + + this.soundPlayer.on('pause', () => { + this.stopProgressUpdate(); + }); + } + + /** + * Shares the universal link using the Web Share API or copies it to the clipboard. + */ + async share() { + const universalLink = this.universalLinkValue; + + if (!universalLink) { + console.error('Universal link not found'); + return; + } + + try { + if (navigator.share) { + await navigator.share({ + title: 'Share this universal link', + url: universalLink, + }); + return; + } + + await copyToClipboard(universalLink); + } catch (err) { + console.error(err); + } + } + + /** + * Toggles the audio playback state and updates the play/pause icon. + */ + toggleAudio() { + const isPlaying = this.soundPlayer.playing(); + if (isPlaying) { + this.soundPlayer.pause(); + } else { + this.soundPlayer.play(); + } + this.updateAudioPreviewIcon(isPlaying); + } + + /** + * Starts updating the audio progress bar. + */ + startAudioProgress() { + this.audioProgressInterval = setInterval(() => { + const duration = this.soundPlayer.duration(); + const seek = this.soundPlayer.seek(); + const progress = (seek / duration) * 100; + this.audioProgressTarget.style.width = `${progress}%`; + }, 10); + } + + /** + * Stops updating the audio progress bar. + */ + stopProgressUpdate() { + clearInterval(this.audioProgressInterval); + } + + /** + * Resets the audio progress bar. + */ + resetProgressBar() { + clearInterval(this.audioProgressInterval); + this.audioProgressTarget.style.width = '0%'; + } + + /** + * Updates the audio preview icon based on the playback state. + * @param {boolean} playing - Indicates if the audio is currently playing. + */ + updateAudioPreviewIcon(playing) { + const iconElement = this.iconTarget; + if (playing) { + iconElement.classList.remove('fa-pause'); + iconElement.classList.add('fa-play'); + } else { + iconElement.classList.remove('fa-play'); + iconElement.classList.add('fa-pause'); + } + } +} diff --git a/src/views/controllers/search_controller.js b/src/views/controllers/search_controller.js new file mode 100644 index 0000000..1943bed --- /dev/null +++ b/src/views/controllers/search_controller.js @@ -0,0 +1,60 @@ +import { Controller } from '@hotwired/stimulus'; + +import { toast } from './helpers'; + +import { SPOTIFY_LINK_REGEX, YOUTUBE_LINK_REGEX } from '~/config/constants'; + +export default class extends Controller { + static targets = ['form', 'link']; + + initialize() { + document.addEventListener('htmx:afterOnLoad', () => { + const searchParams = new URLSearchParams(window.location.search); + const searchId = this.element + .querySelector(`[data-controller="search-card"]`) + ?.getAttribute('data-search-card-id-value'); + + if (searchId) { + searchParams.set('id', searchId); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams}` + ); + } + }); + + document.addEventListener('htmx:timeout', function () { + toast().error('Something went wrong, please try again later.'); + }); + } + + /** + * Submits the form using a link obtained from the clipboard if it matches + * specific patterns and no search ID is present in the URL. + * + * @returns {Promise} + */ + async submitFromClipboard() { + if ('clipboard' in navigator) { + const searchParams = new URLSearchParams(window.location.search); + + try { + const clipboardText = await navigator.clipboard.readText(); + if ( + clipboardText.match( + new RegExp(`${SPOTIFY_LINK_REGEX.source}|${YOUTUBE_LINK_REGEX.source}`) + ) && + !searchParams.get('id') + ) { + this.linkTarget.value = link; + this.formTarget.submit(); + } + } catch (error) { + toast().error('Clipboard access error'); + } + } else { + toast().error('Feature not supported in your browser'); + } + } +} diff --git a/src/views/controllers/search_link_controller.js b/src/views/controllers/search_link_controller.js new file mode 100644 index 0000000..8479a88 --- /dev/null +++ b/src/views/controllers/search_link_controller.js @@ -0,0 +1,11 @@ +import { Controller } from '@hotwired/stimulus'; + +import { copyToClipboard } from './helpers'; + +export default class extends Controller { + static values = { url: String }; + + share() { + copyToClipboard(this.urlValue); + } +} diff --git a/src/views/js/entry.js b/src/views/js/entry.js deleted file mode 100644 index 2713375..0000000 --- a/src/views/js/entry.js +++ /dev/null @@ -1 +0,0 @@ -import './search-bar'; diff --git a/src/views/js/search-bar.js b/src/views/js/search-bar.js deleted file mode 100644 index 62074b5..0000000 --- a/src/views/js/search-bar.js +++ /dev/null @@ -1,93 +0,0 @@ -import { SPOTIFY_LINK_REGEX, YOUTUBE_LINK_REGEX } from '~/config/constants'; - -const searchParams = new URLSearchParams(window.location.search); - -const submitSearch = ({ link }) => { - const searchForm = document.getElementById('search-form'); - searchForm.querySelector('input').value = link; - - htmx.ajax('POST', '/search', { source: '#search-form' }); -}; - -const getSpotifyLinkFromClipboard = async () => { - if ('clipboard' in navigator) { - try { - const clipboardText = await navigator.clipboard.readText(); - - if ( - clipboardText.match( - new RegExp(`${SPOTIFY_LINK_REGEX.source}|${YOUTUBE_LINK_REGEX.source}`) - ) && - !searchParams.get('id') - ) { - submitSearch({ link: clipboardText }); - } - } catch (error) { - console.error('Clipboard access error:', error); - } - } else { - console.error('Clipboard API is not supported.'); - } -}; - -document.addEventListener('htmx:afterOnLoad', () => { - const searchId = document.getElementById('search-card')?.getAttribute('data-id'); - if (searchId) { - searchParams.set('id', searchId); - window.history.replaceState({}, '', `${window.location.pathname}?${searchParams}`); - } -}); - -document.addEventListener('htmx:timeout', function () { - document.getElementById('search-results').innerHTML = - '

Something went wrong, try again later.

'; -}); - -document.addEventListener('DOMContentLoaded', async () => { - await getSpotifyLinkFromClipboard(); -}); - -window.shareLink = async universalLink => { - if (!universalLink) { - console.error('Universal link not found'); - return; - } - - const notyf = new Notyf({ - riple: false, - dismissible: true, - duration: 2000, - types: [ - { - type: 'success', - background: 'black', - }, - ], - }); - - try { - if (navigator.share) { - await navigator.share({ - title: 'Share this universal link', - url: universalLink, - }); - return; - } - - if (navigator.clipboard) { - await navigator.clipboard.writeText(universalLink); - } else { - // Older browser fallback - const textArea = document.createElement('textarea'); - textArea.value = universalLink; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - } - - notyf.success('Link copied to clipboard!'); - } catch (err) { - console.error(err); - } -}; diff --git a/src/views/layouts/main.tsx b/src/views/layouts/main.tsx index 5f0015d..44a6e90 100644 --- a/src/views/layouts/main.tsx +++ b/src/views/layouts/main.tsx @@ -62,7 +62,7 @@ export default function MainLayout({ /> - {children} + {children}