This commit is contained in:
simva committed 2026-10-23 12:05:26 +09:00
commit d9ef64eafe
35 files changed
+4733

No files matched your search

+1
View File
@@ -0,0 +1 @@
@layer vuetify-components{.v-app-bar{display:flex}.v-app-bar.v-toolbar{background:rgb(var(--v-theme-surface));color:color-mix(in srgb,rgb(var(--v-theme-on-surface)) calc(var(--v-high-emphasis-opacity) * 100%),transparent)}.v-app-bar.v-toolbar:not(.v-toolbar--flat){box-shadow:0 1px 2px rgba(var(--v-shadow-color),var(--v-shadow-key-opacity, .3)),0 2px 6px 2px rgba(var(--v-shadow-color),var(--v-shadow-ambient-opacity, .15));--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 4%, transparent)}.v-app-bar:not(.v-toolbar--absolute){padding-inline-end:var(--v-scrollbar-offset)}}@layer vuetify-components{.v-toolbar{align-items:flex-start;display:flex;flex:none;flex-direction:column;justify-content:space-between;max-width:100%;position:relative;transition:.2s cubic-bezier(.4,0,.2,1);transition-property:height,width,transform,max-width,left,right,top,bottom,box-shadow;width:100%}@media(prefers-reduced-motion:reduce){.v-toolbar{transition-property:box-shadow}}.v-toolbar{border-color:rgba(var(--v-border-color),var(--v-border-opacity));border-style:solid;border-width:0}.v-toolbar--border{border-width:thin;box-shadow:none}.v-toolbar{box-shadow:0 0 rgba(var(--v-shadow-color),var(--v-shadow-key-opacity, .3)),0 0 rgba(var(--v-shadow-color),var(--v-shadow-ambient-opacity, .15));--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 0%, transparent)}.v-toolbar{border-radius:0}.v-toolbar{background:rgb(var(--v-theme-surface-light));color:color-mix(in srgb,rgb(var(--v-theme-on-surface-light)) calc(var(--v-high-emphasis-opacity) * 100%),transparent)}.v-toolbar--absolute{position:absolute}.v-toolbar--collapse{max-width:112px;overflow:hidden}.v-toolbar--collapse-end{margin-inline-start:auto}.v-toolbar--collapse.v-toolbar--collapse-start{border-end-end-radius:24px}.v-toolbar--collapse.v-toolbar--collapse-end{border-end-start-radius:24px}.v-toolbar--collapse .v-toolbar-title{display:none}.v-toolbar--flat{box-shadow:0 0 rgba(var(--v-shadow-color),var(--v-shadow-key-opacity, .3)),0 0 rgba(var(--v-shadow-color),var(--v-shadow-ambient-opacity, .15));--v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 0%, transparent)}.v-toolbar--floating{display:inline-flex;width:auto}.v-toolbar--rounded{border-radius:4px}.v-toolbar__content,.v-toolbar__extension{align-items:center;display:flex;flex:0 0 auto;position:relative;transition:inherit;width:100%}.v-toolbar__content{overflow:hidden}.v-toolbar__content>.v-btn:first-child{margin-inline-start:4px}.v-toolbar__content>.v-btn:last-child{margin-inline-end:4px}.v-toolbar__content>.v-toolbar-title{margin-inline-start:20px}.v-toolbar--density-prominent .v-toolbar__content{align-items:flex-start}.v-toolbar__image{display:flex;opacity:var(--v-toolbar-image-opacity, 1);transition-property:opacity}.v-toolbar__image{position:absolute;top:0;left:0;width:100%;height:100%}.v-toolbar__prepend,.v-toolbar__append{align-items:center;align-self:stretch;display:flex}.v-toolbar__prepend{margin-inline:4px auto}.v-toolbar__append{margin-inline:auto 4px}.v-toolbar-title{flex:1 1;font-size:1.25rem;min-width:0}.v-toolbar-title{font-size:1.25rem;font-weight:400;letter-spacing:0;line-height:1.75rem;text-transform:none}.v-toolbar--density-prominent .v-toolbar-title{align-self:flex-end;padding-bottom:6px}.v-toolbar--density-prominent .v-toolbar-title{font-size:1.5rem;font-weight:400;letter-spacing:0;line-height:2.25rem;text-transform:none}.v-toolbar-title__placeholder{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.v-toolbar-items{display:flex;height:inherit;align-self:stretch}.v-toolbar-items>.v-btn{border-radius:0}}@layer vuetify-components{.v-img{--v-theme-overlay-multiplier: 3;z-index:0}.v-img.v-img--absolute{height:100%;left:0;overflow:hidden;position:absolute;top:0;width:100%;z-index:-1}.v-img.v-img--fit-content{max-width:fit-content}.v-img.v-img--fit-content>.v-img__img{position:relative}.v-img.v-img--fit-content>.v-responsive__sizer{display:none}.v-img--booting .v-responsive__sizer{transition:none}.v-img--rounded{border-radius:4px}.v-img__img,.v-img__picture,.v-img__gradient,.v-img__placeholder,.v-img__error{z-index:-1}.v-img__img,.v-img__picture,.v-img__gradient,.v-img__placeholder,.v-img__error{position:absolute;top:0;left:0;width:100%;height:100%}.v-img__img--preload{filter:blur(4px)}.v-img__img--contain{object-fit:contain}.v-img__img--cover{object-fit:cover}.v-img__gradient{background-repeat:no-repeat}}@layer vuetify-components{.v-responsive{display:flex;flex:1 0 auto;max-height:100%;max-width:100%;overflow:hidden;position:relative}.v-responsive--inline{display:inline-flex;flex:0 0 auto}.v-responsive__content{flex:1 0 0px;max-width:100%}.v-responsive__sizer~.v-responsive__content{margin-inline-start:-100%}.v-responsive__sizer{flex:1 0 0px;transition:padding-bottom .2s cubic-bezier(.4,0,.2,1);pointer-events:none}}@layer vuetify-components{.v-btn{align-items:center;border-radius:4px;display:inline-flex;flex-direction:row;font-weight:500;justiLine truncated
+44
View File
@@ -0,0 +1,44 @@
var Np=Object.defineProperty;var Am=e=>{throw TypeError(e)};var Rp=(e,t,n)=>t in e?Np(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var Ln=(e,t,n)=>Rp(e,typeof t!="symbol"?t+"":t,n),Tm=(e,t,n)=>t.has(e)||Am("Cannot "+n);var Ta=(e,t,n)=>(Tm(e,t,"read from private field"),n?n.call(e):t.get(e)),Vc=(e,t,n)=>t.has(e)?Am("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),Ic=(e,t,n,a)=>(Tm(e,t,"write to private field"),a?a.call(e,n):t.set(e,n),n);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const r of i.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function a(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();/**
* @vue/shared v3.5.44
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/function bs(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const mt={},ti=[],fa=()=>{},uy=()=>!1,Wo=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ws=e=>e.startsWith("onUpdate:"),_t=Object.assign,Tf=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Hp=Object.prototype.hasOwnProperty,kt=(e,t)=>Hp.call(e,t),Re=Array.isArray,Al=e=>br(e)==="[object Map]",La=e=>br(e)==="[object Set]",Dm=e=>br(e)==="[object Date]",zp=e=>br(e)==="[object RegExp]",Qe=e=>typeof e=="function",At=e=>typeof e=="string",Jn=e=>typeof e=="symbol",Vt=e=>e!==null&&typeof e=="object",Df=e=>(Vt(e)||Qe(e))&&Qe(e.then)&&Qe(e.catch),sy=Object.prototype.toString,br=e=>sy.call(e),Wp=e=>br(e).slice(8,-1),Ss=e=>br(e)==="[object Object]",ks=e=>At(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ai=bs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ps=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Up=/-\w/g,nn=ps(e=>e.replace(Up,t=>t.slice(1).toUpperCase())),jp=/\B([A-Z])/g,Hn=ps(e=>e.replace(jp,"-$1").toLowerCase()),yl=ps(e=>e.charAt(0).toUpperCase()+e.slice(1)),qr=ps(e=>e?`on${yl(e)}`:""),gn=(e,t)=>!Object.is(e,t),lr=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},cy=(e,t,n,a=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:a,value:n})},xs=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Fu=e=>{const t=At(e)?Number(e):NaN;return isNaN(t)?e:t};let Em;const Cs=()=>Em||(Em=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}),Kp="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",Gp=bs(Kp);function Se(e){if(Re(e)){const t={};for(let n=0;n<e.length;n++){const a=e[n],l=At(a)?Zp(a):Se(a);if(l)for(const i in l)t[i]=l[i]}return t}else if(At(e)||Vt(e))return e}const Yp=/;(?![^(]*\))/g,qp=/:([^]+)/,Xp=/"(?:[^"\\]|\\[^])*"|'(?:[^'\\]|\\[^])*'|\\[^]|\/\*[^]*?\*\//g;function Zp(e){const t={};return e.replace(Xp,n=>n.startsWith("/*")?"":n).split(Yp).forEach(n=>{if(n){const a=n.split(qp);a.length>1&&(t[a[0].trim()]=a[1].trim())}}),t}function fe(e){let t="";if(At(e))t=e;else if(Re(e))for(let n=0;n<e.length;n++){const a=fe(e[n]);a&&(t+=a+" ")}else if(Vt(e))for(const n in e)e[n]&&(t+=n+" ");return t.trim()}function _s(e){if(!e)return null;let{class:t,style:n}=e;return t&&!At(t)&&(e.class=fe(t)),n&&(e.style=Se(n)),e}const Jp="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,playsinline,readonly",Qp=bs(Jp);function dy(e){return!!e||e===""}function ex(e,t,n){if(e.length!==t.length)return!1;let a=!0;for(let l=0;a&&l<e.length;l++)a=va(e[l],t[l],n);return a}function Mm(e,t,n){if(e.size!==t.size)return!1;const a=Array.from(t),l=new Uint8Array(a.length);for(const i of e){let r=-1;for(let o=0;o<a.length;o++)if(!l[o]&&va(i,a[o],n)){r=o;break}if(r<0)return!1;l[r]=1}return!0}function tx(e,t,n){let a=Al(e),l=Al(t);if(a||l||(a=La(e),l=La(t),a||l))return a&&l?Mm(e,t,n):!1;const i=Object.keys(e).length,r=Object.keys(t).length;if(i!==r)return!1;for(const o in e){const u=e.hasOwnProperty(o),d=t.hasOwnProperty(o);if(u&&!d||!u&&d||!va(e[o],t[o],n))return!1}return String(e)===String(t)}function Bm(e,t,n,a){n||(n=[new Map,new Map]);const[l,i]=n;if(l.has(e)||i.has(t))return l.get(e)===t&&i.get(t)===e;l.set(e,t),i.set(t,e);const r=a(e,t,n);return l.delete(e),i.delete(t),r}function va(e,t,n){if(e===t)return!0;let a=Dm(e),l=Dm(t);return a||l?a&&l?e.getTime()===t.getTime():!1:(a=Jn(e),l=Jn(t),a||l?e===t:(a=Re(e),l=Re(t),a||l?a&&l?Bm(e,t,n,ex):!1:(a=Vt(e),l=Vt(t),a||l?!a||!l?!1:Bm(e,t,n,tx):String(e)===String(t))))}function Vs(e,t){return e.findIndex(n=>va(n,t))}const fy=e=>!!(e&&e.__v_isRef===!0),it=e=>At(e)?e:e==null?"":Re(e)||Vt(e)&&(e.toString===sy||!Qe(e.toString))?fy(e)?it(e.value):JSON.stringify(e,vy,2):String(e),vy=(e,t)=>fy(t)?vy(e,t.value):Al(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[a,l],i)=>(n[Pc(a,i)+" =>"]=l,n),{})}:La(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Pc(n))}:Jn(t)?Pc(t):Vt(t)&&!Re(t)&&!Ss(t)?String(t):t,Pc=(e,t="")=>{var n;return Jn(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};function nx(e){return e==null?"initial":typeof e=="string"?e===""?" ":e:String(e)}/**
* @vue/reactivity v3.5.44
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/let dn;class Ef{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&dn&&(dn.active?(this.parent=dn,this.index=(dn.scopes||(dn.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes){const a=this.scopes.slice();for(t=0,n=a.length;t<n;t++)a[t].pause()}for(t=0,n=this.effects.length;t<n;t++)this.effects[t].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let t,n;if(this.scopes){const l=this.scopes.slice();for(t=0,n=l.length;t<n;t++)l[t].resume()}const a=this.effects.slice();for(t=0,n=a.length;t<n;t++)a[t].resume()}}run(t){if(this._active){const n=dn;try{return dn=this,t()}finally{dn=n}}}on(){++this._on===1&&(this.prevScope=dn,dn=this)}off(){if(this._on>0&&--this._on===0){if(dn===this)dn=this.prevScope;else{let t=dn;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,a;for(n=0,a=this.effects.length;n<a;n++)this.effects[n].stop();for(this.effects.length=0,n=0,a=this.cleanups.length;n<a;n++)this.cleanups[n]();if(this.cleanups.length=0,this.scopes){const l=this.scopes.slice();for(n=0,a=l.length;n<a;n++)l[n].stop(!0);this.scopes.length=0}if(!this.detached&&this.parent&&!t){const l=this.parent.scopes.pop();l&&l!==this&&(this.parent.scopes[this.index]=l,l.index=this.index)}this.parent=void 0}}}function Fl(e){return new Ef(e)}function Mf(){return dn}function pt(e,t=!1){dn&&dn.cleanups.push(e)}let Nt;const Ac=new WeakSet;class oo{constructor(t){this.fn=t,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,dn&&(dn.active?dn.effects.push(this):this.flags&=-2)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,Ac.has(this)&&(Ac.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||hy(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,$m(this),gy(this);const t=Nt,n=Sa;Nt=this,Sa=!0;try{return this.fn()}finally{yy(this),Nt=t,Sa=n,this.flags&=-3}}stop(){if(this.flags&1){for(let t=this.deps;t;t=t.nextDep)Ff(t);this.deps=this.depsTail=void 0,$m(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?Ac.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){ud(this)&&this.run()}get dirty(){return ud(this)}}let my=0,Xr,Zr;function hy(e,t=!1){if(e.flags|=8,t){e.next=Zr,Zr=e;return}e.next=Xr,Xr=e}function Bf(){my++}function $f(){if(--my>0)return;if(Zr){let t=Zr;for(Zr=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Xr;){let t=Xr;for(Xr=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(a){e||(e=a)}t=n}}if(e)throw e}function gy(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function yy(e){let t,n=e.depsTail,a=n;for(;a;){const l=a.prevDep;a.version===-1?(a===n&&(n=l),Ff(a),ax(a)):t=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=l}e.deps=t,e.depsTail=n}function ud(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(by(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function by(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===uo)||(e.globalVersion=uo,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!ud(e))))return;e.flags|=2;const t=e.dep,n=Nt,a=Sa;Nt=e,Sa=!0;try{gy(e);const l=e.fn(e._value);(t.version===0||gn(l,e._value))&&(e.flags|=128,e._value=l,t.version++)}catch(l){throw t.version++,l}finally{Nt=n,Sa=a,yy(e),e.flags&=-3}}function Ff(e,t=!1){const{dep:n,prevSub:a,nextSub:l}=e;if(a&&(a.nextSub=l,e.prevSub=void 0),l&&(l.prevSub=a,e.nextSub=void 0),n.subs===e&&(n.subs=a,!a&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Ff(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function ax(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}function lx(e,t){e.effect instanceof oo&&(e=e.effect.fn);const n=new oo(e);t&&_t(n,t);try{n.run()}catch(l){throw n.stop(),l}const a=n.run.bind(n);return a.effect=n,a}function ix(e){e.effect.stop()}let Sa=!0;const wy=[];function Na(){wy.push(Sa),Sa=!1}function Ra(){const e=wy.pop();Sa=e===void 0?!0:e}function $m(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Nt;Nt=void 0;try{t()}finally{Nt=n}}}let uo=0;class rx{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Is{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Nt||!Sa||Nt===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Nt)n=this.activeLink=new rx(Nt,this),Nt.deps?(n.prevDep=Nt.depsTail,Nt.depsTail.nextDep=Line truncated
* @vue/runtime-core v3.5.44
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/const By=[];function Mx(e){By.push(e)}function Bx(){By.pop()}function $x(e,t){}const Fx={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},Ox={sp:"serverPrefetch hook",bc:"beforeCreate hook",c:"created hook",bm:"beforeMount hook",m:"mounted hook",bu:"beforeUpdate hook",u:"updated",bum:"beforeUnmount hook",um:"unmounted hook",a:"activated hook",da:"deactivated hook",ec:"errorCaptured hook",rtc:"renderTracked hook",rtg:"renderTriggered hook",0:"setup function",1:"render function",2:"watcher getter",3:"watcher callback",4:"watcher cleanup function",5:"native event handler",6:"component event handler",7:"vnode hook",8:"directive hook",9:"transition hook",10:"app errorHandler",11:"app warnHandler",12:"ref function",13:"async component loader",14:"scheduler flush",15:"component update",16:"app unmount cleanup function"};function Sr(e,t,n,a){try{return a?e(...a):e()}catch(l){Vi(l,t,n)}}function Qn(e,t,n,a){if(Qe(e)){const l=Sr(e,t,n,a);return l&&Df(l)&&l.catch(i=>{Vi(i,t,n)}),l}if(Re(e)){const l=[];for(let i=0;i<e.length;i++)l.push(Qn(e[i],t,n,a));return l}}function Vi(e,t,n,a=!0){const l=t?t.vnode:null,{errorHandler:i,throwUnhandledErrorInProduction:r}=t&&t.appContext.config||mt;if(t){let o=t.parent;const u=t.proxy,d=`https://vuejs.org/error-reference/#runtime-${n}`;for(;o;){const s=o.ec;if(s){for(let c=0;c<s.length;c++)if(s[c](e,u,d)===!1)return}o=o.parent}if(i){Na(),Sr(i,null,10,[e,u,d]),Ra();return}}Lx(e,n,l,a,r)}function Lx(e,t,n,a=!0,l=!1){if(l)throw e;console.error(e)}const Fn=[];let ba=-1;const $a=[];let Il=null,Ji=0;const $y=Promise.resolve();let Nu=null;function Ne(e){const t=Nu||$y;return e?t.then(this?e.bind(this):e):t}function Nx(e){let t=ba+1,n=Fn.length;for(;t<n;){const a=t+n>>>1,l=Fn[a],i=vo(l);i<e||i===e&&l.flags&2?t=a+1:n=a}return t}function Nf(e){if(!(e.flags&1)){const t=vo(e),n=Fn[Fn.length-1];!n||!(e.flags&2)&&t>=vo(n)?Fn.push(e):Fn.splice(Nx(t),0,e),e.flags|=1,Fy()}}function Fy(){Nu||(Nu=$y.then(Ly))}function fo(e){if(!Re(e))Il&&e.id===-1?Il.splice(Ji+1,0,e):e.flags&1||($a.push(e),e.flags|=1);else for(let t=0;t<e.length;t++)$a.push(e[t]);Fy()}function Oy(e,t,n,a=ba+1){for(;a<Fn.length;a++){const l=Fn[a];if(l&&l.flags&2){if(e&&l.id!==e.uid||t!==void 0&&!(l.id>=t))continue;Fn.splice(a,1),a=e?a-1:ba,l.flags&4&&(l.flags&=-2),l(),l.flags&4||(l.flags&=-2)}}}function Ru(e,t,n=0){if($a.length){let a=$a,l=0;if(t){a=[];for(let r=0;r<$a.length;r++){const o=$a[r];(o.i!==void 0?!o.i||o.i.uid<n:t.has(o))?$a[l++]=o:a.push(o)}}const i=[...new Set(a)].sort((r,o)=>vo(r)-vo(o));if($a.length=l,Il){for(let r=0;r<i.length;r++)Il.push(i[r]);return}for(Il=i,Ji=0;Ji<Il.length;Ji++){const r=Il[Ji];r.flags&4&&(r.flags&=-2),r.flags&8||r(),r.flags&=-2}Il=null,Ji=0}}let Ec=!1;function Rx(e,t){if(Ec){e();return}const n=t===void 0?void 0:new Set($a);e(),Ec=!0;try{Oy(void 0,t),Ru(void 0,n,t)}finally{Ec=!1}}const vo=e=>e.id==null?e.flags&2?-1:1/0:e.id;function Ly(e){try{for(ba=0;ba<Fn.length;ba++){const t=Fn[ba];t&&!(t.flags&8)&&(t.flags&4&&(t.flags&=-2),Sr(t,t.i,t.i?15:14),t.flags&4||(t.flags&=-2))}}finally{for(;ba<Fn.length;ba++){const t=Fn[ba];t&&(t.flags&=-2)}ba=-1,Fn.length=0,Ru(),Nu=null,(Fn.length||$a.length)&&Ly()}}let Qi,su=[];function Ny(e,t){var n,a;Qi=e,Qi?(Qi.enabled=!0,su.forEach(({event:l,args:i})=>Qi.emit(l,...i)),su=[]):typeof window<"u"&&window.HTMLElement&&!((a=(n=window.navigator)==null?void 0:n.userAgent)!=null&&a.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(i=>{Ny(i,t)}),setTimeout(()=>{Qi||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,su=[])},3e3)):su=[]}let pn=null,Ds=null;function mo(e){const t=pn;return pn=e,Ds=e&&e.type.__scopeId||null,t}function Hx(e){Ds=e}function zx(){Ds=null}const Wx=e=>Ve;function Ve(e,t=pn,n){if(!t||e._n)return e;const a=(...l)=>{a._d&&bo(-1);const i=mo(t),r=cl.length;let o;try{o=e(...l)}finally{for(let u=cl.length;u>r;u--)Rs();mo(i),a._d&&bo(1)}return o};return a._n=!0,a._c=!0,a._d=!0,a}function xt(e,t){if(pn===null)return e;const n=Go(pn),a=e.dirs||(e.dirs=[]);for(let l=0;l<t.length;l++){let[i,r,o,u=mt]=t[l];i&&(Qe(i)&&(i={mounted:i,updated:i}),i.deep&&nl(r),a.push({dir:i,instance:n,value:r,oldValue:void 0,arg:o,modifiers:u}))}return e}function Ma(e,t,n,a){const l=e.dirs,i=t&&t.dirs;for(let r=0;r<l.length;r++){const o=l[r];i&&(o.oldValue=i[r].value);let u=o.dir[a];u&&(Na(),Qn(u,n,8,[e.el,o,e,t]),Ra())}}function bt(e,t){if(kn){let n=kn.provides;const a=kn.parent&&kn.parent.provides;a===n&&(n=kn.provides=Object.create(a)),n Line truncated
`&&(k.tagName==="PRE"||k.tagName==="TEXTAREA")&&(U=U.slice(1));const{textContent:G}=k;G!==U&&G!==U.replace(/\r\n|\r/g,`
`)&&(Iu(k,0)||ji(),k.textContent=S.children)}if(A){if(R||W||!_||B&48){const U=k.tagName.includes("-"),G=k.namespaceURI.includes("svg")?"svg":k.namespaceURI.includes("MathML")?"mathml":void 0;for(const Z in A)if(R&&(Z.endsWith("value")||Z==="indeterminate")||Wo(Z)&&!ai(Z)||Z[0]==="."||U&&!ai(Z)||T&&T.includes(Z)){if(tC(k,Z,A[Z]))continue;a(k,Z,null,A[Z],G,w)}}else if(A.onClick)a(k,"onClick",null,A.onClick,void 0,w);else if(B&4&&ka(A.style))for(const U in A.style)A.style[U]}let Y;(Y=A&&A.onVnodeBeforeMount)&&Nn(Y,w,S),P&&Ma(S,null,w,"beforeMount"),((Y=A&&A.onVnodeMounted)||P||N)&&Cb(()=>{Y&&Nn(Y,w,S),N&&L.enter(k),P&&Ma(S,null,w,"mounted")},x)}return k.nextSibling},v=(k,S,w,x,y,_,V)=>{V=V||!!S.dynamicChildren;const T=S.children,A=T.length;let B=!1;for(let E=0;E<A;E++){const P=V?T[E]:T[E]=Rn(T[E]),L=P.type===sl;k?(L&&!V&&E+1<A&&Rn(T[E+1]).type===sl&&(u(l(k.data.slice(P.children.length)),w,i(k)),k.data=P.children),k=c(k,P,x,y,_,V)):L&&!P.children?u(P.el=l(""),w):(B||(B=!0,Iu(w,1)||ji()),n(null,P,w,null,x,y,du(w),_))}return k},m=(k,S,w,x,y,_)=>{const{slotScopeIds:V}=S;V&&(y=y?y.concat(V):V);const T=r(k),A=v(i(k),S,T,w,x,y,_);return A&&tr(A)&&A.data==="]"?i(S.anchor=A):(ji(),u(S.anchor=d("]"),T,A),A)},h=(k,S,w,x,y,_)=>{if(aC(k,S)||ji(),S.el=null,_){const A=g(k);for(;;){const B=i(k);if(B&&B!==A)o(B);else break}}const V=i(k),T=r(k);return o(k),n(null,S,T,V,w,x,du(T),y),w&&(w.vnode.el=S.el,Ns(w,S.el)),V},g=(k,S="[",w="]")=>{let x=0;for(;k;)if(k=i(k),k&&tr(k)&&(k.data===S&&x++,k.data===w)){if(x===0)return i(k);x--}return k},b=(k,S,w)=>{const x=S.parentNode;x&&x.replaceChild(k,S);let y=w;for(;y;)y.vnode.el===S&&(y.vnode.el=y.subTree.el=k),y=y.parent},p=k=>k.nodeType===1&&k.tagName==="TEMPLATE";return[s,c]}const eC=new Set(["src","srcset","href","poster"]);function tC(e,t,n){return eC.has(t)?e.getAttribute(t)===(n==null?null:`${n}`):!1}const Wu="data-allow-mismatch",nC={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function Iu(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(Wu);)e=e.parentElement;return Wf(e&&e.getAttribute(Wu),t)}function Wf(e,t){if(e==null)return!1;if(e==="")return!0;{const n=e.split(",");return t===0&&n.includes("children")?!0:n.includes(nC[t])}}function aC(e,t){return Iu(e.parentElement,1)||lC(e)||iC(t)}function lC(e){return e.nodeType===1&&Wf(e.getAttribute(Wu),1)}function iC({props:e}){const t=e&&e[Wu];return typeof t=="string"&&Wf(t,1)}const rC=Cs().requestIdleCallback||(e=>setTimeout(e,1)),oC=Cs().cancelIdleCallback||(e=>clearTimeout(e)),uC=(e=1e4)=>t=>{const n=rC(t,{timeout:e});return()=>oC(n)};function sC(e){const{top:t,left:n,bottom:a,right:l}=e.getBoundingClientRect(),{innerHeight:i,innerWidth:r}=window;return(t>0&&t<i||a>0&&a<i)&&(n>0&&n<r||l>0&&l<r)}const cC=e=>(t,n)=>{const a=new IntersectionObserver(i=>{for(const r of i)if(r.isIntersecting){a.disconnect(),t();break}},e),l=i=>{if(sC(i))return t(),a.disconnect(),!1;if(getComputedStyle(i).display==="contents"){for(let r=0;r<i.children.length;r++)if(l(i.children[r])===!1)return!1}a.observe(i)};return n(i=>{if(i instanceof Element)return l(i)}),()=>a.disconnect()},dC=e=>t=>{if(e){const n=matchMedia(e);if(n.matches)t();else return n.addEventListener("change",t,{once:!0}),()=>n.removeEventListener("change",t)}},fC=(e=[])=>(t,n)=>{At(e)&&(e=[e]);let a=!1;const l=r=>{a||(a=!0,i(),t(),r.target.dispatchEvent(new r.constructor(r.type,r)))},i=()=>{n(r=>{for(const o of e)r.removeEventListener(o,l)})};return n(r=>{for(const o of e)r.addEventListener(o,l,{once:!0})}),i};function vC(e,t){if(tr(e)&&e.data==="["){let n=1,a=e.nextSibling;for(;a;){if(a.nodeType===1){if(t(a)===!1)break}else if(tr(a))if(a.data==="]"){if(--n===0)break}else a.data==="["&&n++;a=a.nextSibling}}else t(e)}const ul=e=>!!e.type.__asyncLoader;function mC(e){Qe(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:a,delay:l=200,hydrate:i,timeout:r,suspensible:o=!0,onError:u}=e;let d=null,s,c=0;const f=()=>(c++,d=null,v()),v=()=>{let m;return d||(m=d=t().catch(h=>{if(h=h instanceof Error?h:new Error(String(h)),u)return new Promise((g,b)=>{u(h,()=>g(f()),()=>b(h),c+1)});throw h}).then(h=>m!==d&&d?d:(h&&(h.__esModule||h[Symbol.toStringTag]==="Module")&&(h=h.default),s=h,h)))};return Bs({name:"AsyncComponentWrapper",__asyncLoader:v,__asyncHydrate(m,h,g){const b=m.isConnected;let p=!1;(h.bu||(h.bu=[])).push(()=>p=!0);const k=()=>{p||!m.parentNode||b&&!m.isConnected||g()},S=i?()=>{const w=i(k,x=>vC(m,x));w&&(h.bum||(h.bum=[])).push(w)}:k;s?S():v().then(()=>!h.isUnmounted&&S())},get __asyncResolved(){return s},setup(){const m=kn;if(zf(m),s)return()=>fu(s,m);const h=w=>{d=null,Vi(w,m,13,!a)};if(o&&m.suspense||hi)return v().then(w=>()=>fu(w,m)).catch(w=>(h(w),()=>a?C(a,{error:w}):null));const g=oe(!1),b=oe(),p=oe(!!l);let k,S;return Pi(()=>{k!=null&&clearTimeout(k),S!=null&&clearTimeout(S)}),l&&(S=setTimeout(()=>{m.isUnmounted||(p.value=!1)},l)),r!=null&&(k=setTimeout(()=>{if(!m.isUnmounted&&!g.value&&!b.value){const w=new Error(`Async component timed out after ${r}ms.`); Line truncated
* @vue/runtime-dom v3.5.44
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/let Cd;const Xm=typeof window<"u"&&window.trustedTypes;if(Xm)try{Cd=Xm.createPolicy("vue",{createHTML:e=>e})}catch{}const $b=Cd?e=>Cd.createHTML(e):e=>e,D_="http://www.w3.org/2000/svg",E_="http://www.w3.org/1998/Math/MathML",Za=typeof document<"u"?document:null,Zm=Za&&Za.createElement("template"),Fb={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,a)=>{const l=t==="svg"?Za.createElementNS(D_,e):t==="mathml"?Za.createElementNS(E_,e):n?Za.createElement(e,{is:n}):Za.createElement(e);return e==="select"&&a&&a.multiple!=null&&l.setAttribute("multiple",a.multiple),l},createText:e=>Za.createTextNode(e),createComment:e=>Za.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Za.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,a,l,i){const r=n?n.previousSibling:t.lastChild;if(l&&(l===i||l.nextSibling))for(;t.insertBefore(l.cloneNode(!0),n),!(l===i||!(l=l.nextSibling)););else{Zm.innerHTML=$b(a==="svg"?`<svg>${e}</svg>`:a==="mathml"?`<math>${e}</math>`:e);const o=Zm.content;if(a==="svg"||a==="mathml"){const u=o.firstChild;for(;u.firstChild;)o.appendChild(u.firstChild);o.removeChild(u)}t.insertBefore(o,n)}return[r?r.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},xl="transition",Br="animation",cr=Symbol("_vtc"),Ob={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Lb=_t({},Hf,Ob),M_=e=>(e.displayName="Transition",e.props=Lb,e),fl=M_((e,{slots:t})=>Ua(Yy,Nb(e),t)),Xl=(e,t=[])=>{Re(e)?e.forEach(n=>n(...t)):e&&e(...t)},Jm=e=>e?Re(e)?e.some(t=>t.length>1):e.length>1:!1;function Nb(e){const t={};for(const B in e)B in Ob||(t[B]=e[B]);if(e.css===!1)return t;const{name:n="v",type:a,duration:l,enterFromClass:i=`${n}-enter-from`,enterActiveClass:r=`${n}-enter-active`,enterToClass:o=`${n}-enter-to`,appearFromClass:u=i,appearActiveClass:d=r,appearToClass:s=o,leaveFromClass:c=`${n}-leave-from`,leaveActiveClass:f=`${n}-leave-active`,leaveToClass:v=`${n}-leave-to`}=e,m=B_(l),h=m&&m[0],g=m&&m[1],{onBeforeEnter:b,onEnter:p,onEnterCancelled:k,onLeave:S,onLeaveCancelled:w,onBeforeAppear:x=b,onAppear:y=p,onAppearCancelled:_=k}=t,V=(B,E,P,L)=>{B._enterCancelled=L,_l(B,E?s:o),_l(B,E?d:r),P&&P()},T=(B,E)=>{B._isLeaving=!1,_l(B,c),_l(B,v),_l(B,f),E&&E()},A=B=>(E,P)=>{const L=B?y:p,R=()=>V(E,B,P);Xl(L,[E,R]),Qm(()=>{_l(E,B?u:i),Ea(E,B?s:o),Jm(L)||eh(E,a,h,R)})};return _t(t,{onBeforeEnter(B){Xl(b,[B]),Ea(B,i),Ea(B,r)},onBeforeAppear(B){Xl(x,[B]),Ea(B,u),Ea(B,d)},onEnter:A(!1),onAppear:A(!0),onLeave(B,E){B._isLeaving=!0;const P=()=>T(B,E);Ea(B,c),B._enterCancelled?(Ea(B,f),_d(B)):(_d(B),Ea(B,f)),Qm(()=>{B._isLeaving&&(_l(B,c),Ea(B,v),Jm(S)||eh(B,a,g,P))}),Xl(S,[B,P])},onEnterCancelled(B){V(B,!1,void 0,!0),Xl(k,[B])},onAppearCancelled(B){V(B,!0,void 0,!0),Xl(_,[B])},onLeaveCancelled(B){T(B),Xl(w,[B])}})}function B_(e){if(e==null)return null;if(Vt(e))return[Fc(e.enter),Fc(e.leave)];{const t=Fc(e);return[t,t]}}function Fc(e){return Fu(e)}function Ea(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[cr]||(e[cr]=new Set)).add(t)}function _l(e,t){t.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const n=e[cr];n&&(n.delete(t),n.size||(e[cr]=void 0))}function Qm(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let $_=0;function eh(e,t,n,a){const l=e._endId=++$_,i=()=>{l===e._endId&&a()};if(n!=null)return setTimeout(i,n);const{type:r,timeout:o,propCount:u}=Rb(e,t);if(!r)return a();const d=r+"end";let s=0;const c=()=>{e.removeEventListener(d,f),i()},f=v=>{v.target===e&&++s>=u&&c()};setTimeout(()=>{s<u&&c()},o+1),e.addEventListener(d,f)}function Rb(e,t){const n=window.getComputedStyle(e),a=m=>(n[m]||"").split(", "),l=a(`${xl}Delay`),i=a(`${xl}Duration`),r=th(l,i),o=a(`${Br}Delay`),u=a(`${Br}Duration`),d=th(o,u);let s=null,c=0,f=0;t===xl?r>0&&(s=xl,c=r,f=i.length):t===Br?d>0&&(s=Br,c=d,f=u.length):(c=Math.max(r,d),s=c>0?r>d?xl:Br:null,f=s?s===xl?i.length:u.length:0);const v=s===xl&&/\b(?:transform|all)(?:,|$)/.test(a(`${xl}Property`).toString());return{type:s,timeout:c,propCount:f,hasTransform:v}}function th(e,t){for(;e.length<t.length;)e=e.concat(e);return Math.max(...t.map((n,a)=>nh(n)+nh(e[a])))}function nh(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function _d(e){return(e?e.ownerDocument:document).body.offsetHeight}function F_(e,t,n){const a=e[cr];a&&(t=(t?[t,...a]:[...a]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Zu=Symbol("_vod"),Qf=Symbol("_vsh"),na={name:"show",beforeMount(e,{value:t},{transition:n}){e[Zu]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):$r(e,t)},mounted(e,{value:t},{tra Line truncated
`))),e[Xn]=Ll(l);const i=a||l.props&&l.props.type==="number";al(e,t?"change":"input",r=>{r.target.composing||e[Xn](Nc(e.value,n,i))}),(n||i)&&al(e,"change",()=>{e.value=Nc(e.value,n,i)}),t||(al(e,"compositionstart",f1),al(e,"compositionend",fh),al(e,"change",fh))},mounted(e,{value:t,modifiers:{trim:n,number:a}}){const l=t??"",i=e[gu];delete e[gu],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[Xn](Nc(e.value,n,a)):e.value=l},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:a,trim:l,number:i}},r){if(e[Xn]=Ll(r),e.composing)return;const o=(i||e.type==="number")&&!/^0\d/.test(e.value)?xs(e.value):e.value,u=t??"";if(o===u)return;const d=e.getRootNode();(d instanceof Document||d instanceof ShadowRoot)&&d.activeElement===e&&e.type!=="range"&&(a&&t===n||l&&e.value.trim()===u)||(e.value=u)}},ev={deep:!0,created(e,t,n){e[Xn]=Ll(n),al(e,"change",()=>{const a=e._modelValue,l=dr(e),i=e.checked,r=e[Xn];if(Re(a)){const o=Vs(a,l),u=o!==-1;if(i&&!u)r(a.concat(l));else if(!i&&u){const d=[...a];d.splice(o,1),r(d)}}else if(La(a)){const o=new Set(a);i?o.add(l):o.delete(l),r(o)}else r(Yb(e,i))})},mounted:vh,beforeUpdate(e,t,n){e[Xn]=Ll(n),vh(e,t,n)}};function vh(e,{value:t,oldValue:n},a){e._modelValue=t;let l;if(Re(t))l=Vs(t,a.props.value)>-1;else if(La(t))l=t.has(a.props.value);else{if(t===n)return;l=va(t,Yb(e,!0))}e.checked!==l&&(e.checked=l)}const tv={created(e,{value:t},n){e.checked=va(t,n.props.value),e[Xn]=Ll(n),al(e,"change",()=>{e[Xn](dr(e))})},beforeUpdate(e,{value:t,oldValue:n},a){e[Xn]=Ll(a),t!==n&&(e.checked=va(t,a.props.value))}},Gb={deep:!0,created(e,{value:t,modifiers:{number:n}},a){e._modelValue=t,al(e,"change",()=>{const l=Array.prototype.filter.call(e.options,u=>u.selected).map(u=>n?xs(dr(u)):dr(u)),i=e.multiple,r=i?La(e._modelValue)?new Set(l):l:l[0],o=e._pendingValue=[i,i?Re(r)?l.slice():l:r];try{e[Xn](r)}finally{Ne(()=>{e._pendingValue===o&&(e._pendingValue=void 0)})}}),e[Xn]=Ll(a)},mounted(e,{value:t}){mh(e,t)},beforeUpdate(e,{value:t},n){e._modelValue=t,e[Xn]=Ll(n)},updated(e,{value:t}){const n=e._pendingValue;e._pendingValue=void 0,(!n||n[0]!==e.multiple||!v1(t,n[1],n[0]))&&mh(e,t)}};function v1(e,t,n){if(!n||Re(e))return va(e,t);if(La(e)){if(e.size!==t.length)return!1;for(const a of t)if(!e.has(a))return!1;return!0}return!1}function mh(e,t){const n=e.multiple,a=Re(t);if(!(n&&!a&&!La(t))){for(let l=0,i=e.options.length;l<i;l++){const r=e.options[l],o=dr(r);if(n)if(a){const u=typeof o;u==="string"||u==="number"?r.selected=t.some(d=>String(d)===String(o)):r.selected=Vs(t,o)>-1}else r.selected=t.has(o);else if(va(dr(r),t)){e.selectedIndex!==l&&(e.selectedIndex=l);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function dr(e){return"_value"in e?e._value:e.value}function Yb(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const qb={created(e,t,n){yu(e,t,n,null,"created")},mounted(e,t,n){yu(e,t,n,null,"mounted")},beforeUpdate(e,t,n,a){yu(e,t,n,a,"beforeUpdate")},updated(e,t,n,a){yu(e,t,n,a,"updated")}};function Xb(e,t){switch(e){case"SELECT":return Gb;case"TEXTAREA":return So;default:switch(t){case"checkbox":return ev;case"radio":return tv;default:return So}}}function yu(e,t,n,a,l){const r=Xb(e.tagName,n.props&&n.props.type)[l];r&&r(e,t,n,a)}function m1(){So.getSSRProps=({value:e})=>({value:e}),tv.getSSRProps=({value:e},t)=>{if(t.props&&va(t.props.value,e))return{checked:!0}},ev.getSSRProps=({value:e},t)=>{if(Re(e)){if(t.props&&Vs(e,t.props.value)>-1)return{checked:!0}}else if(La(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},qb.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const n=Xb(t.type.toUpperCase(),t.props&&t.props.type);if(n.getSSRProps)return n.getSSRProps(e,t)}}const h1=["ctrl","shift","alt","meta"],g1={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>h1.some(n=>e[`${n}Key`]&&!t.includes(n))},sa=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),a=t.join(".");return n[a]||(n[a]=((l,...i)=>{for(let r=0;r<t.length;r++){const o=g1[t[r]];if(o&&o(l,t))return}return e(l,...i)}))},y1={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Zb=(e,t)=>{const n=e._withKeys||(e._withKeys={}),a=t.join(".");return n[a]||(n[a]=(l=>{if(!("key"in l))return;const i=Hn(l.key);if(t.some(r=>r===i||y1[r]===i))return e(l)}))},Jb=_t({patchProp:zb},Fb);let Qr,hh=!1;function Qb(){return Qr||(Qr=bb(Jb))}function ew(){return Qr=hh?Qr:wb(Jb),hh=!0,Qr}const Us=((...e)=>{Qb().render(...e)}),b1=((...e)=>{ew().hydrate(...e)}),es=((...e)=>{const t=Qb().createApp(...e),{mount:n}=t;return t.mount=a=>{const l=aw(a);if(!l)return;const i=t._component;!Qe(i)&&!i.render&&!i.template&&(i.template=l.innerHTML),l.nodeType===1&&(l.textContent="");const Line truncated
* vue v3.5.44
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/const S1=()=>{},k1=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:Yy,BaseTransitionPropsValidators:Hf,Comment:qt,DeprecationTypes:T_,EffectScope:Ef,ErrorCodes:Fx,ErrorTypeStrings:x_,Fragment:we,KeepAlive:gC,ReactiveEffect:oo,Static:Tl,Suspense:l_,Teleport:Ky,Text:sl,TrackOpTypes:Ax,Transition:fl,TransitionGroup:zs,TriggerOpTypes:Tx,VueElement:Hs,assertNumber:$x,callWithAsyncErrorHandling:Qn,callWithErrorHandling:Sr,camelize:nn,capitalize:yl,cloneVNode:ha,compatUtils:A_,compile:S1,computed:M,createApp:es,createBlock:tn,createCommentVNode:zt,createElementBlock:St,createElementVNode:I,createHydrationRenderer:wb,createPropsRestProxy:$C,createRenderer:bb,createSSRApp:tw,createSlots:SC,createStaticVNode:f_,createTextVNode:nt,createVNode:C,customRef:Dy,defineAsyncComponent:mC,defineComponent:Bs,defineCustomElement:Wb,defineEmits:_C,defineExpose:VC,defineModel:AC,defineOptions:IC,defineProps:CC,defineSSRCustomElement:Q_,defineSlots:PC,devtools:C_,effect:lx,effectScope:Fl,getCurrentInstance:_n,getCurrentScope:Mf,getCurrentWatcher:Dx,getTransitionRawChildren:Ms,guardReactiveProps:Ko,h:Ua,handleError:Vi,hasInjectionContext:Ry,hydrate:b1,hydrateOnIdle:uC,hydrateOnInteraction:fC,hydrateOnMediaQuery:dC,hydrateOnVisible:cC,initCustomFormatter:S_,initDirectivesForSSR:w1,inject:Ye,isMemoSame:Mb,isProxy:wr,isReactive:ka,isReadonly:Ca,isRef:Mt,isRuntimeOnly:y_,isShallow:jn,isVNode:_a,markRaw:Of,mergeDefaults:MC,mergeModels:BC,mergeProps:le,nextTick:Ne,nodeOps:Fb,normalizeClass:fe,normalizeProps:_s,normalizeStyle:Se,onActivated:Uf,onBeforeMount:Ii,onBeforeUnmount:sn,onBeforeUpdate:Os,onDeactivated:$s,onErrorCaptured:eb,onMounted:Rt,onRenderTracked:Qy,onRenderTriggered:Jy,onScopeDispose:pt,onServerPrefetch:Zy,onUnmounted:Pi,onUpdated:kr,onWatcherCleanup:My,openBlock:Ze,patchProp:zb,popScopeId:zx,provide:bt,proxyRefs:Lf,pushScopeId:Hx,queuePostFlushCb:fo,reactive:yn,readonly:Lt,ref:oe,registerRuntimeCompiler:g_,render:Us,renderList:Uu,renderSlot:kC,resolveComponent:tb,resolveDirective:wC,resolveDynamicComponent:ab,resolveFilter:P_,resolveTransitionHooks:sr,setBlockTracking:bo,setDevtoolsHook:__,setTransitionHooks:Ha,shallowReactive:co,shallowReadonly:Wn,shallowRef:ne,ssrContextKey:Hy,ssrUtils:I_,stop:ix,toDisplayString:it,toHandlerKey:qr,toHandlers:pC,toRaw:Oe,toRef:z,toRefs:_i,toValue:pe,transformVNodeArgs:c_,triggerRef:dd,unref:Dt,useAttrs:EC,useCssModule:n1,useCssVars:L_,useHost:Ub,useId:Wt,useModel:UC,useSSRContext:zy,useShadowRoot:t1,useSlots:DC,useTemplateRef:Xx,useTransitionState:Rf,vModelCheckbox:ev,vModelDynamic:qb,vModelRadio:tv,vModelSelect:Gb,vModelText:So,vShow:na,version:Bb,warn:p_,watch:ve,watchEffect:Xt,watchPostEffect:Ux,watchSyncEffect:Wy,withAsyncContext:FC,withCtx:Ve,withDefaults:TC,withDirectives:xt,withKeys:Zb,withMemo:k_,withModifiers:sa,withScopeId:Wx},Symbol.toStringTag,{value:"Module"}));function js(e,t){t=Array.isArray(t)?t.slice(0,-1).map(n=>`'${n}'`).join(", ")+` or '${t.at(-1)}'`:`'${t}'`}var yh={};const zn=typeof window<"u",bh=zn&&"matchMedia"in window&&typeof window.matchMedia=="function",p1=zn&&"ResizeObserver"in window,x1=zn&&"IntersectionObserver"in window,C1=typeof yh.VITE_LOGGER_ENABLED<"u"&&yh.VITE_LOGGER_ENABLED==="true",_1=/[\u0300-\u036F]/g,V1=/ς/g;function Pd(e){return e.toLowerCase().replace(V1,"σ")}const lw={ł:"l",ø:"o",đ:"d",ð:"d",þ:"th",ħ:"h",ŧ:"t",ŋ:"n",ß:"ss",æ:"ae",œ:"oe",ı:"i",Ł:"L",Ø:"O",Đ:"D",Ð:"D",Þ:"Th",Ħ:"H",Ŧ:"T",Ŋ:"N","ẞ":"Ss",Æ:"Ae",Œ:"Oe"},I1=new RegExp(`[${Object.keys(lw).join("")}]`,"g");function iw(e){return e.normalize("NFD").replace(_1,"").replace(I1,t=>lw[t])}function P1(e,t,n){let a="";const l=[];let i=0;for(const r of e){const o=t?Pd(r):r,u=n?iw(o):o;a+=u;for(let d=0;d<u.length;d++)l.push(i);i+=r.length}return l.push(e.length),{folded:a,map:l}}function Rc(e,t,n){const a=[];let l=e.indexOf(t);for(;l!==-1&&(a.push([l,l+t.length]),!!n);)l=e.indexOf(t,l+t.length);return a}function A1(e,t,n){const a=e[t];let l=e[n];if(n>0&&e[n]===e[n-1]){let i=n;const r=e[n];for(;i<e.length&&e[i]===r;)i++;l=e[i]}return[a,l]}function T1(e,t){const n=[];for(const[a,l]of e){const i=A1(t,a,l),r=n.at(-1);(!r||r[0]!==i[0]||r[1]!==i[1])&&n.push(i)}return n}function Hc(e,t,n,a,l,i){const r=a?iw(t):t,o=n?Pd(r):r;if(o.length===0)return[];if(!l){if(!n)return Rc(e,o,i);const s=Pd(e);if(s.length===e.length)return Rc(s,o,i)}const{folded:u,map:d}=P1(e,n,l);return T1(Rc(u,o,i),d)}function D1(e,t){const n=[...e,...t].toSorted((l,i)=>l[0]-i[0]||l[1]-i[1]),a=[];for(const l of n){const i=a.at(-1);i&&l[0]<i[1]||a.push(l)}return a}function rw(e,t,n={}){const{ignoreCase:a=!1,ignoreAccents:l=!1,matchAll:i=!1}=n,r=l===!0||l==="query",o=l===!0||l==="target";if(r===o)return Hc(e,t,a,r,o,i);const u=D1(Hc(e,t,a,!1,!1,!0),Hc(e,t,a,r,o,!0));return i?u:u.slice(0,1)}const wh="currentInstance";function E1(){const e=k1;return wh in e?e[wh]:_n()}function M1(){return!dl(E1())}function Ge(e){return typeof e=="function"}function Ie(e){return typeof e=="string"}function ct(eLine truncated
Expected #hex, #hexa, rgb(), rgba(), hsl(), hsla(), object or number`)}function za(e){const{h:t,s:n,v:a,a:l}=e,i=o=>{const u=(o+t/60)%6;return a-a*n*Math.max(Math.min(u,4-u,1),0)},r=[i(5),i(3),i(1)].map(o=>Math.round(o*255));return{r:r[0],g:r[1],b:r[2],a:l}}function Kh(e){return za(sv(e))}function qo(e){if(!e)return{h:0,s:1,v:1,a:1};const t=e.r/255,n=e.g/255,a=e.b/255,l=Math.max(t,n,a),i=Math.min(t,n,a);let r=0;l!==i&&(l===t?r=60*(0+(n-a)/(l-i)):l===n?r=60*(2+(a-t)/(l-i)):l===a&&(r=60*(4+(t-n)/(l-i)))),r<0&&(r=r+360);const o=l===0?0:(l-i)/l,u=[r,o,l];return{h:u[0],s:u[1],v:u[2],a:e.a}}function Fd(e){const{h:t,s:n,v:a,a:l}=e,i=a-a*n/2,r=i===1||i===0?0:(a-i)/Math.min(i,1-i);return{h:t,s:r,l:i,a:l}}function sv(e){const{h:t,s:n,l:a,a:l}=e,i=a+n*Math.min(a,1-a),r=i===0?0:2-2*a/i;return{h:t,s:r,v:i,a:l}}function Tw({r:e,g:t,b:n,a}){return ct(a)?`rgb(${e}, ${t}, ${n}, ${a})`:`rgb(${e}, ${t}, ${n})`}function Dw(e){return Tw(za(e))}function ku(e){const t=Math.round(e).toString(16);return("00".substr(0,2-t.length)+t).toUpperCase()}function Ew({r:e,g:t,b:n,a}){return`#${[ku(e),ku(t),ku(n),ct(a)?ku(Math.round(a*255)):""].join("")}`}function Mw(e){e=jV(e);let[t,n,a,l]=kV(e,2).map(i=>parseInt(i,16));return l=ct(l)?l/255:l,{r:t,g:n,b:a,a:l}}function UV(e){const t=Mw(e);return qo(t)}function Bw(e){return Ew(za(e))}function jV(e){return e.startsWith("#")&&(e=e.slice(1)),e=e.replace(/([^0-9a-f])/gi,"F"),(e.length===3||e.length===4)&&(e=e.split("").map(t=>t+t).join("")),e.length!==6&&(e=Ah(Ah(e,6),8,"F")),e}function KV(e,t){const n=Iw(uv(e));return n[0]=n[0]+t*10,Aw(Pw(n))}function GV(e,t){const n=Iw(uv(e));return n[0]=n[0]-t*10,Aw(Pw(n))}function Od(e){const t=Zn(e);return uv(t)[1]}function YV(e,t){const n=Od(e),a=Od(t),l=Math.max(n,a),i=Math.min(n,a);return(l+.05)/(i+.05)}function $w(e){const t=Math.abs(Uh(Zn(0),Zn(e)));return Math.abs(Uh(Zn(16777215),Zn(e)))>Math.min(t,50)}function te(e,t){return n=>Object.keys(e).reduce((a,l)=>{const r=je(e[l])?e[l]:{type:e[l]};return n&&l in n?a[l]={...r,default:n[l]}:a[l]=r,t&&!a[l].source&&(a[l].source=t),a},{})}const Ae=te({class:[String,Array,Object],style:{type:[String,Array,Object],default:null}},"component");function Ut(e,t){const n=_n();if(!n)throw new Error(`[Vuetify] ${e} must be called from inside a setup function`);return n}function ja(e="composables"){const t=Ut(e).type;return oi((t==null?void 0:t.aliasName)||(t==null?void 0:t.name))}function Fw(e,t=Ut("injectSelf")){const{provides:n}=t;if(n&&e in n)return n[e]}const bi=Symbol.for("vuetify:defaults"),Ow=Symbol.for("vuetify:defaults:root");function qV(e){return oe(e)}function Ys(){const e=Ye(bi);if(!e)throw new Error("[Vuetify] Could not find defaults instance");return e}function $t(e,t){const n=Ys(),a=Ye(Ow,null),l=oe(e),i=M(()=>{var c;if(Dt(t==null?void 0:t.disabled))return n.value;const o=Dt(t==null?void 0:t.scoped),u=Dt(t==null?void 0:t.reset),d=Dt(t==null?void 0:t.root);if(!l.value&&!(o||u||d))return n.value;let s=xn(l.value,{prev:n.value});if(o)return s;if(u||d){const f=Number(u||1/0),v=Ie(d)?(c=s.prev)==null?void 0:c[d]:void 0;d&&(a!=null&&a.value)&&(s=a.value);for(let m=0;m<=f&&!(!s||!("prev"in s));m++)s=s.prev;return s&&v&&(s=xn(xn(s,{prev:s}),v)),s}return s.prev?xn(s.prev,s,void 0,(f,v)=>v!==void 0):s});return bt(bi,i),i}function Gh(e){const t=Ut("injectComponentDefaults");return z(()=>{var n,a;return(a=(n=Fw(bi,t))==null?void 0:n.value)==null?void 0:a[e]})}function aa(e){const t=Ut("injectNestedDefaults"),n=Ys(),a=t.props._as??t.type.name;return z(()=>{var l,i;return(i=(l=n.value)==null?void 0:l[a])==null?void 0:i[e]})}function XV(e,t){return e.props&&(typeof e.props[t]<"u"||typeof e.props[oi(t)]<"u")}function ZV(e={},t,n=Ys()){const a=Ut("useDefaults");if(t=t??a.type.name??a.type.__name,!t)throw new Error("[Vuetify] Could not determine component name");const l=M(()=>{var u;return(u=n.value)==null?void 0:u[e._as??t]}),i=new Proxy(e,{get(u,d){var v,m,h,g;const s=Reflect.get(u,d);if(d==="class"||d==="style")return[(v=l.value)==null?void 0:v[d],s].filter(b=>b!=null);if(XV(a.vnode,d))return s;const c=(m=l.value)==null?void 0:m[d];if(c!==void 0)return c;const f=(g=(h=n.value)==null?void 0:h.global)==null?void 0:g[d];return f!==void 0?f:s}}),r=ne();Xt(()=>{if(l.value){const u=Object.entries(l.value).filter(([d])=>d.startsWith(d[0].toUpperCase()));r.value=u.length?Object.fromEntries(u):void 0}else r.value=void 0});function o(){const u=Fw(bi,a);bt(bi,M(()=>r.value?xn((u==null?void 0:u.value)??{},r.value):u==null?void 0:u.value))}return{props:i,provideSubDefaults:o}}function In(e){if(e._setup=e._setup??e.setup,!e.name)return e;if(e._setup){e.props=te(e.props??{},e.name)();const t=Object.keys(e.props).filter(n=>n!=="class"&&n!=="style");e.filterProps=function(a){return un(a,t)},e.props._as=String,e.setup=function(a,l){const i=Ys();if(!i.value)return e._setup(a,l);const{props:r,provideSubDefaults:o}=ZV(a,a._as??e.name,i),u=e._setup(r,l);return o(),u}}return e}function de(e=!0){return t=>(e?In:Bs)(t)}function JV(e,t){return t Line truncated
KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PY SA SG SV TH TT TW UM US
VE VI WS YE ZA ZW`.includes(t):return{firstDay:0,firstWeekSize:1};case`AI AL AM AR AU AZ BA BM BN BY CL CM CN CR CY EC GE HR KG KZ LB LK LV
MD ME MK MN MY NZ RO RS SI TJ TM TR UA UY UZ VN XK`.includes(t):return{firstDay:1,firstWeekSize:1};case`AD AN AT AX BE BG CH CZ DE DK EE ES FI FJ FO FR GB GF GP GR HU IE IS
IT LI LT LU MC MQ NL NO PL RE RU SE SK SM VA`.includes(t):return{firstDay:1,firstWeekSize:4};case"AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".includes(t):return{firstDay:6,firstWeekSize:1};case t==="MV":return{firstDay:5,firstWeekSize:1};case t==="PT":return{firstDay:0,firstWeekSize:4};default:return null}}function bI(e,t,n){var s;const a=[];let l=[];const i=jw(e),r=Kw(e),o=n??((s=Xo(t))==null?void 0:s.firstDay)??0,u=(i.getDay()-o+7)%7,d=(r.getDay()-o+7)%7;for(let c=0;c<u;c++){const f=new Date(i);f.setDate(f.getDate()-(u-c)),l.push(f)}for(let c=1;c<=r.getDate();c++){const f=new Date(e.getFullYear(),e.getMonth(),c);l.push(f),l.length===7&&(a.push(l),l=[])}for(let c=1;c<7-d;c++){const f=new Date(r);f.setDate(f.getDate()+c),l.push(f)}return l.length>0&&a.push(l),a}function ao(e,t,n){var i;let a=(n??((i=Xo(t))==null?void 0:i.firstDay)??0)%7;[0,1,2,3,4,5,6].includes(a)||(a=0);const l=new Date(e);for(;l.getDay()!==a;)l.setDate(l.getDate()-1);return l}function wI(e,t){var l;const n=new Date(e),a=((((l=Xo(t))==null?void 0:l.firstDay)??0)+6)%7;for(;n.getDay()!==a;)n.setDate(n.getDate()+1);return n}function jw(e){return new Date(e.getFullYear(),e.getMonth(),1)}function Kw(e){return new Date(e.getFullYear(),e.getMonth()+1,0)}function SI(e){const t=e.split("-").map(Number);return new Date(t[0],t[1]-1,t[2])}const kI=/^([12]\d{3}-([1-9]|0[1-9]|1[0-2])-([1-9]|0[1-9]|[12]\d|3[01]))$/;function Gw(e){if(e==null)return new Date;if(e instanceof Date)return e;if(typeof e=="string"){let t;if(kI.test(e))return SI(e);if(t=Date.parse(e),!isNaN(t))return new Date(t)}return null}const Jh=new Date(2e3,0,2);function pI(e,t,n){var l;const a=t??((l=Xo(e))==null?void 0:l.firstDay)??0;return da(7).map(i=>{const r=new Date(Jh);return r.setDate(Jh.getDate()+a+i),new Intl.DateTimeFormat(e,{weekday:n??"narrow"}).format(r)})}function xI(e,t,n,a){const l=Gw(e)??new Date,i=a==null?void 0:a[t];if(typeof i=="function")return i(l,t,n);let r={};switch(t){case"fullDate":r={year:"numeric",month:"short",day:"numeric"};break;case"fullDateWithWeekday":r={weekday:"long",year:"numeric",month:"long",day:"numeric"};break;case"normalDate":const o=l.getDate(),u=new Intl.DateTimeFormat(n,{month:"long"}).format(l);return`${o} ${u}`;case"normalDateWithWeekday":r={weekday:"short",day:"numeric",month:"short"};break;case"shortDate":r={month:"short",day:"numeric"};break;case"year":r={year:"numeric"};break;case"month":r={month:"long"};break;case"monthShort":r={month:"short"};break;case"monthAndYear":r={month:"long",year:"numeric"};break;case"monthAndDate":r={month:"long",day:"numeric"};break;case"weekday":r={weekday:"long"};break;case"weekdayShort":r={weekday:"short"};break;case"dayOfMonth":return new Intl.NumberFormat(n).format(l.getDate());case"hours12h":r={hour:"numeric",hour12:!0};break;case"hours24h":r={hour:"numeric",hour12:!1};break;case"minutes":r={minute:"numeric"};break;case"seconds":r={second:"numeric"};break;case"fullTime":r={hour:"numeric",minute:"numeric"};break;case"fullTime12h":r={hour:"numeric",minute:"numeric",hour12:!0};break;case"fullTime24h":r={hour:"numeric",minute:"numeric",hour12:!1};break;case"fullDateTime":r={year:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"numeric"};break;case"fullDateTime12h":r={year:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"numeric",hour12:!0};break;case"fullDateTime24h":r={year:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"numeric",hour12:!1};break;case"keyboardDate":r={year:"numeric",month:"2-digit",day:"2-digit"};break;case"keyboardDateTime":return r={year:"numeric",month:"2-digit",day:"2-digit",hour:"numeric",minute:"numeric"},new Intl.DateTimeFormat(n,r).format(l).replace(/, /g," ");case"keyboardDateTime12h":return r={year:"numeric",month:"2-digit",day:"2-digit",hour:"numeric",minute:"numeric",hour12:!0},new Intl.DateTimeFormat(n,r).format(l).replace(/, /g," ");case"keyboardDateTime24h":return r={year:"numeric",month:"2-digit",day:"2-digit",hour:"numeric",minute:"numeric",hour12:!1},new Intl.DateTimeFormat(n,r).format(l).replace(/, /g," ");default:r=i??{timeZone:"UTC",timeZoneName:"short"}}return new Intl.DateTimeFormat(n,r).format(l)}function CI(e,t){const n=e.toJsDate(t),a=n.getFullYear(),l=Th(String(n.getMonth()+1),2,"0"),i=Th(String(n.getDate()),2,"0");return`${a}-${l}-${i}`}function _I(e){const[t,n,a]=e.split("-").map(Number);return new Date(t,n-1,a)}function VI(e,t){const n=new Date(e);return n.setMinutes(n.getMinutes()+t),n}function II(e,t){const n=new Date(e);return n.setHours(n.getHours()+t),n}function ui(e,t){const n=new Date(e);return n.setDate(n.getDate()+t),n}function PI(e,t){const n=new Date(e);return n.setDate(n.getDate()+t*7),n}function AI(e,t){const n=new Date(e);return n.setDate(1),n.setMonth(n.getMonth()+t),n}function Ao(e){return e.getFullYear()}function TI(e){return e.getMonth()}function DI(e,t,n,a){const l=Xo(t),i=n??(l==null?void 0:l.firstDay)??0,r=(l==null?void 0:l.firstWeekSize)??1;return a!==void 0?EI(e,t,i,a):MI(e,t,i,r)}function EI(e, Line truncated
`,...n.map(l=>` ${l};
`),`}
`)}function lg(e,t){const n=e.dark?2:1,a=e.dark?1:2,l=[];for(const[i,r]of Object.entries(e.colors)){const o=Zn(r);l.push(`--${t}theme-${i}: ${o.r},${o.g},${o.b}`+(ct(o.a)?`,${o.a}`:"")),i.startsWith("on-")||l.push(`--${t}theme-${i}-overlay-multiplier: ${Od(r)>.18?n:a}`)}for(const[i,r]of Object.entries(e.variables)){const o=Ie(r)&&r.startsWith("#")?Zn(r):void 0,u=o?`${o.r}, ${o.g}, ${o.b}`+(ct(o.a)?`, ${o.a}`:""):void 0;l.push(`--${t}${i}: ${u??r}`)}return l}function sP(e,t,n){const a={};if(n)for(const l of["lighten","darken"]){const i=l==="lighten"?KV:GV;for(const r of da(n[l],1))a[`${e}-${l}-${r}`]=Ew(i(Zn(t),r))}return a}function cP(e,t){if(!t)return{};let n={};for(const a of t.colors){const l=e[a];l&&(n={...n,...sP(a,l,t)})}return n}function dP(e,t){const n={};for(const a of Object.keys(e)){if(a.startsWith("on-")||e[`on-${a}`])continue;const l=`on-${a}`,i=Zn(e[a]);n[l]=$w(i)?t["theme-on-dark"]:t["theme-on-light"]}return n}function fP(e,t){if(!t)return e;const n=`:where(${t})`;return e===":root"?n:`${n} ${e}`}function vP(e,t,n){const a=mP(e,t);a&&(a.innerHTML=n)}function mP(e,t){if(!rt)return null;let n=document.getElementById(e);return n||(n=document.createElement("style"),n.id=e,n.type="text/css",t&&n.setAttribute("nonce",t),document.head.appendChild(n)),n}function hP(e){const t=uP(e),n=ne(t.defaultTheme),a=oe(t.themes),l=ne("light"),i=M({get(){return n.value==="system"?l.value:n.value},set(x){n.value=x}}),r=M(()=>{const x={};for(const[y,_]of Object.entries(a.value)){const V=_.dark||y==="dark"?a.value.dark:a.value.light,T=xn(V,_),A={...T.colors,...cP(T.colors,t.variations)};x[y]={...T,colors:{...A,...dP(A,T.variables)}}}return x}),o=z(()=>r.value[i.value]),u=z(()=>n.value==="system"),d=M(()=>{var _;const x=[],y=t.scoped?t.prefix:"";x.push(`@layer theme-base {
`),Zl(x,":root",[`color-scheme: ${(_=o.value)!=null&&_.dark?"dark":"light"}`],t.scope),Zl(x,":root",lg(o.value,t.prefix),t.scope);for(const[V,T]of Object.entries(r.value))Zl(x,`.${t.prefix}theme--${V}`,[`color-scheme: ${T.dark?"dark":"light"}`,...lg(T,t.prefix)],t.scope);if(x.push(`}
`),t.utilities){const V=[],T=[],A=new Set(Object.values(r.value).flatMap(B=>Object.keys(B.colors)));for(const B of A)B.startsWith("on-")?Zl(T,`.${B}`,[`color: rgb(var(--${t.prefix}theme-${B}))`],t.scope):(Zl(V,`.${y}bg-${B}`,[`--${t.prefix}theme-overlay-multiplier: var(--${t.prefix}theme-${B}-overlay-multiplier)`,`background-color: rgb(var(--${t.prefix}theme-${B}))`,`color: rgb(var(--${t.prefix}theme-on-${B}))`],t.scope),Zl(T,`.${y}text-${B}`,[`color: rgb(var(--${t.prefix}theme-${B}))`],t.scope),Zl(T,`.${y}border-${B}`,[`--${t.prefix}border-color: var(--${t.prefix}theme-${B})`],t.scope));x.push(`@layer theme-background {
`,...V.map(B=>` ${B}`),`}
`,`@layer theme-foreground {
`,...T.map(B=>` ${B}`),`}
`)}return`@layer vuetify-utilities {
`+x.map(V=>` ${V}`).join("")+`
}`}),s=z(()=>t.isDisabled?void 0:`${t.prefix}theme--${i.value}`),c=z(()=>Object.keys(r.value));if(Ks){let y=function(){l.value=x.matches?"dark":"light"};var w=y;const x=window.matchMedia("(prefers-color-scheme: dark)");y(),x.addEventListener("change",y,{passive:!0}),Mf()&&pt(()=>{x.removeEventListener("change",y)})}function f(x){if(t.isDisabled)return;const y=x._context.provides.usehead;if(y){let T=function(){return{style:[{textContent:d.value,id:t.stylesheetId,nonce:t.cspNonce||!1,tagPosition:"bodyOpen"}]}};var _=T;if(y.push){const A=y.push(T);rt&&ve(d,()=>{A.patch(T)})}else rt?(y.addHeadObjs(z(T)),Xt(()=>y.updateDOM())):y.addHeadObjs(T())}else{let T=function(){vP(t.stylesheetId,t.cspNonce,d.value)};var V=T;rt?ve(d,T,{immediate:!0}):T()}}let v;function m(x){if(!x){v=null;return}let y,_;if(ri(x)){const A=new Cn(x);y=A.left+A.width/2,_=A.top+A.height/2}else y=x.clientX,_=x.clientY;const V=Math.min(98.9,100*y/window.innerWidth),T=100*_/window.innerHeight;v=`${V.toFixed(2)}% ${T.toFixed(2)}%`}function h(x){if(!(x??t.transition)&&!v)return!1;const _=je(t.transition)?t.transition:{},V=je(x)?x:{};return{origin:V.origin??v??_.origin,duration:V.duration??_.duration}}function g(x,y){if(!rt||!document.startViewTransition||ea())return x(),Promise.resolve();const _=y.origin??"50% 0%",V=y.duration??"500ms",T=document.createElement("style");T.textContent=`html:active-view-transition-type(vuetify-theme)::view-transition-old(root) { animation: none; }html:active-view-transition-type(vuetify-theme)::view-transition-new(root) { animation: v-circle-in ${V} ease-in-out; }@keyframes v-circle-in { from { clip-path: circle(0% at ${_}); } to { clip-path: circle(150% at ${_}); }}`,document.head.appendChild(T);const A=document.startViewTransition({update:()=>x(),types:["vuetify-theme"]});return A.finished.then(()=>{v=null,T.remove()}),A.updateCallbackDone}function b(x,y){if(x!=="system"&&!c.value.includes(x))return Promise.resolve();const _=()=>{i.value=x},V=h(y);return V?g(_,V):(_(),Promise.resolve())}function p(x=c.value,y){const _=x.indexOf(i.value),V=_===-1?0:(_+1)%x.length;return b(x[V],y)}function k(x=["light","dark"],y){return p(x,y)}const S=new Proxy(i,{get(x,y){return Reflect.get(x,y)},set(x,y,_){return y==="value"&&js(`theme.global.name.value = ${_}`,`theme.change('${_}')`),Reflect.set(x,y,_)}});return{install:f,change:b,cycle:p,toggle:k,setTransitionOrigin:m,isDisabled:t.isDisabled,isSystem:u,name:i,themes:a,current:o,computedThemes:r,prefix:t.prefix,themeClasses:s,styles:d,global:{name:S,current:o}}}function ft(e){Ut("provideTheme");const t=Ye(Do,null);if(!t)throw new Error("Could not find Vuetify theme injection");const n=z(()=>e.theme??t.name.value),i={...t,name:n,current:z(()=>t.computedThemes.value[n.value]),themeClasses:z(()=>t.isDisabled?void 0:`${t.prefix}theme--${n.value}`)};return bt(Do,i),i}function yv(){Ut("useTheme");const e=Ye(Do,null);if(!e)throw new Error("Could not find Vuetify theme injection");return e}const Eo=Symbol.for("vuetify:layout"),Jw=Symbol.for("vuetify:layout-item"),ig=1e3,Qw=te({overlaps:{type:Array,default:()=>[]},fullHeight:Boolean},"layout"),Ai=te({name:{type:String},order:{type:[Number,String],default:0},absolute:Boolean},"layout-item");function bv(){const e=Ye(Eo);if(!e)throw new Error("[Vuetify] Could not find injected layout");return{getLayoutItem:e.getLayoutItem,mainRect:e.mainRect,mainStyles:e.mainStyles,layoutRect:e.layoutRect}}function Ti(e){const t=Ye(Eo);if(!t)throw new Error("[Vuetify] Could not find injected layout");const n=e.id??`layout-item-${Wt()}`,a=Ut("useLayoutItem");bt(Jw,{id:n});const l=ne(!1);$s(()=>l.value=!0),Uf(()=>l.value=!1);const{layoutItemStyles:i,layoutItemScrimStyles:r}=t.register(a,{...e,active:M(()=>l.value?!1:e.active.value),id:n});return sn(()=>t.unregister(n)),{layoutItemStyles:i,layoutRect:t.layoutRect,layoutItemScrimStyles:r}}const gP=(e,t,n,a,l,i)=>{let r={top:0,left:0,right:0,bottom:0},o={...r};const u=[{id:"",layer:{...r},percent:{...o}}];for(const d of e){const s=t.get(d),c=n.get(d),f=a.get(d);if(!s||!c||!f)continue;const v={...r,[s.value]:r[s.value]+(f.value?l(c.value,s.value):0)},m={...o,[s.value]:o[s.value]+(f.value?i(c.value):0)};u.push({id:d,layer:v,percent:m}),r=v,o=m}return u};function eS(e){const t=Ye(Eo,null),n=M(()=>t?t.rootZIndex.value-100:ig),a=oe([]),l=yn(new Map),i=yn(new Map),r=yn(new Map),o=yn(new Map),u=yn(new Map),d=yi(),s=ne();vl(()=>d.el,A=>{const{left:B,top:E,width:P,height:L}=A[0].contentRect;s.value=new Cn({x:B,y:E,width:P,height:L})});function c(A,B){var P,L;const E=B==="left"||B==="right"?(P=s.value)==null?void 0:P.width:(L=s.value)==null?void 0:L.height;return Td(A,E??0)}function f(A){return!s.value&&ns(A)?parseFloat(A):0}function v(A,B){return B?`calc(${B}% + ${A}px)`:`${A}px`}const m=ne(!1);let h=-1;ve(()=>{var A,B;return[(A=s.value)==null?void 0:A.width,(B=s.value)==null?void 0:B.height]},()=>{m.value=!0,window.clearTimeout(h),h=window.setTimeout(()=>m.value=!1,100)}),pt(()=>window.clearTimeout(h));const g=M(()=>{con Line truncated
`);c=m.pop();for(const h of m){const g=h.match(/^data:\s*(.*)$/m);if(g)try{const b=JSON.parse(g[1]);b.status==="progress"&&i?i(b):b.status==="completed"&&r?r(b):b.status==="error"&&o&&o(b)}catch(b){console.warn("SSE 파싱 에러:",b,g[1])}}}}function Jl(e){return`${Hi}/files/download?path=${encodeURIComponent(e)}`}function Cg(e){return`${Hi}/files/download-folder?path=${encodeURIComponent(e)}`}async function rD(e,t="selected_files.zip"){const n=await fetch(`${Hi}/files/download-batch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({paths:e,zip_name:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.detail||"일괄 다운로드 생성에 실패했습니다.")}const a=await n.blob(),l=window.URL.createObjectURL(a),i=document.createElement("a");i.href=l,i.download=t,document.body.appendChild(i),i.click(),window.URL.revokeObjectURL(l),document.body.removeChild(i)}const r0=Ka("v-alert-title");function oD(e,t){const n=oe(),a=ne(!1),l=ne(!1),{isIntersecting:i}=xr(n),r=oV(()=>e.value=!1,{duration:t}),o=z(()=>e.value&&i.value&&!a.value&&!l.value),u=z(()=>pe(t)>0?r.remaining.value/pe(t)*100:100);return ve([o,()=>pe(t)],()=>o.value?r.start():r.stop()),ve(e,()=>{a.value=!1,l.value=!1}),{rootEl:n,progress:u,events:{onPointerenter:()=>a.value=!0,onPointerleave:()=>a.value=!1,onFocusin:()=>l.value=!0,onFocusout(s){s.currentTarget.contains(s.relatedTarget)||(l.value=!1)}}}}const o0=te({iconSize:[Number,String],iconSizes:{type:Array,default:()=>[["x-small",10],["small",16],["default",24],["large",28],["x-large",32]]}},"iconSize");function u0(e,t){return{iconSize:M(()=>{const a=new Map(e.iconSizes),l=e.iconSize??t()??"default";return a.has(l)?a.get(l):l})}}const uD=["success","info","warning","error"],sD=te({border:{type:[Boolean,String],validator:e=>Bt(e)||["top","end","bottom","start"].includes(e)},borderColor:String,closable:Boolean,closeIcon:{type:De,default:"$close"},closeLabel:{type:String,default:"$vuetify.close"},icon:{type:[Boolean,String,Function,Object],default:null},modelValue:{type:Boolean,default:!0},prominent:Boolean,title:String,text:String,timeout:{type:[Number,String],default:-1},timer:[Boolean,String],type:{type:String,validator:e=>uD.includes(e)},...Ae(),...Ft(),...Jt(),...en(),...o0(),...Ya(),...Vr(),...It(),...Xe(),...st(),...Ga({transition:{component:Di}}),...ia({variant:"flat"})},"VAlert"),s0=de()({name:"VAlert",props:sD(),emits:{"click:close":e=>!0,"update:modelValue":e=>!0},setup(e,{emit:t,slots:n}){const a=Pe(e,"modelValue"),l=z(()=>{if(e.icon!==!1)return e.type?e.icon??`$${e.type}`:e.icon}),{iconSize:i}=u0(e,()=>e.prominent?44:void 0),{themeClasses:r}=ft(e),{colorClasses:o,colorStyles:u,variantClasses:d}=kl(()=>({color:e.color??e.type,variant:e.variant})),{densityClasses:s}=hn(e),{dimensionStyles:c}=Qt(e),{elevationClasses:f}=Yt(e),{locationStyles:v}=Gl(e),{positionClasses:m}=Ir(e),{roundedClasses:h,roundedStyles:g}=Tt(e),{textColorClasses:b,textColorStyles:p}=Gt(()=>e.borderColor),{t:k}=dt(),{rootEl:S,progress:w,events:x}=oD(a,()=>Number(e.timeout)),y=z(()=>({"aria-label":k(e.closeLabel),onClick(_){a.value=!1,t("click:close",_)}}));return()=>{const _=!!(n.prepend||l.value),V=!!(n.title||e.title),T=!!(n.close||e.closable),A=!!e.timer&&Number(e.timeout)>0,B={density:e.density,icon:l.value,size:e.iconSize||e.prominent?i.value:void 0};return C(fn,{transition:e.transition},{default:()=>[a.value&&C(e.tag,le({ref:S,class:["v-alert",e.border&&{"v-alert--border":!!e.border,[`v-alert--border-${e.border===!0?"start":e.border}`]:!0},{"v-alert--prominent":e.prominent},r.value,o.value,s.value,f.value,m.value,h.value,d.value,e.class],style:[u.value,c.value,v.value,g.value,e.style],role:"alert"},x),{default:()=>{var E,P;return[Sl(!1,"v-alert"),A&&e.timer!=="close"&&C(hl,{key:"timer",class:"v-alert__timer",absolute:!0,location:e.timer==="bottom"?"bottom":"top",modelValue:w.value,shape:"flat"},null),e.border&&I("div",{key:"border",class:fe(["v-alert__border",b.value]),style:Se(p.value)},null),_&&I("div",{key:"prepend",class:"v-alert__prepend"},[n.prepend?C(Ue,{key:"prepend-defaults",disabled:!l.value,defaults:{VIcon:{...B}}},n.prepend):C(We,le({key:"prepend-icon"},B),null)]),I("div",{class:"v-alert__content"},[V&&C(r0,{key:"title"},{default:()=>{var L;return[((L=n.title)==null?void 0:L.call(n))??e.title]}}),((E=n.text)==null?void 0:E.call(n))??e.text,(P=n.default)==null?void 0:P.call(n)]),n.append&&I("div",{key:"append",class:"v-alert__append"},[n.append()]),T&&I("div",{key:"close",class:"v-alert__close"},[n.close?C(Ue,{key:"close-defaults",defaults:{VBtn:{icon:e.closeIcon,size:"x-small",variant:"text"}}},{default:()=>{var L;return[(L=n.close)==null?void 0:L.call(n,{props:y.value,timerProgress:w.value})]}}):C(Fe,le({key:"close-btn",icon:e.closeIcon,size:"x-small",variant:"text"},y.value),null),A&&e.timer==="close"&&C(Gn,{key:"timer",class:"v-alert__timer v-alert__timer--close",modelValue:w.value,shape:"flat",width:2},null)])]}})]})}}}),c0=te({gridlines:{type: Line truncated
`,...e.delimiters??[]].map(_o).join("|");return ce.split(new RegExp(`(?:${Te})+`))}async function ze(ce){for(let xe of ce)xe=xe.trim(),xe&&(Je(ca(e,xe)),await Ne())}function tt(ce){c.value=!0,setTimeout(()=>{v.value=!0})}function wt(ce){var xe;if(v.value=!1,!((xe=i.value)!=null&&xe.$el.contains(ce.relatedTarget))){if(D(ce))return;c.value=!1}}let lt=0;function ye(){lt=performance.now()}function $e(ce){var Be;const xe=ce.relatedTarget,Te=(Be=r.value)==null?void 0:Be.contentEl;(Te!=null&&Te.contains(xe)||!xe&&performance.now()-lt<10)&&(c.value=!0)}return ve(c,(ce,xe)=>{if(!(ce||ce===xe)&&(m.value=-1,U.value=!1,P.value)){const Te=e.trimValues?P.value.trim():P.value;if(!Te){P.value="";return}if(e.multiple){Je(ca(e,Te));return}if(!A.value){Te!==P.value&&Je(ca(e,Te));return}x.value.some(({title:Be})=>Be===Te)?B.value="":Je(ca(e,Te))}}),ve(U,ce=>{if(ce||re(null),!e.hideSelected&&ce&&x.value.length&&f.value){const xe=Ce();rt&&!e.noAutoScroll&&window.requestAnimationFrame(()=>{var Te;xe>=0&&((Te=s.value)==null||Te.scrollToIndex(xe,"center"))})}ce&&(E.value=null)}),ve(g,(ce,xe)=>{U.value||c.value&&!xe.length&&ce.length&&(U.value=!0)}),me(()=>{const ce=!!(!e.hideNoData||N.value.length||n["prepend-item"]||n["append-item"]||n["no-data"]),xe=x.value.length>0,Te=Vn.filterProps(e),Be={search:P,filteredItems:R.value};return C(Vn,le({ref:i},Te,{form:e.autocomplete==="suppress"?"":void 0,name:e.autocomplete==="suppress"?e.name:void 0,modelValue:P.value,"onUpdate:modelValue":et=>P.value=et,focused:c.value,"onUpdate:focused":et=>c.value=et,validationValue:x.externalValue,counterValue:L.value,dirty:xe,class:["v-combobox",{"v-combobox--active-menu":U.value,"v-combobox--chips":!!e.chips,"v-combobox--selection-slot":!!A.value,"v-combobox--selecting-index":m.value>-1,[`v-combobox--${e.multiple?"multiple":"single"}`]:!0},e.class],style:e.style,readonly:y.isReadonly.value,placeholder:xe?void 0:e.placeholder,"onClick:clear":K,"onMousedown:control":X,onKeydown:ie,onPaste:_e,onBlur:$e,"aria-expanded":se.value,"aria-controls":Q.value}),{...n,default:({id:et})=>{var ot;return I(we,null,[H.value.map((qe,vt)=>I("input",{key:vt,type:"hidden",name:e.name,value:qe,form:e.form},null)),C(Ul,le({id:Z.value,ref:r,modelValue:U.value,"onUpdate:modelValue":qe=>U.value=qe,activator:"parent",captureFocus:!1,openOnArrow:!1,disabled:Y.value,_disableKeys:!0,eager:e.eager,maxHeight:310,openOnClick:!1,closeOnContentClick:!1,onAfterEnter:Ee,onAfterLeave:Le},e.menuProps,{contentClass:["v-combobox__content",l.value,(ot=e.menuProps)==null?void 0:ot.contentClass]}),{default:()=>[C(Wa,{onFocusin:tt,onKeydown:ee,onMousedown:ye},{default:()=>[n["menu-header"]&&I("header",{ref:u},[n["menu-header"](Be)]),ce&&C(gl,le({key:"combobox-list",ref:o,class:"v-list--navigable",filterable:!0,selected:H.value,selectStrategy:e.multiple?"independent":"single-independent",onMousedown:qe=>qe.preventDefault(),selectable:!!N.value.length,onFocusout:wt,tabindex:"-1","aria-live":"polite","aria-labelledby":`${et.value}-label`,"aria-multiselectable":e.multiple,color:e.itemColor??e.color},ae,e.listProps),{default:()=>{var qe,vt,An;return[(qe=n["prepend-item"])==null?void 0:qe.call(n),!N.value.length&&!e.hideNoData&&(((vt=n["no-data"])==null?void 0:vt.call(n))??C(Mn,{key:"no-data",title:a(e.noDataText)},null)),C(oc,{ref:s,renderless:!0,items:N.value,itemKey:"value"},{default:({item:Ct,index:rn,itemRef:jt})=>{var cn,lu,Pm;const Aa=rv(Ct.props),pl=le(Ct.props,{ref:jt,key:Ct.value,active:O.value&&Ct===$.value?!0:void 0,onClick:()=>Je(Ct,null),"aria-posinset":rn+1,"aria-setsize":N.value.length});return Ct.type==="divider"?((cn=n.divider)==null?void 0:cn.call(n,{props:Ct.raw,index:rn}))??C(Kt,le(Ct.props,{ref:jt,key:`divider-${rn}`}),null):Ct.type==="subheader"?((lu=n.subheader)==null?void 0:lu.call(n,{props:Ct.raw,index:rn}))??C(Dr,le(Ct.props,{ref:jt,key:`subheader-${rn}`}),null):((Pm=n.item)==null?void 0:Pm.call(n,{item:Ct.raw,internalItem:Ct,index:rn,props:pl}))??C(Mn,le(pl,{role:"option"}),{prepend:({isSelected:iu})=>I(we,null,[e.multiple&&!e.hideSelected?C(ta,{key:Ct.value,modelValue:iu,ripple:!1,tabindex:"-1","aria-hidden":!0,onClick:Lp=>Lp.preventDefault()},null):void 0,Aa.prependAvatar&&C(Zt,{image:Aa.prependAvatar},null),Aa.prependIcon&&C(We,{icon:Aa.prependIcon},null)]),title:()=>{var iu;return f.value?Ct.title:C(nu,{text:Ct.title,matches:(iu=W(Ct))==null?void 0:iu.title,markClass:"v-combobox__mask",matchAll:!0,ignoreCase:!0},null)}})}}),(An=n["append-item"])==null?void 0:An.call(n)]}}),n["menu-footer"]&&I("footer",{ref:d},[n["menu-footer"](Be)])]})]}),x.value.map((qe,vt)=>{var Aa,pl;function An(cn){cn.stopPropagation(),cn.preventDefault(),Je(qe,!1)}const Ct=le(On.filterProps(qe.props),{"onClick:close":An,onKeydown(cn){cn.key!=="Enter"&&cn.key!==" "||(cn.preventDefault(),cn.stopPropagation(),An(cn))},onMousedown(cn){cn.preventDefault(),cn.stopPropagation()},modelValue:!0,"onUpdate:modelValue":void 0}),rn=T.value?!!n.chip:!!n.selection,jt=rn?Gs(T.value?n.chip({item:qe.raw,internalItem: Line truncated
`):q}),w=z(()=>{let q=l.date();return q=l.setDate(q,1),q=l.setMonth(q,g.value),q=l.setYear(q,p.value),q}),x=z(()=>l.format(w.value,"monthAndYear")),y=z(()=>l.format(w.value,"monthShort")),_=z(()=>l.format(w.value,"year")),V=z(()=>`date-picker-header${k.value?"-reverse":""}-transition`),T=M(()=>{if(e.disabled)return!0;const q=[];if(u.value!=="month")q.push("prev-month","next-month","prev-year","next-year");else{let H=l.date();if(H=l.startOfMonth(H),H=l.setMonth(H,g.value),H=l.setYear(H,p.value),s.value){const $=l.addDays(l.startOfMonth(H),-1),O=l.addDays(l.startOfYear(H),-1);l.isAfter(s.value,$)&&q.push("prev-month"),l.isAfter(s.value,O)&&q.push("prev-year")}if(c.value){const $=l.addDays(l.endOfMonth(H),1),O=l.addDays(l.endOfYear(H),1);l.isAfter($,c.value)&&q.push("next-month"),l.isAfter(O,c.value)&&q.push("next-year")}}return q}),A=M(()=>e.allowedYears||P),B=M(()=>e.allowedMonths||L);function E(q,H){const $=e.allowedDates;if(!Ge($))return!0;const O=1+tP(l,q,H);for(let D=0;D<O;D++)if($(l.addDays(q,D)))return!0;return!1}function P(q){if(Ge(e.allowedDates)){const H=l.parseISO(`${q}-01-01`);return E(H,l.endOfYear(H))}if(Array.isArray(e.allowedDates)&&e.allowedDates.length){for(const H of e.allowedDates)if(l.getYear(l.date(H))===q)return!0;return!1}return!0}function L(q){if(Ge(e.allowedDates)){const H=String(q+1).padStart(2,"0"),$=l.parseISO(`${p.value}-${H}-01`);return E($,l.endOfMonth($))}if(Array.isArray(e.allowedDates)&&e.allowedDates.length){for(const H of e.allowedDates)if(l.getYear(l.date(H))===p.value&&l.getMonth(l.date(H))===q)return!0;return!1}return!0}function R(){g.value<11?g.value++:(p.value++,g.value=0,re()),J()}function W(){g.value>0?g.value--:(p.value--,g.value=11,re()),J()}function N(){if(p.value++,c.value){const q=String(g.value+1).padStart(2,"0"),H=l.parseISO(`${p.value}-${q}-01`);l.isAfter(H,c.value)&&(g.value=l.getMonth(c.value))}re()}function Y(){if(p.value--,s.value){const q=String(g.value+1).padStart(2,"0"),H=l.endOfMonth(l.parseISO(`${p.value}-${q}-01`));l.isAfter(s.value,H)&&(g.value=l.getMonth(s.value))}re()}const U=ne();function G(q){var H;(H=U.value)==null||H.focusItem(q)}function Z(){u.value="month"}function se(){u.value="month",Ne(()=>{var q;return(q=U.value)==null?void 0:q.focusGrid()})}function Q(){u.value=u.value==="months"?"month":"months"}function ae(){u.value=u.value==="year"?"month":"year"}function J(){u.value==="months"&&Q()}function re(){u.value==="year"&&ae()}return ve(o,(q,H)=>{if(e.noAutoNavigation)return;const $=gt(H),O=gt(q);if(!O.length)return;const D=l.date($[$.length-1]),F=l.date(O[O.length-1]);if(l.isSameDay(D,F))return;const K=l.getMonth(F),X=l.getYear(F);K!==g.value&&(g.value=K,J()),X!==p.value&&(p.value=X,re()),k.value=l.isBefore(D,F)}),me(()=>{const q=pi.filterProps(e),H=He(vf.filterProps(e),["viewMode"]),$=mf.filterProps(e),O=hf.filterProps(e),D=He(gf.filterProps(e),["modelValue"]),F=He(yf.filterProps(e),["modelValue"]),K={color:m.value,header:S.value,transition:V.value};return C(pi,le(q,{color:m.value,class:["v-date-picker",`v-date-picker--${u.value}`,{"v-date-picker--show-week":e.showWeek},r.value,e.class],style:[{"--v-date-picker-landscape-header-width":ge(e.landscapeHeaderWidth)},e.style]}),{title:()=>{var X;return((X=n.title)==null?void 0:X.call(n))??I("div",{class:"v-date-picker__title"},[i(e.title)])},header:()=>n.header?C(Ue,{defaults:{VDatePickerHeader:{...K}}},{default:()=>{var X;return[(X=n.header)==null?void 0:X.call(n,K)]}}):C(mf,le({key:"header"},$,K,{onClick:u.value!=="month"?Z:void 0}),{prepend:n.prepend,append:n.append}),default:()=>I(we,null,[C(vf,le(H,{disabled:T.value,viewMode:u.value,text:x.value,monthText:y.value,yearText:_.value,"onClick:next":R,"onClick:prev":W,"onClick:nextYear":N,"onClick:prevYear":Y,"onClick:month":Q,"onClick:year":ae}),{default:n.controls}),C(Mo,{hideOnLeave:!0},{default:()=>[u.value==="months"?C(gf,le({key:"date-picker-months"},D,{modelValue:g.value,"onUpdate:modelValue":[X=>g.value=X,J],min:s.value,max:c.value,year:p.value,allowedMonths:B.value,onEscape:se}),{month:n.month}):u.value==="year"?C(yf,le({key:"date-picker-years"},F,{modelValue:p.value,"onUpdate:modelValue":[X=>p.value=X,re],min:s.value,max:c.value,allowedYears:A.value,onEscape:se}),{year:n.year}):C(hf,le({ref:U,key:"date-picker-month"},fr(a,[/^on.+:day$/])[0],O,{modelValue:o.value,"onUpdate:modelValue":X=>o.value=X,month:g.value,"onUpdate:month":[X=>g.value=X,J],year:p.value,"onUpdate:year":[X=>p.value=X,re],previewValue:d.value,"onUpdate:previewValue":X=>d.value=X,onBoundaryNavigate:X=>t("boundary-navigate",X),min:s.value,max:c.value}),{day:n.day})]})]),actions:n.actions})}),{focusDate:G}}});function dF(e,t,n,a,l){var c;const i=e.max??10**e.size-1,r=Math.min(i,((c=e.softMax)==null?void 0:c.call(e,l))??i);let o=n,u="",d=!1;for(;o<t.length&&!d&&/\d/.test(t[o]);){if(Number(u+t[o])>r){u=String(r).padStart(u.length+1,"0"),o++,d=!0;break}u+=t[o++],e.min&&!Number(u)&&(u="0"),d=u.length===e.size}const s=o;if(a)for(;o<t.length&&!/\d/.test(t[o]);)o++;else o<t Line truncated
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧲</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Transmission Magnet Manager</title>
<!-- Pretendard web font for Korean -->
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
<script type="module" crossorigin src="/assets/index-DGu27qaY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cn6C5Cpg.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧲</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Transmission Magnet Manager</title>
<!-- Pretendard web font for Korean -->
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+1457
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "transmission-magnet-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:all": "bash ../start_dev.sh",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"lucide-vue-next": "^0.468.0",
"vite-plugin-vuetify": "^2.1.3",
"vue": "^3.5.12",
"vuetify": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.3"
}
}
+281
View File
@@ -0,0 +1,281 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Header from './components/Header.vue';
import MagnetForm from './components/MagnetForm.vue';
import TorrentList from './components/TorrentList.vue';
import FileViewer from './components/FileViewer.vue';
import {
fetchStatus,
fetchTorrents,
addMagnet,
removeTorrent,
startTorrent,
stopTorrent
} from './api/torrent';
// 사이드 메뉴 & 탭 상태
const drawer = ref(true);
const currentTab = ref('torrents'); // 'torrents' | 'files'
const status = ref({ connected: false, version: '', torrents_count: 0 });
const torrents = ref([]);
const loading = ref(false);
// Vuetify Snackbar
const snackbar = ref({
show: false,
message: '',
color: 'info',
icon: 'mdi-information-outline',
timeout: 3500,
});
let pollTimer = null;
const showToast = (message, type = 'info', timeout = 3500) => {
const iconMap = {
success: 'mdi-check-circle-outline',
error: 'mdi-alert-circle-outline',
info: 'mdi-information-outline',
warning: 'mdi-alert-outline',
};
snackbar.value = {
show: true,
message,
color: type,
icon: iconMap[type] || 'mdi-information-outline',
timeout,
};
};
const handleNotify = ({ message, type }) => {
showToast(message, type);
};
const loadData = async (silent = false) => {
if (!silent) loading.value = true;
try {
const [statusRes, torrentsRes] = await Promise.all([
fetchStatus(),
fetchTorrents(),
]);
if (statusRes.success) {
status.value = statusRes.data;
} else {
status.value.connected = false;
}
if (torrentsRes.success) {
torrents.value = torrentsRes.data;
}
} catch (err) {
status.value.connected = false;
if (!silent) {
showToast(err.message || '데이터를 가져오는데 실패했습니다.', 'error');
}
} finally {
if (!silent) loading.value = false;
}
};
const handleAddMagnet = async (formData) => {
try {
const res = await addMagnet(formData);
showToast(res.message || '마그넷이 추가되었습니다.', 'success');
await loadData(true);
} catch (err) {
showToast(err.message || '마그넷 추가 실패', 'error');
throw err;
}
};
const handleStart = async (id) => {
try {
await startTorrent(id);
showToast('토렌트를 시작했습니다.', 'success', 2000);
await loadData(true);
} catch (err) {
showToast(err.message || '토렌트 시작 실패', 'error');
}
};
const handleStop = async (id) => {
try {
await stopTorrent(id);
showToast('토렌트를 일시정지했습니다.', 'info', 2000);
await loadData(true);
} catch (err) {
showToast(err.message || '토렌트 일시정지 실패', 'error');
}
};
const handleRemove = async ({ id, deleteData }) => {
try {
await removeTorrent(id, deleteData);
showToast(`토렌트를 삭제했습니다. (데이터 삭제: ${deleteData ? '예' : '아니오'})`, 'success');
await loadData(true);
} catch (err) {
showToast(err.message || '토렌트 삭제 실패', 'error');
}
};
onMounted(() => {
loadData();
// 4초마다 토렌트 정보 자동 갱신
pollTimer = setInterval(() => {
if (currentTab.value === 'torrents') {
loadData(true);
}
}, 4000);
});
onUnmounted(() => {
if (pollTimer) clearInterval(pollTimer);
});
</script>
<template>
<v-app>
<!-- 사이드 메뉴 (Navigation Drawer) -->
<v-navigation-drawer v-model="drawer" width="260">
<div class="pa-4 d-flex align-center border-b">
<v-avatar color="primary" variant="flat" size="36" class="mr-3">
<v-icon icon="mdi-download-network" color="white" size="22"></v-icon>
</v-avatar>
<div>
<div class="text-subtitle-2 font-weight-bold" style="line-height: 1.2;">
Transmission
</div>
<div class="text-caption text-medium-emphasis">
Torrent & File Hub
</div>
</div>
</div>
<!-- 메뉴 목록 -->
<v-list density="comfortable" nav class="pa-2">
<!-- 1. 토렌트 관리 -->
<v-list-item
value="torrents"
:active="currentTab === 'torrents'"
color="primary"
rounded="lg"
@click="currentTab = 'torrents'"
>
<template #prepend>
<v-icon icon="mdi-magnet" size="22"></v-icon>
</template>
<v-list-item-title class="font-weight-medium">토렌트</v-list-item-title>
<template #append>
<v-badge
v-if="torrents.length > 0"
:content="torrents.length"
color="primary"
inline
></v-badge>
</template>
</v-list-item>
<!-- 2. 다운로드 파일 뷰어 -->
<v-list-item
value="files"
:active="currentTab === 'files'"
color="primary"
rounded="lg"
@click="currentTab = 'files'"
>
<template #prepend>
<v-icon icon="mdi-folder-eye-outline" size="22"></v-icon>
</template>
<v-list-item-title class="font-weight-medium">다운로드 파일 뷰어</v-list-item-title>
</v-list-item>
</v-list>
<!-- 사이드바 하단 정보 -->
<template #append>
<div class="pa-3 border-t">
<div class="d-flex align-center justify-space-between text-caption text-medium-emphasis">
<span>Transmission 상태</span>
<v-chip
:color="status.connected ? 'success' : 'error'"
size="x-small"
variant="flat"
class="font-weight-bold"
>
{{ status.connected ? '온라인' : '오프라인' }}
</v-chip>
</div>
</div>
</template>
</v-navigation-drawer>
<!-- 상단 헤더 -->
<Header
:status="status"
:loading="loading"
:current-tab="currentTab"
@toggle-drawer="drawer = !drawer"
@refresh="() => loadData(false)"
/>
<!-- 메인 컨텐츠 영역 -->
<v-main>
<v-container class="py-6" style="max-width: 1040px;">
<!-- 토렌트 탭 -->
<div v-if="currentTab === 'torrents'" class="tab-content">
<MagnetForm @add="handleAddMagnet" />
<TorrentList
:torrents="torrents"
:loading="loading"
@start="handleStart"
@stop="handleStop"
@remove="handleRemove"
/>
</div>
<!-- 다운로드 파일 뷰어 탭 -->
<div v-else-if="currentTab === 'files'" class="tab-content">
<FileViewer @notify="handleNotify" />
</div>
</v-container>
</v-main>
<!-- Vuetify Snackbar 알림 -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
location="top right"
rounded="lg"
elevation="4"
>
<div class="d-flex align-center">
<v-icon :icon="snackbar.icon" class="mr-2"></v-icon>
<span class="font-weight-medium">{{ snackbar.message }}</span>
</div>
<template #actions>
<v-btn
variant="text"
icon="mdi-close"
size="small"
@click="snackbar.show = false"
></v-btn>
</template>
</v-snackbar>
</v-app>
</template>
<style>
/* 전역 Pretendard 폰트 적용 */
html, body, .v-application {
font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif !important;
}
.border-b {
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.border-t {
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
</style>
+141
View File
@@ -0,0 +1,141 @@
const BASE_URL = '/api';
export async function fetchFiles(path = '') {
const query = path ? `?path=${encodeURIComponent(path)}` : '';
const res = await fetch(`${BASE_URL}/files${query}`);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || '파일 목록을 가져올 수 없습니다.');
}
return await res.json();
}
export async function deleteFileOrFolder(path) {
const res = await fetch(`${BASE_URL}/files?path=${encodeURIComponent(path)}`, {
method: 'DELETE',
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || '파일 삭제에 실패했습니다.');
}
return await res.json();
}
export async function checkArchive(path) {
const res = await fetch(`${BASE_URL}/files/check-archive?path=${encodeURIComponent(path)}`);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || '압축 파일 상태 검사에 실패했습니다.');
}
return await res.json();
}
export async function extractArchive({ path, password = null, create_subfolder = true, custom_dest = null, delete_archive = false }) {
const res = await fetch(`${BASE_URL}/files/extract`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path,
password: password ? password.trim() : null,
create_subfolder,
custom_dest,
delete_archive,
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || '압축 해제에 실패했습니다.');
}
return data;
}
export async function extractArchiveStream({
path,
password = null,
create_subfolder = true,
custom_dest = null,
delete_archive = false,
onProgress,
onSuccess,
onError,
}) {
const res = await fetch(`${BASE_URL}/files/extract-stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path,
password: password ? password.trim() : null,
create_subfolder,
custom_dest,
delete_archive,
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || '압축 해제 스트림 연결에 실패했습니다.');
}
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop(); // 아직 완료되지 않은 마지막 조각은 보관
for (const chunk of lines) {
const match = chunk.match(/^data:\s*(.*)$/m);
if (match) {
try {
const payload = JSON.parse(match[1]);
if (payload.status === 'progress' && onProgress) {
onProgress(payload);
} else if (payload.status === 'completed' && onSuccess) {
onSuccess(payload);
} else if (payload.status === 'error' && onError) {
onError(payload);
}
} catch (e) {
console.warn('SSE 파싱 에러:', e, match[1]);
}
}
}
}
}
export function getDownloadUrl(path) {
return `${BASE_URL}/files/download?path=${encodeURIComponent(path)}`;
}
export function getFolderZipUrl(path) {
return `${BASE_URL}/files/download-folder?path=${encodeURIComponent(path)}`;
}
export async function downloadBatchZip(paths, zipName = 'selected_files.zip') {
const res = await fetch(`${BASE_URL}/files/download-batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths, zip_name: zipName }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || '일괄 다운로드 생성에 실패했습니다.');
}
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = zipName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
+59
View File
@@ -0,0 +1,59 @@
const BASE_URL = '/api';
export async function fetchStatus() {
const res = await fetch(`${BASE_URL}/status`);
if (!res.ok) throw new Error('상태 정보를 가져올 수 없습니다.');
return await res.json();
}
export async function fetchTorrents() {
const res = await fetch(`${BASE_URL}/torrents`);
if (!res.ok) throw new Error('토렌트 목록을 불러오지 못했습니다.');
return await res.json();
}
export async function addMagnet({ magnet, download_dir, paused }) {
const res = await fetch(`${BASE_URL}/torrents/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ magnet, download_dir, paused }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || '토렌트 추가에 실패했습니다.');
}
return data;
}
export async function removeTorrent(torrentId, deleteData = false) {
const res = await fetch(`${BASE_URL}/torrents/${torrentId}?delete_data=${deleteData}`, {
method: 'DELETE',
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || '토렌트 삭제에 실패했습니다.');
}
return data;
}
export async function startTorrent(torrentId) {
const res = await fetch(`${BASE_URL}/torrents/${torrentId}/start`, {
method: 'POST',
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || '토렌트 시작 실패');
}
return data;
}
export async function stopTorrent(torrentId) {
const res = await fetch(`${BASE_URL}/torrents/${torrentId}/stop`, {
method: 'POST',
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || '토렌트 일시정지 실패');
}
return data;
}
+885
View File
@@ -0,0 +1,885 @@
<script setup>
import { ref, computed, nextTick, onMounted, watch } from 'vue';
import {
fetchFiles,
deleteFileOrFolder,
getDownloadUrl,
getFolderZipUrl,
downloadBatchZip,
checkArchive,
extractArchiveStream
} from '../api/files';
const emit = defineEmits(['notify']);
const currentPath = ref('');
const items = ref([]);
const totalSize = ref(0);
const loading = ref(false);
// 체크박스 다중 선택 상태
const selectedPaths = ref([]);
const isBatchDownloading = ref(false);
// 삭제 다이얼로그
const deleteDialog = ref(false);
const itemToDelete = ref(null);
const isBatchDelete = ref(false);
// 미리보기 다이얼로그
const previewDialog = ref(false);
const previewItem = ref(null);
// 압축 해제 다이얼로그 & 상태
const extractDialog = ref(false);
const archiveToExtract = ref(null);
const createSubfolder = ref(true);
const deleteArchiveAfter = ref(false);
const isExtracting = ref(false);
const extractPassword = ref('');
const showPassword = ref(false);
const isEncryptedArchive = ref(false);
const checkingArchive = ref(false);
const extractError = ref('');
const passwordInputRef = ref(null);
// 실시간 진행률 상태
const progressPercent = ref(0);
const currentExtractingFile = ref('');
const filesDoneCount = ref(0);
const loadFiles = async (path = '') => {
loading.value = true;
selectedPaths.value = []; // 폴더 변경 시 선택 초기화
try {
const res = await fetchFiles(path);
if (res.success) {
currentPath.value = res.data.current_path;
items.value = res.data.items;
totalSize.value = res.data.total_size;
}
} catch (err) {
emit('notify', { message: err.message || '파일 목록 로드 실패', type: 'error' });
} finally {
loading.value = false;
}
};
// 전체 선택 여부 계산
const isAllSelected = computed(() => {
return items.value.length > 0 && selectedPaths.value.length === items.value.length;
});
const isSomeSelected = computed(() => {
return selectedPaths.value.length > 0 && selectedPaths.value.length < items.value.length;
});
const toggleSelectAll = () => {
if (isAllSelected.value) {
selectedPaths.value = [];
} else {
selectedPaths.value = items.value.map(i => i.path);
}
};
// 선택된 항목들의 총 용량 계산
const selectedTotalSize = computed(() => {
const selectedSet = new Set(selectedPaths.value);
return items.value
.filter(i => selectedSet.has(i.path) && !i.is_dir)
.reduce((acc, cur) => acc + (cur.size || 0), 0);
});
// 선택 항목 ZIP 일괄 다운로드
const handleBatchDownload = async () => {
if (selectedPaths.value.length === 0) return;
isBatchDownloading.value = true;
try {
const currentFolderName = currentPath.value.split('/').pop() || 'downloads';
const zipName = `${currentFolderName}_선택항목.zip`;
await downloadBatchZip(selectedPaths.value, zipName);
emit('notify', { message: '일괄 ZIP 다운로드가 시작되었습니다.', type: 'success' });
} catch (err) {
emit('notify', { message: err.message || '일괄 다운로드 실패', type: 'error' });
} finally {
isBatchDownloading.value = false;
}
};
// 선택 항목 일괄 삭제 모달 오픈
const openBatchDelete = () => {
if (selectedPaths.value.length === 0) return;
isBatchDelete.value = true;
deleteDialog.value = true;
};
// 브레드크럼 파싱
const breadcrumbs = computed(() => {
const parts = currentPath.value ? currentPath.value.split('/') : [];
const crumbs = [{ title: '다운로드 루트', path: '' }];
let accumulated = '';
for (const part of parts) {
if (!part) continue;
accumulated = accumulated ? `${accumulated}/${part}` : part;
crumbs.push({
title: part,
path: accumulated,
});
}
return crumbs;
});
const navigateTo = (path) => {
loadFiles(path);
};
const navigateUp = () => {
if (!currentPath.value) return;
const parts = currentPath.value.split('/');
parts.pop();
loadFiles(parts.join('/'));
};
const handleItemClick = (item) => {
if (item.is_dir) {
loadFiles(item.path);
} else if (item.category === 'archive') {
openExtract(item);
} else {
handlePreview(item);
}
};
const openExtract = async (item) => {
archiveToExtract.value = item;
createSubfolder.value = true;
deleteArchiveAfter.value = false;
extractPassword.value = '';
showPassword.value = false;
extractError.value = '';
isEncryptedArchive.value = false;
checkingArchive.value = true;
progressPercent.value = 0;
currentExtractingFile.value = '';
filesDoneCount.value = 0;
extractDialog.value = true;
try {
const res = await checkArchive(item.path);
if (res.success && res.data.is_encrypted) {
isEncryptedArchive.value = true;
nextTick(() => {
passwordInputRef.value?.focus();
});
}
} catch (err) {
console.warn('암호화 검사 실패:', err);
} finally {
checkingArchive.value = false;
}
};
const confirmExtract = async () => {
if (!archiveToExtract.value) return;
extractError.value = '';
isExtracting.value = true;
progressPercent.value = 0;
currentExtractingFile.value = '압축 해제 시작 중...';
filesDoneCount.value = 0;
try {
await extractArchiveStream({
path: archiveToExtract.value.path,
password: extractPassword.value,
create_subfolder: createSubfolder.value,
delete_archive: deleteArchiveAfter.value,
onProgress: (p) => {
progressPercent.value = p.percent || 0;
if (p.current_file) {
currentExtractingFile.value = p.current_file;
}
if (p.files_done !== undefined) {
filesDoneCount.value = p.files_done;
}
},
onSuccess: async (res) => {
progressPercent.value = 100;
emit('notify', { message: res.message || '압축 해제가 완료되었습니다.', type: 'success' });
setTimeout(async () => {
extractDialog.value = false;
archiveToExtract.value = null;
isExtracting.value = false;
await loadFiles(currentPath.value);
}, 600);
},
onError: (errPayload) => {
isExtracting.value = false;
const errType = errPayload.error_type || '';
const errMsg = errPayload.message || '압축 해제 실패';
if (errType === 'PASSWORD_REQUIRED' || errType === 'WRONG_PASSWORD') {
isEncryptedArchive.value = true;
extractError.value = errMsg;
nextTick(() => {
passwordInputRef.value?.focus();
});
} else {
extractError.value = errMsg;
emit('notify', { message: errMsg, type: 'error' });
}
},
});
} catch (err) {
isExtracting.value = false;
extractError.value = err.message || '압축 해제 중 네트워크 오류';
emit('notify', { message: err.message || '압축 해제 중 오류 발생', type: 'error' });
}
};
const handlePreview = (item) => {
if (['video', 'audio', 'image', 'document'].includes(item.category)) {
previewItem.value = item;
previewDialog.value = true;
} else {
window.open(getDownloadUrl(item.path), '_blank');
}
};
const openDelete = (item) => {
isBatchDelete.value = false;
itemToDelete.value = item;
deleteDialog.value = true;
};
const confirmDelete = async () => {
try {
if (isBatchDelete.value) {
// 선택 항목 일괄 삭제
for (const p of selectedPaths.value) {
await deleteFileOrFolder(p);
}
emit('notify', { message: `${selectedPaths.value.length}개 항목 일괄 삭제 완료`, type: 'success' });
selectedPaths.value = [];
} else if (itemToDelete.value) {
await deleteFileOrFolder(itemToDelete.value.path);
emit('notify', { message: `'${itemToDelete.value.name}' 삭제 완료`, type: 'success' });
}
await loadFiles(currentPath.value);
} catch (err) {
emit('notify', { message: err.message || '삭제 실패', type: 'error' });
} finally {
deleteDialog.value = false;
itemToDelete.value = null;
isBatchDelete.value = false;
}
};
const formatBytes = (bytes) => {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
const formatDate = (timestamp) => {
if (!timestamp) return '';
const d = new Date(timestamp * 1000);
return d.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
const getCategoryIcon = (category) => {
switch (category) {
case 'folder': return { icon: 'mdi-folder', color: 'amber-darken-1' };
case 'video': return { icon: 'mdi-file-video', color: 'deep-purple-accent-2' };
case 'audio': return { icon: 'mdi-file-music', color: 'green-accent-4' };
case 'image': return { icon: 'mdi-file-image', color: 'light-blue-accent-3' };
case 'archive': return { icon: 'mdi-folder-zip', color: 'orange-accent-3' };
case 'document': return { icon: 'mdi-file-document-outline', color: 'teal-accent-3' };
default: return { icon: 'mdi-file-outline', color: 'blue-grey-lighten-1' };
}
};
onMounted(() => {
loadFiles();
});
</script>
<template>
<v-card rounded="lg" elevation="1" border>
<!-- 상단 툴바: 브레드크럼 & 액션 -->
<v-card-item class="pb-2">
<div class="d-flex align-center justify-space-between flex-wrap gap-2">
<div class="d-flex align-center flex-wrap">
<v-avatar color="primary" variant="tonal" size="32" class="mr-2">
<v-icon icon="mdi-folder-eye-outline" size="20"></v-icon>
</v-avatar>
<span class="text-h6 font-weight-bold mr-3">다운로드 파일 뷰어</span>
<!-- 상위 폴더 이동 버튼 -->
<v-btn
icon="mdi-arrow-up"
size="small"
variant="tonal"
density="comfortable"
:disabled="!currentPath"
title="상위 폴더로 이동"
class="mr-2"
@click="navigateUp"
></v-btn>
</div>
<div class="d-flex align-center gap-2 flex-wrap">
<v-chip size="small" variant="tonal" color="secondary" class="font-weight-medium">
항목 {{ items.length }}개 • 총 {{ formatBytes(totalSize) }}
</v-chip>
<!-- 🌟 현재 폴더 전체 ZIP 다운로드 버튼 -->
<v-btn
v-if="currentPath && items.length > 0"
color="primary"
variant="tonal"
size="small"
prepend-icon="mdi-folder-zip-outline"
:href="getFolderZipUrl(currentPath)"
target="_blank"
title="현재 폴더 안의 모든 파일을 하나의 ZIP으로 묶어서 다운로드"
>
폴더 전체 ZIP 받기
</v-btn>
<v-btn
icon="mdi-refresh"
size="small"
variant="tonal"
:loading="loading"
title="새로고침"
@click="loadFiles(currentPath)"
></v-btn>
</div>
</div>
<!-- 브레드크럼 경로 네비게이션 -->
<div class="d-flex align-center flex-wrap mt-3 px-2 py-1 bg-surface-variant rounded text-caption">
<v-icon icon="mdi-home" size="16" class="mr-1 text-medium-emphasis"></v-icon>
<template v-for="(crumb, idx) in breadcrumbs" :key="crumb.path">
<span
class="crumb-link font-weight-medium cursor-pointer"
:class="{ 'text-primary font-weight-bold': idx === breadcrumbs.length - 1 }"
@click="navigateTo(crumb.path)"
>
{{ crumb.title }}
</span>
<span v-if="idx < breadcrumbs.length - 1" class="mx-1 text-medium-emphasis">/</span>
</template>
</div>
<!-- 🌟 다중 선택 시 나타나는 일괄 액션 바 -->
<v-slide-y-transition>
<div
v-if="selectedPaths.length > 0"
class="d-flex align-center justify-space-between flex-wrap gap-2 mt-3 pa-2 rounded bg-primary-container"
>
<div class="d-flex align-center gap-2">
<v-icon icon="mdi-checkbox-marked-circle" color="primary"></v-icon>
<span class="text-body-2 font-weight-bold">
{{ selectedPaths.length }}개 항목 선택됨
</span>
<span v-if="selectedTotalSize > 0" class="text-caption text-medium-emphasis">
({{ formatBytes(selectedTotalSize) }})
</span>
</div>
<div class="d-flex align-center gap-2">
<!-- 선택 항목 ZIP 다운로드 버튼 -->
<v-btn
color="primary"
variant="elevated"
size="small"
prepend-icon="mdi-archive-arrow-down"
:loading="isBatchDownloading"
@click="handleBatchDownload"
>
선택 항목 ZIP 다운로드
</v-btn>
<!-- 선택 항목 일괄 삭제 버튼 -->
<v-btn
color="error"
variant="tonal"
size="small"
prepend-icon="mdi-trash-can-outline"
@click="openBatchDelete"
>
일괄 삭제
</v-btn>
<!-- 선택 해제 -->
<v-btn
variant="text"
size="small"
@click="selectedPaths = []"
>
선택 해제
</v-btn>
</div>
</div>
</v-slide-y-transition>
</v-card-item>
<v-divider></v-divider>
<!-- 파일 테이블 / 리스트 -->
<v-card-text class="pa-0">
<div v-if="loading && items.length === 0" class="text-center py-10">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<div class="text-caption text-medium-emphasis mt-2">파일 목록 불러오는 중...</div>
</div>
<div v-else-if="items.length === 0" class="text-center py-10">
<v-icon icon="mdi-folder-open-outline" size="56" color="secondary" class="mb-2 opacity-50"></v-icon>
<div class="text-subtitle-2 font-weight-medium">폴더가 비어 있습니다.</div>
</div>
<v-table v-else hover density="comfortable">
<thead>
<tr>
<!-- 전체 선택 체크박스 열 -->
<th class="text-center" style="width: 48px;">
<v-checkbox-btn
:model-value="isAllSelected"
:indeterminate="isSomeSelected"
color="primary"
@click.stop="toggleSelectAll"
></v-checkbox-btn>
</th>
<th class="text-left" style="min-width: 250px;">이름</th>
<th class="text-right" style="width: 120px;">크기</th>
<th class="text-left d-none d-md-table-cell" style="width: 180px;">수정일시</th>
<th class="text-center" style="width: 160px;">작업</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in items"
:key="item.path"
class="file-row"
:class="{ 'selected-row': selectedPaths.includes(item.path) }"
>
<!-- 개별 선택 체크박스 -->
<td class="text-center" @click.stop>
<v-checkbox-btn
v-model="selectedPaths"
:value="item.path"
color="primary"
></v-checkbox-btn>
</td>
<!-- 파일/폴더 이름 & 아이콘 -->
<td>
<div
class="d-flex align-center cursor-pointer py-1"
@click="handleItemClick(item)"
>
<v-icon
:icon="getCategoryIcon(item.category).icon"
:color="getCategoryIcon(item.category).color"
class="mr-3 flex-shrink-0"
size="22"
></v-icon>
<span class="text-body-2 font-weight-medium text-truncate" :title="item.name">
{{ item.name }}
</span>
<v-chip
v-if="item.category === 'archive'"
size="x-small"
color="orange"
variant="outlined"
class="ml-2 flex-shrink-0"
>
압축 파일
</v-chip>
</div>
</td>
<!-- 크기 -->
<td class="text-right text-caption text-medium-emphasis">
<span v-if="!item.is_dir">{{ formatBytes(item.size) }}</span>
<span v-else class="text-disabled">-</span>
</td>
<!-- 수정일시 -->
<td class="text-left text-caption text-medium-emphasis d-none d-md-table-cell">
{{ formatDate(item.modified) }}
</td>
<!-- 작업 버튼 -->
<td class="text-center">
<div class="d-flex align-center justify-center gap-1">
<!-- 폴더 열기 -->
<v-btn
v-if="item.is_dir"
icon="mdi-folder-open"
size="x-small"
variant="text"
color="primary"
title="폴더 열기"
@click.stop="loadFiles(item.path)"
></v-btn>
<!-- 🌟 폴더 ZIP 다운로드 버튼 -->
<v-btn
v-if="item.is_dir"
icon="mdi-folder-zip-outline"
size="x-small"
variant="text"
color="teal"
title="이 폴더를 ZIP으로 한 번에 다운로드"
:href="getFolderZipUrl(item.path)"
target="_blank"
></v-btn>
<!-- 압축 풀기 버튼 (압축 파일 전용) -->
<v-btn
v-if="item.category === 'archive'"
icon="mdi-archive-arrow-down-outline"
size="x-small"
variant="tonal"
color="orange-darken-1"
title="압축 풀기"
@click.stop="openExtract(item)"
></v-btn>
<!-- 개별 파일 다운로드 -->
<v-btn
v-if="!item.is_dir"
icon="mdi-download"
size="x-small"
variant="text"
color="primary"
title="다운로드"
:href="getDownloadUrl(item.path)"
target="_blank"
></v-btn>
<!-- 미디어 미리보기 -->
<v-btn
v-if="!item.is_dir && ['video', 'audio', 'image', 'document'].includes(item.category)"
icon="mdi-eye-outline"
size="x-small"
variant="text"
color="info"
title="미리보기 / 재생"
@click.stop="handlePreview(item)"
></v-btn>
<!-- 삭제 -->
<v-btn
icon="mdi-trash-can-outline"
size="x-small"
variant="text"
color="error"
title="삭제"
@click.stop="openDelete(item)"
></v-btn>
</div>
</td>
</tr>
</tbody>
</v-table>
</v-card-text>
<!-- 📦 압축 풀기 확인 & 실시간 진행률 다이얼로그 -->
<v-dialog v-model="extractDialog" max-width="540" :persistent="isExtracting">
<v-card rounded="lg">
<v-card-item class="pb-2">
<template #prepend>
<v-icon icon="mdi-archive-arrow-down-outline" color="orange-darken-1" size="28" class="mr-2"></v-icon>
</template>
<v-card-title class="text-h6 font-weight-bold">
압축 파일 풀기
</v-card-title>
</v-card-item>
<v-card-text>
<!-- 파일 정보 표시 -->
<div class="pa-3 rounded bg-surface-variant text-body-2 font-weight-medium text-truncate mb-3">
📦 {{ archiveToExtract?.name }}
<span class="text-caption text-medium-emphasis ml-2">({{ formatBytes(archiveToExtract?.size) }})</span>
</div>
<!-- 암호화된 파일 감지 알림 -->
<v-alert
v-if="isEncryptedArchive"
type="warning"
variant="tonal"
density="compact"
icon="mdi-lock-alert"
class="mb-3 text-caption"
>
<strong>암호로 보호된 파일:</strong> 이 파일의 압축을 해제하려면 올바른 암호(비밀번호)를 입력해야 합니다.
</v-alert>
<!-- 암호 입력 필드 -->
<div class="mb-3">
<v-text-field
ref="passwordInputRef"
v-model="extractPassword"
:label="isEncryptedArchive ? '압축 해제 암호 (필수)' : '압축 해제 암호 (암호가 걸려있는 경우 입력)'"
placeholder="비밀번호 입력"
:type="showPassword ? 'text' : 'password'"
prepend-inner-icon="mdi-lock-outline"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
variant="outlined"
density="comfortable"
clearable
:error="!!extractError"
:error-messages="extractError"
:disabled="isExtracting"
@click:append-inner="showPassword = !showPassword"
@keyup.enter="confirmExtract"
></v-text-field>
</div>
<!-- 옵션 체크박스들 -->
<v-checkbox
v-model="createSubfolder"
label="새 하위 폴더를 생성하여 압축 풀기"
color="primary"
density="compact"
hide-details
class="mb-1"
:disabled="isExtracting"
></v-checkbox>
<v-checkbox
v-model="deleteArchiveAfter"
label="압축 해제 완료 후 원본 압축 파일 삭제"
color="error"
density="compact"
hide-details
:disabled="isExtracting"
></v-checkbox>
<!-- 🚀 실시간 압축 해제 진행 과정 (Progress) -->
<div v-if="isExtracting" class="mt-4 pt-3 border-t">
<div class="d-flex justify-space-between align-center mb-1">
<span class="text-caption font-weight-bold d-flex align-center">
<v-icon icon="mdi-progress-download" size="16" class="mr-1 text-orange-darken-1 rotating"></v-icon>
<span>압축 해제 진행 중</span>
</span>
<span class="text-subtitle-2 font-weight-black text-orange-darken-1">
{{ progressPercent }}%
</span>
</div>
<!-- 실시간 진행률 프로그레스 바 -->
<v-progress-linear
:model-value="progressPercent"
color="orange-darken-1"
height="20"
rounded
striped
class="mb-2"
>
<template #default="{ value }">
<strong class="text-caption text-white font-weight-bold">{{ Math.ceil(value) }}%</strong>
</template>
</v-progress-linear>
<!-- 현재 풀고 있는 파일명 및 처리 개수 -->
<div class="d-flex justify-space-between align-center text-caption text-medium-emphasis">
<div class="text-truncate mr-2" style="max-width: 360px;" :title="currentExtractingFile">
<v-icon icon="mdi-file-outline" size="14" class="mr-1"></v-icon>
<span class="font-weight-medium">{{ currentExtractingFile || '처리 중...' }}</span>
</div>
<div v-if="filesDoneCount > 0" class="flex-shrink-0 text-caption font-weight-medium">
{{ filesDoneCount }}개 파일 처리됨
</div>
</div>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="px-4 py-3">
<v-spacer></v-spacer>
<v-btn
variant="text"
color="secondary"
:disabled="isExtracting"
@click="extractDialog = false"
>
취소
</v-btn>
<v-btn
color="orange-darken-1"
variant="elevated"
prepend-icon="mdi-archive-arrow-down"
:loading="isExtracting"
@click="confirmExtract"
>
압축 풀기
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 삭제 확인 다이얼로그 (단일 / 일괄 공용) -->
<v-dialog v-model="deleteDialog" max-width="460">
<v-card rounded="lg">
<v-card-item class="pb-2">
<template #prepend>
<v-icon icon="mdi-alert-circle-outline" color="error" size="28" class="mr-2"></v-icon>
</template>
<v-card-title class="text-h6 font-weight-bold">
{{ isBatchDelete ? '선택 항목 일괄 삭제' : (itemToDelete?.is_dir ? '폴더' : '파일') + ' 삭제 확인' }}
</v-card-title>
</v-card-item>
<v-card-text>
<div v-if="isBatchDelete">
<p class="mb-2">선택한 <strong>{{ selectedPaths.length }}개</strong>의 항목을 디스크에서 모두 삭제하시겠습니까?</p>
<div class="text-caption text-error">
⚠️ 선택한 항목 중 폴더가 포함된 경우 하위 모든 파일도 함께 영구 삭제됩니다.
</div>
</div>
<div v-else>
<p class="mb-2">정말 다음 {{ itemToDelete?.is_dir ? '폴더' : '파일' }}을 디스크에서 삭제하시겠습니까?</p>
<div class="pa-3 rounded bg-surface-variant text-body-2 font-weight-medium text-truncate">
{{ itemToDelete?.name }}
</div>
<div v-if="itemToDelete?.is_dir" class="text-caption text-error mt-2">
⚠️ 폴더 안의 모든 파일과 하위 폴더도 함께 영구 삭제됩니다.
</div>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="px-4 py-3">
<v-spacer></v-spacer>
<v-btn variant="text" color="secondary" @click="deleteDialog = false">
취소
</v-btn>
<v-btn color="error" variant="elevated" prepend-icon="mdi-delete" @click="confirmDelete">
삭제하기
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 미디어 미리보기 모달 다이얼로그 -->
<v-dialog v-model="previewDialog" max-width="850">
<v-card rounded="lg">
<v-card-item class="pb-2">
<div class="d-flex align-center justify-space-between w-100">
<div class="d-flex align-center text-truncate mr-2">
<v-icon
:icon="getCategoryIcon(previewItem?.category).icon"
:color="getCategoryIcon(previewItem?.category).color"
class="mr-2"
></v-icon>
<v-card-title class="text-subtitle-1 font-weight-bold text-truncate pa-0">
{{ previewItem?.name }}
</v-card-title>
</div>
<v-btn icon="mdi-close" variant="text" size="small" @click="previewDialog = false"></v-btn>
</div>
</v-card-item>
<v-divider></v-divider>
<v-card-text class="pa-4 d-flex justify-center align-center" style="min-height: 250px; background: #000;">
<!-- 비디오 플레이어 -->
<video
v-if="previewItem?.category === 'video'"
:src="getDownloadUrl(previewItem.path)"
controls
autoplay
style="max-width: 100%; max-height: 60vh; border-radius: 4px;"
></video>
<!-- 오디오 플레이어 -->
<audio
v-else-if="previewItem?.category === 'audio'"
:src="getDownloadUrl(previewItem.path)"
controls
autoplay
style="width: 100%; max-width: 500px;"
></audio>
<!-- 이미지 뷰어 -->
<img
v-else-if="previewItem?.category === 'image'"
:src="getDownloadUrl(previewItem.path)"
style="max-width: 100%; max-height: 65vh; object-fit: contain; border-radius: 4px;"
/>
<!-- 문서/텍스트 iframe -->
<iframe
v-else-if="previewItem?.category === 'document'"
:src="getDownloadUrl(previewItem.path)"
style="width: 100%; height: 60vh; border: none; background: #fff;"
></iframe>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="px-4 py-2">
<span class="text-caption text-medium-emphasis">
크기: {{ formatBytes(previewItem?.size) }}
</span>
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="tonal"
prepend-icon="mdi-download"
size="small"
:href="getDownloadUrl(previewItem?.path)"
target="_blank"
>
다운로드
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<style scoped>
.crumb-link:hover {
text-decoration: underline;
}
.cursor-pointer {
cursor: pointer;
}
.file-row:hover {
background-color: rgba(var(--v-theme-primary), 0.04);
}
.selected-row {
background-color: rgba(var(--v-theme-primary), 0.08) !important;
}
.bg-primary-container {
background-color: rgba(var(--v-theme-primary), 0.12);
border: 1px solid rgba(var(--v-theme-primary), 0.3);
}
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.border-t {
border-top: 1px dashed rgba(var(--v-border-color), var(--v-border-opacity));
}
.rotating {
animation: rotate 1.8s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<script setup>
import { useTheme } from 'vuetify';
const props = defineProps({
status: {
type: Object,
default: () => ({ connected: false, version: '', torrents_count: 0 }),
},
loading: Boolean,
currentTab: {
type: String,
default: 'torrents',
},
});
defineEmits(['refresh', 'toggleDrawer']);
const theme = useTheme();
const toggleTheme = () => {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark';
};
</script>
<template>
<v-app-bar flat border density="comfortable" class="px-2">
<!-- 사이드 메뉴 토글 버튼 -->
<v-app-bar-nav-icon
variant="text"
@click="$emit('toggleDrawer')"
title="사이드 메뉴 토글"
></v-app-bar-nav-icon>
<!-- 로고 & 제목 -->
<div class="d-flex align-center ml-1">
<v-avatar color="primary" variant="tonal" size="34" class="mr-3">
<v-icon :icon="currentTab === 'torrents' ? 'mdi-magnet' : 'mdi-folder-eye-outline'" size="20"></v-icon>
</v-avatar>
<div>
<div class="text-subtitle-1 font-weight-bold" style="line-height: 1.2;">
{{ currentTab === 'torrents' ? '토렌트 관리' : '다운로드 파일 뷰어' }}
</div>
<div class="text-caption text-medium-emphasis d-none d-sm-block">
Transmission & Local File Manager
</div>
</div>
</div>
<v-spacer></v-spacer>
<!-- 연결 상태 칩 -->
<v-chip
:color="status.connected ? 'success' : 'error'"
variant="tonal"
size="small"
class="mr-2 font-weight-medium"
:prepend-icon="status.connected ? 'mdi-check-circle' : 'mdi-alert-circle'"
>
<span v-if="status.connected">
Transmission v{{ status.version || '연결됨' }}
</span>
<span v-else>연결 끊김</span>
</v-chip>
<!-- 테마 토글 버튼 -->
<v-btn
icon
variant="text"
size="small"
@click="toggleTheme"
title="테마 변경"
class="mr-1"
>
<v-icon :icon="theme.global.current.value.dark ? 'mdi-weather-night' : 'mdi-weather-sunny'"></v-icon>
</v-btn>
<!-- 새로고침 버튼 -->
<v-btn
icon="mdi-refresh"
variant="tonal"
size="small"
:loading="loading"
@click="$emit('refresh')"
title="새로고침"
></v-btn>
</v-app-bar>
</template>
+141
View File
@@ -0,0 +1,141 @@
<script setup>
import { ref } from 'vue';
const emit = defineEmits(['add']);
const magnet = ref('');
const downloadDir = ref('');
const paused = ref(false);
const showAdvanced = ref(false);
const isSubmitting = ref(false);
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
if (text) {
magnet.value = text.trim();
}
} catch (err) {
console.warn('클립보드 접근 권한이 없거나 지원되지 않습니다.', err);
}
};
const handleSubmit = async () => {
const trimmed = magnet.value.trim();
if (!trimmed) return;
isSubmitting.value = true;
try {
await emit('add', {
magnet: trimmed,
download_dir: downloadDir.value.trim() || undefined,
paused: paused.value,
});
magnet.value = '';
} finally {
isSubmitting.value = false;
}
};
</script>
<template>
<v-card rounded="lg" elevation="1" border class="mb-4">
<v-card-item class="pb-2">
<template #prepend>
<v-icon icon="mdi-plus-circle-outline" color="primary" class="mr-2"></v-icon>
</template>
<v-card-title class="text-h6 font-weight-bold">
마그넷 링크 추가
</v-card-title>
<v-card-subtitle>
Magnet URI 또는 토렌트 URL을 입력하여 Transmission에 추가합니다.
</v-card-subtitle>
</v-card-item>
<v-card-text class="pt-2">
<v-form @submit.prevent="handleSubmit">
<!-- 마그넷 입력창 -->
<v-text-field
v-model="magnet"
label="마그넷 링크 또는 토렌트 주소"
placeholder="magnet:?xt=urn:btih:... 또는 http://.../.torrent"
prepend-inner-icon="mdi-magnet"
append-inner-icon="mdi-content-paste"
clearable
variant="outlined"
density="comfortable"
hide-details="auto"
:disabled="isSubmitting"
@click:append-inner="handlePaste"
>
<template #append>
<v-btn
color="primary"
variant="elevated"
size="large"
height="48"
class="px-5 font-weight-bold"
:disabled="!magnet.trim() || isSubmitting"
:loading="isSubmitting"
prepend-icon="mdi-download"
@click="handleSubmit"
>
추가
</v-btn>
</template>
</v-text-field>
<!-- 고급 옵션 토글 영역 -->
<v-expand-transition>
<div v-if="showAdvanced" class="mt-4 pt-4 border-t">
<v-row dense>
<v-col cols="12" md="8">
<v-text-field
v-model="downloadDir"
label="저장 디렉토리 (선택)"
placeholder="/downloads"
prepend-inner-icon="mdi-folder-download-outline"
variant="outlined"
density="compact"
hint="비워둘 시 Transmission 기본 다운로드 폴더를 사용합니다."
persistent-hint
:disabled="isSubmitting"
></v-text-field>
</v-col>
<v-col cols="12" md="4" class="d-flex align-center">
<v-switch
v-model="paused"
label="추가 후 일시정지 상태 유지"
color="warning"
hide-details
density="compact"
:disabled="isSubmitting"
></v-switch>
</v-col>
</v-row>
</div>
</v-expand-transition>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="px-4 py-2">
<v-btn
variant="text"
size="small"
color="secondary"
:prepend-icon="showAdvanced ? 'mdi-chevron-up' : 'mdi-cog-outline'"
@click="showAdvanced = !showAdvanced"
>
{{ showAdvanced ? '고급 옵션 닫기' : '고급 옵션 (저장 경로 / 일시정지)' }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<style scoped>
.border-t {
border-top: 1px dashed rgba(var(--v-border-color), var(--v-border-opacity));
}
</style>
+247
View File
@@ -0,0 +1,247 @@
<script setup>
import { ref } from 'vue';
const props = defineProps({
torrents: {
type: Array,
default: () => [],
},
loading: Boolean,
});
const emit = defineEmits(['start', 'stop', 'remove']);
// 삭제 다이얼로그 상태
const deleteDialog = ref(false);
const selectedTorrent = ref(null);
const deleteLocalData = ref(false);
const openDeleteDialog = (torrent) => {
selectedTorrent.value = torrent;
deleteLocalData.value = false;
deleteDialog.value = true;
};
const confirmDelete = () => {
if (selectedTorrent.value) {
emit('remove', {
id: selectedTorrent.value.id,
deleteData: deleteLocalData.value,
});
}
deleteDialog.value = false;
selectedTorrent.value = null;
};
const formatBytes = (bytes) => {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
const formatSpeed = (bytesPerSec) => {
if (!bytesPerSec || bytesPerSec === 0) return '0 KB/s';
return `${formatBytes(bytesPerSec)}/s`;
};
const getStatusColor = (status) => {
switch (status) {
case 4: return 'primary'; // 다운로드 중
case 6: return 'success'; // 시딩 중
case 0: return 'secondary'; // 일시정지됨
default: return 'warning';
}
};
const getStatusIcon = (status) => {
switch (status) {
case 4: return 'mdi-download';
case 6: return 'mdi-check-all';
case 0: return 'mdi-pause';
default: return 'mdi-clock-outline';
}
};
</script>
<template>
<v-card rounded="lg" elevation="1" border>
<v-card-item class="pb-2">
<div class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon icon="mdi-format-list-bulleted" color="primary" class="mr-2"></v-icon>
<v-card-title class="text-h6 font-weight-bold">
토렌트 목록
</v-card-title>
<v-chip size="small" variant="tonal" color="primary" class="ml-2 font-weight-bold">
{{ torrents.length }}개
</v-chip>
</div>
</div>
</v-card-item>
<v-divider></v-divider>
<v-card-text class="pa-4">
<!-- 빈 목록일 때 -->
<div v-if="torrents.length === 0" class="text-center py-10">
<v-icon icon="mdi-folder-open-outline" size="64" color="secondary" class="mb-3 opacity-60"></v-icon>
<div class="text-subtitle-1 font-weight-medium">
현재 다운로드 또는 시딩 중인 토렌트가 없습니다.
</div>
<div class="text-caption text-medium-emphasis mt-1">
상단의 입력창에 마그넷 링크를 입력하여 다운로드를 시작해보세요.
</div>
</div>
<!-- 토렌트 목록 카드들 -->
<div v-else class="d-flex flex-column gap-3">
<v-card
v-for="torrent in torrents"
:key="torrent.id"
variant="outlined"
rounded="lg"
class="pa-4 bg-surface"
>
<!-- 헤더: 상태 칩, 파일명, 액션 버튼들 -->
<div class="d-flex align-start justify-space-between flex-wrap gap-2 mb-2">
<div class="d-flex align-center flex-grow-1 mr-2" style="min-width: 260px;">
<v-chip
:color="getStatusColor(torrent.status)"
:prepend-icon="getStatusIcon(torrent.status)"
size="small"
variant="tonal"
class="font-weight-medium mr-2 flex-shrink-0"
>
{{ torrent.status_label }}
</v-chip>
<span class="text-subtitle-2 font-weight-bold text-truncate" :title="torrent.name">
{{ torrent.name }}
</span>
</div>
<!-- 액션 버튼 -->
<div class="d-flex align-center gap-1">
<!-- 시작 / 일시정지 -->
<v-btn
v-if="torrent.status === 0"
icon="mdi-play"
size="small"
variant="tonal"
color="success"
title="다운로드 재개"
@click="$emit('start', torrent.id)"
></v-btn>
<v-btn
v-else
icon="mdi-pause"
size="small"
variant="tonal"
color="warning"
title="일시정지"
@click="$emit('stop', torrent.id)"
></v-btn>
<!-- 삭제 버튼 (다이얼로그 오픈) -->
<v-btn
icon="mdi-trash-can-outline"
size="small"
variant="tonal"
color="error"
title="삭제"
class="ml-1"
@click="openDeleteDialog(torrent)"
></v-btn>
</div>
</div>
<!-- 진행률 바 -->
<v-progress-linear
:model-value="torrent.percent_done"
:color="torrent.percent_done === 100 ? 'success' : torrent.status === 0 ? 'secondary' : 'primary'"
height="8"
rounded
striped
class="my-2"
></v-progress-linear>
<!-- 메타 정보: 진행률, 전체 용량, 다운로드/업로드 속도 -->
<div class="d-flex align-center justify-space-between flex-wrap text-caption text-medium-emphasis mt-2">
<div class="d-flex align-center gap-2">
<span class="font-weight-bold text-high-emphasis">
{{ torrent.percent_done }}%
</span>
<span>•</span>
<span>{{ formatBytes(torrent.total_size) }}</span>
<span v-if="torrent.download_dir" class="text-truncate d-none d-sm-inline" style="max-width: 250px;" :title="torrent.download_dir">
({{ torrent.download_dir }})
</span>
</div>
<div class="d-flex align-center gap-3">
<span v-if="torrent.rate_download > 0" class="text-primary font-weight-medium">
<v-icon icon="mdi-arrow-down-bold" size="14"></v-icon>
{{ formatSpeed(torrent.rate_download) }}
</span>
<span v-if="torrent.rate_upload > 0" class="text-success font-weight-medium">
<v-icon icon="mdi-arrow-up-bold" size="14"></v-icon>
{{ formatSpeed(torrent.rate_upload) }}
</span>
</div>
</div>
</v-card>
</div>
</v-card-text>
<!-- 삭제 확인 모달 다이얼로그 -->
<v-dialog v-model="deleteDialog" max-width="480">
<v-card rounded="lg">
<v-card-item class="pb-2">
<template #prepend>
<v-icon icon="mdi-alert-circle-outline" color="error" size="28" class="mr-2"></v-icon>
</template>
<v-card-title class="text-h6 font-weight-bold">
토렌트 삭제 확인
</v-card-title>
</v-card-item>
<v-card-text>
<div class="mb-3">
다음 토렌트를 삭제하시겠습니까?
</div>
<div class="pa-3 rounded bg-surface-variant text-body-2 font-weight-medium text-truncate mb-3">
{{ selectedTorrent?.name }}
</div>
<!-- 로컬 데이터 삭제 체크박스 -->
<v-checkbox
v-model="deleteLocalData"
label="디스크에 다운로드된 파일도 함께 완전히 삭제"
color="error"
density="compact"
hide-details
></v-checkbox>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="px-4 py-3">
<v-spacer></v-spacer>
<v-btn variant="text" color="secondary" @click="deleteDialog = false">
취소
</v-btn>
<v-btn color="error" variant="elevated" prepend-icon="mdi-delete" @click="confirmDelete">
삭제하기
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<style scoped>
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
</style>
+49
View File
@@ -0,0 +1,49 @@
import { createApp } from 'vue'
import App from './App.vue'
// Vuetify
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
const vuetify = createVuetify({
components,
directives,
theme: {
defaultTheme: 'dark',
themes: {
dark: {
colors: {
primary: '#2563eb',
secondary: '#64748b',
accent: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
background: '#0f172a',
surface: '#1e293b',
},
},
light: {
colors: {
primary: '#2563eb',
secondary: '#64748b',
accent: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
background: '#f8fafc',
surface: '#ffffff',
},
},
},
},
})
const app = createApp(App)
app.use(vuetify)
app.mount('#app')
+9
View File
@@ -0,0 +1,9 @@
:root {
--font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
}
body {
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}
+31
View File
@@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'
import path from 'path'
export default defineConfig({
plugins: [
vue(),
vuetify({ autoImport: true }),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
configure: (proxy, _options) => {
proxy.on('error', (err, _req, _res) => {
console.warn('\x1b[33m%s\x1b[0m', '⚠️ [FastAPI 백엔드 연결 실패] 포트 8000에 백엔드 서버가 실행 중인지 확인하세요: ./venv/bin/python main.py');
});
},
},
},
},
})