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:
byntangxyz committed 2026-05-26 12:37:41 +07:00
1 parent 9fdd78e737
commit b2f3e9661e
14 files changed
+433 -269

No files matched your search

+1
View File
@@ -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
-9
View File
@@ -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
+9
View File
@@ -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
-1
View File
@@ -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
+2 -2
View File
@@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Noctune - Music That Hits Different</title> <title>Noctune - Music That Hits Different</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-BO7gh51n.js"></script> <script type="module" crossorigin src="/assets/index-iVZX2_zV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-mUm8ch4i.css"> <link rel="stylesheet" crossorigin href="/assets/index-3bN2l3Ol.css">
</head> </head>
<body class="bg-noctune-cream"> <body class="bg-noctune-cream">
<div id="app"></div> <div id="app"></div>
+51 -31
View File
@@ -1,10 +1,15 @@
<template> <template>
<div class="min-h-screen bg-noctune-cream"> <div class="min-h-screen bg-noctune-cream">
<div v-if="!isAuthPage" class="flex flex-col min-h-screen"> <div v-if="!isAuthPage">
<AppHeader /> <AppHeader class="sticky top-0 z-30" />
<div class="flex flex-1"> <div class="flex">
<Sidebar /> <Sidebar
<main class="flex-1 overflow-auto" :class="{ 'pb-20': hasPlayer }"> 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 /> <router-view />
</main> </main>
</div> </div>
@@ -12,13 +17,20 @@
</div> </div>
<router-view v-else /> <router-view v-else />
<div class="fixed top-4 right-4 z-[100] space-y-2"> <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="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)" :class="toastClass(toast.type)"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>{{ toast.message }}</span> <span>{{ toast.message }}</span>
<button @click="uiStore.dismissToast(toast.id)" class="ml-2 opacity-60 hover:opacity-100">&times;</button> <button
@click="uiStore.dismissToast(toast.id)"
class="ml-2 opacity-60 hover:opacity-100"
>
&times;
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -26,37 +38,39 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue';
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router';
import { useAuthStore } from './stores/authStore.js' import { useAuthStore } from './stores/authStore.js';
import { useUiStore } from './stores/uiStore.js' import { useUiStore } from './stores/uiStore.js';
import { usePlayerStore } from './stores/playerStore.js' import { usePlayerStore } from './stores/playerStore.js';
import AppHeader from './components/AppHeader.vue' import AppHeader from './components/AppHeader.vue';
import Sidebar from './components/Sidebar.vue' import Sidebar from './components/Sidebar.vue';
import PlayerBar from './components/PlayerBar.vue' import PlayerBar from './components/PlayerBar.vue';
const route = useRoute() const route = useRoute();
const authStore = useAuthStore() const authStore = useAuthStore();
const uiStore = useUiStore() const uiStore = useUiStore();
const playerStore = usePlayerStore() const playerStore = usePlayerStore();
const isAuthPage = computed(() => { 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) { function toastClass(type) {
return { return (
info: 'bg-noctune-teal text-black', {
success: 'bg-green-400 text-black', info: 'bg-noctune-teal text-black',
error: 'bg-red-400 text-white', success: 'bg-green-400 text-black',
}[type] || 'bg-noctune-yellow text-black' error: 'bg-red-400 text-white',
}[type] || 'bg-noctune-yellow text-black'
);
} }
onMounted(async () => { onMounted(async () => {
await authStore.checkSession() await authStore.checkSession();
}) });
</script> </script>
<style> <style>
@@ -64,7 +78,13 @@ onMounted(async () => {
animation: slideIn 0.3s ease-out; animation: slideIn 0.3s ease-out;
} }
@keyframes slideIn { @keyframes slideIn {
from { opacity: 0; transform: translateX(100%); } from {
to { opacity: 1; transform: translateX(0); } opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
} }
</style> </style>
+16 -25
View File
@@ -42,8 +42,8 @@
</div> </div>
<div class="w-80 flex items-center justify-end gap-3"> <div class="w-80 flex items-center justify-end gap-3">
<button @click="toggleLike" class="text-gray-400 hover:text-red-400 transition-colors"> <button @click="toggleLike" class="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> <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> </button>
<div class="flex items-center gap-2"> <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> <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 { ref, computed, watch, onMounted } from 'vue'
import { usePlayerStore } from '../stores/playerStore.js' import { usePlayerStore } from '../stores/playerStore.js'
import { useQueueStore } from '../stores/queueStore.js' import { useQueueStore } from '../stores/queueStore.js'
import api from '../lib/api.js' import { useAuthStore } from '../stores/authStore.js'
const playerStore = usePlayerStore() const playerStore = usePlayerStore()
const queueStore = useQueueStore() const queueStore = useQueueStore()
const authStore = useAuthStore()
const isLiked = ref(false) const isLiked = ref(false)
function onTrackEnd() { 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() { async function toggleLike() {
if (!playerStore.currentTrack) return if (!playerStore.currentTrack) return
const id = playerStore.currentTrack._id || playerStore.currentTrack.id const id = playerStore.currentTrack._id || playerStore.currentTrack.id
try { await authStore.toggleLike(id)
if (isLiked.value) {
await api.delete(`/tracks/${id}/like`)
} else {
await api.post(`/tracks/${id}/like`)
}
isLiked.value = !isLiked.value
} catch {
// ignore
}
} }
// 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> </script>
<style scoped> <style scoped>
+36 -27
View File
@@ -1,5 +1,5 @@
<template> <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"> <nav class="p-4 space-y-2">
<router-link to="/home" <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" 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> <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> <span class="tracking-wide">HOME</span>
</router-link> </router-link>
<router-link to="/library" <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="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') }"> :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> <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> <span class="tracking-wide">LIBRARY</span>
</router-link> </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" <div class="relative group">
:class="{ 'bg-noctune-teal text-black': isActive('/playlist') }"> <router-link to="/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> class="flex items-center gap-3 px-4 py-3 rounded-lg font-bold text-black hover:bg-noctune-teal/20 transition-colors"
<span class="tracking-wide">PLAYLIST</span> :class="{ 'bg-noctune-teal text-black': isActive('/playlist') || isActivePrefix('/playlist/') }">
</router-link> <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" <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="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') }"> :class="{ 'bg-noctune-orange text-black': isActive('/admin/tracks') }">
@@ -36,18 +61,7 @@
</button> </button>
</div> </div>
<div class="flex-1 overflow-y-auto px-4 mt-4"> <div class="flex-1"></div>
<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>
</aside> </aside>
</template> </template>
@@ -55,26 +69,23 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/authStore.js' import { useAuthStore } from '../stores/authStore.js'
import { useQueueStore } from '../stores/queueStore.js'
import api from '../lib/api.js' import api from '../lib/api.js'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
const queueStore = useQueueStore()
const playlists = ref([]) const playlists = ref([])
const isActive = (path) => route.path === path const isActive = (path) => route.path === path
const isActivePrefix = (prefix) => route.path.startsWith(prefix)
async function fetchPlaylists() { async function fetchPlaylists() {
if (!authStore.isLoggedIn) return if (!authStore.isLoggedIn) return
try { try {
const { data } = await api.get('/playlists') const { data } = await api.get('/playlists')
if (data.success) playlists.value = data.data if (data.success) playlists.value = data.data
} catch { } catch {}
// ignore
}
} }
async function openCreatePlaylist() { async function openCreatePlaylist() {
@@ -85,9 +96,7 @@ async function openCreatePlaylist() {
if (data.success) { if (data.success) {
playlists.value.push(data.data) playlists.value.push(data.data)
} }
} catch { } catch {}
// ignore
}
} }
onMounted(fetchPlaylists) onMounted(fetchPlaylists)
+16 -9
View File
@@ -1,10 +1,17 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import { createPinia } from 'pinia' import { createPinia } from 'pinia';
import App from './App.vue' import App from './App.vue';
import router from './router/index.js' import router from './router/index.js';
import './style.css' import './style.css';
import { useAuthStore } from './stores/authStore.js';
const app = createApp(App) const app = createApp(App);
app.use(createPinia()) const pinia = createPinia();
app.use(router) app.use(pinia);
app.mount('#app') 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
View File
@@ -9,6 +9,7 @@ export const useAuthStore = defineStore('auth', () => {
const isLoggedIn = computed(() => !!user.value) const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin') const isAdmin = computed(() => user.value?.role === 'admin')
const isArtist = computed(() => user.value?.role === 'artist') const isArtist = computed(() => user.value?.role === 'artist')
const likedTrackIds = computed(() => new Set(user.value?.likedTracks || []))
async function checkSession() { async function checkSession() {
try { 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
View File
@@ -1,49 +1,122 @@
<template> <template>
<div class="min-h-screen bg-noctune-cream flex flex-col"> <div class="min-h-screen bg-noctune-cream flex flex-col">
<AppHeader />
<div class="flex flex-1"> <div class="flex flex-1">
<Sidebar />
<main class="flex-1 p-8 overflow-auto"> <main class="flex-1 p-8 overflow-auto">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<h1 class="text-4xl font-black text-black mb-2">Upload Track</h1> <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 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="error" class="text-red-500 text-sm mb-4 font-medium">
<p v-if="success" class="text-green-600 text-sm mb-4 font-medium">{{ success }}</p> {{ 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 class="bg-white rounded-2xl border-4 border-black p-8 space-y-6">
<div> <div>
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">TITLE</label> <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" /> 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>
<div> <div>
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">ARTIST</label> <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" /> 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>
<div> <div>
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">ALBUM</label> <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" /> 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>
<div> <div>
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">GENRE</label> <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" /> 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>
<div> <div>
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">COVER IMAGE</label> <label
<div class="border-2 border-dashed border-black rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50" @click="coverInput.click()"> class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
<img v-if="coverPreview" :src="coverPreview" class="w-32 h-32 object-cover mx-auto mb-2 rounded-lg border-2 border-black" /> >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> <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> </div>
<div> <div>
<label class="block text-xs font-bold text-gray-600 mb-2 tracking-wider">AUDIO FILE</label> <label
<div class="border-2 border-dashed border-black rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50" @click="audioInput.click()"> class="block text-xs font-bold text-gray-600 mb-2 tracking-wider"
<p v-if="audioFile" class="text-black font-medium">{{ audioFile.name }}</p> >AUDIO FILE</label
<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
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>
</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"> <button
<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> @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' }} {{ uploading ? 'Uploading...' : 'UPLOAD TRACK' }}
</button> </button>
</div> </div>
@@ -54,79 +127,86 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue';
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router';
import AppHeader from '../components/AppHeader.vue' import AppHeader from '../components/AppHeader.vue';
import Sidebar from '../components/Sidebar.vue' import Sidebar from '../components/Sidebar.vue';
import api from '../lib/api.js' import api from '../lib/api.js';
const router = useRouter() const router = useRouter();
const title = ref('') const title = ref('');
const artist = ref('') const artist = ref('');
const album = ref('') const album = ref('');
const genre = ref('') const genre = ref('');
const coverFile = ref(null) const coverFile = ref(null);
const coverPreview = ref('') const coverPreview = ref('');
const audioFile = ref(null) const audioFile = ref(null);
const uploading = ref(false) const uploading = ref(false);
const error = ref('') const error = ref('');
const success = ref('') const success = ref('');
const coverInput = ref(null) const coverInput = ref(null);
const audioInput = ref(null) const audioInput = ref(null);
function onCoverChange(e) { function onCoverChange(e) {
const file = e.target.files[0] const file = e.target.files[0];
if (!file) return if (!file) return;
coverFile.value = file coverFile.value = file;
coverPreview.value = URL.createObjectURL(file) coverPreview.value = URL.createObjectURL(file);
} }
function onAudioChange(e) { function onAudioChange(e) {
const file = e.target.files[0] const file = e.target.files[0];
if (!file) return if (!file) return;
audioFile.value = file audioFile.value = file;
} }
async function handleUpload() { async function handleUpload() {
error.value = '' error.value = '';
success.value = '' success.value = '';
if (!title.value || !artist.value || !album.value || !genre.value || !coverFile.value || !audioFile.value) { if (
error.value = 'All fields are required' !title.value ||
return !artist.value ||
!album.value ||
!genre.value ||
!coverFile.value ||
!audioFile.value
) {
error.value = 'All fields are required';
return;
} }
uploading.value = true uploading.value = true;
try { try {
const formData = new FormData() const formData = new FormData();
formData.append('title', title.value) formData.append('title', title.value);
formData.append('artist', artist.value) formData.append('artist', artist.value);
formData.append('album', album.value) formData.append('album', album.value);
formData.append('genre', genre.value) formData.append('genre', genre.value);
formData.append('cover', coverFile.value) formData.append('cover', coverFile.value);
formData.append('audio', audioFile.value) formData.append('audio', audioFile.value);
const { data } = await api.post('/admin/tracks', formData, { const { data } = await api.post('/admin/tracks', formData, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
}) });
if (data.success) { if (data.success) {
success.value = 'Track uploaded successfully!' success.value = 'Track uploaded successfully!';
title.value = '' title.value = '';
artist.value = '' artist.value = '';
album.value = '' album.value = '';
genre.value = '' genre.value = '';
coverFile.value = null coverFile.value = null;
coverPreview.value = '' coverPreview.value = '';
audioFile.value = null audioFile.value = null;
setTimeout(() => router.push('/home'), 1500) setTimeout(() => router.push('/home'), 1500);
} }
} catch (err) { } catch (err) {
error.value = err.response?.data?.message || 'Upload failed' error.value = err.response?.data?.message || 'Upload failed';
} finally { } finally {
uploading.value = false uploading.value = false;
} }
} }
</script> </script>
+62 -37
View File
@@ -1,50 +1,57 @@
<template> <template>
<div class="min-h-screen bg-noctune-cream flex flex-col"> <div class="p-8">
<div class="p-8"> <div class="flex items-start gap-8 mb-12">
<div class="flex items-start gap-8 mb-12"> <div class="flex-1">
<div class="flex-1"> <h1 class="text-5xl font-black text-black leading-tight mb-2">MUSIC<br/>THAT HITS</h1>
<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">
<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>
<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> </div>
<div class="w-80 h-80 rounded-2xl overflow-hidden border-4 border-black flex-shrink-0"> <p class="text-gray-600 mb-6">You Vibe. Your sound. Only on Noctune.</p>
<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 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> </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> <section>
<h2 class="text-2xl font-black text-black mb-6 flex items-center"> <h2 class="text-2xl font-black text-black mb-6 flex items-center">
TRENDING NOW TRENDING NOW
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg> <svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</h2> </h2>
<div v-if="loading" class="text-center py-12 text-gray-500">Loading tracks...</div> <div v-if="loading" class="text-center py-12 text-gray-500">Loading tracks...</div>
<div v-else class="grid grid-cols-4 gap-4"> <div v-else class="grid grid-cols-4 gap-4">
<div v-for="track in tracks" :key="track._id" <div v-for="track in tracks" :key="track._id"
@click="playTrack(track)" @click="playTrack(track)"
class="bg-white rounded-2xl border-2 border-black overflow-hidden hover:scale-[1.02] transition-transform cursor-pointer group" class="bg-white rounded-2xl 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"> <div class="aspect-square bg-gray-300 relative overflow-hidden">
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" /> <img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center"> <div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
<div class="w-12 h-12 bg-noctune-yellow rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"> <div class="w-12 h-12 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
<svg class="w-6 h-6 ml-1 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg> :class="isCurrentTrack(track) ? 'bg-white' : 'bg-noctune-yellow'">
</div> <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> </div>
<div class="p-3"> </div>
<p class="text-sm font-bold text-black truncate">{{ track.title }}</p> <div class="p-3">
<p class="text-xs text-gray-500 truncate">{{ track.artist }}</p> <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> </div>
</div> </div>
</section> </div>
</div> </section>
</div> </div>
</template> </template>
@@ -52,10 +59,12 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { usePlayerStore } from '../stores/playerStore.js' import { usePlayerStore } from '../stores/playerStore.js'
import { useQueueStore } from '../stores/queueStore.js' import { useQueueStore } from '../stores/queueStore.js'
import { useAuthStore } from '../stores/authStore.js'
import api from '../lib/api.js' import api from '../lib/api.js'
const playerStore = usePlayerStore() const playerStore = usePlayerStore()
const queueStore = useQueueStore() const queueStore = useQueueStore()
const authStore = useAuthStore()
const tracks = ref([]) const tracks = ref([])
const loading = ref(true) const loading = ref(true)
@@ -65,7 +74,19 @@ function formatUrl(url) {
return url.startsWith('http') ? url : `http://localhost:3001${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) { function playTrack(track) {
if (isCurrentTrack(track)) {
playerStore.togglePlay()
return
}
const idx = tracks.value.findIndex((t) => t._id === track._id) const idx = tracks.value.findIndex((t) => t._id === track._id)
queueStore.setQueue(tracks.value, idx) queueStore.setQueue(tracks.value, idx)
playerStore.setTrack(track) playerStore.setTrack(track)
@@ -76,6 +97,10 @@ function playFirstTrack() {
if (tracks.value.length) playTrack(tracks.value[0]) if (tracks.value.length) playTrack(tracks.value[0])
} }
async function toggleLike(track) {
await authStore.toggleLike(track._id)
}
onMounted(async () => { onMounted(async () => {
try { try {
const { data } = await api.get('/tracks') const { data } = await api.get('/tracks')
+37 -30
View File
@@ -9,13 +9,15 @@
</div> </div>
<div v-else class="grid grid-cols-4 gap-6"> <div v-else class="grid grid-cols-4 gap-6">
<div v-for="track in likedTracks" :key="track._id" <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" 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"> <div class="aspect-square bg-gray-300 relative overflow-hidden">
<img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" /> <img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
<div class="absolute bottom-2 right-2 w-10 h-10 bg-noctune-yellow rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"> <div class="absolute bottom-2 right-2 w-10 h-10 rounded-full flex items-center justify-center border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity"
<svg class="w-5 h-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg> :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> </div>
<div class="p-3"> <div class="p-3">
@@ -41,20 +43,23 @@
</div> </div>
<div class="divide-y divide-gray-200"> <div class="divide-y divide-gray-200">
<div v-for="(track, index) in allTracks" :key="track._id" <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" 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"> <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"> <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" /> <img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
</div> </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> </div>
<span class="w-40 text-center text-gray-600 text-sm truncate">{{ track.artist }}</span> <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> <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"> <button @click.stop="toggleLike(track)" class="w-12 flex justify-center 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> <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> </button>
</div> </div>
</div> </div>
@@ -67,10 +72,12 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { usePlayerStore } from '../stores/playerStore.js' import { usePlayerStore } from '../stores/playerStore.js'
import { useQueueStore } from '../stores/queueStore.js' import { useQueueStore } from '../stores/queueStore.js'
import { useAuthStore } from '../stores/authStore.js'
import api from '../lib/api.js' import api from '../lib/api.js'
const playerStore = usePlayerStore() const playerStore = usePlayerStore()
const queueStore = useQueueStore() const queueStore = useQueueStore()
const authStore = useAuthStore()
const likedTracks = ref([]) const likedTracks = ref([])
const allTracks = ref([]) const allTracks = ref([])
@@ -82,26 +89,31 @@ function formatUrl(url) {
return url.startsWith('http') ? url : `http://localhost:3001${url}` return url.startsWith('http') ? url : `http://localhost:3001${url}`
} }
function playTrack(track) { function isCurrentTrack(track) {
const tracks = allTracks.value.length ? allTracks.value : likedTracks.value return playerStore.currentTrack?._id === track._id
const idx = tracks.findIndex((t) => t._id === track._id) }
queueStore.setQueue(tracks, idx >= 0 ? idx : 0)
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.setTrack(track)
playerStore.play() playerStore.play()
} }
async function toggleLike(track) { async function toggleLike(track) {
try { const wasLiked = isLiked(track)
if (track._isLiked) { await authStore.toggleLike(track._id)
await api.delete(`/tracks/${track._id}/like`) if (wasLiked) {
track._isLiked = false likedTracks.value = likedTracks.value.filter((t) => t._id !== track._id)
likedTracks.value = likedTracks.value.filter((t) => t._id !== track._id)
} else {
await api.post(`/tracks/${track._id}/like`)
track._isLiked = true
}
} catch {
// ignore
} }
} }
@@ -109,8 +121,7 @@ onMounted(async () => {
try { try {
const { data: liked } = await api.get('/tracks/liked') const { data: liked } = await api.get('/tracks/liked')
if (liked.success) { if (liked.success) {
likedTracks.value = liked.data.likedTracks || [] likedTracks.value = liked.data || []
likedTracks.value.forEach((t) => (t._isLiked = true))
} }
} catch { } catch {
// ignore // ignore
@@ -120,11 +131,7 @@ onMounted(async () => {
try { try {
const { data: all } = await api.get('/tracks') const { data: all } = await api.get('/tracks')
if (all.success) { if (all.success) allTracks.value = all.data
allTracks.value = all.data
const likedIds = new Set(likedTracks.value.map((t) => t._id))
allTracks.value.forEach((t) => (t._isLiked = likedIds.has(t._id)))
}
} catch { } catch {
// ignore // ignore
} finally { } finally {
+26 -24
View File
@@ -52,16 +52,19 @@
@click="playTrack(index)" @click="playTrack(index)"
class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer" class="px-6 py-3 flex items-center hover:bg-gray-50 transition-colors cursor-pointer"
> >
<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="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"> <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" /> <img v-if="track.coverUrl" :src="formatUrl(track.coverUrl)" class="w-full h-full object-cover" />
</div> </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> </div>
<span class="w-48 text-center text-gray-600 text-sm truncate">{{ track.artist }}</span> <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"> <button @click.stop="toggleLike(track)" class="w-12 flex justify-center 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> <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> </button>
</div> </div>
<div v-if="!playlist.tracks?.length" class="p-8 text-center text-gray-500">No tracks in this playlist yet</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 { useRoute } from 'vue-router'
import { usePlayerStore } from '../stores/playerStore.js' import { usePlayerStore } from '../stores/playerStore.js'
import { useQueueStore } from '../stores/queueStore.js' import { useQueueStore } from '../stores/queueStore.js'
import { useAuthStore } from '../stores/authStore.js'
import api from '../lib/api.js' import api from '../lib/api.js'
const route = useRoute() const route = useRoute()
const playerStore = usePlayerStore() const playerStore = usePlayerStore()
const queueStore = useQueueStore() const queueStore = useQueueStore()
const authStore = useAuthStore()
const playlist = ref(null) const playlist = ref(null)
const loading = ref(true) const loading = ref(true)
@@ -91,11 +96,24 @@ function formatUrl(url) {
return url.startsWith('http') ? url : `http://localhost:3001${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) { function playTrack(index) {
if (!playlist.value?.tracks?.length) return if (!playlist.value?.tracks?.length) return
const track = playlist.value.tracks[index]
if (isCurrentTrack(track)) {
playerStore.togglePlay()
return
}
const tracks = playlist.value.tracks const tracks = playlist.value.tracks
queueStore.setQueue(tracks, index) queueStore.setQueue(tracks, index)
playerStore.setTrack(tracks[index]) playerStore.setTrack(track)
playerStore.play() playerStore.play()
} }
@@ -104,17 +122,7 @@ function playAll() {
} }
async function toggleLike(track) { async function toggleLike(track) {
try { await authStore.toggleLike(track._id)
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
}
} }
onMounted(async () => { onMounted(async () => {
@@ -122,18 +130,12 @@ onMounted(async () => {
try { try {
if (id) { if (id) {
const { data } = await api.get(`/playlists/${id}`) const { data } = await api.get(`/playlists/${id}`)
if (data.success) { if (data.success) playlist.value = data.data
playlist.value = data.data
playlist.value.tracks?.forEach((t) => (t._isLiked = false))
}
} else { } else {
const { data } = await api.get('/playlists') const { data } = await api.get('/playlists')
if (data.success && data.data.length) { if (data.success && data.data.length) {
const { data: detail } = await api.get(`/playlists/${data.data[0]._id}`) const { data: detail } = await api.get(`/playlists/${data.data[0]._id}`)
if (detail.success) { if (detail.success) playlist.value = detail.data
playlist.value = detail.data
playlist.value.tracks?.forEach((t) => (t._isLiked = false))
}
} else { } else {
playlist.value = { name: 'My Playlist', tracks: [] } playlist.value = { name: 'My Playlist', tracks: [] }
} }