Refactor Sidebar and enhance playlist functionality; update main.js for session restoration; add toggleLike feature in authStore; improve AdminPage and LandingPage UI; integrate liked tracks in LibraryPage and PlaylistPage; optimize track playback logic.
This commit is contained in:
1 parent
9fdd78e737
commit
b2f3e9661e
14 files changed
+433
-269
No files matched your search
Vendored
+1
@@ -0,0 +1 @@
|
||||
.range-player[data-v-e17af23f]::-webkit-slider-thumb{-webkit-appearance:none;cursor:pointer;background:#fff;border-radius:50%;width:12px;height:12px}.range-vol[data-v-e17af23f]::-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
@@ -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 _e(e){let t=``;if(y(e))t=e;else if(m(e))for(let n=0;n<e.length;n++){let r=_e(e[n]);r&&(t+=r+` `)}else if(x(e))for(let n in e)e[n]&&(t+=n+` `);return t.trim()}var ve=`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`,ye=r(ve);ve+``;function be(e){return!!e||e===``}function xe(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=Se(e[r],t[r]);return n}function Se(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?xe(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||!Se(e[n],t[n]))return!1}}return String(e)===String(t)}function Ce(e,t){return e.findIndex(e=>Se(e,t))}var we=e=>!!(e&&e.__v_isRef===!0),A=e=>y(e)?e:e==null?``:m(e)||x(e)&&(e.toString===C||!v(e.toString))?we(e)?A(e.value):JSON.stringify(e,Te,2):String(e),Te=(e,t)=>we(t)?Te(e,t.value):h(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[Ee(t,r)+` =>`]=n,e),{})}:g(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>Ee(e))}:b(t)?Ee(t):x(t)&&!m(t)&&!te(t)?String(t):t,Ee=(e,t=``)=>b(e)?`Symbol(${e.description??t})`:e,j,De=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&&j&&(j.active?(this.parent=j,this.index=(j.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=j;try{return j=this,e()}finally{j=t}}}on(){++this._on===1&&(this.prevScope=j,j=this)}off(){if(this._on>0&&--this._on===0){if(j===this)j=this.prevScope;else{let e=j;for(;e;){if(e.prevScope===this){e.prevScope=this.pLine 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
+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]&&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
-1
@@ -1 +0,0 @@
|
||||
.range-player[data-v-b7738cdf]::-webkit-slider-thumb{-webkit-appearance:none;cursor:pointer;background:#fff;border-radius:50%;width:12px;height:12px}.range-vol[data-v-b7738cdf]::-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}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.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-16{top:4rem}.top-20{top:5rem}.top-4{top:1rem}.top-8{top:2rem}.top-full{top:100%}.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-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-2{margiLine truncated
|
||||
Vendored
+2
-2
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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">
|
||||
<script type="module" crossorigin src="/assets/index-BO7gh51n.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-mUm8ch4i.css">
|
||||
<script type="module" crossorigin src="/assets/index-iVZX2_zV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-3bN2l3Ol.css">
|
||||
</head>
|
||||
<body class="bg-noctune-cream">
|
||||
<div id="app"></div>
|
||||
|
||||
+51
-31
@@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-noctune-cream">
|
||||
<div v-if="!isAuthPage" class="flex flex-col min-h-screen">
|
||||
<AppHeader />
|
||||
<div class="flex flex-1">
|
||||
<Sidebar />
|
||||
<main class="flex-1 overflow-auto" :class="{ 'pb-20': hasPlayer }">
|
||||
<div v-if="!isAuthPage">
|
||||
<AppHeader class="sticky top-0 z-30" />
|
||||
<div class="flex">
|
||||
<Sidebar
|
||||
class="fixed left-0 top-[57px] bottom-0 w-64 border-r-4 border-black overflow-y-auto z-20"
|
||||
/>
|
||||
<main
|
||||
class="ml-64 flex-1 min-h-[calc(100vh-57px)]"
|
||||
:class="{ 'pb-20': hasPlayer }"
|
||||
>
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
@@ -12,13 +17,20 @@
|
||||
</div>
|
||||
<router-view v-else />
|
||||
<div class="fixed top-4 right-4 z-[100] space-y-2">
|
||||
<div v-for="toast in uiStore.toasts" :key="toast.id"
|
||||
<div
|
||||
v-for="toast in uiStore.toasts"
|
||||
:key="toast.id"
|
||||
class="px-4 py-3 rounded-lg border-2 border-black font-bold text-sm shadow-[4px_4px_0px_0px_#000] transition-all animate-slide-in"
|
||||
:class="toastClass(toast.type)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ toast.message }}</span>
|
||||
<button @click="uiStore.dismissToast(toast.id)" class="ml-2 opacity-60 hover:opacity-100">×</button>
|
||||
<button
|
||||
@click="uiStore.dismissToast(toast.id)"
|
||||
class="ml-2 opacity-60 hover:opacity-100"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,37 +38,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from './stores/authStore.js'
|
||||
import { useUiStore } from './stores/uiStore.js'
|
||||
import { usePlayerStore } from './stores/playerStore.js'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import Sidebar from './components/Sidebar.vue'
|
||||
import PlayerBar from './components/PlayerBar.vue'
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAuthStore } from './stores/authStore.js';
|
||||
import { useUiStore } from './stores/uiStore.js';
|
||||
import { usePlayerStore } from './stores/playerStore.js';
|
||||
import AppHeader from './components/AppHeader.vue';
|
||||
import Sidebar from './components/Sidebar.vue';
|
||||
import PlayerBar from './components/PlayerBar.vue';
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const uiStore = useUiStore()
|
||||
const playerStore = usePlayerStore()
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const playerStore = usePlayerStore();
|
||||
|
||||
const isAuthPage = computed(() => {
|
||||
return route.meta?.guest === true || route.path === '/register'
|
||||
})
|
||||
return route.meta?.guest === true || route.path === '/register';
|
||||
});
|
||||
|
||||
const hasPlayer = computed(() => !!playerStore.currentTrack)
|
||||
const hasPlayer = computed(() => !!playerStore.currentTrack);
|
||||
|
||||
function toastClass(type) {
|
||||
return {
|
||||
info: 'bg-noctune-teal text-black',
|
||||
success: 'bg-green-400 text-black',
|
||||
error: 'bg-red-400 text-white',
|
||||
}[type] || 'bg-noctune-yellow text-black'
|
||||
return (
|
||||
{
|
||||
info: 'bg-noctune-teal text-black',
|
||||
success: 'bg-green-400 text-black',
|
||||
error: 'bg-red-400 text-white',
|
||||
}[type] || 'bg-noctune-yellow text-black'
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.checkSession()
|
||||
})
|
||||
await authStore.checkSession();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -64,7 +78,13 @@ onMounted(async () => {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateX(100%); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -42,8 +42,8 @@
|
||||
</div>
|
||||
|
||||
<div class="w-80 flex items-center justify-end gap-3">
|
||||
<button @click="toggleLike" class="text-gray-400 hover:text-red-400 transition-colors">
|
||||
<svg class="w-5 h-5" :class="isLiked ? 'text-red-400 fill-current' : ''" 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 @click="toggleLike" class="transition-colors">
|
||||
<svg class="w-5 h-5" :class="isLiked ? '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 class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>
|
||||
@@ -58,10 +58,12 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { usePlayerStore } from '../stores/playerStore.js'
|
||||
import { useQueueStore } from '../stores/queueStore.js'
|
||||
import api from '../lib/api.js'
|
||||
import { useAuthStore } from '../stores/authStore.js'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const queueStore = useQueueStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isLiked = ref(false)
|
||||
|
||||
function onTrackEnd() {
|
||||
@@ -106,32 +108,21 @@ function playPrev() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => playerStore.currentTrack?.id, async (id) => {
|
||||
if (!id) return
|
||||
try {
|
||||
const { data } = await api.get('/auth/me')
|
||||
if (data.success) {
|
||||
isLiked.value = data.data.likedTracks?.includes(id) || false
|
||||
}
|
||||
} catch {
|
||||
isLiked.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function toggleLike() {
|
||||
if (!playerStore.currentTrack) return
|
||||
const id = playerStore.currentTrack._id || playerStore.currentTrack.id
|
||||
try {
|
||||
if (isLiked.value) {
|
||||
await api.delete(`/tracks/${id}/like`)
|
||||
} else {
|
||||
await api.post(`/tracks/${id}/like`)
|
||||
}
|
||||
isLiked.value = !isLiked.value
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await authStore.toggleLike(id)
|
||||
}
|
||||
|
||||
// Sync isLiked ref reactively from authStore
|
||||
watch(() => playerStore.currentTrack?._id, (id) => {
|
||||
isLiked.value = id ? authStore.likedTrackIds.has(id) : false
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => authStore.user?.likedTracks?.length, () => {
|
||||
const id = playerStore.currentTrack?._id
|
||||
if (id) isLiked.value = authStore.likedTrackIds.has(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+36
-27
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="w-64 bg-noctune-cream border-r-4 border-black flex flex-col h-full">
|
||||
<aside class="bg-noctune-cream flex flex-col h-full">
|
||||
<nav class="p-4 space-y-2">
|
||||
<router-link to="/home"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg font-bold text-black hover:bg-noctune-teal/20 transition-colors"
|
||||
@@ -7,18 +7,43 @@
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
|
||||
<span class="tracking-wide">HOME</span>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/library"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg font-bold text-black hover:bg-noctune-teal/20 transition-colors"
|
||||
:class="{ 'bg-noctune-teal text-black': isActive('/library') }">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
|
||||
<span class="tracking-wide">LIBRARY</span>
|
||||
</router-link>
|
||||
<router-link to="/playlist"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg font-bold text-black hover:bg-noctune-teal/20 transition-colors"
|
||||
:class="{ 'bg-noctune-teal text-black': isActive('/playlist') }">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M3 10h11v2H3v-2zm0-4h11v2H3V6zm0 8h7v2H3v-2zm13-1v8l6-4-6-4z"/></svg>
|
||||
<span class="tracking-wide">PLAYLIST</span>
|
||||
</router-link>
|
||||
|
||||
<div class="relative group">
|
||||
<router-link to="/playlist"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg font-bold text-black hover:bg-noctune-teal/20 transition-colors"
|
||||
:class="{ 'bg-noctune-teal text-black': isActive('/playlist') || isActivePrefix('/playlist/') }">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M3 10h11v2H3v-2zm0-4h11v2H3V6zm0 8h7v2H3v-2zm13-1v8l6-4-6-4z"/></svg>
|
||||
<span class="tracking-wide">PLAYLIST</span>
|
||||
<svg class="w-4 h-4 ml-auto text-gray-500 group-hover:rotate-180 transition-transform" fill="currentColor" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></svg>
|
||||
</router-link>
|
||||
|
||||
<div class="absolute left-0 top-full mt-1 w-56 bg-white border-2 border-black rounded-lg shadow-[4px_4px_0px_0px_#000] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 max-h-72 overflow-y-auto">
|
||||
<div v-if="playlists.length" class="p-2">
|
||||
<div v-for="pl in playlists" :key="pl._id"
|
||||
@click="router.push(`/playlist/${pl._id}`)"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-noctune-teal/20 transition-colors text-sm font-medium text-black"
|
||||
>
|
||||
<svg class="w-4 h-4 flex-shrink-0 text-gray-500" fill="currentColor" viewBox="0 0 24 24"><path d="M3 10h11v2H3v-2zm0-4h11v2H3V6zm0 8h7v2H3v-2zm13-1v8l6-4-6-4z"/></svg>
|
||||
<span class="truncate">{{ pl.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-4 text-center text-sm text-gray-500">No playlists yet</div>
|
||||
<div class="border-t-2 border-gray-200 p-2">
|
||||
<router-link to="/playlist"
|
||||
class="block px-3 py-2 rounded-lg text-sm font-bold text-noctune-teal hover:bg-noctune-teal/20 transition-colors text-center">
|
||||
Browse All Playlists
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link v-if="authStore.isAdmin" to="/admin/tracks"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg font-bold text-black hover:bg-noctune-orange/20 transition-colors"
|
||||
:class="{ 'bg-noctune-orange text-black': isActive('/admin/tracks') }">
|
||||
@@ -36,18 +61,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-4 mt-4">
|
||||
<div v-if="playlists.length" class="space-y-1">
|
||||
<p class="text-xs font-bold text-gray-500 px-2 mb-2">YOUR PLAYLISTS</p>
|
||||
<div v-for="pl in playlists" :key="pl._id"
|
||||
@click="router.push(`/playlist/${pl._id}`)"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-noctune-teal/20 transition-colors text-sm text-black font-medium truncate"
|
||||
>
|
||||
<svg class="w-4 h-4 flex-shrink-0 text-gray-500" fill="currentColor" viewBox="0 0 24 24"><path d="M3 10h11v2H3v-2zm0-4h11v2H3V6zm0 8h7v2H3v-2zm13-1v8l6-4-6-4z"/></svg>
|
||||
{{ pl.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -55,26 +69,23 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/authStore.js'
|
||||
import { useQueueStore } from '../stores/queueStore.js'
|
||||
import api from '../lib/api.js'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const queueStore = useQueueStore()
|
||||
|
||||
const playlists = ref([])
|
||||
|
||||
const isActive = (path) => route.path === path
|
||||
const isActivePrefix = (prefix) => route.path.startsWith(prefix)
|
||||
|
||||
async function fetchPlaylists() {
|
||||
if (!authStore.isLoggedIn) return
|
||||
try {
|
||||
const { data } = await api.get('/playlists')
|
||||
if (data.success) playlists.value = data.data
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function openCreatePlaylist() {
|
||||
@@ -85,9 +96,7 @@ async function openCreatePlaylist() {
|
||||
if (data.success) {
|
||||
playlists.value.push(data.data)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(fetchPlaylists)
|
||||
|
||||
+16
-9
@@ -1,10 +1,17 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router/index.js'
|
||||
import './style.css'
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import router from './router/index.js';
|
||||
import './style.css';
|
||||
import { useAuthStore } from './stores/authStore.js';
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
|
||||
// Restore session before mounting so route guards see logged-in state
|
||||
const authStore = useAuthStore(pinia);
|
||||
authStore.checkSession().finally(() => {
|
||||
app.mount('#app');
|
||||
});
|
||||
+24
-1
@@ -9,6 +9,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const isLoggedIn = computed(() => !!user.value)
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
const isArtist = computed(() => user.value?.role === 'artist')
|
||||
const likedTrackIds = computed(() => new Set(user.value?.likedTracks || []))
|
||||
|
||||
async function checkSession() {
|
||||
try {
|
||||
@@ -71,5 +72,27 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { user, loading, isLoggedIn, isAdmin, isArtist, checkSession, login, register, logout, updateProfile }
|
||||
async function toggleLike(trackId) {
|
||||
const isLiked = likedTrackIds.value.has(trackId)
|
||||
try {
|
||||
if (isLiked) {
|
||||
await api.delete(`/tracks/${trackId}/like`)
|
||||
if (user.value) {
|
||||
user.value.likedTracks = user.value.likedTracks.filter((id) => id !== trackId)
|
||||
}
|
||||
} else {
|
||||
await api.post(`/tracks/${trackId}/like`)
|
||||
if (user.value) {
|
||||
user.value.likedTracks.push(trackId)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user, loading, isLoggedIn, isAdmin, isArtist, likedTrackIds,
|
||||
checkSession, login, register, logout, updateProfile, toggleLike,
|
||||
}
|
||||
})
|
||||
+153
-73
@@ -1,49 +1,122 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-noctune-cream flex flex-col">
|
||||
<AppHeader />
|
||||
<div class="flex flex-1">
|
||||
<Sidebar />
|
||||
<main class="flex-1 p-8 overflow-auto">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-4xl font-black text-black mb-2">Upload Track</h1>
|
||||
<p class="text-gray-600 mb-8">Add a new track to the library</p>
|
||||
<p v-if="error" class="text-red-500 text-sm mb-4 font-medium">{{ error }}</p>
|
||||
<p v-if="success" class="text-green-600 text-sm mb-4 font-medium">{{ success }}</p>
|
||||
<p v-if="error" class="text-red-500 text-sm mb-4 font-medium">
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-if="success" class="text-green-600 text-sm mb-4 font-medium">
|
||||
{{ success }}
|
||||
</p>
|
||||
<div class="bg-white rounded-2xl border-4 border-black p-8 space-y-6">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">TITLE</label>
|
||||
<input type="text" v-model="title" class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black" placeholder="Track title" />
|
||||
<label
|
||||
class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
|
||||
>TITLE</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="title"
|
||||
class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black"
|
||||
placeholder="Track title"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">ARTIST</label>
|
||||
<input type="text" v-model="artist" class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black" placeholder="Artist name" />
|
||||
<label
|
||||
class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
|
||||
>ARTIST</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="artist"
|
||||
class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black"
|
||||
placeholder="Artist name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">ALBUM</label>
|
||||
<input type="text" v-model="album" class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black" placeholder="Album name" />
|
||||
<label
|
||||
class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
|
||||
>ALBUM</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="album"
|
||||
class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black"
|
||||
placeholder="Album name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">GENRE</label>
|
||||
<input type="text" v-model="genre" class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black" placeholder="e.g. Pop, Rock, Jazz" />
|
||||
<label
|
||||
class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
|
||||
>GENRE</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="genre"
|
||||
class="w-full border-2 border-black rounded-lg px-4 py-3 focus:outline-none text-black"
|
||||
placeholder="e.g. Pop, Rock, Jazz"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">COVER IMAGE</label>
|
||||
<div class="border-2 border-dashed border-black rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50" @click="coverInput.click()">
|
||||
<img v-if="coverPreview" :src="coverPreview" class="w-32 h-32 object-cover mx-auto mb-2 rounded-lg border-2 border-black" />
|
||||
<label
|
||||
class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
|
||||
>COVER IMAGE</label
|
||||
>
|
||||
<div
|
||||
class="border-2 border-dashed border-black rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50"
|
||||
@click="coverInput.click()"
|
||||
>
|
||||
<img
|
||||
v-if="coverPreview"
|
||||
:src="coverPreview"
|
||||
class="w-32 h-32 object-cover mx-auto mb-2 rounded-lg border-2 border-black"
|
||||
/>
|
||||
<p v-else class="text-gray-500">Click to upload cover image</p>
|
||||
<input ref="coverInput" type="file" accept="image/*" @change="onCoverChange" class="hidden" />
|
||||
<input
|
||||
ref="coverInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="onCoverChange"
|
||||
class="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">AUDIO FILE</label>
|
||||
<div class="border-2 border-dashed border-black rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50" @click="audioInput.click()">
|
||||
<p v-if="audioFile" class="text-black font-medium">{{ audioFile.name }}</p>
|
||||
<p v-else class="text-gray-500">Click to upload audio file (MP3, WAV, FLAC)</p>
|
||||
<input ref="audioInput" type="file" accept=".mp3,.wav,.flac" @change="onAudioChange" class="hidden" />
|
||||
<label
|
||||
class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
|
||||
>AUDIO FILE</label
|
||||
>
|
||||
<div
|
||||
class="border-2 border-dashed border-black rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50"
|
||||
@click="audioInput.click()"
|
||||
>
|
||||
<p v-if="audioFile" class="text-black font-medium">
|
||||
{{ audioFile.name }}
|
||||
</p>
|
||||
<p v-else class="text-gray-500">
|
||||
Click to upload audio file (MP3, WAV, FLAC)
|
||||
</p>
|
||||
<input
|
||||
ref="audioInput"
|
||||
type="file"
|
||||
accept=".mp3,.wav,.flac"
|
||||
@change="onAudioChange"
|
||||
class="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="handleUpload" :disabled="uploading" class="w-full bg-noctune-orange border-2 border-black rounded-lg py-4 font-bold text-white text-xl hover:bg-orange-600 transition-colors disabled:opacity-50">
|
||||
<span v-if="uploading" class="inline-block w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></span>
|
||||
<button
|
||||
@click="handleUpload"
|
||||
:disabled="uploading"
|
||||
class="w-full bg-noctune-orange border-2 border-black rounded-lg py-4 font-bold text-white text-xl hover:bg-orange-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span
|
||||
v-if="uploading"
|
||||
class="inline-block w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"
|
||||
></span>
|
||||
{{ uploading ? 'Uploading...' : 'UPLOAD TRACK' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -54,79 +127,86 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import api from '../lib/api.js'
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import AppHeader from '../components/AppHeader.vue';
|
||||
import Sidebar from '../components/Sidebar.vue';
|
||||
import api from '../lib/api.js';
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const title = ref('')
|
||||
const artist = ref('')
|
||||
const album = ref('')
|
||||
const genre = ref('')
|
||||
const coverFile = ref(null)
|
||||
const coverPreview = ref('')
|
||||
const audioFile = ref(null)
|
||||
const uploading = ref(false)
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
const title = ref('');
|
||||
const artist = ref('');
|
||||
const album = ref('');
|
||||
const genre = ref('');
|
||||
const coverFile = ref(null);
|
||||
const coverPreview = ref('');
|
||||
const audioFile = ref(null);
|
||||
const uploading = ref(false);
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
|
||||
const coverInput = ref(null)
|
||||
const audioInput = ref(null)
|
||||
const coverInput = ref(null);
|
||||
const audioInput = ref(null);
|
||||
|
||||
function onCoverChange(e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
coverFile.value = file
|
||||
coverPreview.value = URL.createObjectURL(file)
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
coverFile.value = file;
|
||||
coverPreview.value = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
function onAudioChange(e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
audioFile.value = file
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
audioFile.value = file;
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
|
||||
if (!title.value || !artist.value || !album.value || !genre.value || !coverFile.value || !audioFile.value) {
|
||||
error.value = 'All fields are required'
|
||||
return
|
||||
if (
|
||||
!title.value ||
|
||||
!artist.value ||
|
||||
!album.value ||
|
||||
!genre.value ||
|
||||
!coverFile.value ||
|
||||
!audioFile.value
|
||||
) {
|
||||
error.value = 'All fields are required';
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
uploading.value = true;
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('title', title.value)
|
||||
formData.append('artist', artist.value)
|
||||
formData.append('album', album.value)
|
||||
formData.append('genre', genre.value)
|
||||
formData.append('cover', coverFile.value)
|
||||
formData.append('audio', audioFile.value)
|
||||
const formData = new FormData();
|
||||
formData.append('title', title.value);
|
||||
formData.append('artist', artist.value);
|
||||
formData.append('album', album.value);
|
||||
formData.append('genre', genre.value);
|
||||
formData.append('cover', coverFile.value);
|
||||
formData.append('audio', audioFile.value);
|
||||
|
||||
const { data } = await api.post('/admin/tracks', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
success.value = 'Track uploaded successfully!'
|
||||
title.value = ''
|
||||
artist.value = ''
|
||||
album.value = ''
|
||||
genre.value = ''
|
||||
coverFile.value = null
|
||||
coverPreview.value = ''
|
||||
audioFile.value = null
|
||||
setTimeout(() => router.push('/home'), 1500)
|
||||
success.value = 'Track uploaded successfully!';
|
||||
title.value = '';
|
||||
artist.value = '';
|
||||
album.value = '';
|
||||
genre.value = '';
|
||||
coverFile.value = null;
|
||||
coverPreview.value = '';
|
||||
audioFile.value = null;
|
||||
setTimeout(() => router.push('/home'), 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Upload failed'
|
||||
error.value = err.response?.data?.message || 'Upload failed';
|
||||
} finally {
|
||||
uploading.value = false
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+62
-37
@@ -1,50 +1,57 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-noctune-cream flex flex-col">
|
||||
<div class="p-8">
|
||||
<div class="flex items-start gap-8 mb-12">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-5xl font-black text-black leading-tight mb-2">MUSIC<br/>THAT HITS</h1>
|
||||
<div class="inline-block bg-noctune-orange px-4 py-2 mb-4 border-b-4 border-black">
|
||||
<span class="text-4xl font-black text-black">DIFFERENT.</span>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-6">You Vibe. Your sound. Only on Noctune.</p>
|
||||
<div class="flex gap-4">
|
||||
<button @click="playFirstTrack" class="bg-noctune-yellow border-2 border-black px-6 py-3 font-bold hover:bg-yellow-400 transition-colors rounded-lg">PLAY NOW</button>
|
||||
<button class="bg-gray-200 border-2 border-black px-6 py-3 font-bold hover:bg-gray-300 transition-colors rounded-lg">EXPLORE</button>
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="flex items-start gap-8 mb-12">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-5xl font-black text-black leading-tight mb-2">MUSIC<br/>THAT HITS</h1>
|
||||
<div class="inline-block bg-noctune-orange px-4 py-2 mb-4 border-b-4 border-black">
|
||||
<span class="text-4xl font-black text-black">DIFFERENT.</span>
|
||||
</div>
|
||||
<div class="w-80 h-80 rounded-2xl overflow-hidden border-4 border-black flex-shrink-0">
|
||||
<img src="https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop" alt="Music illustration" class="w-full h-full object-cover" />
|
||||
<p class="text-gray-600 mb-6">You Vibe. Your sound. Only on Noctune.</p>
|
||||
<div class="flex gap-4">
|
||||
<button @click="playFirstTrack" class="bg-noctune-yellow border-2 border-black px-6 py-3 font-bold hover:bg-yellow-400 transition-colors rounded-lg">PLAY NOW</button>
|
||||
<button class="bg-gray-200 border-2 border-black px-6 py-3 font-bold hover:bg-gray-300 transition-colors rounded-lg">EXPLORE</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-80 h-80 rounded-2xl overflow-hidden border-4 border-black flex-shrink-0">
|
||||
<img src="https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop" alt="Music illustration" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 class="text-2xl font-black text-black mb-6 flex items-center">
|
||||
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>
|
||||
</h2>
|
||||
<div v-if="loading" class="text-center py-12 text-gray-500">Loading tracks...</div>
|
||||
<div v-else class="grid grid-cols-4 gap-4">
|
||||
<div v-for="track in tracks" :key="track._id"
|
||||
@click="playTrack(track)"
|
||||
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="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="w-12 h-12 bg-noctune-yellow rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<svg class="w-6 h-6 ml-1 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
<section>
|
||||
<h2 class="text-2xl font-black text-black mb-6 flex items-center">
|
||||
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>
|
||||
</h2>
|
||||
<div v-if="loading" class="text-center py-12 text-gray-500">Loading tracks...</div>
|
||||
<div v-else class="grid grid-cols-4 gap-4">
|
||||
<div v-for="track in tracks" :key="track._id"
|
||||
@click="playTrack(track)"
|
||||
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="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="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'">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="text-sm font-bold text-black truncate">{{ track.title }}</p>
|
||||
<p class="text-xs text-gray-500 truncate">{{ track.artist }}</p>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-bold text-black truncate">{{ track.title }}</p>
|
||||
<p class="text-xs text-gray-500 truncate">{{ track.artist }}</p>
|
||||
</div>
|
||||
<button @click.stop="toggleLike(track)" class="ml-2 flex-shrink-0">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,10 +59,12 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { usePlayerStore } from '../stores/playerStore.js'
|
||||
import { useQueueStore } from '../stores/queueStore.js'
|
||||
import { useAuthStore } from '../stores/authStore.js'
|
||||
import api from '../lib/api.js'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const queueStore = useQueueStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const tracks = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -65,7 +74,19 @@ function formatUrl(url) {
|
||||
return url.startsWith('http') ? url : `http://localhost:3001${url}`
|
||||
}
|
||||
|
||||
function isCurrentTrack(track) {
|
||||
return playerStore.currentTrack?._id === track._id
|
||||
}
|
||||
|
||||
function isLiked(track) {
|
||||
return authStore.likedTrackIds?.has(track._id)
|
||||
}
|
||||
|
||||
function playTrack(track) {
|
||||
if (isCurrentTrack(track)) {
|
||||
playerStore.togglePlay()
|
||||
return
|
||||
}
|
||||
const idx = tracks.value.findIndex((t) => t._id === track._id)
|
||||
queueStore.setQueue(tracks.value, idx)
|
||||
playerStore.setTrack(track)
|
||||
@@ -76,6 +97,10 @@ function playFirstTrack() {
|
||||
if (tracks.value.length) playTrack(tracks.value[0])
|
||||
}
|
||||
|
||||
async function toggleLike(track) {
|
||||
await authStore.toggleLike(track._id)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await api.get('/tracks')
|
||||
|
||||
+37
-30
@@ -9,13 +9,15 @@
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-4 gap-6">
|
||||
<div v-for="track in likedTracks" :key="track._id"
|
||||
@click="playTrack(track)"
|
||||
@click="playTrack(track, true)"
|
||||
class="group cursor-pointer bg-white rounded-2xl border-2 border-black overflow-hidden hover:scale-[1.02] transition-transform"
|
||||
>
|
||||
<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" />
|
||||
<div class="absolute bottom-2 right-2 w-10 h-10 bg-noctune-yellow rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<svg class="w-5 h-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
<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'">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
@@ -41,20 +43,23 @@
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<div v-for="(track, index) in allTracks" :key="track._id"
|
||||
@click="playTrack(track)"
|
||||
@click="playTrack(track, false, index)"
|
||||
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-3">
|
||||
<span class="text-sm text-gray-400 w-6">{{ index + 1 }}</span>
|
||||
<span class="text-sm text-gray-400 w-6">
|
||||
<svg v-if="isCurrentTrack(track) && playerStore.isPlaying" class="w-4 h-4 text-noctune-teal inline" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
</span>
|
||||
<div class="w-10 h-10 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-black text-sm truncate">{{ track.title }}</span>
|
||||
<span class="font-medium text-sm truncate" :class="isCurrentTrack(track) ? 'text-noctune-teal' : 'text-black'">{{ track.title }}</span>
|
||||
</div>
|
||||
<span class="w-40 text-center text-gray-600 text-sm truncate">{{ track.artist }}</span>
|
||||
<span class="w-32 text-center text-gray-600 text-sm truncate">{{ track.album }}</span>
|
||||
<button @click.stop="toggleLike(track)" class="w-12 flex justify-center text-gray-400 hover:text-red-400 transition-colors">
|
||||
<svg class="w-5 h-5" :class="track._isLiked ? 'fill-red-400 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 @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>
|
||||
@@ -67,10 +72,12 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { usePlayerStore } from '../stores/playerStore.js'
|
||||
import { useQueueStore } from '../stores/queueStore.js'
|
||||
import { useAuthStore } from '../stores/authStore.js'
|
||||
import api from '../lib/api.js'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const queueStore = useQueueStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const likedTracks = ref([])
|
||||
const allTracks = ref([])
|
||||
@@ -82,26 +89,31 @@ function formatUrl(url) {
|
||||
return url.startsWith('http') ? url : `http://localhost:3001${url}`
|
||||
}
|
||||
|
||||
function playTrack(track) {
|
||||
const tracks = allTracks.value.length ? allTracks.value : likedTracks.value
|
||||
const idx = tracks.findIndex((t) => t._id === track._id)
|
||||
queueStore.setQueue(tracks, idx >= 0 ? idx : 0)
|
||||
function isCurrentTrack(track) {
|
||||
return playerStore.currentTrack?._id === track._id
|
||||
}
|
||||
|
||||
function isLiked(track) {
|
||||
return authStore.likedTrackIds?.has(track._id)
|
||||
}
|
||||
|
||||
function playTrack(track, fromLiked = false, idx) {
|
||||
if (isCurrentTrack(track)) {
|
||||
playerStore.togglePlay()
|
||||
return
|
||||
}
|
||||
const source = fromLiked ? likedTracks.value : allTracks.value
|
||||
const i = idx ?? source.findIndex((t) => t._id === track._id)
|
||||
queueStore.setQueue(source, i)
|
||||
playerStore.setTrack(track)
|
||||
playerStore.play()
|
||||
}
|
||||
|
||||
async function toggleLike(track) {
|
||||
try {
|
||||
if (track._isLiked) {
|
||||
await api.delete(`/tracks/${track._id}/like`)
|
||||
track._isLiked = false
|
||||
likedTracks.value = likedTracks.value.filter((t) => t._id !== track._id)
|
||||
} else {
|
||||
await api.post(`/tracks/${track._id}/like`)
|
||||
track._isLiked = true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
const wasLiked = isLiked(track)
|
||||
await authStore.toggleLike(track._id)
|
||||
if (wasLiked) {
|
||||
likedTracks.value = likedTracks.value.filter((t) => t._id !== track._id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +121,7 @@ onMounted(async () => {
|
||||
try {
|
||||
const { data: liked } = await api.get('/tracks/liked')
|
||||
if (liked.success) {
|
||||
likedTracks.value = liked.data.likedTracks || []
|
||||
likedTracks.value.forEach((t) => (t._isLiked = true))
|
||||
likedTracks.value = liked.data || []
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -120,11 +131,7 @@ onMounted(async () => {
|
||||
|
||||
try {
|
||||
const { data: all } = await api.get('/tracks')
|
||||
if (all.success) {
|
||||
allTracks.value = all.data
|
||||
const likedIds = new Set(likedTracks.value.map((t) => t._id))
|
||||
allTracks.value.forEach((t) => (t._isLiked = likedIds.has(t._id)))
|
||||
}
|
||||
if (all.success) allTracks.value = all.data
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
|
||||
+26
-24
@@ -52,16 +52,19 @@
|
||||
@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 text-gray-500">{{ index + 1 }}</span>
|
||||
<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-black text-sm truncate">{{ track.title }}</span>
|
||||
<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 text-gray-400 hover:text-red-400 transition-colors">
|
||||
<svg class="w-5 h-5" :class="track._isLiked ? 'fill-red-400 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 @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>
|
||||
@@ -77,11 +80,13 @@ 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'
|
||||
|
||||
const route = useRoute()
|
||||
const playerStore = usePlayerStore()
|
||||
const queueStore = useQueueStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const playlist = ref(null)
|
||||
const loading = ref(true)
|
||||
@@ -91,11 +96,24 @@ function formatUrl(url) {
|
||||
return url.startsWith('http') ? url : `http://localhost:3001${url}`
|
||||
}
|
||||
|
||||
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(tracks[index])
|
||||
playerStore.setTrack(track)
|
||||
playerStore.play()
|
||||
}
|
||||
|
||||
@@ -104,17 +122,7 @@ function playAll() {
|
||||
}
|
||||
|
||||
async function toggleLike(track) {
|
||||
try {
|
||||
if (track._isLiked) {
|
||||
await api.delete(`/tracks/${track._id}/like`)
|
||||
track._isLiked = false
|
||||
} else {
|
||||
await api.post(`/tracks/${track._id}/like`)
|
||||
track._isLiked = true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await authStore.toggleLike(track._id)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -122,18 +130,12 @@ onMounted(async () => {
|
||||
try {
|
||||
if (id) {
|
||||
const { data } = await api.get(`/playlists/${id}`)
|
||||
if (data.success) {
|
||||
playlist.value = data.data
|
||||
playlist.value.tracks?.forEach((t) => (t._isLiked = false))
|
||||
}
|
||||
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
|
||||
playlist.value.tracks?.forEach((t) => (t._isLiked = false))
|
||||
}
|
||||
if (detail.success) playlist.value = detail.data
|
||||
} else {
|
||||
playlist.value = { name: 'My Playlist', tracks: [] }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user