feat: add CreatePlaylistModal component for creating new playlists
feat: implement useKeyboard composable for keyboard controls in the player feat: create PlaylistDetail view to display playlist information and tracks
This commit is contained in:
1 parent
c0be5d76d6
commit
a109f5bab1
15 files changed
+433
-143
No files matched your search
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
.range-player[data-v-0368441b]::-webkit-slider-thumb{-webkit-appearance:none;cursor:pointer;background:#fff;border-radius:50%;width:12px;height:12px}.range-vol[data-v-0368441b]::-webkit-slider-thumb{-webkit-appearance:none;cursor:pointer;background:#fff;border-radius:50%;width:12px;height:12px}.animate-slide-in{animation:.3s ease-out slideIn}@keyframes slideIn{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:Inter,system-ui,sans-serif;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:Space Mono,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.-top-6{top:-1.5rem}.bottom-0{bottom:0}.bottom-12{bottom:3rem}.bottom-2{bottom:.5rem}.bottom-24{bottom:6rem}.left-0{left:0}.left-1\/2{left:50%}.left-12{left:3rem}.left-20{left:5rem}.left-3{left:.75rem}.right-0{right:0}.right-12{right:3rem}.right-2{right:.5rem}.right-20{right:5rem}.right-4{right:1rem}.right-8{right:2rem}.top-0{top:0}.top-16{top:4rem}.top-20{top:5rem}.top-4{top:1rem}.top-8{top:2rem}.top-\[57px\]{top:57px}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-\[100\]{z-index:100}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-8{margin-left:2rem;margin-right:2rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-boLine truncated
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
.range-player[data-v-bb2272cc]::-webkit-slider-thumb{-webkit-appearance:none;cursor:pointer;background:#fff;border-radius:50%;width:12px;height:12px}.range-vol[data-v-bb2272cc]::-webkit-slider-thumb{-webkit-appearance:none;cursor:pointer;background:#fff;border-radius:50%;width:12px;height:12px}.animate-slide-in{animation:.3s ease-out slideIn}@keyframes slideIn{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:Inter,system-ui,sans-serif;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:Space Mono,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-12{bottom:3rem}.bottom-2{bottom:.5rem}.bottom-24{bottom:6rem}.left-0{left:0}.left-12{left:3rem}.left-20{left:5rem}.left-3{left:.75rem}.right-0{right:0}.right-12{right:3rem}.right-2{right:.5rem}.right-20{right:5rem}.right-4{right:1rem}.right-8{right:2rem}.top-0{top:0}.top-16{top:4rem}.top-20{top:5rem}.top-4{top:1rem}.top-8{top:2rem}.top-\[57px\]{top:57px}.top-full{top:100%}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-\[100\]{z-index:100}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-8{margin-left:2rem;margin-right:2rem}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottomLine truncated
|
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
var e=Object.defineProperty,t=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),n=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function r(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var i={},a=[],o=()=>{},s=()=>!1,c=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),l=e=>e.startsWith(`onUpdate:`),u=Object.assign,d=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},f=Object.prototype.hasOwnProperty,p=(e,t)=>f.call(e,t),m=Array.isArray,h=e=>w(e)===`[object Map]`,g=e=>w(e)===`[object Set]`,_=e=>w(e)===`[object Date]`,v=e=>typeof e==`function`,y=e=>typeof e==`string`,b=e=>typeof e==`symbol`,x=e=>typeof e==`object`&&!!e,S=e=>(x(e)||v(e))&&v(e.then)&&v(e.catch),C=Object.prototype.toString,w=e=>C.call(e),ee=e=>w(e).slice(8,-1),te=e=>w(e)===`[object Object]`,T=e=>y(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,E=r(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),D=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},ne=/-\w/g,O=D(e=>e.replace(ne,e=>e.slice(1).toUpperCase())),re=/\B([A-Z])/g,ie=D(e=>e.replace(re,`-$1`).toLowerCase()),ae=D(e=>e.charAt(0).toUpperCase()+e.slice(1)),k=D(e=>e?`on${ae(e)}`:``),oe=(e,t)=>!Object.is(e,t),se=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},ce=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},le=e=>{let t=parseFloat(e);return isNaN(t)?e:t},ue,de=()=>ue||=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{};function fe(e){if(m(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],i=y(r)?ge(r):fe(r);if(i)for(let e in i)t[e]=i[e]}return t}else if(y(e)||x(e))return e}var pe=/;(?![^(]*\))/g,me=/:([^]+)/,he=/\/\*[^]*?\*\//g;function ge(e){let t={};return e.replace(he,``).split(pe).forEach(e=>{if(e){let n=e.split(me);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function A(e){let t=``;if(y(e))t=e;else if(m(e))for(let n=0;n<e.length;n++){let r=A(e[n]);r&&(t+=r+` `)}else if(x(e))for(let n in e)e[n]&&(t+=n+` `);return t.trim()}var _e=`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`,ve=r(_e);_e+``;function ye(e){return!!e||e===``}function be(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=xe(e[r],t[r]);return n}function xe(e,t){if(e===t)return!0;let n=_(e),r=_(t);if(n||r)return n&&r?e.getTime()===t.getTime():!1;if(n=b(e),r=b(t),n||r)return e===t;if(n=m(e),r=m(t),n||r)return n&&r?be(e,t):!1;if(n=x(e),r=x(t),n||r){if(!n||!r||Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e){let r=e.hasOwnProperty(n),i=t.hasOwnProperty(n);if(r&&!i||!r&&i||!xe(e[n],t[n]))return!1}}return String(e)===String(t)}function Se(e,t){return e.findIndex(e=>xe(e,t))}var Ce=e=>!!(e&&e.__v_isRef===!0),j=e=>y(e)?e:e==null?``:m(e)||x(e)&&(e.toString===C||!v(e.toString))?Ce(e)?j(e.value):JSON.stringify(e,we,2):String(e),we=(e,t)=>Ce(t)?we(e,t.value):h(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[Te(t,r)+` =>`]=n,e),{})}:g(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>Te(e))}:b(t)?Te(t):x(t)&&!m(t)&&!te(t)?String(t):t,Te=(e,t=``)=>b(e)?`Symbol(${e.description??t})`:e,M,Ee=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&M&&(M.active?(this.parent=M,this.index=(M.scopes||=[]).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].pause();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].resume();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].resume()}}run(e){if(this._active){let t=M;try{return M=this,e()}finally{M=t}}}on(){++this._on===1&&(this.prevScope=M,M=this)}off(){if(this._on>0&&--this._on===0){if(M===this)M=this.prevScope;else{let e=M;for(;e;){if(e.prevScope===this){e.prevScope=this.preLine truncated
|
||||||
|
`).forEach(function(e){i=e.indexOf(`:`),n=e.substring(0,i).trim().toLowerCase(),r=e.substring(i+1).trim(),!(!n||t[n]&&Bu[n])&&(n===`set-cookie`?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+`, `+r:r)}),t};function Hu(e){let t=0,n=e.length;for(;t<n;){let n=e.charCodeAt(t);if(n!==9&&n!==32)break;t+=1}for(;n>t;){let t=e.charCodeAt(n-1);if(t!==9&&t!==32)break;--n}return t===0&&n===e.length?e:e.slice(t,n)}var Uu=RegExp(`[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+`,`g`),Wu=RegExp(`[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+`,`g`);function Gu(e,t){return X.isArray(e)?e.map(e=>Gu(e,t)):Hu(String(e).replace(t,``))}var Ku=e=>Gu(e,Uu),qu=e=>Gu(e,Wu);function Ju(e){let t=Object.create(null);return X.forEach(e.toJSON(),(e,n)=>{t[n]=qu(e)}),t}var Yu=Symbol(`internals`);function Xu(e){return e&&String(e).trim().toLowerCase()}function Zu(e){return e===!1||e==null?e:X.isArray(e)?e.map(Zu):Ku(String(e))}function Qu(e){let t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g,r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}var $u=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function ed(e,t,n,r,i){if(X.isFunction(r))return r.call(this,t,n);if(i&&(t=n),X.isString(t)){if(X.isString(r))return t.indexOf(r)!==-1;if(X.isRegExp(r))return r.test(t)}}function td(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,t,n)=>t.toUpperCase()+n)}function nd(e,t){let n=X.toCamelCase(` `+t);[`get`,`set`,`has`].forEach(r=>{Object.defineProperty(e,r+n,{__proto__:null,value:function(e,n,i){return this[r].call(this,t,e,n,i)},configurable:!0})})}var rd=class{constructor(e){e&&this.set(e)}set(e,t,n){let r=this;function i(e,t,n){let i=Xu(t);if(!i)throw Error(`header name must be a non-empty string`);let a=X.findKey(r,i);(!a||r[a]===void 0||n===!0||n===void 0&&r[a]!==!1)&&(r[a||t]=Zu(e))}let a=(e,t)=>X.forEach(e,(e,n)=>i(e,n,t));if(X.isPlainObject(e)||e instanceof this.constructor)a(e,t);else if(X.isString(e)&&(e=e.trim())&&!$u(e))a(Vu(e),t);else if(X.isObject(e)&&X.isIterable(e)){let n={},r,i;for(let t of e){if(!X.isArray(t))throw TypeError(`Object iterator must return a key-value pair`);n[i=t[0]]=(r=n[i])?X.isArray(r)?[...r,t[1]]:[r,t[1]]:t[1]}a(n,t)}else e!=null&&i(t,e,n);return this}get(e,t){if(e=Xu(e),e){let n=X.findKey(this,e);if(n){let e=this[n];if(!t)return e;if(t===!0)return Qu(e);if(X.isFunction(t))return t.call(this,e,n);if(X.isRegExp(t))return t.exec(e);throw TypeError(`parser must be boolean|regexp|function`)}}}has(e,t){if(e=Xu(e),e){let n=X.findKey(this,e);return!!(n&&this[n]!==void 0&&(!t||ed(this,this[n],n,t)))}return!1}delete(e,t){let n=this,r=!1;function i(e){if(e=Xu(e),e){let i=X.findKey(n,e);i&&(!t||ed(n,n[i],i,t))&&(delete n[i],r=!0)}}return X.isArray(e)?e.forEach(i):i(e),r}clear(e){let t=Object.keys(this),n=t.length,r=!1;for(;n--;){let i=t[n];(!e||ed(this,this[i],i,e,!0))&&(delete this[i],r=!0)}return r}normalize(e){let t=this,n={};return X.forEach(this,(r,i)=>{let a=X.findKey(n,i);if(a){t[a]=Zu(r),delete t[i];return}let o=e?td(i):String(i).trim();o!==i&&delete t[i],t[o]=Zu(r),n[o]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){let t=Object.create(null);return X.forEach(this,(n,r)=>{n!=null&&n!==!1&&(t[r]=e&&X.isArray(n)?n.join(`, `):n)}),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,t])=>e+`: `+t).join(`
|
||||||
|
`)}getSetCookie(){return this.get(`set-cookie`)||[]}get[Symbol.toStringTag](){return`AxiosHeaders`}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){let n=new this(e);return t.forEach(e=>n.set(e)),n}static accessor(e){let t=(this[Yu]=this[Yu]={accessors:{}}).accessors,n=this.prototype;function r(e){let r=Xu(e);t[r]||(nd(n,e),t[r]=!0)}return X.isArray(e)?e.forEach(r):r(e),this}};rd.accessor([`Content-Type`,`Content-Length`,`Accept`,`Accept-Encoding`,`User-Agent`,`Authorization`]),X.reduceDescriptors(rd.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}}),X.freezeMethods(rd);var id=`[REDACTED ****]`;function ad(e){if(X.hasOwnProp(e,`toJSON`))return!0;let t=Object.getPrototypeOf(e);for(;t&&t!==Object.prototype;){if(X.hasOwnProp(t,`toJSON`))return!0;t=Object.getPrototypeOf(t)}return!1}function od(e,t){let n=new Set(t.map(e=>String(e).toLowerCase())),r=[],i=e=>{if(typeof e!=`object`||!e||X.isBuffer(e))return e;if(r.indexOf(e)!==-1)return;e instanceof rd&&(e=e.toJSON()),r.push(e);let t;if(X.isArray(e))t=[],e.forEach((e,n)=>{let r=i(e);X.isUndefined(r)||(t[n]=r)});else{if(!X.isPlainObject(e)&&ad(e))return r.pop(),e;t=Object.create(null);for(let[r,a]of Object.entries(e)){let e=n.has(r.toLowerCase())?id:i(a);X.isUndefined(e)||(t[r]=e)}}return r.pop(),t};return i(e)}var Z=class e extends Error{static from(t,n,r,i,a,o){let s=new e(t.message,n||t.code,r,i,a);return s.cause=t,s.name=t.name,t.status!=null&&s.status==null&&(s.status=t.status),o&&Object.assign(s,o),s}constructor(e,t,n,r,i){super(e),Object.defineProperty(this,"message",{__proto__:null,value:e,enumerable:!0,writable:!0,configurable:!0}),this.name=`AxiosError`,this.isAxiosError=!0,t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),i&&(this.response=i,this.status=i.status)}toJSON(){let e=this.config,t=e&&X.hasOwnProp(e,`redact`)?e.redact:void 0,n=X.isArray(t)&&t.length>0?od(e,t):X.toJSONObject(e);return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:n,code:this.code,status:this.status}}};Z.ERR_BAD_OPTION_VALUE=`ERR_BAD_OPTION_VALUE`,Z.ERR_BAD_OPTION=`ERR_BAD_OPTION`,Z.ECONNABORTED=`ECONNABORTED`,Z.ETIMEDOUT=`ETIMEDOUT`,Z.ECONNREFUSED=`ECONNREFUSED`,Z.ERR_NETWORK=`ERR_NETWORK`,Z.ERR_FR_TOO_MANY_REDIRECTS=`ERR_FR_TOO_MANY_REDIRECTS`,Z.ERR_DEPRECATED=`ERR_DEPRECATED`,Z.ERR_BAD_RESPONSE=`ERR_BAD_RESPONSE`,Z.ERR_BAD_REQUEST=`ERR_BAD_REQUEST`,Z.ERR_CANCELED=`ERR_CANCELED`,Z.ERR_NOT_SUPPORT=`ERR_NOT_SUPPORT`,Z.ERR_INVALID_URL=`ERR_INVALID_URL`,Z.ERR_FORM_DATA_DEPTH_EXCEEDED=`ERR_FORM_DATA_DEPTH_EXCEEDED`;function sd(e){return X.isPlainObject(e)||X.isArray(e)}function cd(e){return X.endsWith(e,`[]`)?e.slice(0,-2):e}function ld(e,t,n){return e?e.concat(t).map(function(e,t){return e=cd(e),!n&&t?`[`+e+`]`:e}).join(n?`.`:``):t}function ud(e){return X.isArray(e)&&!e.some(sd)}var dd=X.toFlatObject(X,{},null,function(e){return/^is[A-Z]/.test(e)});function fd(e,t,n){if(!X.isObject(e))throw TypeError(`target must be an object`);t||=new FormData,n=X.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(e,t){return!X.isUndefined(t[e])});let r=n.metaTokens,i=n.visitor||d,a=n.dots,o=n.indexes,s=n.Blob||typeof Blob<`u`&&Blob,c=n.maxDepth===void 0?100:n.maxDepth,l=s&&X.isSpecCompliantForm(t);if(!X.isFunction(i))throw TypeError(`visitor must be a function`);function u(e){if(e===null)return``;if(X.isDate(e))return e.toISOString();if(X.isBoolean(e))return e.toString();if(!l&&X.isBlob(e))throw new Z(`Blob is not supported. Use a Buffer instead.`);return X.isArrayBuffer(e)||X.isTypedArray(e)?l&&typeof Blob==`function`?new Blob([e]):Buffer.from(e):e}function d(e,n,i){let s=e;if(X.isReactNative(t)&&X.isReactNativeBlob(e))return t.append(ld(i,n,a),u(e)),!1;if(e&&!i&&typeof e==`object`){if(X.endsWith(n,`{}`))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(X.isArray(e)&&ud(e)||(X.isFileList(e)||X.endsWith(n,`[]`))&&(s=X.toArray(e)))return n=cd(n),s.forEach(function(e,r){!(X.isUndefined(e)||e===null)&&t.append(o===!0?ld([n],r,a):o===null?n:n+`[]`,u(e))}),!1}return sd(e)?!0:(t.append(ld(i,n,a),u(e)),!1)}let f=[],p=Object.assign(dd,{defaultVisitor:d,convertValue:u,isVisitable:sd});function m(e,n,r=0){if(!X.isUndefined(e)){if(r>c)throw new Z(`Object is too deeply nested (`+r+` levels). Max depth: `+c,Z.ERR_FORM_DATA_DEPTH_EXCEEDED);if(f.indexOf(e)!==-1)throw Error(`Circular reference detected in `+n.join(`.`));f.push(e),X.forEach(e,function(e,a){(!(X.isUndefined(e)||e===null)&&i.call(t,e,X.isString(a)?a.trim():a,n,p))===!0&&m(e,n?n.concat(a):[a],r+1)}),f.pop()}}if(!X.isObject(e))throw TypeError(`data must be an object`);return m(e),t}function pd(e){let t={"!":`%21`,"'":`%27`,"(":`%28`,")":`%29`,"~":`%7E`,"%20":`+`};return encodeURIComponent(e).replace(/[!'()~]|%20/g,function(e){return t[e]})}function md(e,t){this._pairs=[],e&&fd(e,this,t)}var hd=md.prototype;hd.append=fun
Line truncated
|
||||||
|
`+e.map(vf).join(`
|
||||||
|
`):` `+vf(e[0]):`as no adapter specified`),`ERR_NOT_SUPPORT`)}return i}var xf={getAdapter:bf,adapters:_f};function Sf(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Ld(null,e)}function Cf(e){return Sf(e),e.headers=rd.from(e.headers),e.data=Fd.call(e,e.transformRequest),[`post`,`put`,`patch`].indexOf(e.method)!==-1&&e.headers.setContentType(`application/x-www-form-urlencoded`,!1),xf.getAdapter(e.adapter||Pd.adapter,e)(e).then(function(t){Sf(e),e.response=t;try{t.data=Fd.call(e,e.transformResponse,t)}finally{delete e.response}return t.headers=rd.from(t.headers),t},function(t){if(!Id(t)&&(Sf(e),t&&t.response)){e.response=t.response;try{t.response.data=Fd.call(e,e.transformResponse,t.response)}finally{delete e.response}t.response.headers=rd.from(t.response.headers)}return Promise.reject(t)})}var wf={};[`object`,`boolean`,`number`,`function`,`string`,`symbol`].forEach((e,t)=>{wf[e]=function(n){return typeof n===e||`a`+(t<1?`n `:` `)+e}});var Tf={};wf.transitional=function(e,t,n){function r(e,t){return`[Axios v`+uf+`] Transitional option '`+e+`'`+t+(n?`. `+n:``)}return(n,i,a)=>{if(e===!1)throw new Z(r(i,` has been removed`+(t?` in `+t:``)),Z.ERR_DEPRECATED);return t&&!Tf[i]&&(Tf[i]=!0,console.warn(r(i,` has been deprecated since v`+t+` and will be removed in the near future`))),e?e(n,i,a):!0}},wf.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};function Ef(e,t,n){if(typeof e!=`object`)throw new Z(`options must be an object`,Z.ERR_BAD_OPTION_VALUE);let r=Object.keys(e),i=r.length;for(;i-- >0;){let a=r[i],o=Object.prototype.hasOwnProperty.call(t,a)?t[a]:void 0;if(o){let t=e[a],n=t===void 0||o(t,a,e);if(n!==!0)throw new Z(`option `+a+` must be `+n,Z.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new Z(`Unknown option `+a,Z.ERR_BAD_OPTION)}}var Df={assertOptions:Ef,validators:wf},Of=Df.validators,kf=class{constructor(e){this.defaults=e||{},this.interceptors={request:new vd,response:new vd}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=Error();let n=(()=>{if(!t.stack)return``;let e=t.stack.indexOf(`
|
||||||
|
`);return e===-1?``:t.stack.slice(e+1)})();try{if(!e.stack)e.stack=n;else if(n){let t=n.indexOf(`
|
||||||
|
`),r=t===-1?-1:n.indexOf(`
|
||||||
|
`,t+1),i=r===-1?``:n.slice(r+1);String(e.stack).endsWith(i)||(e.stack+=`
|
||||||
|
`+n)}}catch{}}throw e}}_request(e,t){typeof e==`string`?(t||={},t.url=e):t=e||{},t=Zd(this.defaults,t);let{transitional:n,paramsSerializer:r,headers:i}=t;n!==void 0&&Df.assertOptions(n,{silentJSONParsing:Of.transitional(Of.boolean),forcedJSONParsing:Of.transitional(Of.boolean),clarifyTimeoutError:Of.transitional(Of.boolean),legacyInterceptorReqResOrdering:Of.transitional(Of.boolean)},!1),r!=null&&(X.isFunction(r)?t.paramsSerializer={serialize:r}:Df.assertOptions(r,{encode:Of.function,serialize:Of.function},!0)),t.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls===void 0?t.allowAbsoluteUrls=!0:t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls),Df.assertOptions(t,{baseUrl:Of.spelling(`baseURL`),withXsrfToken:Of.spelling(`withXSRFToken`)},!0),t.method=(t.method||this.defaults.method||`get`).toLowerCase();let a=i&&X.merge(i.common,i[t.method]);i&&X.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`,`query`,`common`],e=>{delete i[e]}),t.headers=rd.concat(a,i);let o=[],s=!0;this.interceptors.request.forEach(function(e){if(typeof e.runWhen==`function`&&e.runWhen(t)===!1)return;s&&=e.synchronous;let n=t.transitional||yd;n&&n.legacyInterceptorReqResOrdering?o.unshift(e.fulfilled,e.rejected):o.push(e.fulfilled,e.rejected)});let c=[];this.interceptors.response.forEach(function(e){c.push(e.fulfilled,e.rejected)});let l,u=0,d;if(!s){let e=[Cf.bind(this),void 0];for(e.unshift(...o),e.push(...c),d=e.length,l=Promise.resolve(t);u<d;)l=l.then(e[u++],e[u++]);return l}d=o.length;let f=t;for(;u<d;){let e=o[u++],t=o[u++];try{f=e(f)}catch(e){t.call(this,e);break}}try{l=Cf.call(this,f)}catch(e){return Promise.reject(e)}for(u=0,d=c.length;u<d;)l=l.then(c[u++],c[u++]);return l}getUri(e){return e=Zd(this.defaults,e),_d(Yd(e.baseURL,e.url,e.allowAbsoluteUrls),e.params,e.paramsSerializer)}};X.forEach([`delete`,`get`,`head`,`options`],function(e){kf.prototype[e]=function(t,n){return this.request(Zd(n||{},{method:e,url:t,data:(n||{}).data}))}}),X.forEach([`post`,`put`,`patch`,`query`],function(e){function t(t){return function(n,r,i){return this.request(Zd(i||{},{method:e,headers:t?{"Content-Type":`multipart/form-data`}:{},url:n,data:r}))}}kf.prototype[e]=t(),e!==`query`&&(kf.prototype[e+`Form`]=t(!0))});var Af=class e{constructor(e){if(typeof e!=`function`)throw TypeError(`executor must be a function.`);let t;this.promise=new Promise(function(e){t=e});let n=this;this.promise.then(e=>{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null}),this.promise.then=e=>{let t,r=new Promise(e=>{n.subscribe(e),t=e}).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e(function(e,r,i){n.reason||(n.reason=new Ld(e,r,i),t(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;let t=this._listeners.indexOf(e);t!==-1&&this._listeners.splice(t,1)}toAbortSignal(){let e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let t;return{token:new e(function(e){t=e}),cancel:t}}};function jf(e){return function(t){return e.apply(null,t)}}function Mf(e){return X.isObject(e)&&e.isAxiosError===!0}var Nf={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Nf).forEach(([e,t])=>{Nf[t]=e});function Pf(e){let t=new kf(e),n=kl(kf.prototype.request,t);return X.extend(n,kf.prototype,t,{allOwnKeys:!0}),X.extend(n,t,null,{allOwnKeys:!0}),n.create=function(t){return Pf(Zd(e,t))},n}var Q=Pf(Pd);Q.Axios=kf,Q.CanceledError=Ld,Q.CancelToken=Af,Q.isCancel=Id,Q.VERSION=uf,Q.toFormData=fd,Q.AxiosError=Z,Q.Can
Line truncated
|
||||||
Vendored
-9
@@ -1,9 +0,0 @@
|
|||||||
var e=Object.defineProperty,t=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),n=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function r(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var i={},a=[],o=()=>{},s=()=>!1,c=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),l=e=>e.startsWith(`onUpdate:`),u=Object.assign,d=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},f=Object.prototype.hasOwnProperty,p=(e,t)=>f.call(e,t),m=Array.isArray,h=e=>w(e)===`[object Map]`,g=e=>w(e)===`[object Set]`,_=e=>w(e)===`[object Date]`,v=e=>typeof e==`function`,y=e=>typeof e==`string`,b=e=>typeof e==`symbol`,x=e=>typeof e==`object`&&!!e,S=e=>(x(e)||v(e))&&v(e.then)&&v(e.catch),C=Object.prototype.toString,w=e=>C.call(e),ee=e=>w(e).slice(8,-1),te=e=>w(e)===`[object Object]`,T=e=>y(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,E=r(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),D=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},ne=/-\w/g,O=D(e=>e.replace(ne,e=>e.slice(1).toUpperCase())),re=/\B([A-Z])/g,ie=D(e=>e.replace(re,`-$1`).toLowerCase()),ae=D(e=>e.charAt(0).toUpperCase()+e.slice(1)),k=D(e=>e?`on${ae(e)}`:``),oe=(e,t)=>!Object.is(e,t),se=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},ce=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},le=e=>{let t=parseFloat(e);return isNaN(t)?e:t},ue,de=()=>ue||=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{};function fe(e){if(m(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],i=y(r)?ge(r):fe(r);if(i)for(let e in i)t[e]=i[e]}return t}else if(y(e)||x(e))return e}var pe=/;(?![^(]*\))/g,me=/:([^]+)/,he=/\/\*[^]*?\*\//g;function ge(e){let t={};return e.replace(he,``).split(pe).forEach(e=>{if(e){let n=e.split(me);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function A(e){let t=``;if(y(e))t=e;else if(m(e))for(let n=0;n<e.length;n++){let r=A(e[n]);r&&(t+=r+` `)}else if(x(e))for(let n in e)e[n]&&(t+=n+` `);return t.trim()}var _e=`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`,ve=r(_e);_e+``;function ye(e){return!!e||e===``}function be(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=xe(e[r],t[r]);return n}function xe(e,t){if(e===t)return!0;let n=_(e),r=_(t);if(n||r)return n&&r?e.getTime()===t.getTime():!1;if(n=b(e),r=b(t),n||r)return e===t;if(n=m(e),r=m(t),n||r)return n&&r?be(e,t):!1;if(n=x(e),r=x(t),n||r){if(!n||!r||Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e){let r=e.hasOwnProperty(n),i=t.hasOwnProperty(n);if(r&&!i||!r&&i||!xe(e[n],t[n]))return!1}}return String(e)===String(t)}function Se(e,t){return e.findIndex(e=>xe(e,t))}var Ce=e=>!!(e&&e.__v_isRef===!0),j=e=>y(e)?e:e==null?``:m(e)||x(e)&&(e.toString===C||!v(e.toString))?Ce(e)?j(e.value):JSON.stringify(e,we,2):String(e),we=(e,t)=>Ce(t)?we(e,t.value):h(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[Te(t,r)+` =>`]=n,e),{})}:g(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>Te(e))}:b(t)?Te(t):x(t)&&!m(t)&&!te(t)?String(t):t,Te=(e,t=``)=>b(e)?`Symbol(${e.description??t})`:e,M,Ee=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&M&&(M.active?(this.parent=M,this.index=(M.scopes||=[]).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].pause();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].resume();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].resume()}}run(e){if(this._active){let t=M;try{return M=this,e()}finally{M=t}}}on(){++this._on===1&&(this.prevScope=M,M=this)}off(){if(this._on>0&&--this._on===0){if(M===this)M=this.prevScope;else{let e=M;for(;e;){if(e.prevScope===this){e.prevScope=this.preLine truncated
|
|
||||||
`).forEach(function(e){i=e.indexOf(`:`),n=e.substring(0,i).trim().toLowerCase(),r=e.substring(i+1).trim(),!(!n||t[n]&&Vu[n])&&(n===`set-cookie`?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+`, `+r:r)}),t};function Uu(e){let t=0,n=e.length;for(;t<n;){let n=e.charCodeAt(t);if(n!==9&&n!==32)break;t+=1}for(;n>t;){let t=e.charCodeAt(n-1);if(t!==9&&t!==32)break;--n}return t===0&&n===e.length?e:e.slice(t,n)}var Wu=RegExp(`[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+`,`g`),Gu=RegExp(`[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+`,`g`);function Ku(e,t){return Y.isArray(e)?e.map(e=>Ku(e,t)):Uu(String(e).replace(t,``))}var qu=e=>Ku(e,Wu),Ju=e=>Ku(e,Gu);function Yu(e){let t=Object.create(null);return Y.forEach(e.toJSON(),(e,n)=>{t[n]=Ju(e)}),t}var Xu=Symbol(`internals`);function Zu(e){return e&&String(e).trim().toLowerCase()}function Qu(e){return e===!1||e==null?e:Y.isArray(e)?e.map(Qu):qu(String(e))}function $u(e){let t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g,r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}var ed=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function td(e,t,n,r,i){if(Y.isFunction(r))return r.call(this,t,n);if(i&&(t=n),Y.isString(t)){if(Y.isString(r))return t.indexOf(r)!==-1;if(Y.isRegExp(r))return r.test(t)}}function nd(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,t,n)=>t.toUpperCase()+n)}function rd(e,t){let n=Y.toCamelCase(` `+t);[`get`,`set`,`has`].forEach(r=>{Object.defineProperty(e,r+n,{__proto__:null,value:function(e,n,i){return this[r].call(this,t,e,n,i)},configurable:!0})})}var id=class{constructor(e){e&&this.set(e)}set(e,t,n){let r=this;function i(e,t,n){let i=Zu(t);if(!i)throw Error(`header name must be a non-empty string`);let a=Y.findKey(r,i);(!a||r[a]===void 0||n===!0||n===void 0&&r[a]!==!1)&&(r[a||t]=Qu(e))}let a=(e,t)=>Y.forEach(e,(e,n)=>i(e,n,t));if(Y.isPlainObject(e)||e instanceof this.constructor)a(e,t);else if(Y.isString(e)&&(e=e.trim())&&!ed(e))a(Hu(e),t);else if(Y.isObject(e)&&Y.isIterable(e)){let n={},r,i;for(let t of e){if(!Y.isArray(t))throw TypeError(`Object iterator must return a key-value pair`);n[i=t[0]]=(r=n[i])?Y.isArray(r)?[...r,t[1]]:[r,t[1]]:t[1]}a(n,t)}else e!=null&&i(t,e,n);return this}get(e,t){if(e=Zu(e),e){let n=Y.findKey(this,e);if(n){let e=this[n];if(!t)return e;if(t===!0)return $u(e);if(Y.isFunction(t))return t.call(this,e,n);if(Y.isRegExp(t))return t.exec(e);throw TypeError(`parser must be boolean|regexp|function`)}}}has(e,t){if(e=Zu(e),e){let n=Y.findKey(this,e);return!!(n&&this[n]!==void 0&&(!t||td(this,this[n],n,t)))}return!1}delete(e,t){let n=this,r=!1;function i(e){if(e=Zu(e),e){let i=Y.findKey(n,e);i&&(!t||td(n,n[i],i,t))&&(delete n[i],r=!0)}}return Y.isArray(e)?e.forEach(i):i(e),r}clear(e){let t=Object.keys(this),n=t.length,r=!1;for(;n--;){let i=t[n];(!e||td(this,this[i],i,e,!0))&&(delete this[i],r=!0)}return r}normalize(e){let t=this,n={};return Y.forEach(this,(r,i)=>{let a=Y.findKey(n,i);if(a){t[a]=Qu(r),delete t[i];return}let o=e?nd(i):String(i).trim();o!==i&&delete t[i],t[o]=Qu(r),n[o]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){let t=Object.create(null);return Y.forEach(this,(n,r)=>{n!=null&&n!==!1&&(t[r]=e&&Y.isArray(n)?n.join(`, `):n)}),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,t])=>e+`: `+t).join(`
|
|
||||||
`)}getSetCookie(){return this.get(`set-cookie`)||[]}get[Symbol.toStringTag](){return`AxiosHeaders`}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){let n=new this(e);return t.forEach(e=>n.set(e)),n}static accessor(e){let t=(this[Xu]=this[Xu]={accessors:{}}).accessors,n=this.prototype;function r(e){let r=Zu(e);t[r]||(rd(n,e),t[r]=!0)}return Y.isArray(e)?e.forEach(r):r(e),this}};id.accessor([`Content-Type`,`Content-Length`,`Accept`,`Accept-Encoding`,`User-Agent`,`Authorization`]),Y.reduceDescriptors(id.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}}),Y.freezeMethods(id);var ad=`[REDACTED ****]`;function od(e){if(Y.hasOwnProp(e,`toJSON`))return!0;let t=Object.getPrototypeOf(e);for(;t&&t!==Object.prototype;){if(Y.hasOwnProp(t,`toJSON`))return!0;t=Object.getPrototypeOf(t)}return!1}function sd(e,t){let n=new Set(t.map(e=>String(e).toLowerCase())),r=[],i=e=>{if(typeof e!=`object`||!e||Y.isBuffer(e))return e;if(r.indexOf(e)!==-1)return;e instanceof id&&(e=e.toJSON()),r.push(e);let t;if(Y.isArray(e))t=[],e.forEach((e,n)=>{let r=i(e);Y.isUndefined(r)||(t[n]=r)});else{if(!Y.isPlainObject(e)&&od(e))return r.pop(),e;t=Object.create(null);for(let[r,a]of Object.entries(e)){let e=n.has(r.toLowerCase())?ad:i(a);Y.isUndefined(e)||(t[r]=e)}}return r.pop(),t};return i(e)}var X=class e extends Error{static from(t,n,r,i,a,o){let s=new e(t.message,n||t.code,r,i,a);return s.cause=t,s.name=t.name,t.status!=null&&s.status==null&&(s.status=t.status),o&&Object.assign(s,o),s}constructor(e,t,n,r,i){super(e),Object.defineProperty(this,"message",{__proto__:null,value:e,enumerable:!0,writable:!0,configurable:!0}),this.name=`AxiosError`,this.isAxiosError=!0,t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),i&&(this.response=i,this.status=i.status)}toJSON(){let e=this.config,t=e&&Y.hasOwnProp(e,`redact`)?e.redact:void 0,n=Y.isArray(t)&&t.length>0?sd(e,t):Y.toJSONObject(e);return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:n,code:this.code,status:this.status}}};X.ERR_BAD_OPTION_VALUE=`ERR_BAD_OPTION_VALUE`,X.ERR_BAD_OPTION=`ERR_BAD_OPTION`,X.ECONNABORTED=`ECONNABORTED`,X.ETIMEDOUT=`ETIMEDOUT`,X.ECONNREFUSED=`ECONNREFUSED`,X.ERR_NETWORK=`ERR_NETWORK`,X.ERR_FR_TOO_MANY_REDIRECTS=`ERR_FR_TOO_MANY_REDIRECTS`,X.ERR_DEPRECATED=`ERR_DEPRECATED`,X.ERR_BAD_RESPONSE=`ERR_BAD_RESPONSE`,X.ERR_BAD_REQUEST=`ERR_BAD_REQUEST`,X.ERR_CANCELED=`ERR_CANCELED`,X.ERR_NOT_SUPPORT=`ERR_NOT_SUPPORT`,X.ERR_INVALID_URL=`ERR_INVALID_URL`,X.ERR_FORM_DATA_DEPTH_EXCEEDED=`ERR_FORM_DATA_DEPTH_EXCEEDED`;function cd(e){return Y.isPlainObject(e)||Y.isArray(e)}function ld(e){return Y.endsWith(e,`[]`)?e.slice(0,-2):e}function ud(e,t,n){return e?e.concat(t).map(function(e,t){return e=ld(e),!n&&t?`[`+e+`]`:e}).join(n?`.`:``):t}function dd(e){return Y.isArray(e)&&!e.some(cd)}var fd=Y.toFlatObject(Y,{},null,function(e){return/^is[A-Z]/.test(e)});function pd(e,t,n){if(!Y.isObject(e))throw TypeError(`target must be an object`);t||=new FormData,n=Y.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(e,t){return!Y.isUndefined(t[e])});let r=n.metaTokens,i=n.visitor||d,a=n.dots,o=n.indexes,s=n.Blob||typeof Blob<`u`&&Blob,c=n.maxDepth===void 0?100:n.maxDepth,l=s&&Y.isSpecCompliantForm(t);if(!Y.isFunction(i))throw TypeError(`visitor must be a function`);function u(e){if(e===null)return``;if(Y.isDate(e))return e.toISOString();if(Y.isBoolean(e))return e.toString();if(!l&&Y.isBlob(e))throw new X(`Blob is not supported. Use a Buffer instead.`);return Y.isArrayBuffer(e)||Y.isTypedArray(e)?l&&typeof Blob==`function`?new Blob([e]):Buffer.from(e):e}function d(e,n,i){let s=e;if(Y.isReactNative(t)&&Y.isReactNativeBlob(e))return t.append(ud(i,n,a),u(e)),!1;if(e&&!i&&typeof e==`object`){if(Y.endsWith(n,`{}`))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(Y.isArray(e)&&dd(e)||(Y.isFileList(e)||Y.endsWith(n,`[]`))&&(s=Y.toArray(e)))return n=ld(n),s.forEach(function(e,r){!(Y.isUndefined(e)||e===null)&&t.append(o===!0?ud([n],r,a):o===null?n:n+`[]`,u(e))}),!1}return cd(e)?!0:(t.append(ud(i,n,a),u(e)),!1)}let f=[],p=Object.assign(fd,{defaultVisitor:d,convertValue:u,isVisitable:cd});function m(e,n,r=0){if(!Y.isUndefined(e)){if(r>c)throw new X(`Object is too deeply nested (`+r+` levels). Max depth: `+c,X.ERR_FORM_DATA_DEPTH_EXCEEDED);if(f.indexOf(e)!==-1)throw Error(`Circular reference detected in `+n.join(`.`));f.push(e),Y.forEach(e,function(e,a){(!(Y.isUndefined(e)||e===null)&&i.call(t,e,Y.isString(a)?a.trim():a,n,p))===!0&&m(e,n?n.concat(a):[a],r+1)}),f.pop()}}if(!Y.isObject(e))throw TypeError(`data must be an object`);return m(e),t}function md(e){let t={"!":`%21`,"'":`%27`,"(":`%28`,")":`%29`,"~":`%7E`,"%20":`+`};return encodeURIComponent(e).replace(/[!'()~]|%20/g,function(e){return t[e]})}function hd(e,t){this._pairs=[],e&&pd(e,this,t)}var gd=hd.prototype;gd.append=fun
Line truncated
|
|
||||||
`+e.map(yf).join(`
|
|
||||||
`):` `+yf(e[0]):`as no adapter specified`),`ERR_NOT_SUPPORT`)}return i}var Sf={getAdapter:xf,adapters:vf};function Cf(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Rd(null,e)}function wf(e){return Cf(e),e.headers=id.from(e.headers),e.data=Id.call(e,e.transformRequest),[`post`,`put`,`patch`].indexOf(e.method)!==-1&&e.headers.setContentType(`application/x-www-form-urlencoded`,!1),Sf.getAdapter(e.adapter||Fd.adapter,e)(e).then(function(t){Cf(e),e.response=t;try{t.data=Id.call(e,e.transformResponse,t)}finally{delete e.response}return t.headers=id.from(t.headers),t},function(t){if(!Ld(t)&&(Cf(e),t&&t.response)){e.response=t.response;try{t.response.data=Id.call(e,e.transformResponse,t.response)}finally{delete e.response}t.response.headers=id.from(t.response.headers)}return Promise.reject(t)})}var Tf={};[`object`,`boolean`,`number`,`function`,`string`,`symbol`].forEach((e,t)=>{Tf[e]=function(n){return typeof n===e||`a`+(t<1?`n `:` `)+e}});var Ef={};Tf.transitional=function(e,t,n){function r(e,t){return`[Axios v`+df+`] Transitional option '`+e+`'`+t+(n?`. `+n:``)}return(n,i,a)=>{if(e===!1)throw new X(r(i,` has been removed`+(t?` in `+t:``)),X.ERR_DEPRECATED);return t&&!Ef[i]&&(Ef[i]=!0,console.warn(r(i,` has been deprecated since v`+t+` and will be removed in the near future`))),e?e(n,i,a):!0}},Tf.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};function Df(e,t,n){if(typeof e!=`object`)throw new X(`options must be an object`,X.ERR_BAD_OPTION_VALUE);let r=Object.keys(e),i=r.length;for(;i-- >0;){let a=r[i],o=Object.prototype.hasOwnProperty.call(t,a)?t[a]:void 0;if(o){let t=e[a],n=t===void 0||o(t,a,e);if(n!==!0)throw new X(`option `+a+` must be `+n,X.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new X(`Unknown option `+a,X.ERR_BAD_OPTION)}}var Of={assertOptions:Df,validators:Tf},kf=Of.validators,Af=class{constructor(e){this.defaults=e||{},this.interceptors={request:new yd,response:new yd}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=Error();let n=(()=>{if(!t.stack)return``;let e=t.stack.indexOf(`
|
|
||||||
`);return e===-1?``:t.stack.slice(e+1)})();try{if(!e.stack)e.stack=n;else if(n){let t=n.indexOf(`
|
|
||||||
`),r=t===-1?-1:n.indexOf(`
|
|
||||||
`,t+1),i=r===-1?``:n.slice(r+1);String(e.stack).endsWith(i)||(e.stack+=`
|
|
||||||
`+n)}}catch{}}throw e}}_request(e,t){typeof e==`string`?(t||={},t.url=e):t=e||{},t=Qd(this.defaults,t);let{transitional:n,paramsSerializer:r,headers:i}=t;n!==void 0&&Of.assertOptions(n,{silentJSONParsing:kf.transitional(kf.boolean),forcedJSONParsing:kf.transitional(kf.boolean),clarifyTimeoutError:kf.transitional(kf.boolean),legacyInterceptorReqResOrdering:kf.transitional(kf.boolean)},!1),r!=null&&(Y.isFunction(r)?t.paramsSerializer={serialize:r}:Of.assertOptions(r,{encode:kf.function,serialize:kf.function},!0)),t.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls===void 0?t.allowAbsoluteUrls=!0:t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls),Of.assertOptions(t,{baseUrl:kf.spelling(`baseURL`),withXsrfToken:kf.spelling(`withXSRFToken`)},!0),t.method=(t.method||this.defaults.method||`get`).toLowerCase();let a=i&&Y.merge(i.common,i[t.method]);i&&Y.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`,`query`,`common`],e=>{delete i[e]}),t.headers=id.concat(a,i);let o=[],s=!0;this.interceptors.request.forEach(function(e){if(typeof e.runWhen==`function`&&e.runWhen(t)===!1)return;s&&=e.synchronous;let n=t.transitional||bd;n&&n.legacyInterceptorReqResOrdering?o.unshift(e.fulfilled,e.rejected):o.push(e.fulfilled,e.rejected)});let c=[];this.interceptors.response.forEach(function(e){c.push(e.fulfilled,e.rejected)});let l,u=0,d;if(!s){let e=[wf.bind(this),void 0];for(e.unshift(...o),e.push(...c),d=e.length,l=Promise.resolve(t);u<d;)l=l.then(e[u++],e[u++]);return l}d=o.length;let f=t;for(;u<d;){let e=o[u++],t=o[u++];try{f=e(f)}catch(e){t.call(this,e);break}}try{l=wf.call(this,f)}catch(e){return Promise.reject(e)}for(u=0,d=c.length;u<d;)l=l.then(c[u++],c[u++]);return l}getUri(e){return e=Qd(this.defaults,e),vd(Xd(e.baseURL,e.url,e.allowAbsoluteUrls),e.params,e.paramsSerializer)}};Y.forEach([`delete`,`get`,`head`,`options`],function(e){Af.prototype[e]=function(t,n){return this.request(Qd(n||{},{method:e,url:t,data:(n||{}).data}))}}),Y.forEach([`post`,`put`,`patch`,`query`],function(e){function t(t){return function(n,r,i){return this.request(Qd(i||{},{method:e,headers:t?{"Content-Type":`multipart/form-data`}:{},url:n,data:r}))}}Af.prototype[e]=t(),e!==`query`&&(Af.prototype[e+`Form`]=t(!0))});var jf=class e{constructor(e){if(typeof e!=`function`)throw TypeError(`executor must be a function.`);let t;this.promise=new Promise(function(e){t=e});let n=this;this.promise.then(e=>{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null}),this.promise.then=e=>{let t,r=new Promise(e=>{n.subscribe(e),t=e}).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e(function(e,r,i){n.reason||(n.reason=new Rd(e,r,i),t(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;let t=this._listeners.indexOf(e);t!==-1&&this._listeners.splice(t,1)}toAbortSignal(){let e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let t;return{token:new e(function(e){t=e}),cancel:t}}};function Mf(e){return function(t){return e.apply(null,t)}}function Nf(e){return Y.isObject(e)&&e.isAxiosError===!0}var Pf={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Pf).forEach(([e,t])=>{Pf[t]=e});function Ff(e){let t=new Af(e),n=Al(Af.prototype.request,t);return Y.extend(n,Af.prototype,t,{allOwnKeys:!0}),Y.extend(n,t,null,{allOwnKeys:!0}),n.create=function(t){return Ff(Qd(e,t))},n}var Z=Ff(Fd);Z.Axios=Af,Z.CanceledError=Rd,Z.CancelToken=jf,Z.isCancel=Ld,Z.VERSION=df,Z.toFormData=pd,Z.AxiosError=X,Z.Can
Line truncated
|
|
||||||
Vendored
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Noctune - Music That Hits Different</title>
|
<title>Noctune - Music That Hits Different</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<script type="module" crossorigin src="/assets/index-Ulrd01cQ.js"></script>
|
<script type="module" crossorigin src="/assets/index-C8FI4Eix.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B-WaF5DI.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-7ON3mYgU.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-noctune-cream">
|
<body class="bg-noctune-cream">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { usePlayerStore } from './stores/playerStore.js';
|
|||||||
import AppHeader from './components/AppHeader.vue';
|
import AppHeader from './components/AppHeader.vue';
|
||||||
import Sidebar from './components/Sidebar.vue';
|
import Sidebar from './components/Sidebar.vue';
|
||||||
import PlayerBar from './components/PlayerBar.vue';
|
import PlayerBar from './components/PlayerBar.vue';
|
||||||
|
import { useKeyboard } from './composables/useKeyboard.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -56,6 +57,8 @@ const isAuthPage = computed(() => route.meta?.guest === true)
|
|||||||
|
|
||||||
const hasPlayer = computed(() => !!playerStore.currentTrack);
|
const hasPlayer = computed(() => !!playerStore.currentTrack);
|
||||||
|
|
||||||
|
useKeyboard();
|
||||||
|
|
||||||
function toastClass(type) {
|
function toastClass(type) {
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="$emit('close')">
|
||||||
|
<div class="bg-white rounded-2xl border-4 border-black p-6 w-full max-w-md mx-4 shadow-[8px_8px_0px_0px_#000]">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-xl font-black text-black">CREATE PLAYLIST</h2>
|
||||||
|
<button @click="$emit('close')" class="text-gray-500 hover:text-black text-2xl leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error" class="text-red-500 text-sm mb-4 font-medium">{{ error }}</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1 tracking-wider">NAME</label>
|
||||||
|
<input
|
||||||
|
v-model="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="My awesome playlist"
|
||||||
|
class="w-full border-2 border-black rounded-lg px-4 py-2 focus:outline-none text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1 tracking-wider">DESCRIPTION</label>
|
||||||
|
<textarea
|
||||||
|
v-model="description"
|
||||||
|
placeholder="Optional description"
|
||||||
|
rows="2"
|
||||||
|
class="w-full border-2 border-black rounded-lg px-4 py-2 focus:outline-none resize-none text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1 tracking-wider">COVER IMAGE</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
@change="onCoverChange"
|
||||||
|
class="w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:border-2 file:border-black file:rounded-lg file:font-bold file:text-sm file:bg-noctune-yellow file:cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="handleCreate"
|
||||||
|
:disabled="creating"
|
||||||
|
class="w-full bg-noctune-teal border-2 border-black rounded-lg py-3 font-bold text-black hover:bg-teal-600 transition-colors disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<span v-if="creating" class="inline-block w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin mr-2" />
|
||||||
|
{{ creating ? 'Creating...' : 'CREATE PLAYLIST' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import api from '../lib/api.js'
|
||||||
|
|
||||||
|
const props = defineProps({ show: Boolean })
|
||||||
|
const emit = defineEmits(['close', 'created'])
|
||||||
|
|
||||||
|
const name = ref('')
|
||||||
|
const description = ref('')
|
||||||
|
const coverFile = ref(null)
|
||||||
|
const creating = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
function onCoverChange(e) {
|
||||||
|
coverFile.value = e.target.files[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
error.value = ''
|
||||||
|
if (!name.value?.trim()) {
|
||||||
|
error.value = 'Playlist name is required'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
creating.value = true
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('name', name.value.trim())
|
||||||
|
if (description.value?.trim()) formData.append('description', description.value.trim())
|
||||||
|
if (coverFile.value) formData.append('playlistCover', coverFile.value)
|
||||||
|
|
||||||
|
const { data } = await api.post('/playlists', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
emit('created', data.data)
|
||||||
|
name.value = ''
|
||||||
|
description.value = ''
|
||||||
|
coverFile.value = null
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.response?.data?.message || 'Failed to create playlist'
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -36,7 +36,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 w-full max-w-lg">
|
<div class="flex items-center gap-3 w-full max-w-lg">
|
||||||
<span class="text-xs text-gray-400 w-10 text-right">{{ playerStore.formattedProgress }}</span>
|
<span class="text-xs text-gray-400 w-10 text-right">{{ playerStore.formattedProgress }}</span>
|
||||||
<input type="range" :value="playerStore.progress" @input="onSeek" min="0" :max="playerStore.duration || 1" step="0.1" class="flex-1 h-1 bg-gray-600 rounded-full appearance-none cursor-pointer range-player" />
|
<div class="relative flex-1" @mousemove="onSeekHover" @mouseleave="seekTooltipVal = null">
|
||||||
|
<div v-if="seekTooltipVal !== null" class="absolute -top-6 left-1/2 -translate-x-1/2 bg-black text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10"
|
||||||
|
:style="{ left: `${(seekTooltipVal / (playerStore.duration || 1)) * 100}%` }">
|
||||||
|
{{ formatSeekTime(seekTooltipVal) }}
|
||||||
|
</div>
|
||||||
|
<input type="range" :value="playerStore.progress" @input="onSeek" min="0" :max="playerStore.duration || 1" step="0.1" class="w-full h-1 bg-gray-600 rounded-full appearance-none cursor-pointer range-player" />
|
||||||
|
</div>
|
||||||
<span class="text-xs text-gray-400 w-10">{{ playerStore.formattedDuration }}</span>
|
<span class="text-xs text-gray-400 w-10">{{ playerStore.formattedDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +74,19 @@ const authStore = useAuthStore()
|
|||||||
const { requireAuth } = useAuthGate()
|
const { requireAuth } = useAuthGate()
|
||||||
|
|
||||||
const isLiked = ref(false)
|
const isLiked = ref(false)
|
||||||
|
const seekTooltipVal = ref(null)
|
||||||
|
|
||||||
|
function formatSeekTime(seconds) {
|
||||||
|
const m = Math.floor(seconds / 60)
|
||||||
|
const s = Math.floor(seconds % 60)
|
||||||
|
return `${m}:${s.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSeekHover(e) {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const pct = (e.clientX - rect.left) / rect.width
|
||||||
|
seekTooltipVal.value = pct * playerStore.duration
|
||||||
|
}
|
||||||
|
|
||||||
function onTrackEnd() {
|
function onTrackEnd() {
|
||||||
const next = queueStore.nextTrack()
|
const next = queueStore.nextTrack()
|
||||||
|
|||||||
+10
-10
@@ -62,6 +62,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
|
<CreatePlaylistModal :show="showCreateModal" @close="showCreateModal = false" @created="onPlaylistCreated" />
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -71,12 +73,14 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useAuthStore } from '../stores/authStore.js'
|
import { useAuthStore } from '../stores/authStore.js'
|
||||||
import api from '../lib/api.js'
|
import api from '../lib/api.js'
|
||||||
import { useAuthGate } from '../composables/useAuthGate.js'
|
import { useAuthGate } from '../composables/useAuthGate.js'
|
||||||
|
import CreatePlaylistModal from './CreatePlaylistModal.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const playlists = ref([])
|
const playlists = ref([])
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
|
||||||
const { requireAuth } = useAuthGate()
|
const { requireAuth } = useAuthGate()
|
||||||
|
|
||||||
@@ -94,18 +98,14 @@ async function fetchPlaylists() {
|
|||||||
function openCreatePlaylist() {
|
function openCreatePlaylist() {
|
||||||
requireAuth({
|
requireAuth({
|
||||||
redirectTo: '/login',
|
redirectTo: '/login',
|
||||||
onAuthenticated: async () => {
|
onAuthenticated: () => { showCreateModal.value = true }
|
||||||
const name = prompt('Playlist name:')
|
|
||||||
if (!name?.trim()) return
|
|
||||||
try {
|
|
||||||
const { data } = await api.post('/playlists', { name: name.trim() })
|
|
||||||
if (data.success) {
|
|
||||||
playlists.value.push(data.data)
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPlaylistCreated(playlist) {
|
||||||
|
showCreateModal.value = false
|
||||||
|
playlists.value.unshift(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(fetchPlaylists)
|
onMounted(fetchPlaylists)
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { usePlayerStore } from '../stores/playerStore.js'
|
||||||
|
import { useQueueStore } from '../stores/queueStore.js'
|
||||||
|
|
||||||
|
export function useKeyboard() {
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
const queueStore = useQueueStore()
|
||||||
|
|
||||||
|
function handler(e) {
|
||||||
|
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return
|
||||||
|
|
||||||
|
switch (e.code) {
|
||||||
|
case 'Space':
|
||||||
|
e.preventDefault()
|
||||||
|
playerStore.togglePlay()
|
||||||
|
break
|
||||||
|
case 'ArrowLeft':
|
||||||
|
playerStore.seek(Math.max(0, playerStore.progress - 5))
|
||||||
|
break
|
||||||
|
case 'ArrowRight':
|
||||||
|
playerStore.seek(Math.min(playerStore.duration, playerStore.progress + 5))
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
playerStore.setVolume(Math.min(100, playerStore.volume + 5))
|
||||||
|
break
|
||||||
|
case 'ArrowDown':
|
||||||
|
playerStore.setVolume(Math.max(0, playerStore.volume - 5))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('keydown', handler))
|
||||||
|
onUnmounted(() => window.removeEventListener('keydown', handler))
|
||||||
|
}
|
||||||
+2
-1
@@ -6,6 +6,7 @@ import RegisterPage from '../views/RegisterPage.vue'
|
|||||||
import LandingPage from '../views/LandingPage.vue'
|
import LandingPage from '../views/LandingPage.vue'
|
||||||
import LibraryPage from '../views/LibraryPage.vue'
|
import LibraryPage from '../views/LibraryPage.vue'
|
||||||
import PlaylistPage from '../views/PlaylistPage.vue'
|
import PlaylistPage from '../views/PlaylistPage.vue'
|
||||||
|
import PlaylistDetail from '../views/PlaylistDetail.vue'
|
||||||
import ProfilePage from '../views/ProfilePage.vue'
|
import ProfilePage from '../views/ProfilePage.vue'
|
||||||
import AdminPage from '../views/AdminPage.vue'
|
import AdminPage from '../views/AdminPage.vue'
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ const routes = [
|
|||||||
{ path: '/home', name: 'Home', component: LandingPage },
|
{ path: '/home', name: 'Home', component: LandingPage },
|
||||||
{ path: '/library', name: 'Library', component: LibraryPage },
|
{ path: '/library', name: 'Library', component: LibraryPage },
|
||||||
{ path: '/playlist', name: 'Playlist', component: PlaylistPage },
|
{ path: '/playlist', name: 'Playlist', component: PlaylistPage },
|
||||||
{ path: '/playlist/:id', name: 'PlaylistDetail', component: PlaylistPage },
|
{ path: '/playlist/:id', name: 'PlaylistDetail', component: PlaylistDetail },
|
||||||
{ path: '/profile', name: 'Profile', component: ProfilePage },
|
{ path: '/profile', name: 'Profile', component: ProfilePage },
|
||||||
{ path: '/admin/tracks', name: 'AdminTracks', component: AdminPage, meta: { admin: true } },
|
{ path: '/admin/tracks', name: 'AdminTracks', component: AdminPage, meta: { admin: true } },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,17 +22,32 @@
|
|||||||
TRENDING NOW
|
TRENDING NOW
|
||||||
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||||
</h2>
|
</h2>
|
||||||
<div v-if="loading" class="text-center py-12 text-gray-500">Loading tracks...</div>
|
<div v-if="loading" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<div v-else class="grid grid-cols-4 gap-4">
|
<div v-for="i in 4" :key="i" class="animate-pulse rounded-2xl border-2 border-black overflow-hidden">
|
||||||
|
<div class="aspect-square bg-gray-300" />
|
||||||
|
<div class="p-3 space-y-2">
|
||||||
|
<div class="h-3 bg-gray-300 rounded w-3/4" />
|
||||||
|
<div class="h-2 bg-gray-300 rounded w-1/2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="tracks.length === 0" class="text-center py-20 bg-white rounded-2xl border-2 border-black">
|
||||||
|
<svg class="w-24 h-24 mx-auto text-gray-300 mb-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||||
|
<p class="text-gray-500 mb-2 text-lg">No tracks yet</p>
|
||||||
|
<p class="text-gray-400 text-sm mb-4">Be the first to upload music!</p>
|
||||||
|
<router-link to="/admin/tracks" class="inline-block bg-noctune-orange border-2 border-black px-6 py-3 font-bold rounded-lg hover:bg-orange-500 transition-colors">UPLOAD TRACK</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<div v-for="track in tracks" :key="track._id"
|
<div v-for="track in tracks" :key="track._id"
|
||||||
@click="playTrack(track)"
|
@click="playTrack(track)"
|
||||||
class="bg-white rounded-2xl border-2 border-black overflow-hidden hover:scale-[1.02] transition-transform cursor-pointer group"
|
class="bg-white rounded-2xl overflow-hidden hover:scale-[1.02] transition-transform cursor-pointer group"
|
||||||
|
:class="isCurrentTrack(track) ? 'border-4 border-noctune-teal' : 'border-2 border-black'"
|
||||||
>
|
>
|
||||||
<div class="aspect-square bg-gray-300 relative overflow-hidden">
|
<div class="aspect-square bg-gray-300 relative overflow-hidden">
|
||||||
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
|
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
|
||||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||||
<div class="w-12 h-12 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
|
<div class="w-12 h-12 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
:class="isCurrentTrack(track) ? 'bg-white' : 'bg-noctune-yellow'">
|
:class="isCurrentTrack(track) && playerStore.isPlaying ? 'bg-white' : 'bg-noctune-yellow'">
|
||||||
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-6 h-6 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-6 h-6 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||||
<svg v-else class="w-6 h-6 ml-1 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
<svg v-else class="w-6 h-6 ml-1 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="aspect-square bg-gray-300 relative overflow-hidden">
|
<div class="aspect-square bg-gray-300 relative overflow-hidden">
|
||||||
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
|
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
|
||||||
<div class="absolute bottom-2 right-2 w-10 h-10 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
|
<div class="absolute bottom-2 right-2 w-10 h-10 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
:class="isCurrentTrack(track) ? 'bg-white' : 'bg-noctune-yellow'">
|
:class="isCurrentTrack(track) && playerStore.isPlaying ? 'bg-white' : 'bg-noctune-yellow'">
|
||||||
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-5 h-5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-5 h-5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||||
<svg v-else class="w-5 h-5 ml-0.5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
<svg v-else class="w-5 h-5 ml-0.5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,15 @@
|
|||||||
All Tracks
|
All Tracks
|
||||||
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||||
</h2>
|
</h2>
|
||||||
<div v-if="allLoading" class="text-center py-8 text-gray-500">Loading...</div>
|
<div v-if="allLoading" class="space-y-3 p-4">
|
||||||
|
<div v-for="i in 5" :key="i" class="flex items-center gap-4 animate-pulse">
|
||||||
|
<div class="w-8 h-4 bg-gray-200 rounded" />
|
||||||
|
<div class="w-10 h-10 bg-gray-200 rounded-lg" />
|
||||||
|
<div class="h-3 bg-gray-200 rounded flex-1" />
|
||||||
|
<div class="h-3 bg-gray-200 rounded w-24" />
|
||||||
|
<div class="h-3 bg-gray-200 rounded w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="bg-white rounded-2xl border-4 border-black overflow-hidden">
|
<div v-else class="bg-white rounded-2xl border-4 border-black overflow-hidden">
|
||||||
<div class="bg-noctune-teal px-6 py-3 flex items-center text-black font-bold border-b-4 border-black">
|
<div class="bg-noctune-teal px-6 py-3 flex items-center text-black font-bold border-b-4 border-black">
|
||||||
<span class="flex-1">TITLE</span>
|
<span class="flex-1">TITLE</span>
|
||||||
@@ -49,6 +57,7 @@
|
|||||||
<div v-for="(track, index) in allTracks" :key="track._id"
|
<div v-for="(track, index) in allTracks" :key="track._id"
|
||||||
@click="playTrack(track, false, index)"
|
@click="playTrack(track, false, index)"
|
||||||
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
|
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
:class="isCurrentTrack(track) ? 'bg-noctune-teal/10' : ''"
|
||||||
>
|
>
|
||||||
<div class="flex-1 flex items-center gap-3">
|
<div class="flex-1 flex items-center gap-3">
|
||||||
<span class="text-sm text-gray-400 w-6">
|
<span class="text-sm text-gray-400 w-6">
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-8">
|
||||||
|
<div v-if="loading" class="space-y-6 animate-pulse">
|
||||||
|
<div class="flex items-start gap-8 mb-8">
|
||||||
|
<div class="w-56 h-56 rounded-2xl bg-gray-300 border-4 border-black" />
|
||||||
|
<div class="flex-1 space-y-4">
|
||||||
|
<div class="h-6 bg-gray-200 rounded w-24" />
|
||||||
|
<div class="h-10 bg-gray-200 rounded w-64" />
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-32" />
|
||||||
|
<div class="h-10 bg-gray-200 rounded w-28" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="i in 5" :key="i" class="flex items-center gap-4">
|
||||||
|
<div class="w-8 h-4 bg-gray-200 rounded" />
|
||||||
|
<div class="w-10 h-10 bg-gray-200 rounded-lg" />
|
||||||
|
<div class="h-3 bg-gray-200 rounded flex-1" />
|
||||||
|
<div class="h-3 bg-gray-200 rounded w-24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else-if="playlist">
|
||||||
|
<div class="flex items-start gap-8 mb-8">
|
||||||
|
<div class="w-56 h-56 rounded-2xl overflow-hidden border-4 border-black flex-shrink-0 bg-black">
|
||||||
|
<img v-if="playlist.coverUrl" :src="formatUrl(playlist.coverUrl)" class="w-full h-full object-cover" />
|
||||||
|
<div v-else class="w-full h-full flex items-center justify-center">
|
||||||
|
<svg class="w-40 h-24 text-white/60" viewBox="0 0 100 50">
|
||||||
|
<rect x="5" y="20" width="4" height="15" fill="currentColor"/>
|
||||||
|
<rect x="12" y="10" width="4" height="30" fill="currentColor"/>
|
||||||
|
<rect x="19" y="15" width="4" height="20" fill="currentColor"/>
|
||||||
|
<rect x="26" y="5" width="4" height="40" fill="currentColor"/>
|
||||||
|
<rect x="33" y="12" width="4" height="26" fill="currentColor"/>
|
||||||
|
<rect x="40" y="8" width="4" height="34" fill="currentColor"/>
|
||||||
|
<rect x="47" y="15" width="4" height="20" fill="currentColor"/>
|
||||||
|
<rect x="54" y="18" width="4" height="14" fill="currentColor"/>
|
||||||
|
<rect x="61" y="12" width="4" height="26" fill="currentColor"/>
|
||||||
|
<rect x="68" y="8" width="4" height="34" fill="currentColor"/>
|
||||||
|
<rect x="75" y="15" width="4" height="20" fill="currentColor"/>
|
||||||
|
<rect x="82" y="20" width="4" height="15" fill="currentColor"/>
|
||||||
|
<rect x="89" y="22" width="4" height="10" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="inline-block bg-noctune-teal text-black text-xs font-bold px-3 py-1 rounded mb-3 border border-black">PLAYLIST</span>
|
||||||
|
<h1 class="text-4xl font-black text-black mb-4 leading-tight">{{ playlist.name }}</h1>
|
||||||
|
<p v-if="playlist.description" class="text-gray-600 mb-4">{{ playlist.description }}</p>
|
||||||
|
<div class="flex items-center gap-3 text-gray-600 mb-6">
|
||||||
|
<span class="font-medium">{{ playlist.tracks?.length || 0 }} songs</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button @click="playAll" :disabled="!playlist.tracks?.length" class="bg-noctune-teal border-2 border-black px-6 py-2 font-bold flex items-center gap-2 rounded-lg hover:bg-teal-600 transition-colors disabled:opacity-50 cursor-pointer">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
PLAY
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-2xl border-4 border-black overflow-hidden">
|
||||||
|
<div class="bg-noctune-teal px-6 py-3 flex items-center text-black font-bold border-b-4 border-black">
|
||||||
|
<span class="w-12 text-center">#</span>
|
||||||
|
<span class="flex-1 text-center">TITLE</span>
|
||||||
|
<span class="w-48 text-center">ARTIST</span>
|
||||||
|
<span class="w-12 text-center"></span>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
<div v-for="(track, index) in playlist.tracks" :key="track._id"
|
||||||
|
@click="playTrack(index)"
|
||||||
|
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
:class="isCurrentTrack(track) ? 'bg-noctune-teal/10' : ''"
|
||||||
|
>
|
||||||
|
<span class="w-12 text-center">
|
||||||
|
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-4 h-4 text-noctune-teal mx-auto" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||||
|
<span v-else class="text-gray-500">{{ index + 1 }}</span>
|
||||||
|
</span>
|
||||||
|
<div class="flex-1 flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 rounded-lg flex-shrink-0 overflow-hidden bg-gray-300 border border-black">
|
||||||
|
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-sm truncate" :class="isCurrentTrack(track) ? 'text-noctune-teal' : 'text-black'">{{ track.title }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="w-48 text-center text-gray-600 text-sm truncate">{{ track.artist }}</span>
|
||||||
|
<button @click.stop="toggleLike(track)" class="w-12 flex justify-center transition-colors cursor-pointer">
|
||||||
|
<svg class="w-5 h-5" :class="isLiked(track) ? 'fill-red-400 text-red-400' : 'text-gray-400 hover:text-red-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!playlist.tracks?.length" class="p-8 text-center text-gray-500">No tracks in this playlist yet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="text-center py-20 text-gray-500">Playlist not found</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { usePlayerStore } from '../stores/playerStore.js'
|
||||||
|
import { useQueueStore } from '../stores/queueStore.js'
|
||||||
|
import { useAuthStore } from '../stores/authStore.js'
|
||||||
|
import api from '../lib/api.js'
|
||||||
|
import { formatUrl } from '../lib/utils.js'
|
||||||
|
import { useAuthGate } from '../composables/useAuthGate.js'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
const queueStore = useQueueStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const { requireAuth } = useAuthGate()
|
||||||
|
|
||||||
|
const playlist = ref(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
function isCurrentTrack(track) {
|
||||||
|
return playerStore.currentTrack?._id === track._id
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLiked(track) {
|
||||||
|
return authStore.likedTrackIds?.has(track._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function playTrack(index) {
|
||||||
|
if (!playlist.value?.tracks?.length) return
|
||||||
|
const track = playlist.value.tracks[index]
|
||||||
|
if (isCurrentTrack(track)) {
|
||||||
|
playerStore.togglePlay()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const tracks = playlist.value.tracks
|
||||||
|
queueStore.setQueue(tracks, index)
|
||||||
|
playerStore.setTrack(track)
|
||||||
|
playerStore.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
function playAll() {
|
||||||
|
if (playlist.value?.tracks?.length) playTrack(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLike(track) {
|
||||||
|
requireAuth({
|
||||||
|
redirectTo: '/login',
|
||||||
|
onAuthenticated: () => authStore.toggleLike(track._id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const id = route.params.id
|
||||||
|
try {
|
||||||
|
if (id) {
|
||||||
|
const { data } = await api.get(`/playlists/${id}`)
|
||||||
|
if (data.success) playlist.value = data.data
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
playlist.value = null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+60
-113
@@ -1,143 +1,90 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-8">
|
<div class="p-8">
|
||||||
<div v-if="loading" class="text-center py-20 text-gray-500">Loading playlist...</div>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<template v-else-if="playlist">
|
<h1 class="text-3xl font-black text-black">YOUR PLAYLISTS</h1>
|
||||||
<div class="flex items-start gap-8 mb-8">
|
<button @click="openCreate"
|
||||||
<div class="w-56 h-56 rounded-2xl overflow-hidden border-4 border-black flex-shrink-0 bg-black">
|
class="bg-noctune-teal border-2 border-black px-4 py-2 font-bold flex items-center gap-2 rounded-lg hover:bg-teal-600 transition-colors cursor-pointer">
|
||||||
<img v-if="playlist.coverUrl" :src="formatUrl(playlist.coverUrl)" class="w-full h-full object-cover" />
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M12 4v16m8-8H4"/></svg>
|
||||||
<div v-else class="w-full h-full flex items-center justify-center">
|
NEW PLAYLIST
|
||||||
<svg class="w-40 h-24 text-white/60" viewBox="0 0 100 50">
|
</button>
|
||||||
<rect x="5" y="20" width="4" height="15" fill="currentColor"/>
|
</div>
|
||||||
<rect x="12" y="10" width="4" height="30" fill="currentColor"/>
|
|
||||||
<rect x="19" y="15" width="4" height="20" fill="currentColor"/>
|
|
||||||
<rect x="26" y="5" width="4" height="40" fill="currentColor"/>
|
|
||||||
<rect x="33" y="12" width="4" height="26" fill="currentColor"/>
|
|
||||||
<rect x="40" y="8" width="4" height="34" fill="currentColor"/>
|
|
||||||
<rect x="47" y="15" width="4" height="20" fill="currentColor"/>
|
|
||||||
<rect x="54" y="18" width="4" height="14" fill="currentColor"/>
|
|
||||||
<rect x="61" y="12" width="4" height="26" fill="currentColor"/>
|
|
||||||
<rect x="68" y="8" width="4" height="34" fill="currentColor"/>
|
|
||||||
<rect x="75" y="15" width="4" height="20" fill="currentColor"/>
|
|
||||||
<rect x="82" y="20" width="4" height="15" fill="currentColor"/>
|
|
||||||
<rect x="89" y="22" width="4" height="10" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
<div v-if="loading" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<span class="inline-block bg-noctune-teal text-black text-xs font-bold px-3 py-1 rounded mb-3 border border-black">PLAYLIST</span>
|
<div v-for="i in 4" :key="i" class="animate-pulse bg-gray-200 rounded-2xl border-2 border-black h-64" />
|
||||||
<h1 class="text-4xl font-black text-black mb-4 leading-tight">{{ playlist.name }}</h1>
|
</div>
|
||||||
<p v-if="playlist.description" class="text-gray-600 mb-4">{{ playlist.description }}</p>
|
|
||||||
<div class="flex items-center gap-3 text-gray-600 mb-6">
|
<div v-else-if="playlists.length === 0" class="text-center py-20">
|
||||||
<span class="font-medium">{{ playlist.tracks?.length || 0 }} songs</span>
|
<svg class="w-24 h-24 mx-auto text-gray-300 mb-4" fill="currentColor" viewBox="0 0 24 24"><path d="M3 10h11v2H3v-2zm0-4h11v2H3V6zm0 8h7v2H3v-2zm13-1v8l6-4-6-4z"/></svg>
|
||||||
</div>
|
<p class="text-gray-500 mb-2 text-lg">No playlists yet</p>
|
||||||
<div class="flex gap-4">
|
<p class="text-gray-400 text-sm mb-4">Create your first playlist to start organizing your music</p>
|
||||||
<button @click="playAll" :disabled="!playlist.tracks?.length" class="bg-noctune-teal border-2 border-black px-6 py-2 font-bold flex items-center gap-2 rounded-lg hover:bg-teal-600 transition-colors disabled:opacity-50">
|
<button @click="openCreate"
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
class="bg-noctune-yellow border-2 border-black px-6 py-3 font-bold rounded-lg hover:bg-yellow-400 transition-colors cursor-pointer">
|
||||||
PLAY
|
CREATE YOUR FIRST PLAYLIST
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
<div v-for="pl in playlists" :key="pl._id"
|
||||||
|
@click="router.push(`/playlist/${pl._id}`)"
|
||||||
|
class="bg-white rounded-2xl border-2 border-black overflow-hidden hover:scale-[1.02] transition-transform cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div class="aspect-square bg-gray-300 relative overflow-hidden">
|
||||||
|
<img v-if="pl.coverUrl" :src="formatUrl(pl.coverUrl)" class="w-full h-full object-cover" />
|
||||||
|
<div v-else class="w-full h-full flex items-center justify-center bg-gradient-to-br from-noctune-teal/20 to-noctune-yellow/20">
|
||||||
|
<svg class="w-20 h-16 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M3 10h11v2H3v-2zm0-4h11v2H3V6zm0 8h7v2H3v-2zm13-1v8l6-4-6-4z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="font-bold text-sm truncate text-black">{{ pl.name }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ pl.tracks?.length || 0 }} songs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl border-4 border-black overflow-hidden">
|
<CreatePlaylistModal :show="showCreateModal" @close="showCreateModal = false" @created="onPlaylistCreated" />
|
||||||
<div class="bg-noctune-teal px-6 py-3 flex items-center text-black font-bold border-b-4 border-black">
|
|
||||||
<span class="w-12 text-center">#</span>
|
|
||||||
<span class="flex-1 text-center">TITLE</span>
|
|
||||||
<span class="w-48 text-center">ARTIST</span>
|
|
||||||
<span class="w-12 text-center"></span>
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-gray-200">
|
|
||||||
<div v-for="(track, index) in playlist.tracks" :key="track._id"
|
|
||||||
@click="playTrack(index)"
|
|
||||||
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<span class="w-12 text-center">
|
|
||||||
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-4 h-4 text-noctune-teal mx-auto" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
||||||
<span v-else class="text-gray-500">{{ index + 1 }}</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex-1 flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 rounded-lg flex-shrink-0 overflow-hidden bg-gray-300 border border-black">
|
|
||||||
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
|
|
||||||
</div>
|
|
||||||
<span class="font-medium text-sm truncate" :class="isCurrentTrack(track) ? 'text-noctune-teal' : 'text-black'">{{ track.title }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="w-48 text-center text-gray-600 text-sm truncate">{{ track.artist }}</span>
|
|
||||||
<button @click.stop="toggleLike(track)" class="w-12 flex justify-center transition-colors">
|
|
||||||
<svg class="w-5 h-5" :class="isLiked(track) ? 'fill-red-400 text-red-400' : 'text-gray-400 hover:text-red-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!playlist.tracks?.length" class="p-8 text-center text-gray-500">No tracks in this playlist yet</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else class="text-center py-20 text-gray-500">Playlist not found</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { usePlayerStore } from '../stores/playerStore.js'
|
|
||||||
import { useQueueStore } from '../stores/queueStore.js'
|
|
||||||
import { useAuthStore } from '../stores/authStore.js'
|
import { useAuthStore } from '../stores/authStore.js'
|
||||||
import api from '../lib/api.js'
|
import api from '../lib/api.js'
|
||||||
import { formatUrl } from '../lib/utils.js'
|
import { formatUrl } from '../lib/utils.js'
|
||||||
|
import { useAuthGate } from '../composables/useAuthGate.js'
|
||||||
|
import CreatePlaylistModal from '../components/CreatePlaylistModal.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const router = useRouter()
|
||||||
const playerStore = usePlayerStore()
|
|
||||||
const queueStore = useQueueStore()
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const { requireAuth } = useAuthGate()
|
||||||
|
|
||||||
const playlist = ref(null)
|
const playlists = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
|
||||||
function isCurrentTrack(track) {
|
function openCreate() {
|
||||||
return playerStore.currentTrack?._id === track._id
|
requireAuth({
|
||||||
|
redirectTo: '/login',
|
||||||
|
onAuthenticated: () => { showCreateModal.value = true }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLiked(track) {
|
function onPlaylistCreated(playlist) {
|
||||||
return authStore.likedTrackIds?.has(track._id)
|
showCreateModal.value = false
|
||||||
}
|
playlists.value.unshift(playlist)
|
||||||
|
|
||||||
function playTrack(index) {
|
|
||||||
if (!playlist.value?.tracks?.length) return
|
|
||||||
const track = playlist.value.tracks[index]
|
|
||||||
if (isCurrentTrack(track)) {
|
|
||||||
playerStore.togglePlay()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tracks = playlist.value.tracks
|
|
||||||
queueStore.setQueue(tracks, index)
|
|
||||||
playerStore.setTrack(track)
|
|
||||||
playerStore.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
function playAll() {
|
|
||||||
if (playlist.value?.tracks?.length) playTrack(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleLike(track) {
|
|
||||||
await authStore.toggleLike(track._id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const id = route.params.id
|
if (!authStore.isLoggedIn) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (id) {
|
const { data } = await api.get('/playlists')
|
||||||
const { data } = await api.get(`/playlists/${id}`)
|
if (data.success) playlists.value = data.data
|
||||||
if (data.success) playlist.value = data.data
|
|
||||||
} else {
|
|
||||||
const { data } = await api.get('/playlists')
|
|
||||||
if (data.success && data.data.length) {
|
|
||||||
const { data: detail } = await api.get(`/playlists/${data.data[0]._id}`)
|
|
||||||
if (detail.success) playlist.value = detail.data
|
|
||||||
} else {
|
|
||||||
playlist.value = { name: 'My Playlist', tracks: [] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
playlist.value = null
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user