diff --git a/.gitignore b/.gitignore deleted file mode 100644 index ae748ab..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -.vscode -bun.lockb \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 351f6b1..0000000 --- a/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Danbooru Viewer -A modern style viewer for [Danbooru](https://danbooru.donmai.us) or other Booru API base site. - -## How To Use -- Enter this URL: [https://danbooru.defaultkavy.com](https://danbooru.defaultkavy.com). -- Or, replace `danbooru.donmai.us` to `danbooru.defaultkavy.com` without changing pathname and url query, will directly open the same page on Danbooru Viewer. -- Or, clone this repository and run commands for self-hosting: - ```sh - bun i --production - bun run start - ``` - -## Features -- Same path as the original website. - - Support URL query like `/posts?tags=ord:fav+minato_aqua`. -- Search tags with autocomplete. -- Infinite scroll posts with waterfall image layout. -- Mobile friendly. - -## Hotkeys -- Global Shortcut - - `Q`: Back. - - `E`: Forward. - - `/`: Open search bar. -- Posts Browser Page - - `W/A/S/D`: Navigation posts in direction. - - `Tab`: Toogle post detail panel. - - `Space/Enter`: Open selected post page. -- Post Page - - `A/D`: Switch to previous/next post page. - - `Spacebar`: Play/Pause video. - -## Roadmap to V1.0 -- [x] Posts Page -- [x] Posts Search with any tags -- [x] Booru Account Login (Using API keys) -- [x] Favorite Post with Account -- [x] Post Detail Panel in Posts Browser -- [ ] Saved Searches -- [ ] User Page -- [ ] Post Commentary -- [ ] More... - -## Tools -- [Elexis](https://git.defaultkavy.com/defaultkavy/elexis): Web Builder. -- [Elysia](https://elysiajs.com/): Server Framework. -- [ionicons](https://ionic.io/ionicons): Open Souces Icons. \ No newline at end of file diff --git a/danbooru-viewer-logo.png b/danbooru-viewer-logo.png new file mode 100644 index 0000000..148e19b Binary files /dev/null and b/danbooru-viewer-logo.png differ diff --git a/dist/assets/index-C_mg4_Pk.js b/dist/assets/index-C_mg4_Pk.js deleted file mode 100644 index 6702ba5..0000000 --- a/dist/assets/index-C_mg4_Pk.js +++ /dev/null @@ -1 +0,0 @@ -var ee=Object.defineProperty;var Et=r=>{throw TypeError(r)};var se=(r,e,t)=>e in r?ee(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var h=(r,e,t)=>se(r,typeof e!="symbol"?e+"":e,t),Lt=(r,e,t)=>e.has(r)||Et("Cannot "+t);var S=(r,e,t)=>(Lt(r,e,"read from private field"),t?t.call(r):e.get(r)),V=(r,e,t)=>e.has(r)?Et("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),W=(r,e,t,s)=>(Lt(r,e,"write to private field"),s?s.call(r,t):e.set(r,t),t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const n of i)if(n.type==="childList")for(const a of n.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function t(i){const n={};return i.integrity&&(n.integrity=i.integrity),i.referrerPolicy&&(n.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?n.credentials="include":i.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function s(i){if(i.ep)return;i.ep=!0;const n=t(i);fetch(i.href,n)}})();class C{constructor(){h(this,"eventMap",new Map)}fire(e,...t){var s;return(s=this.eventMap.get(e))==null||s.forEach(i=>i(...t)),this}on(e,t){const s=this.eventMap.get(e)??this.eventMap.set(e,new Set).get(e);return s==null||s.add(t),this}off(e,t){var s;return(s=this.eventMap.get(e))==null||s.delete(t),this}once(e,t){const s=(...n)=>{var a;(a=this.eventMap.get(e))==null||a.delete(s),t(...n)},i=this.eventMap.get(e)??this.eventMap.set(e,new Set).get(e);return i==null||i.add(s),this}}class Nt{constructor(){h(this,"domEvents",{});h(this,"events",new C)}on(e,t,s){e=$.orArrayResolve(e);for(const i of e){this.domEvents[i]||(this.domEvents[i]=new Map);const n=a=>{t(a,this)};this.domEvents[i].set(t,n),this.events.on(i,t),this.dom.addEventListener(i,n,s)}return this}off(e,t,s){var n;const i=(n=this.domEvents[e])==null?void 0:n.get(t);return i&&this.dom.removeEventListener(e,i,s),this.events.off(e,t),this}once(e,t,s){const i=n=>{this.dom.removeEventListener(e,i,s),t(n,this)};return this.dom.addEventListener(e,i,s),this.events.once(e,t),this}trigger(e){this.dom.dispatchEvent(new Event(e))}}class st extends Nt{constructor(){super(...arguments);h(this,"__$property__",{hidden:!1,coordinate:void 0});h(this,"parent")}hide(t,s=!0){return b.fluent(this,arguments,()=>this.__$property__.hidden,()=>{var i;if(t!==void 0)return t instanceof M?(this.__$property__.hidden=t.value,t.use(this,"hide")):this.__$property__.hidden=t,s&&((i=this.parent)==null||i.children.render()),this})}remove(){var t;return(t=this.parent)==null||t.children.remove(this).render(),this}replace(t){var s;return(s=this.parent)==null||s.children.replace(this,t).render(),this}contains(t){return t?t instanceof st?this.dom.contains(t.dom):t instanceof EventTarget?this.dom.contains(b(t).dom):this.dom.contains(t):!1}coordinate(t){return b.fluent(this,arguments,()=>this.__$property__.coordinate,()=>b.set(this.__$property__,"coordinate",t))}self(t){return b.orArrayResolve(t).forEach(s=>s(this)),this}inDOM(){return document.contains(this.dom)}isElement(){return this instanceof F}get element(){return this instanceof F?this:null}get htmlElement(){return this instanceof X?this:null}}class F extends st{constructor(t,s){super();h(this,"dom");h(this,"static_classes",new Set);this.dom=this.createDom(t,s),this.dom.$=this,this.setOptions(s)}createDom(t,s){return s!=null&&s.dom?s.dom:t==="svg"?document.createElementNS("http://www.w3.org/2000/svg","svg"):document.createElement((s==null?void 0:s.tagname)??t)}setOptions(t){return this.id(t==null?void 0:t.id),t&&t.class&&this.class(...t.class),this}id(t){return $.fluent(this,arguments,()=>this.dom.id,()=>$.set(this.dom,"id",t))}class(...t){return $.fluent(this,arguments,()=>this.dom.classList,()=>{this.dom.classList.forEach(s=>this.static_classes.has(s)??this.dom.classList.remove(s)),this.dom.classList.add(...t.detype())})}addClass(...t){return $.fluent(this,arguments,()=>this,()=>{this.dom.classList.add(...t.detype())})}removeClass(...t){return $.fluent(this,arguments,()=>this,()=>{this.dom.classList.remove(...t.detype())})}staticClass(...t){return $.fluent(this,arguments,()=>this.static_classes,()=>{this.removeClass(...this.static_classes),this.static_classes.clear(),this.addStaticClass(...t)})}addStaticClass(...t){return $.fluent(this,arguments,()=>this,()=>{t.detype().forEach(s=>this.static_classes.add(s)),this.addClass(...t)})}removeStaticClass(...t){return $.fluent(this,arguments,()=>this,()=>{t.detype().forEach(s=>this.static_classes.delete(s)),this.removeClass(...t)})}css(t){return $.fluent(this,arguments,()=>this.dom.style,()=>{Object.assign(this.dom.style,t)})}attribute(t,s){return arguments.length?arguments.length===1?t===void 0?null:this.dom.getAttribute(t):arguments.length===2?t?(s===null?this.dom.removeAttribute(t):s!==void 0&&this.dom.setAttribute(t,`${s}`),this):this:this:null}tabIndex(t){return $.fluent(this,arguments,()=>this.dom.tabIndex,()=>$.set(this.dom,"tabIndex",t))}focus(t){return this.dom.focus(t),this}blur(){return this.dom.blur(),this}animate(t,s,i){const n=this.dom.animate(t,s);return i&&(n.onfinish=()=>i(n)),n}getAnimations(t){return this.dom.getAnimations(t)}get dataset(){return this.dom.dataset}domRect(t){const s=this.dom.getBoundingClientRect();if(!t)return s;const i=t instanceof F?t.dom.getBoundingClientRect():t;return{...s,top:s.top-i.top,left:s.left-i.left,right:s.right-i.left,bottom:s.bottom-i.top,x:s.x-i.x,y:s.y-i.y}}}class ot extends st{constructor(t){super();h(this,"dom");this.dom=t,this.dom.$=this}domRect(t){const s={bottom:innerHeight,height:innerHeight,left:0,right:innerWidth,top:0,width:innerWidth,x:0,y:0};if(!t)return s;const i=t instanceof F?t.dom.getBoundingClientRect():t;return{...s,top:s.top-i.top,left:s.left-i.left,right:s.right-i.left,bottom:s.bottom-i.top,x:s.x-i.x,y:s.y-i.y}}static from(t){return t.$ instanceof ot?t.$:new ot(t)}}const ie="modulepreload",re=function(r){return"/"+r},Pt={},ne=function(e,t,s){let i=Promise.resolve();if(t&&t.length>0){document.getElementsByTagName("link");const a=document.querySelector("meta[property=csp-nonce]"),o=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));i=Promise.allSettled(t.map(l=>{if(l=re(l),l in Pt)return;Pt[l]=!0;const c=l.endsWith(".css"),d=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${l}"]${d}`))return;const u=document.createElement("link");if(u.rel=c?"stylesheet":ie,c||(u.as="script"),u.crossOrigin="",u.href=l,o&&u.setAttribute("nonce",o),document.head.appendChild(u),c)return new Promise((f,g)=>{u.addEventListener("load",f),u.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${l}`)))})}))}function n(a){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=a,window.dispatchEvent(o),!o.defaultPrevented)throw a}return i.then(a=>{for(const o of a||[])o.status==="rejected"&&n(o.reason);return e().catch(n)})};class oe{constructor(e){h(this,"$container");h(this,"childList",new Set);this.$container=e}add(e,t=-1){if(t===-1||this.childList.size-1===t)this.childList.add(e);else{const s=[...this.childList];s.splice(t,0,e),this.childList.clear(),s.forEach(i=>this.childList.add(i))}e.parent=this.$container}remove(e){return this.childList.has(e)?(this.childList.delete(e),e.parent=void 0,this):this}removeAll(e=!0){this.childList.forEach(t=>this.remove(t)),e&&this.render()}replace(e,t){const s=this.array;return s.splice(s.indexOf(e),1,t),e.remove(),this.childList.clear(),s.forEach(i=>this.childList.add(i)),t.parent=this.$container,this}render(){const[e,t]=[this.array.map(i=>i.dom),Array.from(this.dom.childNodes)],s=[];for(;t.length||e.length;){const[i,n]=[t.at(0),e.at(0)];n?i?n!==i?(n.$.hide()||(this.dom.insertBefore(n,i),s.push(n)),e.shift()):(n.$.hide()&&this.dom.removeChild(n),e.shift(),t.shift()):(n.$.hide()||this.dom.append(n),e.shift()):(i&&!s.includes(i)&&i.remove(),t.shift())}}indexOf(e){return this.array.indexOf(e)}get array(){return[...this.childList.values()]}get dom(){return this.$container.dom}}class M extends C{constructor(t,s){super();h(this,"_value");h(this,"_convert");h(this,"attributes",new Map);h(this,"linkStates",new Set);h(this,"options",{});this.set(t),s&&(this.options=s)}set(t){this._value=t,t instanceof M&&t.linkStates.add(this),this.update(),this.linkStates.forEach(s=>s.update())}static toJSON(t){const s={};for(let[i,n]of Object.entries(t))n instanceof M?n=n.toJSON():n instanceof Object&&M.toJSON(n),Object.assign(s,{[i]:n});return s}update(){for(const[t,s]of this.attributes.entries())for(const i of s)t[i]instanceof Function?this.options.format?t[i](this.options.format(this.value)):t[i](this.value):i in t&&(t[i]=this.value);this.fire("update",{state$:this})}use(t,s){const i=this.attributes.get(t);i?i.add(s):this.attributes.set(t,new Set().add(s))}convert(t){const s=new M(this);return s._convert=t,s}get value(){return this._value instanceof M?this._convert?this._convert(this._value.value):this._value.value:this._value}toString(){return this.options.format?`${this.options.format(this.value)}`:this.value instanceof Object?JSON.stringify(this.toJSON()):`${this.value}`}toJSON(){return this.value instanceof M?this.value.toJSON():this.value instanceof Object?M.toJSON(this.value):this.toString()}}class J extends st{constructor(t){super();h(this,"dom");this.dom=new Text(t),this.dom.$=this}content(t){return $.fluent(this,arguments,()=>this.dom.textContent,()=>$.set(this.dom,"textContent",t))}}class X extends F{constructor(e,t){super(e,t)}autocapitalize(e){return $.fluent(this,arguments,()=>this.dom.autocapitalize,()=>$.set(this.dom,"autocapitalize",e))}innerText(e){return $.fluent(this,arguments,()=>this.dom.innerText,()=>$.set(this.dom,"innerText",e))}title(e){return $.fluent(this,arguments,()=>this.dom.title,()=>$.set(this.dom,"title",e))}dir(e){return $.fluent(this,arguments,()=>this.dom.dir,()=>$.set(this.dom,"dir",e))}translate(e){return $.fluent(this,arguments,()=>this.dom.translate,()=>$.set(this.dom,"translate",e))}popover(e){return $.fluent(this,arguments,()=>this.dom.popover,()=>$.set(this.dom,"popover",e))}spellcheck(e){return $.fluent(this,arguments,()=>this.dom.spellcheck,()=>$.set(this.dom,"spellcheck",e))}inert(e){return $.fluent(this,arguments,()=>this.dom.inert,()=>$.set(this.dom,"inert",e))}lang(e){return $.fluent(this,arguments,()=>this.dom.lang,()=>$.set(this.dom,"lang",e))}draggable(e){return $.fluent(this,arguments,()=>this.dom.draggable,()=>$.set(this.dom,"draggable",e))}hidden(e){return $.fluent(this,arguments,()=>this.dom.hidden,()=>$.set(this.dom,"hidden",e))}click(){return this.dom.click(),this}attachInternals(){return this.dom.attachInternals()}hidePopover(){return this.dom.hidePopover(),this}showPopover(){return this.dom.showPopover(),this}togglePopover(){return this.dom.togglePopover(),this}get accessKeyLabel(){return this.dom.accessKeyLabel}get offsetHeight(){return this.dom.offsetHeight}get offsetLeft(){return this.dom.offsetLeft}get offsetParent(){return $(this.dom.offsetParent)}get offsetTop(){return this.dom.offsetTop}get offsetWidth(){return this.dom.offsetWidth}}class p extends X{constructor(t,s){super(t,s);h(this,"children",new oe(this));h(this,"__position_cursor",0)}content(t){return $.fluent(this,arguments,()=>this,()=>{this.children.removeAll(!1),this.insert(t)})}insert(t,s=-1){return $.fluent(this,arguments,()=>this,async()=>{if(t instanceof Function){let i=t(this);i instanceof Promise?t=await i:t=i}else t instanceof Promise&&(t=await t);t=$.orArrayResolve(t),this.__position_cursor=s<0?this.children.array.length+s:s;for(const i of t)if(i!=null){if(i instanceof Array)this.insert(i,this.__position_cursor);else if(typeof i=="string")this.children.add(new J(i),s);else if(i instanceof M){const n=new J(i.toString());i.use(n,"content"),this.children.add(n,s)}else if(i instanceof Promise){const n=(await ne(async()=>{const{$Async:o}=await Promise.resolve().then(()=>pe);return{$Async:o}},void 0)).$Async,a=new n().await(i);this.children.add(a,s)}else this.children.add(i,s);this.__position_cursor+=1}this.children.render()})}clear(){return this.children.removeAll(),this}$(t){if(t.startsWith("::"))return Array.from(this.dom.querySelectorAll(t.replace(/^::/,""))).map(s=>$(s));if(t.startsWith(":"))return $(this.dom.querySelector(t.replace(/^:/,"")))}get scrollHeight(){return this.dom.scrollHeight}get scrollWidth(){return this.dom.scrollWidth}scrollTop(t){return $.fluent(this,arguments,()=>this.dom.scrollTop,()=>$.set(this.dom,"scrollTop",t))}scrollLeft(t){return $.fluent(this,arguments,()=>this.dom.scrollLeft,()=>$.set(this.dom,"scrollLeft",t))}}class Ht extends p{constructor(e){super("a",e),this.dom.addEventListener("click",t=>{$.anchorHandler&&this.href()&&(t.preventDefault(),$.anchorHandler(this,t))})}href(e){return $.fluent(this,arguments,()=>this.dom.href,()=>$.set(this.dom,"href",e))}target(e){return $.fluent(this,arguments,()=>this.dom.target??void 0,()=>{e&&(this.dom.target=e)})}}class ae extends F{constructor(e,t){super(e,t)}}var T;(r=>{function e(o,l,c,d){return l.length?(d(),o):c()}r.fluent=e;function t(o){return o instanceof Array?o:[o]}r.orArrayResolve=t;function s(o,l){return t(l).forEach(c=>{Object.getOwnPropertyNames(c.prototype).forEach(d=>{d!=="constructor"&&Object.defineProperty(o.prototype,d,Object.getOwnPropertyDescriptor(c.prototype,d)||Object.create(null))})}),o}r.mixin=s;function i(o,l,c){c!==void 0&&(o[l]=c)}r.set=i;function n(o){return new M(o)}r.state=n;function a(o){if(o.$)return o.$;if(o.nodeName.toLowerCase()==="body")return new p("body",{dom:o});if(o.nodeName.toLowerCase()==="head")return new p("head",{dom:o});if(o.nodeName.toLowerCase()==="#document")return ot.from(o);if(o instanceof HTMLElement){const l=$.TagNameElementMap[o.tagName.toLowerCase()],c=l?l===p?new l(o.tagName,{dom:o}):new l({dom:o}):new p(o.tagName,{dom:o});if(c instanceof p)for(const d of Array.from(c.dom.childNodes))c.children.add($(d));return c}else if(o instanceof Text){const l=new J(o.textContent??"");return l.dom=o,l}else if(o instanceof SVGElement&&o.tagName.toLowerCase()==="svg")return new ae("svg",{dom:o});throw`$NODE.FROM: NOT SUPPORT TARGET ELEMENT TYPE (${o.nodeName})`}r.from=a})(T||(T={}));class A{static create(...e){const t=class{};return Object.getOwnPropertyNames(A.prototype).forEach(s=>{s!=="constructor"&&e.includes(s)&&Object.defineProperty(t.prototype,s,Object.getOwnPropertyDescriptor(A.prototype,s)||Object.create(null))}),t}disabled(e){return $.fluent(this,arguments,()=>this.dom.disabled,()=>$.set(this.dom,"disabled",e))}checkValidity(){return this.dom.checkValidity()}reportValidity(){return this.dom.reportValidity()}formAction(e){return $.fluent(this,arguments,()=>this.dom.formAction,()=>$.set(this.dom,"formAction",e))}formEnctype(e){return $.fluent(this,arguments,()=>this.dom.formEnctype,()=>$.set(this.dom,"formEnctype",e))}formMethod(e){return $.fluent(this,arguments,()=>this.dom.formMethod,()=>$.set(this.dom,"formMethod",e))}formNoValidate(e){return $.fluent(this,arguments,()=>this.dom.formNoValidate,()=>$.set(this.dom,"formNoValidate",e))}formTarget(e){return $.fluent(this,arguments,()=>this.dom.formTarget,()=>$.set(this.dom,"formTarget",e))}autocomplete(e){return $.fluent(this,arguments,()=>this.dom.autocomplete,()=>$.set(this.dom,"autocomplete",e))}name(e){return $.fluent(this,arguments,()=>this.dom.name,()=>$.set(this.dom,"name",e))}maxLength(e){return $.fluent(this,arguments,()=>this.dom.maxLength,()=>$.set(this.dom,"maxLength",e))}minLength(e){return $.fluent(this,arguments,()=>this.dom.minLength,()=>$.set(this.dom,"minLength",e))}required(e){return $.fluent(this,arguments,()=>this.dom.required,()=>$.set(this.dom,"required",e))}label(e){return $.fluent(this,arguments,()=>this.dom.label,()=>$.set(this.dom,"label",e))}get form(){return this.dom.form?$(this.dom.form):null}get validationMessage(){return this.dom.validationMessage}get validity(){return this.dom.validity}get willValidate(){return this.dom.willValidate}}class Tt extends p{constructor(e){super("button",e)}type(e){return $.fluent(this,arguments,()=>this.dom.type,()=>$.set(this.dom,"type",e))}}T.mixin(Tt,A.create("disabled","checkValidity","formAction","formEnctype","formMethod","formNoValidate","formTarget","reportValidity"));class Ft extends p{constructor(e){super("form",e)}action(e){return $.fluent(this,arguments,()=>this.dom.formAction,()=>$.set(this.dom,"formAction",e))}enctype(e){return $.fluent(this,arguments,()=>this.dom.formEnctype,()=>$.set(this.dom,"formEnctype",e))}method(e){return $.fluent(this,arguments,()=>this.dom.formMethod,()=>$.set(this.dom,"formMethod",e))}noValidate(e){return $.fluent(this,arguments,()=>this.dom.formNoValidate,()=>$.set(this.dom,"formNoValidate",e))}acceptCharset(e){return $.fluent(this,arguments,()=>this.dom.acceptCharset,()=>$.set(this.dom,"acceptCharset",e))}target(e){return $.fluent(this,arguments,()=>this.dom.formTarget,()=>$.set(this.dom,"formTarget",e))}requestSubmit(){return this.dom.requestSubmit(),this}reset(){return this.dom.reset(),this}submit(){return this.dom.submit(),this}get length(){return this.dom.length}get elements(){return Array.from(this.dom.elements).map(e=>$(e))}}T.mixin(Ft,A.create("checkValidity","reportValidity","autocomplete"));class O extends X{constructor(e){super("input",e)}value(e){return $.fluent(this,arguments,()=>this.type()==="number"?Number(this.dom.value):this.dom.value,()=>$.set(this.dom,"value",e,t=>{this.on("input",()=>{t.attributes.has(this.dom)!==!1&&(typeof t.value=="string"&&t.set(`${this.value()}`),typeof t.value=="number"&&t.set(Number(this.value())))})}))}type(e){return $.fluent(this,arguments,()=>this.dom.type,()=>$.set(this.dom,"type",e))}capture(e){return $.fluent(this,arguments,()=>this.dom.capture,()=>$.set(this.dom,"capture",e))}alt(e){return $.fluent(this,arguments,()=>this.dom.alt,()=>$.set(this.dom,"alt",e))}height(e){return $.fluent(this,arguments,()=>this.dom.height,()=>$.set(this.dom,"height",e))}width(e){return $.fluent(this,arguments,()=>this.dom.width,()=>$.set(this.dom,"width",e))}defaultValue(e){return $.fluent(this,arguments,()=>this.dom.defaultValue,()=>$.set(this.dom,"defaultValue",e))}dirName(e){return $.fluent(this,arguments,()=>this.dom.dirName,()=>$.set(this.dom,"dirName",e))}pattern(e){return $.fluent(this,arguments,()=>this.dom.pattern,()=>$.set(this.dom,"pattern",e))}placeholder(e){return $.fluent(this,arguments,()=>this.dom.placeholder,()=>$.set(this.dom,"placeholder",e))}readOnly(e){return $.fluent(this,arguments,()=>this.dom.readOnly,()=>$.set(this.dom,"readOnly",e))}selectionDirection(e){return $.fluent(this,arguments,()=>this.dom.selectionDirection,()=>$.set(this.dom,"selectionDirection",e))}selectionEnd(e){return $.fluent(this,arguments,()=>this.dom.selectionEnd,()=>$.set(this.dom,"selectionEnd",e))}selectionStart(e){return $.fluent(this,arguments,()=>this.dom.selectionStart,()=>$.set(this.dom,"selectionStart",e))}size(e){return $.fluent(this,arguments,()=>this.dom.size,()=>$.set(this.dom,"size",e))}src(e){return $.fluent(this,arguments,()=>this.dom.src,()=>$.set(this.dom,"src",e))}inputMode(e){return $.fluent(this,arguments,()=>this.dom.inputMode,()=>$.set(this.dom,"inputMode",e))}valueAsDate(e){return $.fluent(this,arguments,()=>this.dom.valueAsDate,()=>$.set(this.dom,"valueAsDate",e))}valueAsNumber(e){return $.fluent(this,arguments,()=>this.dom.valueAsNumber,()=>$.set(this.dom,"valueAsNumber",e))}webkitdirectory(e){return $.fluent(this,arguments,()=>this.dom.webkitdirectory,()=>$.set(this.dom,"webkitdirectory",e))}select(){return this.dom.select(),this}setCustomValidity(e){return this.dom.setCustomValidity(e),this}setRangeText(e,t,s,i){return typeof t=="number"&&typeof s=="number"&&this.dom.setRangeText(e,t,s,i),this.dom.setRangeText(e),this}setSelectionRange(e,t,s){return this.dom.setSelectionRange(e,t,s),this}showPicker(){return this.dom.showPicker(),this}get files(){return this.dom.files}get webkitEntries(){return this.dom.webkitEntries}get labels(){return Array.from(this.dom.labels??[]).map(e=>$(e))}}T.mixin(O,A.create("checkValidity","reportValidity","autocomplete","name","form","required","validationMessage","validity","willValidate","formAction","formEnctype","formMethod","formNoValidate","formTarget"));class he extends O{constructor(e){super(e),this.type("number")}static from(e){return $.mixin(O,this)}stepDown(){return this.dom.stepDown(),this}stepUp(){return this.dom.stepUp(),this}max(e){return $.fluent(this,arguments,()=>this.dom.max===""?null:parseInt(this.dom.min),()=>$.set(this.dom,"max",e==null?void 0:e.toString()))}min(e){return $.fluent(this,arguments,()=>this.dom.min===""?null:parseInt(this.dom.min),()=>$.set(this.dom,"min",e==null?void 0:e.toString()))}step(e){return $.fluent(this,arguments,()=>Number(this.dom.step),()=>$.set(this.dom,"step",e==null?void 0:e.toString()))}}class le extends O{constructor(e){super(e),this.type("radio")}static from(e){return $.mixin(O,this)}checked(e){return $.fluent(this,arguments,()=>this.dom.checked,()=>$.set(this.dom,"checked",e))}defaultChecked(e){return $.fluent(this,arguments,()=>this.dom.defaultChecked,()=>$.set(this.dom,"defaultChecked",e))}}class ce extends O{constructor(e){super(e),this.type("file")}static from(e){return $.mixin(O,this)}multiple(e){return $.fluent(this,arguments,()=>this.dom.multiple,()=>$.set(this.dom,"multiple",e))}accept(...e){return $.fluent(this,arguments,()=>this.dom.accept.split(","),()=>this.dom.accept=e.toString())}}T.mixin(O,[he,le,ce]);class Vt extends p{constructor(e){super("label",e)}for(e){return $.fluent(this,arguments,()=>this.dom.htmlFor,()=>{$.set(this.dom,"htmlFor",e)})}get control(){return this.dom.control}}T.mixin(Vt,A.create("form"));class ue extends X{constructor(e){super("img",e)}async load(e){return new Promise(t=>{const s=this.once("load",()=>{t(s)});typeof e=="string"?s.src(e):e.then(i=>s.src(i))})}static resize(e,t){return new Promise(s=>{const i=new Image;if(i.addEventListener("load",()=>{const n=document.createElement("canvas"),a=n.getContext("2d");if(!a)throw"$Image.resize: context undefined";const o=i.width/i.height,[l,c,d]=[o>1,o<1,o===1],u=t instanceof Array?t[0]:c?t:t*o,f=t instanceof Array?t[1]:l?t:t/o;n.height=f,n.width=u,a.drawImage(i,0,0,u,f),s(n.toDataURL())},{once:!0}),e instanceof File){const n=new FileReader;n.addEventListener("load",()=>i.src=n.result),n.readAsDataURL(e)}else i.src=e})}alt(e){return $.fluent(this,arguments,()=>this.dom.alt,()=>$.set(this.dom,"alt",e))}crossOrigin(e){return $.fluent(this,arguments,()=>this.dom.crossOrigin,()=>$.set(this.dom,"crossOrigin",e))}decoding(e){return $.fluent(this,arguments,()=>this.dom.decoding,()=>$.set(this.dom,"decoding",e))}height(e){return $.fluent(this,arguments,()=>this.dom.height,()=>$.set(this.dom,"height",e))}isMap(e){return $.fluent(this,arguments,()=>this.dom.isMap,()=>$.set(this.dom,"isMap",e))}loading(e){return $.fluent(this,arguments,()=>this.dom.loading,()=>$.set(this.dom,"loading",e))}referrerPolicy(e){return $.fluent(this,arguments,()=>this.dom.referrerPolicy,()=>$.set(this.dom,"referrerPolicy",e))}sizes(e){return $.fluent(this,arguments,()=>this.dom.sizes,()=>$.set(this.dom,"sizes",e))}src(e){return $.fluent(this,arguments,()=>this.dom.src,()=>$.set(this.dom,"src",e))}srcset(e){return $.fluent(this,arguments,()=>this.dom.srcset,()=>$.set(this.dom,"srcset",e))}useMap(e){return $.fluent(this,arguments,()=>this.dom.useMap,()=>$.set(this.dom,"useMap",e))}width(e){return $.fluent(this,arguments,()=>this.dom.width,()=>$.set(this.dom,"width",e))}decode(){return this.dom.decode()}get complete(){return this.dom.complete}get currentSrc(){return this.dom.currentSrc}get naturalHeight(){return this.dom.naturalHeight}get naturalWidth(){return this.dom.naturalWidth}get x(){return this.dom.x}get y(){return this.dom.y}}class de extends p{constructor(e){super("canvas",e)}height(e){return $.fluent(this,arguments,()=>this.dom.height,()=>{$.set(this.dom,"height",e)})}width(e){return $.fluent(this,arguments,()=>this.dom.width,()=>{$.set(this.dom,"width",e)})}captureStream(e){return this.dom.captureStream(e)}getContext(e,t){return this.dom.getContext(e)}toBlob(e,t,s){return this.dom.toBlob(e,t,s),this}toDataURL(e,t){return this.dom.toDataURL(e,t)}transferControlToOffscreen(){return this.dom.transferControlToOffscreen()}}class fe extends p{constructor(e){super("dialog",e)}open(e){return $.fluent(this,arguments,()=>this.dom.open,()=>$.set(this.dom,"open",e))}returnValue(e){return $.fluent(this,arguments,()=>this.dom.returnValue,()=>$.set(this.dom,"returnValue",e))}close(){return this.dom.close(),this}show(){return this.dom.show(),this}showModal(){return this.dom.showModal(),this}}class Wt extends p{constructor(e){super("select")}add(e){return this.insert(e),this}item(e){return $(this.dom.item(e))}namedItem(e){return $(this.dom.namedItem(e))}multiple(e){return $.fluent(this,arguments,()=>this.dom.multiple,()=>$.set(this.dom,"multiple",e))}get length(){return this.dom.length}get size(){return this.dom.size}get options(){return Array.from(this.dom.options).map(e=>$(e))}get selectedIndex(){return this.dom.selectedIndex}get selectedOptions(){return Array.from(this.dom.selectedOptions).map(e=>$(e))}value(e){return $.fluent(this,arguments,()=>this.dom.value,()=>$.set(this.dom,"value",e,t=>{this.on("input",()=>{t.attributes.has(this.dom)!==!1&&(typeof t.value=="string"&&t.set(`${this.value()}`),typeof t.value=="number"&&t.set(Number(this.value())))})}))}get labels(){return Array.from(this.dom.labels??[]).map(e=>$(e))}}T.mixin(Wt,A.create("checkValidity","reportValidity","autocomplete","name","form","required","disabled","validationMessage","validity","willValidate"));class jt extends p{constructor(e){super("option",e)}defaultSelected(e){return $.fluent(this,arguments,()=>this.dom.defaultSelected,()=>$.set(this.dom,"defaultSelected",e))}selected(e){return $.fluent(this,arguments,()=>this.dom.selected,()=>$.set(this.dom,"selected",e))}text(e){return $.fluent(this,arguments,()=>this.dom.text,()=>$.set(this.dom,"text",e))}value(e){return $.fluent(this,arguments,()=>this.dom.value,()=>$.set(this.dom,"value",e))}get form(){return this.dom.form?$(this.dom.form):null}get index(){return this.dom.index}}T.mixin(jt,A.create("form","disabled","label"));class Ut extends p{constructor(e){super("optgroup",e)}disabled(e){return $.fluent(this,arguments,()=>this.dom.disabled,()=>$.set(this.dom,"disabled",e))}}T.mixin(Ut,A.create("disabled","label"));class zt extends p{constructor(e){super("textarea",e)}cols(e){return $.fluent(this,arguments,()=>this.dom.cols,()=>$.set(this.dom,"cols",e))}wrap(e){return $.fluent(this,arguments,()=>this.dom.wrap,()=>$.set(this.dom,"wrap",e))}value(e){return $.fluent(this,arguments,()=>this.dom.value,()=>$.set(this.dom,"value",e))}defaultValue(e){return $.fluent(this,arguments,()=>this.dom.defaultValue,()=>$.set(this.dom,"defaultValue",e))}dirName(e){return $.fluent(this,arguments,()=>this.dom.dirName,()=>$.set(this.dom,"dirName",e))}placeholder(e){return $.fluent(this,arguments,()=>this.dom.placeholder,()=>$.set(this.dom,"placeholder",e))}readOnly(e){return $.fluent(this,arguments,()=>this.dom.readOnly,()=>$.set(this.dom,"readOnly",e))}selectionDirection(e){return $.fluent(this,arguments,()=>this.dom.selectionDirection,()=>$.set(this.dom,"selectionDirection",e))}selectionEnd(e){return $.fluent(this,arguments,()=>this.dom.selectionEnd,()=>$.set(this.dom,"selectionEnd",e))}selectionStart(e){return $.fluent(this,arguments,()=>this.dom.selectionStart,()=>$.set(this.dom,"selectionStart",e))}type(e){return $.fluent(this,arguments,()=>this.dom.type,()=>$.set(this.dom,"type",e))}inputMode(e){return $.fluent(this,arguments,()=>this.dom.inputMode,()=>$.set(this.dom,"inputMode",e))}select(){return this.dom.select(),this}setCustomValidity(e){return this.dom.setCustomValidity(e),this}setRangeText(e,t,s,i){return typeof t=="number"&&typeof s=="number"&&this.dom.setRangeText(e,t,s,i),this.dom.setRangeText(e),this}setSelectionRange(e,t,s){return this.dom.setSelectionRange(e,t,s),this}get labels(){return Array.from(this.dom.labels??[]).map(e=>$(e))}}T.mixin(zt,A.create("checkValidity","reportValidity","autocomplete","name","form","required","disabled","minLength","maxLength","validationMessage","validity","willValidate"));var at;class Gt extends p{constructor(t){super("async",t);V(this,at,!1)}await(t){return t instanceof Function?t(this).then(s=>this._loaded(s)):t.then(s=>this._loaded(s)),this}_loaded(t){if(W(this,at,!0),typeof t=="string")this.replace(new J(t));else if(t instanceof M){const s=new J(t.toString());t.use(s,"content"),this.replace(s)}else t==null?this.replace(new J(String(t))):this.replace(t);this.dom.dispatchEvent(new Event("load"))}get loaded(){return S(this,at)}}at=new WeakMap;const pe=Object.freeze(Object.defineProperty({__proto__:null,$Async:Gt},Symbol.toStringTag,{value:"Module"}));class me extends X{constructor(e,t){super(e,t)}autoplay(e){return $.fluent(this,arguments,()=>this.dom.autoplay,()=>$.set(this.dom,"autoplay",e))}get buffered(){return this.dom.buffered}controls(e){return $.fluent(this,arguments,()=>this.dom.controls,()=>$.set(this.dom,"controls",e))}crossOrigin(e){return $.fluent(this,arguments,()=>this.dom.crossOrigin,()=>$.set(this.dom,"crossOrigin",e))}get currentSrc(){return this.dom.currentSrc}currentTime(e){return $.fluent(this,arguments,()=>this.dom.currentTime,()=>$.set(this.dom,"currentTime",e))}defaultMuted(e){return $.fluent(this,arguments,()=>this.dom.defaultMuted,()=>$.set(this.dom,"defaultMuted",e))}defaultPlaybackRate(e){return $.fluent(this,arguments,()=>this.dom.defaultPlaybackRate,()=>$.set(this.dom,"defaultPlaybackRate",e))}disableRemotePlayback(e){return $.fluent(this,arguments,()=>this.dom.disableRemotePlayback,()=>$.set(this.dom,"disableRemotePlayback",e))}get duration(){return this.dom.duration}get ended(){return this.dom.ended}get error(){return this.dom.error}loop(e){return $.fluent(this,arguments,()=>this.dom.loop,()=>$.set(this.dom,"loop",e))}mediaKeys(e){return $.fluent(this,arguments,()=>this.dom.mediaKeys,()=>$.set(this.dom,"setMediaKeys",[e]))}muted(e){return $.fluent(this,arguments,()=>this.dom.muted,()=>$.set(this.dom,"muted",e))}get networkState(){return this.dom.networkState}get paused(){return this.dom.paused}playbackRate(e){return $.fluent(this,arguments,()=>this.dom.playbackRate,()=>$.set(this.dom,"playbackRate",e))}get played(){return this.dom.played}preload(e){return $.fluent(this,arguments,()=>this.dom.preload,()=>$.set(this.dom,"preload",e))}preservesPitch(e){return $.fluent(this,arguments,()=>this.dom.preservesPitch,()=>$.set(this.dom,"preservesPitch",e))}get readyState(){return this.dom.readyState}get remote(){return this.dom.remote}get seekable(){return this.dom.seekable}get seeking(){return this.dom.seeking}sinkId(e){return $.fluent(this,arguments,()=>this.dom.sinkId,()=>$.set(this.dom,"setSinkId",[e]))}src(e){return $.fluent(this,arguments,()=>this.dom.src,()=>$.set(this.dom,"src",e))}srcObject(e){return $.fluent(this,arguments,()=>this.dom.srcObject,()=>$.set(this.dom,"srcObject",e))}get textTracks(){return this.dom.textTracks}volume(e){return $.fluent(this,arguments,()=>this.dom.volume,()=>$.set(this.dom,"volume",e))}addTextTrack(e,t,s){return this.dom.addTextTrack(e,t,s)}canPlayType(e){return this.dom.canPlayType(e)}fastSeek(e){return this.dom.fastSeek(e),this}load(){return this.dom.load(),this}pause(){return this.dom.pause(),this}async play(){return await this.dom.play(),this}get isPlaying(){return this.currentTime()>0&&!this.paused&&!this.ended&&this.readyState>2}}class ge extends me{constructor(e){super("video",e)}disablePictureInPicture(e){return $.fluent(this,arguments,()=>this.dom.disablePictureInPicture,()=>$.set(this.dom,"disablePictureInPicture",e))}height(e){return $.fluent(this,arguments,()=>this.dom.height,()=>$.set(this.dom,"height",e))}width(e){return $.fluent(this,arguments,()=>this.dom.width,()=>$.set(this.dom,"width",e))}playsInline(e){return $.fluent(this,arguments,()=>this.dom.playsInline,()=>$.set(this.dom,"playsInline",e))}poster(e){return $.fluent(this,arguments,()=>this.dom.poster,()=>$.set(this.dom,"poster",e))}get videoHeight(){return this.dom.videoHeight}get videoWidth(){return this.dom.videoWidth}cancelVideoFrameCallback(e){return this.dom.cancelVideoFrameCallback(e),this}getVideoPlaybackQuality(){return this.dom.getVideoPlaybackQuality()}requestPictureInPicture(){return this.dom.requestPictureInPicture()}requestVideoFrameCallback(e){return this.dom.requestVideoFrameCallback(e)}}const gt=class gt extends Nt{constructor(){super(...arguments);h(this,"dom",window)}};h(gt,"$",new gt);let vt=gt;class $e{constructor(e){h(this,"keyMap",new Map);h(this,"conditional");e.on("keydown",t=>{var s;this.conditional&&!this.conditional(t)||(s=this.keyMap.get(t.key))==null||s.keydown.forEach(i=>i(t))}),e.on("keyup",t=>{var s;this.conditional&&!this.conditional(t)||(s=this.keyMap.get(t.key))==null||s.keyup.forEach(i=>i(t))}),e.on("keypress",t=>{var s;this.conditional&&!this.conditional(t)||(s=this.keyMap.get(t.key))==null||s.keypress.forEach(i=>i(t))})}if(e){return this.conditional=e,this}assigns(e,t,s){e=T.orArrayResolve(e),t=T.orArrayResolve(t);for(const i of e){const n=this.keyMap.get(i)??{keydown:new Set,keypress:new Set,keyup:new Set};for(const a of t)n[a].add(s);this.keyMap.set(i,n)}return this}unassign(e,t,s){e=T.orArrayResolve(e),t=t?T.orArrayResolve(t):["keydown","keypress","keyup"];for(const i of e){const n=this.keyMap.get(i)??{keydown:new Set,keypress:new Set,keyup:new Set};for(const a of t)s?n[a].delete(s):n[a].clear();this.keyMap.set(i,n)}return this}keydown(e,t){return this.assigns(e,"keydown",t),this}keyup(e,t){return this.assigns(e,"keyup",t),this}keypress(e,t){return this.assigns(e,"keypress",t),this}self(e){return e(this),this}}function b(r){if(typeof r>"u"||r===null||r instanceof st)return r;if(typeof r=="string"){if(r.startsWith("::"))return Array.from(document.querySelectorAll(r.replace(/^::/,""))).map(e=>b(e));if(r.startsWith(":"))return b(document.querySelector(r.replace(/^:/,"")));if(r in b.TagNameElementMap){const e=b.TagNameElementMap[r];return e===X?new X(r):e===p?new p(r):new e}else return new p(r)}if(r instanceof Node)return r.$?r.$:T.from(r);if(r instanceof Window)return vt.$;throw`$: NOT SUPPORT TARGET ELEMENT TYPE ('${r}')`}(r=>{r.anchorHandler=null,r.TagNameElementMap={html:p,head:p,document:ot,body:p,a:Ht,p,pre:p,code:p,blockquote:p,strong:p,h1:p,h2:p,h3:p,h4:p,h5:p,h6:p,div:p,ol:p,ul:p,dl:p,li:p,input:O,label:Vt,button:Tt,form:Ft,img:ue,dialog:fe,canvas:de,select:Wt,option:jt,optgroup:Ut,textarea:zt,video:ge,async:Gt};function e(m,w,_,L){return w.length?(L(),m):_()}r.fluent=e;function t(m,w,_,L){if(_!==void 0){if(_ instanceof M){_.use(m,w),m[w]instanceof Function?m[w](..._.value):m[w]=_.value,L&&L(_);return}m[w]instanceof Function?m[w](..._):m[w]=_}}r.set=t;function s(m,w){return new M(m,w)}r.state=s;async function i(m,w){return new Promise(_=>{const L=new FileReader;L.onload=z=>{const P=r("img");if(P.once("load",lt=>{const G=r("canvas"),B=G.getContext("2d"),k=P.height()/P.width(),[Q,it]=[k>1?w/k:w,k>1?w:w*k];G.height(it).width(Q),B==null||B.drawImage(P.dom,0,0,Q,it),_(G.toDataURL(m.type))}),!z.target)throw"$.resize(): e.target is null";P.src(z.target.result)},L.readAsDataURL(m)})}r.resize=i;function n(m){const w=new DOMParser().parseFromString(m,"text/html").body;return Array.from(w.children).map(_=>r(_))}r.html=n;function a(m,w){return Object.assign(r.TagNameElementMap,{[m]:w}),r.TagNameElementMap}r.registerTagName=a;function o(m){return m instanceof Array?m:[m]}r.orArrayResolve=o;function l(m,w){return T.mixin(m,w)}r.mixin=l;function c(m=1){return parseInt(getComputedStyle(document.documentElement).fontSize)*m}r.rem=c;function d(m){return m()}r.call=d;function u(){return new C}r.events=u;function f(m){return new Bt(m)}r.pointers=f;function g(m){return new $e(m)}r.keys=g;function x(){return new ve}r.focus=x})(b||(b={}));globalThis.$=b;class ve extends C{constructor(){super();h(this,"layerMap",new Map);h(this,"currentLayer");h(this,"historyList",[])}layer(t){const s=this.layerMap.get(t)??new ye(t).on("blur",i=>this.fire("blur",i)).on("focus",i=>this.fire("focus",i));return this.layerMap.set(s.id,s),s}next(){return this.select(0)}prev(){return this.select(1)}up(){return this.select(2)}down(){return this.select(3)}right(){return this.select(4)}left(){return this.select(5)}blur(){var t;return(t=this.currentLayer)==null||t.blur(),this}select(t){var a;if(this.currentLayer=this.currentLayer??[...this.layerMap.values()].at(0),!this.currentLayer)return this;const s=this.currentLayer.currentFocus,i=this.currentLayer.elementSet.array;if(!s){const o=this.currentLayer.beforeBlur??i.at(0);return o?(this.currentLayer.focus(o),this):this}const n=i.indexOf(s);switch(t){case 0:case 1:{let o=t===0?n+1:n-1;(o===i.length&&this.currentLayer.loop()||o===-1&&!this.currentLayer.loop())&&(o=0);const l=i.at(o);if(!l)break;this.currentLayer.focus(l);break}case 3:case 5:case 4:case 2:{const o=s.coordinate();if(!o)break;const l=$.call(()=>{switch(t){case 2:return{y:o.y,x:o.x/2};case 3:return{y:o.y+o.height,x:o.x/2};case 5:return{y:o.y/2,x:o.x};case 4:return{y:o.y/2,x:o.x+o.width}}}),d=(a=i.map(u=>{if(u===s)return;const f=u.coordinate();if(!f)return;const g=$.call(()=>{switch(t){case 2:return{y:f.y+f.height,x:f.x/2};case 3:return{y:f.y,x:f.x/2};case 5:return{y:f.y/2,x:f.x+f.width};case 4:return{y:f.y/2,x:f.x}}});return{$ele:u,elePosition:f,distance:Math.sqrt((g.x-l.x)**2+(g.y-l.y)**2)}}).detype(void 0).filter(({elePosition:u})=>{switch(t){case 2:if(u.y+u.height>=o.y)return!1;break;case 3:if(u.y<=o.y+o.height)return!1;break;case 5:if(u.x+u.width>=o.x)return!1;break;case 4:if(u.x<=o.x+o.width)return!1;break}return!0}).sort((u,f)=>u.distance-f.distance).at(0))==null?void 0:a.$ele;if(!d)break;this.currentLayer.focus(d)}}return this}}class ye extends C{constructor(t){super();h(this,"id");h(this,"elementSet",new Set);h(this,"entrySet",new Set);h(this,"beforeBlur");h(this,"currentFocus");h(this,"focusHandler",(t,s)=>this.focus(s,!0));h(this,"blurHandler",(t,s)=>this.blur(s));h(this,"__$property__",{loop:!0,scrollThreshold:0});this.id=t,this.add=this.add.bind(this),this.entry=this.entry.bind(this)}add(t){return $.orArrayResolve(t).forEach(s=>{this.elementSet.has(s)||(this.elementSet.add(s),s.tabIndex(0))}),this}remove(t){return this.currentFocus===t&&this.blur(),this.beforeBlur===t&&(this.beforeBlur=void 0),this.elementSet.delete(t),this}entry(t){return $.orArrayResolve(t).forEach(this.entrySet.add.bind(this.entrySet)),this}focus(t,s=!1){if(!t)return this;t.hide(!1);const{scrollTop:i,scrollLeft:n}=document.documentElement,a=$.call(()=>{const c=t.domRect();return{left:c.left+n,top:c.top+i,right:c.right+n,bottom:c.bottom+i,height:c.height,width:c.width}}),{scrollThreshold:o}=this.__$property__,l=this.currentFocus;return this.blur(),this.currentFocus=t,(i>a.top-o||i>a.bottom+o)&&document.documentElement.scrollTo({left:a.left-o,top:a.top-o}),(i+innerHeightthis.remove(t)),this}loop(t){return $.fluent(this,arguments,()=>this.__$property__.loop,()=>$.set(this.__$property__,"loop",t))}scrollThreshold(t){return $.fluent(this,arguments,()=>this.__$property__.scrollThreshold,()=>$.set(this.__$property__,"scrollThreshold",t))}}class Bt extends C{constructor(t){super();h(this,"$node");h(this,"map",new Map);this.$node=t,this.$node.on("pointerdown",s=>this.down(s)),this.$node.on("pointerup",s=>this.up(s)),this.$node.on("pointermove",s=>this.move(s)),this.$node.on("pointercancel",s=>this.cancel(s))}down(t){const s=new we(this,this.toData(t),$(t.target));this.map.set(s.id,s),this.fire("down",s,t)}up(t){const s=this.map.get(t.pointerId);s&&(this.map.delete(t.pointerId),this.fire("up",s,t))}move(t){const s=this.map.get(t.pointerId);s&&(this.map.set(s.id,s),s.update(this.toData(t)),s.direction||(s.move_x>5||s.move_x<-5?s.direction=0:(s.move_y>5||s.move_y<-5)&&(s.direction=1)),this.fire("move",s,t))}cancel(t){const s=this.map.get(t.pointerId);s&&(s.update(this.toData(t)),this.map.delete(s.id),this.fire("cancel",s,t))}toData(t){return{id:t.pointerId,type:t.pointerType,width:t.width,height:t.height,x:t.x,y:t.y,movement_x:t.movementX,movement_y:t.movementY}}}class we{constructor(e,t,s){h(this,"initial_x");h(this,"initial_y");h(this,"$target");h(this,"direction",null);h(this,"manager");Object.assign(this,t),this.manager=e,this.$target=s,this.initial_x=t.x,this.initial_y=t.y}get move_x(){return this.x-this.initial_x}get move_y(){return this.y-this.initial_y}update(e){return Object.assign(this,e),this}delete(){return this.manager.map.delete(this.id),this}}var Kt=(r=>(r[r.Horizontal=0]="Horizontal",r[r.Vertical=1]="Vertical",r))(Kt||{});Array.prototype.detype=function(...r){return this.filter(e=>{if(r.length)for(const t of r)return typeof e!=typeof t;else return e!==void 0})};Object.defineProperties(Set.prototype,{array:{get:function(){return Array.from(this)}}});Set.prototype.sort=function(r){return this.array.sort(r)};class qt extends p{constructor(t){super("layout",t);h(this,"_property",{ROW_MAX_HEIGHT:200,GAP:0,IS_RENDERING:!1,RENDER_REQUEST:!1,COLUNM:1,TYPE:"justified",ROOT:null,ITEM_PROPERTIES:new Map});this.css({display:"block",position:"relative"}),new ResizeObserver(s=>{this.inDOM()&&(this.render(),this.dom.dispatchEvent(new Event("resize")))}).observe(this.dom),document.addEventListener("scroll",s=>{s.target===this.root().dom&&this.scrollCompute()},{passive:!0}),new IntersectionObserver(s=>{this.inDOM()&&this.render()}).observe(this.dom)}type(t){return $.fluent(this,arguments,()=>this._property.TYPE,()=>$.set(this._property,"TYPE",t))}maxHeight(t){return $.fluent(this,arguments,()=>this._property.ROW_MAX_HEIGHT,()=>$.set(this._property,"ROW_MAX_HEIGHT",t))}column(t){return $.fluent(this,arguments,()=>this._property.COLUNM,()=>$.set(this._property,"COLUNM",t))}gap(t){return $.fluent(this,arguments,()=>this._property.GAP,()=>$.set(this._property,"GAP",t))}root(t){return $.fluent(this,arguments,()=>this._property.ROOT??$(document),()=>$.set(this._property,"ROOT",t))}get COL_WIDTH(){return(this.offsetWidth-this._property.GAP*(this._property.COLUNM-1))/this._property.COLUNM}justifiedCompute(){const t=[],s=this.offsetWidth;for(const i of this.children.array){const n=$(i);if(!(n instanceof F))continue;const a=n.attribute("layout-item-ratio"),o=a?parseFloat(a):n.dom.offsetWidth/n.dom.offsetHeight,l={$node:n,ratio:o};let c=t.at(-1);(!c||c.heightd+=g.ratio);const u=d+o,f=(s-this._property.GAP*c.items.length)/u;c.items.push(l),c.ratio=u,c.height=f}return t}waterfallCompute(){const t=[],s=this.COL_WIDTH,i=()=>{if(t.lengthn.height-a.height)[0]};for(const n of this.children.array){const a=$(n);if(!(a instanceof F))continue;const o=a.attribute("layout-item-ratio"),l=o?parseFloat(o):a.dom.offsetWidth/a.dom.offsetHeight,c={$node:a,ratio:l},d=i();let u=0;d.items.forEach(x=>u+=x.ratio);const f=s/(d.height+s/l),g=s/f;d.items.push(c),d.ratio=f,d.height=g}return t}render(){if(!this.inDOM())return this;if(this.events.fire("beforeRender"),this._property.ITEM_PROPERTIES.clear(),this._property.TYPE==="justified"){const t=this.justifiedCompute();let s=0;for(const i of t){let n=0;i.height>this._property.ROW_MAX_HEIGHT&&(i.height=this._property.ROW_MAX_HEIGHT);for(const a of i.items){const o=a.ratio*i.height;a.$node.css({position:"absolute",height:`${i.height}px`,width:`${o}px`,top:`${s}px`,left:`${n}px`}),a.$node.attribute("layout-item-ratio",a.ratio),this._property.ITEM_PROPERTIES.set(a.$node,{height:i.height,width:o,top:s,left:n,ratio:a.ratio,$node:a.$node}),a.$node.coordinate({x:n,y:s,height:i.height,width:o}),n+=i.height*a.ratio+this._property.GAP}s+=i.height+this._property.GAP}this.css({height:`${s}px`})}else if(this._property.TYPE="waterfall"){const t=this.waterfallCompute(),s=this.COL_WIDTH;let i=0;for(const n of t){let a=0;for(const o of n.items){const l=s/o.ratio;o.$node.css({position:"absolute",height:`${l}px`,width:`${s}px`,top:`${a}px`,left:`${i}px`}),o.$node.attribute("layout-item-ratio",o.ratio),this._property.ITEM_PROPERTIES.set(o.$node,{height:l,width:s,top:a,left:i,ratio:o.ratio,$node:o.$node}),o.$node.coordinate({x:i,y:a,height:l,width:s}),a+=l+this._property.GAP}i+=s+this._property.GAP}if(t.length){const n=t.sort((a,o)=>o.height-a.height)[0];this.css({height:`${n.height+n.items.length*this._property.GAP}px`})}else this.css({height:""})}return this.scrollCompute(),this.events.fire("afterRender"),this}scrollCompute(){if(this.inDOM()===!1)return;const t=document.documentElement.scrollTop-this.dom.offsetTop;this._property.ITEM_PROPERTIES.forEach((s,i)=>{const n=s.top,a=s.top+s.height;i.attribute("focus")===""||document.activeElement===i.dom||(a>t&&n{this.events.fire("rendered",{$view:this,previousContent:i,nextContent:s})},o=()=>{this.events.fire("afterSwitch",{$view:this,previousId:t})};return this.events.fire("beforeSwitch",{$view:this,preventDefault:()=>n=!0,targetId:t,previousContent:i,nextContent:s,switched:o,rendered:a}),n||(this.content(s),a(),o()),this}get currentContent(){if(this.contentId)return this.viewCache.get(this.contentId)}}$.registerTagName("view",Xt);var j;const y=class y extends Xt{constructor(t){super({tagname:"router",...t});V(this,j,"");h(this,"routes",new Map);y.routers.add(this)}base(t){return $.fluent(this,arguments,()=>S(this,j),()=>{W(this,j,t??S(this,j))})}map(t){t=$.orArrayResolve(t);for(const s of t)s instanceof Array?this.map(s):this.routes.set(s.path(),s);return this.resolve(),this}resolve(){return new Promise(t=>{var x;if(!location.pathname.startsWith(S(this,j)))return t(2);const s=location.pathname.replace(S(this,j),"/").replace("//","/"),i=s.split("/").map(m=>`/${m}`),n=location.search,a=new Map(n.replace("?","").split("&").map(m=>m.split("="))),l=(()=>{const m=[];for(const[w,_]of this.routes){const L=$.orArrayResolve(w);for(const z of L){let P=0,lt={},G={};const B=z.split("/").map(k=>`/${k}`);if(!(i.length{const kt=a.get(Ot);kt!==void 0&&P++,Object.assign(G,{[Ot]:kt})}),Q.startsWith("/:")){P++,Object.assign(lt,{[Q.replace("/:","")]:i[k].replace("/","")});continue}else if(z.startsWith("#")&&z===location.hash){P++;continue}else if(Q===i[k]){P++;continue}else break}m.push({deep:P,$route:_,params:lt,query:G,routePath:z,pathId:_.static()?Object.keys(G).length!==0?s+n:Object.keys(lt).length!==0?s:L[0]:L[0]})}}}return m.sort((w,_)=>_.deep-w.deep).at(0)})();if(!l)return t(1);const{$route:c,params:d,pathId:u,query:f}=l;if(c.static()&&u===this.contentId)return t(0);this.events.once("rendered",({nextContent:m,previousContent:w})=>{w==null||w.events.fire("afterShift",{$route:w}),m.events.fire("rendered",{$route:m,params:d,query:f}),t(0)});const g=this.viewCache.get(u)??c.build({params:d,query:f});this.viewCache.has(u)||this.setView(u,g),this.events.once("beforeSwitch",()=>{var m;g.events.fire("beforeShift",{$route:g}),(m=this.currentContent)==null||m.events.fire("beforeShift",{$route:this.currentContent})}),this.events.once("afterSwitch",()=>g.events.fire("afterShift",{$route:g})),(x=this.currentContent)==null||x.events.fire("close",{$route:this.currentContent}),g.events.fire("open",{$route:g,params:d,query:f}),this.switchView(u)})}static init(){if(!history.state||!("index"in history.state)){const t={index:y.index};history.replaceState(t,"")}else y.index=history.state.index;return y.navigationDirection=0,y.resolve(),window.addEventListener("popstate",()=>y.popstate()),window.addEventListener("scroll",()=>{this.setScrollHistory(this.index,location.href,document.documentElement.scrollTop)},{passive:!0}),history.scrollRestoration="manual",this}static open(t,s){return t===void 0?this:(t=this.urlResolver(t),t.href===this.url.href?this:t.origin!==this.url.origin?(window.open(t,s),this):(y.clearForwardScrollHistory(),y.forwardIndex=0,y.index++,history.pushState(y.historyState,"",t),y.stateChange(0),y.resolve(),this))}static back(){return history.back(),this}static forward(){return this.forwardIndex===0?this:(history.forward(),this)}static replace(t){return t===void 0?this:(t=this.urlResolver(t),history.replaceState(y.historyState,"",t),this.stateChange(2),this.setScrollHistory(this.index,location.href,0),y.resolve(),this)}static urlResolver(t){return t instanceof URL?t:(t.startsWith("/")&&(t=`${location.origin}${t}`),t.startsWith("#")&&(t=`${location.origin}${location.pathname}${t}`),new URL(t))}static popstate(){const t=history.state.index>y.index?0:history.state.indext.resolve())),this.scrollRestoration(),this.setScrollHistory(this.index,location.href,document.documentElement.scrollTop)}static get historyState(){return{index:y.index}}static stateChange(t){const s=this.url,i=new URL(location.href);this.url=i,y.events.fire("stateChange",{beforeURL:s,afterURL:i,direction:t}),y.navigationDirection=t}static setScrollHistory(t,s,i){const n=this.getScrollHistory();if(!n)return sessionStorage.setItem(this.scrollHistoryKey,JSON.stringify({[t]:{url:s,value:i}}));n[t]={url:s,value:i},sessionStorage.setItem(this.scrollHistoryKey,JSON.stringify(n))}static getScrollHistory(){const t=sessionStorage.getItem(this.scrollHistoryKey);if(t)return JSON.parse(t)}static clearForwardScrollHistory(){const t=this.getScrollHistory();if(t)for(const s in t)Number(s)>this.index&&delete t[s],sessionStorage.setItem(this.scrollHistoryKey,JSON.stringify(t))}static scrollRestoration(){const t=this.getScrollHistory();if(t&&t[this.index])document.documentElement.scrollTop=t[this.index].value??0;else if(location.hash.length){const s=$(document.body).$(`:${location.hash}`);s&&(document.documentElement.scrollTop=s.dom.offsetTop)}else document.documentElement.scrollTop=0}};j=new WeakMap,h(y,"routers",new Set),h(y,"events",new C),h(y,"navigationDirection"),h(y,"index",0),h(y,"forwardIndex",0),h(y,"url",new URL(location.href)),h(y,"scrollHistoryKey","$ROUTER_SCROLL_HISTORY");let I=y;var K=(r=>(r[r.Forward=0]="Forward",r[r.Back=1]="Back",r[r.Replace=2]="Replace",r))(K||{});I.init();var Z,U;const Mt=class Mt extends p{constructor(t){super("route",t);V(this,Z,"");V(this,U);h(this,"rendered",!1);this.__$property__.static=!0}path(t){return $.fluent(this,arguments,()=>S(this,Z),()=>W(this,Z,t??S(this,Z)))}static(t){return $.fluent(this,arguments,()=>this.__$property__.static,()=>$.set(this.__$property__,"static",t))}builder(t){return W(this,U,t),this}render(t){return S(this,U)&&this.content(S(this,U).call(this,{$route:this,params:t.params,query:t.query})),this.rendered=!0,this}build(t){return new Mt({dom:this.dom.cloneNode()}).self(s=>{S(this,U)&&s.builder(S(this,U)).render({params:t.params,query:t.query})})}};Z=new WeakMap,U=new WeakMap;let yt=Mt;class Qt extends Ht{constructor(e){super(e),Object.assign(this.__$property__,{preventDefault:!1}),this.on("click",t=>{t.preventDefault(),this.__$property__.preventDefault||$.open(this.href(),this.target())})}preventDefault(e){return $.fluent(this,arguments,()=>this.__$property__.preventDefault,()=>$.set(this.__$property__,"preventDefault",e))}}$.registerTagName("router",I);$.registerTagName("route",yt);$.registerTagName("router-a",Qt);Object.assign($,{open(r,e){return I.open(r,e)},replace(r){return I.replace(r)},back(){return I.back()},forward(){return I.forward()}});class be{constructor(e,t){h(this,"booru");Object.assign(this,t),this.booru=e}static async fetchUserFavorites(e,t,s,i=100,n){return(await e.fetch(`/favorites.json?${s}&${`search[user_id]=${t.id}`}&limit=${i}&page=${n}`)).map(o=>(t.favorites.add(o.post_id),o.post_id))}update(e){return Object.assign(this,e),this}}class wt{constructor(e,t,s=!0){h(this,"name$",$.state("..."));h(this,"post_upload_count$",$.state(0));h(this,"level$",$.state(10));h(this,"level_string$",$.state("..."));h(this,"booru");h(this,"favorites",new Set);this.booru=e,Object.assign(this,t),s&&this.update$()}static async fetch(e,t){var n;let s;if(typeof t=="string"){const a=(await e.fetch(`/users.json?search[name]=${t}`)).at(0);if(!a)throw"User Not Found";return s=a}else s=await e.fetch(`/users/${t}.json`);const i=((n=e.users.get(s.id))==null?void 0:n.update(s))??new this(e,s);return e.users.set(i.id,i),i}static async fetchMultiple(e,t,s=200){let i="";if(t)for(const[o,l]of Object.entries(t))if(l instanceof Array)i+=`&search[${o}]=${l}`;else if(l instanceof Object)for(const[c,d]of Object.entries(l))i+=`&search[${o}${c}]=${d}`;else i+=`&search[${o}]=${l}`;return(await e.fetch(`/users.json?limit=${s}${i}`)).map(o=>{const l=new this(e,o);return e.users.set(l.id,l),l})}update(e){return Object.assign(this,e),this.update$(),this}update$(){this.name$.set(this.name),this.post_upload_count$.set(this.post_upload_count),this.level$.set(this.level),this.level_string$.set(this.level_string)}get booruURL(){return`${this.booru.origin}/users/${this.id}`}get url(){return`/users/${this.id}`}}const $t=class $t extends wt{constructor(t,s,i){super(t,i,!1);h(this,"apiKey");h(this,"favorite_count$",$.state(0));h(this,"forum_post_count$",$.state(0));this.apiKey=s,this.update$()}update$(){var t,s;super.update$(),(t=this.forum_post_count$)==null||t.set(this.forum_post_count),(s=this.favorite_count$)==null||s.set(this.favorite_count)}async init(){await this.fetchFavorites()}async fetchFavorites(){const t=Array.from(this.favorites.keys()).at(-1),s=await be.fetchUserFavorites(this.booru,this,"",1e3,t?`b${t}`:1);return $t.events.fire("favoriteUpdate",this),s.length>=1e3&&this.fetchFavorites(),s}static get storageUserData(){const t=localStorage.getItem("user_data");return t?JSON.parse(t):null}static set storageUserData(t){localStorage.setItem("user_data",JSON.stringify(t))}};h($t,"events",new C);let N=$t;const R=class R{constructor(e){h(this,"user");h(this,"posts",new Map);h(this,"tags",new Map);h(this,"users",new Map);h(this,"favorites",new Map);Object.assign(this,e),this.origin.endsWith("/")&&(this.origin=this.origin.slice(0,-1)),R.manager.set(this.name,this)}static set(e){this.used=e,this.name$.set(e.name),this.storageAPI=e.name;const t=N.storageUserData;return t&&e.login(t.username,t.apiKey),this.events.fire("set"),this}static get storageAPI(){return localStorage.getItem("booru_api")}static set storageAPI(e){e?localStorage.setItem("booru_api",e):localStorage.removeItem("booru_api")}async fetch(e,t="GET"){const s=this.user?`${e.includes("?")?"&":"?"}login=${this.user.name}&api_key=${this.user.apiKey}`:"",i=await fetch(`${this.origin}${e}${s}`,{method:t}).then(n=>n.json());if(i.success===!1)throw i.message;return i}async login(e,t){const s=await this.fetch(`/profile.json?login=${e}&api_key=${t}`);return this.user=new N(this,t,s),this.user.init(),R.events.fire("login",this.user),this.user}logout(){return this.user=void 0,N.storageUserData=null,R.events.fire("logout"),this}};h(R,"used"),h(R,"events",new C),h(R,"name$",$.state(R.name)),h(R,"manager",new Map);let v=R;class At{constructor(e,t){h(this,"post_count$",$.state(0));h(this,"name$",$.state(""));h(this,"booru");this.booru=e,Object.assign(this,t),this.$update()}static async fetch(e,t){var n;const s=await e.fetch(`/tags/${t}.json`),i=((n=e.tags.get(s.id))==null?void 0:n.update(s))??new this(e,s);return e.tags.set(i.id,i),i}static async fetchMultiple(e,t,s=1e3){let i="";if(t)for(const[o,l]of Object.entries(t))if(l instanceof Array)i+=`&search[${o}]=${l}`;else if(l instanceof Object)for(const[c,d]of Object.entries(l))i+=`&search[${o}${c}]=${d}`;else i+=`&search[${o}]=${l}`;return(await e.fetch(`/tags.json?limit=${s}${i}`)).map(o=>{var c;const l=((c=e.tags.get(o.id))==null?void 0:c.update(o))??new this(e,o);return e.tags.set(l.id,l),l})}static get(e,t){return[...e.tags.values()].find(s=>s.name===t)}update(e){return Object.assign(this,e),this.$update(),this}$update(){this.post_count$.set(this.post_count),this.name$.set(this.name)}}var q=(r=>(r[r.General=0]="General",r[r.Artist=1]="Artist",r[r.Copyright=3]="Copyright",r[r.Character=4]="Character",r[r.Meta=5]="Meta",r))(q||{});const It=1e3,dt=It*60,ft=dt*60,pt=ft*24,_e=pt*7,ct=new Intl.RelativeTimeFormat("en",{style:"long"});function bt(r){r=Math.floor(r);const e=r/It,t=r/dt,s=r/ft,i=r/pt,n=r%1e3,a=Math.floor(r%6e4/1e3),o=Math.floor(r%36e5/6e4),l=Math.floor(r%(36e5*24)/36e5),c=a.toString().padStart(2,"0"),d=o.toString().padStart(2,"0"),u=l.toString().padStart(2,"0");return{seconds:e,minutes:t,hours:s,days:i,mil:n,s:a,min:o,h:l,ss:c,mm:d,hh:u}}function xe(r,e=Date.now()){const t=r-e,s=Math.abs(t);if(sthis.update$()),this}static async fetchMultiple(t,s,i=20,n){let a="";if(s){if(typeof s=="string")a=s;else for(const[u,f]of Object.entries(s))if(f!==void 0){if(u==="tags"){a+=`${f}`;continue}a.at(-1)!=="="&&(a+=" "),a+=`${u}:${f}`}}const o=await t.fetch(`/posts.json?limit=${i}&tags=${a}${n?`&page=${n}`:""}&_method=get`);if(!(o instanceof Array))return[];const l=new Set,c=o.map(u=>{var g;const f=((g=t.posts.get(u.id))==null?void 0:g.update(u))??new this(t,u.id,u);return t.posts.set(f.id,f),f.tag_string.split(" ").forEach(x=>l.add(x)),f});if(!c.length)return c;const d=[...new Set(o.map(u=>[u.approver_id,u.uploader_id].detype(null)).flat())];return wt.fetchMultiple(t,{id:d}).then(()=>c.forEach(u=>u.update$())),c}update$(){var t,s,i,n;this.uploader$.set(((t=this.uploader)==null?void 0:t.name$)??((s=this.uploader_id)==null?void 0:s.toString())),this.approver$.set(((i=this.approver)==null?void 0:i.name$)??((n=this.approver_id)==null?void 0:n.toString())??"None"),this.created_date$.set(xe(+new Date(this.created_at))),this.favcount$.set(this.fav_count),this.score$.set(this.score),this.file_size$.set(Se(this.file_size)),this.file_ext$.set(this.file_ext),this.file_url$.set(this.file_url),this.source$.set(this.source),this.dimension$.set(`${this.image_width}x${this.image_height}`),this.booruUrl$.set(`${this.booruUrl}`),this.isUgoria&&this.webm_url$.set(this.large_file_url),this.createdDate=new Date(this.created_at),this.fire("update")}update(t){return Object.assign(this,t),this.update$(),this}async fetchTags(){await this.ready;const t=this.tag_string.split(" ").filter(s=>!At.get(this.booru,s));return t.length?(await At.fetchMultiple(this.booru,{name:{_space:t.toString().replaceAll(","," ")}}),this):this}async createFavorite(){if(!this.booru.user)return;const t=await this.booru.fetch(`/favorites.json?post_id=${this.id}`,"POST");return this.update(t),this.booru.user.favorites.add(t.id),N.events.fire("favoriteUpdate",this.booru.user),t.id}async deleteFavorite(){!this.booru.user||await fetch(`/api/favorites/${this.id}?login=${this.booru.user.name}&api_key=${this.booru.user.apiKey}&origin=${this.booru.origin}`,{method:"DELETE"}).then(s=>s.json())===!1||(this.fav_count--,this.favcount$.set(this.fav_count),this.booru.user.favorites.delete(this.id),N.events.fire("favoriteUpdate",this.booru.user))}get pathname(){return`/posts/${this.id}`}get uploader(){return this.booru.users.get(this.uploader_id)}get approver(){return this.approver_id?this.booru.users.get(this.approver_id):null}get isVideo(){return this.file_ext==="mp4"||this.file_ext==="webm"||this.file_ext==="zip"}get isGif(){return this.file_ext==="gif"}get isUgoria(){return this.file_ext==="zip"}get hasSound(){return this.tag_string_meta.includes("sound")}get tags(){const t=this.tag_string.split(" ");return[...this.booru.tags.values()].filter(s=>t.includes(s.name))}get previewURL(){var t,s,i;return((i=(s=(t=this.media_asset)==null?void 0:t.variants)==null?void 0:s.find(n=>n.file_ext==="webp"))==null?void 0:i.url)??this.large_file_url}get booruUrl(){return`${this.booru.origin}/posts/${this.id}`}get url(){return`https://danbooru.defaultkavy.com/posts/${this.id}`}get isFileSource(){return this.source.startsWith("file://")}get isLargeFile(){return this.file_size>5e6}}class Jt{constructor(e){Object.assign(this,e)}static async fetch(e,t){const s=await e.fetch(`/artist_commentaries/${t}.json`);return new this(s)}static async fetchMultiple(e,t,s=200){let i="";if(t)for(const[o,l]of Object.entries(t))if(l instanceof Array)i+=`&search[${o}]=${l}`;else if(l instanceof Object)for(const[c,d]of Object.entries(l))i+=`&search[${o}${c}]=${d}`;else i+=`&search[${o}]=${l}`;return(await e.fetch(`/artist_commentaries.json?limit=${s}${i}`)).map(o=>{const l=new this(o);return this.manager.set(l.id,l),l})}}h(Jt,"manager",new Map);class Yt extends p{constructor(t){super("detail-panel");h(this,"post",null);h(this,"options");this.options={preview:(t==null?void 0:t.preview)??!1,tagsType:(t==null?void 0:t.tagsType)??"detail"},this.build()}build(){this.post?this.content([this.options.preview?$("div").class("preview").content([$("img").src(this.post.previewURL)]):null,$("div").class("detail").content([$("section").class("post-info").content([new E("id").name("Post").content(`#${this.post.id}`),new E("uploader").name("Uploader").content(this.post.uploader$),new E("approver").name("Approver").content(this.post.approver$),new E("date").name("Date").content(this.post.created_date$),new E("size").name("Size").content([this.post.file_size$,this.post.dimension$]),new E("file-type").name("File Type").content(this.post.file_ext$),$("div").class("inline").content([new E("favorites").name("Favorites").content(this.post.favcount$),new E("score").name("Score").content(this.post.score$)]),this.post.file_url?new E("file-url").name("File").content([$("a").href(this.post.file_url$).content(this.post.file_url$.convert(t=>t?t.replace("https://",""):"")).target("_blank"),$("ion-icon").name("clipboard").on("click",(t,s)=>this.copyButtonHandler(s,this.post.file_url))]):null,new E("source-url").name("Source").content([$("a").href(this.post.source$).content(this.post.source$.convert(t=>t.replace("https://",""))).target("_blank"),$("ion-icon").name("clipboard").on("click",(t,s)=>this.copyButtonHandler(s,this.post.source))]),new E("booru-url").name(v.name$).content([$("a").href(this.post.booruUrl$).content(this.post.booruUrl$.convert(t=>t.replace("https://",""))).target("_blank"),$("ion-icon").name("clipboard").on("click",(t,s)=>this.copyButtonHandler(s,this.post.booruUrl))]),new E("webm-url").name("Webm").hide(!0).self(async t=>{await this.post.ready,this.post.isUgoria&&t.content($("a").href(this.post.webm_url$).content(this.post.webm_url$.convert(s=>s.replace("https://",""))).target("_blank")).hide(!1)})]),$("div").class("post-tags").content(async t=>{if(this.options.tagsType==="detail"){let s=function(d,u){return u.length?[$("h3").content(d),$("section").content([u.map(f=>$("div").class("tag").content([$("a").class("tag-name").content(f.name).href(`/posts?tags=${f.name}`),$("span").class("tag-post-count").content(f.post_count$.convert(rt))]))])]:null};const i=(await this.post.fetchTags()).tags,[n,a,o,l,c]=[i.filter(d=>d.category===q.Artist),i.filter(d=>d.category===q.Character),i.filter(d=>d.category===q.General),i.filter(d=>d.category===q.Meta),i.filter(d=>d.category===q.Copyright)];return[s("Artist",n),s("Character",a),s("Copyright",c),s("Meta",l),s("General",o)]}else{let s=function(i,n){var a;return(a=n.at(0))!=null&&a.length?[$("h3").content(i),$("section").class("tag-name-only").content([n.map(o=>$("a").class("tag").content(o).href(`/posts?tags=${o}`))])]:null};return[s("Artist",this.post.tag_string_artist.split(" ")),s("Character",this.post.tag_string_character.split(" ")),s("Copyright",this.post.tag_string_copyright.split(" ")),s("Meta",this.post.tag_string_meta.split(" ")),s("General",this.post.tag_string_general.split(" "))]}})])]):this.content($("span").class("no-content").content("No Selected"))}update(t){return this.post=t,this.build(),this}copyButtonHandler(t,s){t.name("checkmark"),navigator.clipboard.writeText(s),setTimeout(()=>t.name("clipboard"),3e3)}position(t){let s=0;return addEventListener("scroll",()=>{this.inDOM()&&(s=document.documentElement.scrollTop)},{passive:!0}),t.on("beforeShift",()=>{innerWidth>800&&this.css({position:"absolute",top:`calc(${s}px + var(--nav-height) + var(--padding))`})}).on("afterShift",()=>this.css({position:"",top:""})),this}}class E extends p{constructor(t){super("div");h(this,"$name",$("span").class("property-name"));h(this,"$values",$("div").class("property-values"));this.staticClass("property").attribute("property-id",t),super.content([this.$name,this.$values.hide(!0)])}name(t){return this.$name.content(t),this}content(t){this.$values.hide(!1);const s=$.orArrayResolve(t);return this.$values.content(s.map(i=>$("span").staticClass("property-value").content(i))),this}}const nt=class nt{constructor(e){h(this,"orderMap",new Map);h(this,"cache",new Set);h(this,"limit",100);h(this,"tags");h(this,"finished",!1);h(this,"events",new C);this.tags=e,nt.managers.set(this.tags,this),v.events.on("set",()=>{this.clear(),this.finished&&(this.finished=!1)})}static get(e){const t=this.managers.get(e)??new nt(e);return this.managers.set(t.tags,t),t}clear(){this.orderMap.clear(),this.cache.clear()}addPosts(e){e=$.orArrayResolve(e);for(const t of e)t.file_url&&(this.cache.has(t)||this.cache.add(t));return this}async fetchPosts(e){const t=this.tags?decodeURIComponent(this.tags).split("+"):void 0,s=[],i=[];let n=this.limit,a=[];if(t)for(const o of t)o.startsWith("ordfav:")||o.startsWith("order:")?i.push(o):o.startsWith("limit:")?n=Number(o.split(":")[1]):s.push(o);if(i.length){if(i.length>1)return this.events.fire("post_error",`Error: These query can't be used together [${i}].`),[];const o=i[0];if(o.startsWith("ordfav:")){const l=o.split(":")[1],c=s.length?`&search[post_tags_match]=${s.toString().replaceAll(",","+")}`:"",d=this.orderKeyList.length?e==="newer"?`&search[id]=>${this.orderKeyList.at(0)}`:`&search[id]=<${this.orderKeyList.at(-1)}`:void 0,u=await v.used.fetch(`/favorites.json?search[user_name]=${l}${d??""}${c}&limit=${n}`);a=await Y.fetchMultiple(v.used,{tags:`id:${u.map(g=>g.post_id).toString()}`});const f=new Map;for(const g of u){const x=a.find(m=>m.id===g.post_id);x&&x.file_url&&f.set(g.id,x)}return this.orderMap=new Map(e==="newer"?[...f,...this.orderMap]:[...this.orderMap,...f]),this.events.fire("post_fetch",{manager:this,postList:a}),a}if(o.startsWith("order:")){const l=this.orderKeyList.length?e==="newer"?1:this.orderMap.size/n+1:void 0;a=await Y.fetchMultiple(v.used,{tags:this.tags},n,l);const c=new Map(a.filter(d=>d.file_url).map(d=>[d.id,d]));c.forEach((d,u)=>{this.orderMap.has(u)&&c.delete(u)}),this.orderMap=new Map(e==="newer"?[...c,...this.orderMap]:[...this.orderMap,...c]),this.events.fire("post_fetch",{manager:this,postList:a})}}else{const o=this.orderKeyList.length?e==="newer"?`a${this.orderKeyList.at(0)}`:`b${this.orderKeyList.at(-1)}`:void 0;a=await Y.fetchMultiple(v.used,{tags:this.tags},n,o);const l=new Map(a.filter(c=>c.file_url).map(c=>[c.id,c]));this.orderMap=new Map(e==="newer"?[...l,...this.orderMap]:[...this.orderMap,...l])}return a.length||(this.finished=!0,this.cache.size?this.events.fire("endPost"):this.events.fire("noPost")),this.events.fire("post_fetch",{manager:this,postList:a}),this.addPosts(a),a}get orderKeyList(){return[...this.orderMap.keys()]}};h(nt,"managers",new Map);let mt=nt;class Ie extends p{constructor(t,s,i){super("video-controller");h(this,"$video");h(this,"$viewer");h(this,"duration$",$.state("00:00"));h(this,"post");this.$video=t,this.$viewer=s,this.post=i,this.build()}build(){const t=$.events();this.$video.on("timeupdate",()=>this.durationUpdate()),this.content([$("div").class("video-details").content([$("div").class("left").content([$("ion-icon").class("play").title("Play").name("play").self(s=>{this.$video.on("play",()=>s.name("pause")).on("pause",()=>s.name("play")),s.on("click",()=>this.$video.isPlaying?this.$video.pause():this.$video.play())}),$("div").class("duration").content([$("span").class("current-time").content(this.duration$),$("span").content("/"),$("span").class("total-time").content("00:00").self(s=>{this.$video.on("loadeddata",()=>{const i=bt(this.$video.duration*1e3);s.content(Number(i.hh)>0?`${i.hh}:${i.mm}:${i.ss}`:`${i.mm}:${i.ss}`)})})])]),$("div").class("right").content([$("ion-icon").class("volume").title("Volume").name("volume-high").disable(!this.post.hasSound).self(s=>{const i=()=>{this.$video.muted()?s.name("volume-mute"):s.name("volume-high")};s.on("click",()=>{this.$video.muted(!this.$video.muted()),i()})}),$("ion-icon").class("full-screen").title("Full-Screen").name("scan").self(s=>{s.on("click",()=>{document.fullscreenElement?document.exitFullscreen():this.$viewer.dom.requestFullscreen()})})])]),$("div").class("progressbar-container").content([$("div").class("progressbar").content([$("div").class("progress").self(s=>{this.$video.on("timeupdate",i=>{s.css({width:`${this.$video.currentTime()/this.$video.duration*100}%`})}),t.on("progressChange",i=>{s.css({width:`${i*100}%`})})})])]).self(s=>{const i=$.pointers($(document.body));let n=!1;i.on("down",(a,o)=>{if(!s.contains(a.$target))return a.delete();o.preventDefault(),this.$video.isPlaying&&(n=!0,this.$video.pause());const l=(a.x-s.domRect().x)/s.offsetWidth;this.$video.currentTime(l*this.$video.duration)}),i.on("move",(a,o)=>{o.preventDefault();const l=(a.x-s.domRect().x)/s.offsetWidth;this.$video.currentTime(l*this.$video.duration),t.fire("progressChange",l)}),i.on("up",(a,o)=>{n&&this.$video.play(),n=!1})})])}durationUpdate(){const t=bt(this.$video.currentTime()*1e3);this.duration$.set(Number(t.hh)>0?`${t.hh}:${t.mm}:${t.ss}`:`${t.mm}:${t.ss}`)}}class Me extends p{constructor(t){super("div");h(this,"$video",$("video"));h(this,"post");this.post=t,this.class("viewer"),this.build()}async build(){await this.post.ready,this.events.on("video_play_pause",()=>{this.$video.isPlaying?this.$video.pause():this.$video.play()}),this.content([$("div").class("viewer-panel").hide(!1).content(s=>(this.events.on("viewerPanel_hide",()=>s.hide(!0)).on("viewerPanel_show",()=>s.hide(!1)).on("viewerPanel_switch",()=>{s.hide(!s.hide())}),[$("div").class("panel").content([this.post.isVideo?new Ie(this.$video,this,this.post):null,$("div").class("buttons").content([$("ion-icon").title("Favorite").name("heart-outline").self(i=>{var n;N.events.on("favoriteUpdate",a=>{a.favorites.has(this.post.id)?i.name("heart"):i.name("heart-outline")}),(n=v.used.user)!=null&&n.favorites.has(this.post.id)&&i.name("heart"),i.on("click",()=>{var a;(a=v.used.user)!=null&&a.favorites.has(this.post.id)?this.post.deleteFavorite():this.post.createFavorite()})}),$("ion-icon").title("Original Size").name("resize-outline").self(i=>{i.on("click",()=>{this.events.fire("original_size"),i.disable(!0)}),(!this.post.isLargeFile||this.post.isVideo)&&i.disable(!0)})])]),$("div").class("overlay")])),this.post.isVideo?this.$video.height(this.post.image_height).width(this.post.image_width).src(this.post.file_ext==="zip"?this.post.large_file_url:this.post.file_url).controls(!1).loop(!0).disablePictureInPicture(!0):$("img").height(this.post.image_height).width(this.post.image_width).self(s=>{s.once("load",()=>s.once("load",()=>s.removeClass("loading")).src(this.post.isLargeFile?this.post.large_file_url:this.post.file_url)).src(this.post.preview_file_url),s.complete||s.class("loading"),this.events.on("original_size",()=>s.src(this.post.file_url))})]),this.on("pointerleave",s=>{s.pointerType!=="touch"&&this.events.fire("viewerPanel_hide")}),this.on("pointermove",s=>{(s.pointerType==="mouse"||s.pointerType==="pen")&&this.events.fire("viewerPanel_show")});let t=null;$.pointers(this).on("up",s=>{var i;(i=this.$(":.viewer-panel .panel"))!=null&&i.contains($(s.$target))||(s.type==="mouse"?this.events.fire("video_play_pause"):(t!==null&&this.events.fire("video_play_pause"),t=setTimeout(()=>{t=null},300),this.events.fire("viewerPanel_switch")))}),$.keys($(window)).self(s=>s.if(i=>{if(!($(i.target)instanceof O)&&this.inDOM())return!0}).keydown(" ",i=>{i.preventDefault(),this.$video.isPlaying?this.$video.pause():this.$video.play()}))}}var tt;class Oe extends p{constructor(){super("slide-viewer");h(this,"pointers",new Bt(this));h(this,"$container",$("div").class("slide-container"));h(this,"slideMap",new Map);h(this,"slideId",null);V(this,tt);this.css({position:"relative"}),this.__build__(),new ResizeObserver(()=>{this.inDOM()&&(this.__render__(),this.trigger("resize"))}).observe(this.dom)}__build__(){this.content([this.$container]),this.$container.css({position:"relative",height:"100%"});let t=0,s=0;this.pointers.on("down",(i,n)=>{if(S(this,tt)&&!S(this,tt).call(this,i,n))return i.delete();t=this.$container.offsetLeft}),this.pointers.on("move",(i,n)=>{var a,o;i.direction===Kt.Horizontal&&(n.preventDefault(),s=t+i.move_x,!(s>t&&((a=this.slideList.at(0))==null?void 0:a.slideId())===this.slideId)&&(s{const n=this.domRect().width,a=t-this.$container.offsetLeft;i.move_x!==0&&(i.movement_x<-5||a>n/2?this.next():i.movement_x>5||a+n{const n=this.slideMap.get(i);n&&s.set(i,n)}),this.slideMap=s,this.__render__(),this}switch(t){if(t===void 0)return this;const s=this.slideMap.get(t);if(!s)throw"target undefined";return s.slideId()===this.slideId?this:(this.events.fire("beforeSwitch",{prevSlide:this.currentSlide,nextSlide:s}),this.slideId=t,this.__slideAnimate__(),this.events.fire("switch",{nextSlide:s}),this)}__slideAnimate__(){const t=this.currentSlide?this.slideList.indexOf(this.currentSlide):void 0;if(t===void 0)return;const s=Math.abs(this.getPositionLeft(t)-this.$container.offsetLeft)===this.dom.clientWidth;this.$container.animate({left:`-${this.getPositionLeft(t)}px`},{duration:300,easing:s?"ease":"ease-out"},i=>{this.$container.css({left:`-${this.getPositionLeft(t)}px`}),this.__render__(!1)})}__navigation__(t){var l;const s=this.currentSlide,i=this.slideList,n=s?i.indexOf(s):void 0;if(n===void 0)return this.switch((l=i.at(0))==null?void 0:l.slideId()),this;const a=$.call(()=>{switch(t){case"next":return n===i.length?n:n+1;case"prev":return n===0?n:n-1}}),o=this.slideList.at(a);return this.switch(o==null?void 0:o.slideId()),this}next(){return this.__navigation__("next")}prev(){return this.__navigation__("prev")}get currentSlide(){return this.slideId?this.slideMap.get(this.slideId):void 0}get slideIdList(){return Array.from(this.slideMap.keys())}get slideList(){return Array.from(this.slideMap.values())}getPositionLeft(t){return t*this.dom.clientWidth}__render__(t=!0){var n,a;let s=0;if(this.slideMap.forEach(o=>{o.hide(!0,!1),o.css({top:"0",left:`${this.getPositionLeft(s)}px`}),s++}),!this.currentSlide)return;const i=this.slideList.indexOf(this.currentSlide);this.currentSlide.build().hide(!1,!1),i!==0&&((n=this.slideList.at(i-1))==null||n.build().hide(!1,!1)),i!==this.slideList.length-1&&((a=this.slideList.at(i+1))==null||a.build().hide(!1,!1)),this.$container.children.render(),t&&this.$container.css({left:`-${this.getPositionLeft(i)}px`})}pointerException(t){return W(this,tt,t),this}}tt=new WeakMap;var et,ht;class ke extends p{constructor(){super("slide");V(this,et);h(this,"builded",!1);V(this,ht);this.css({width:"100%",height:"100%",display:"block",position:"absolute"})}builder(t){return W(this,et,t),this}build(){return!this.builded&&S(this,et)&&(this.content(S(this,et).call(this)),this.builded=!0),this}slideId(t){return $.fluent(this,arguments,()=>S(this,ht),()=>W(this,ht,t))}}et=new WeakMap,ht=new WeakMap;const Ee=$("route").path("/posts/:id?q").id("post").static(!1).builder(({$route:r,params:e})=>{if(!Number(e.id))return $("h1").content("404: POST NOT FOUND");const t=$.events();let s,i;$.keys($(window)).self(c=>c.if(d=>{if(!($(d.target)instanceof O)&&r.inDOM())return!0}).keydown(["f","F"],d=>{var u;(u=v.used.user)!=null&&u.favorites.has(s.id)?s.deleteFavorite():s.createFavorite()}).keydown(["a","A"],d=>o("prev")).keydown(["d","D"],d=>{o("next")}));const n=new Map;r.on("open",async({params:c,query:d})=>{if(i=mt.get(d.q),s=Y.get(v.used,+c.id),i.events.on("post_fetch",l),!i.orderMap.size||!i.cache.has(s))await s.ready,i.addPosts(s),i.orderMap.set(s.id,s),i.fetchPosts("newer"),i.fetchPosts("older");else{const f=[...i.orderMap.values()],g=f.indexOf(s);!i.finished&&g===f.length-1?i.fetchPosts("older"):g===0&&i.fetchPosts("newer")}l({manager:i}),a(i.tags).switch(s.id),t.fire("post_switch",s)});function a(c){const d=n.get(c)??new Oe().pointerException(u=>{var f;return!((f=d.currentSlide)!=null&&f.$("::.progressbar-container").find(g=>g.contains(u.$target))||u.type==="mouse")}).on("switch",({nextSlide:u})=>{$.replace(`/posts/${u.slideId()}${c?`?q=${c}`:""}`)}).on("beforeSwitch",({prevSlide:u,nextSlide:f})=>{const g=u==null?void 0:u.$(":video");g!=null&&g.isPlaying&&g.pause();const x=f.$(":video");(x==null?void 0:x.isPlaying)===!1&&x.play()});return n.set(c,d),d}function o(c){const d=[...i.orderMap.values()],u=d.indexOf(s);if(c==="prev"&&u===0)return;const f=d.at(c==="next"?u+1:u-1);f&&$.replace(`/posts/${f.id}${i.tags?`?q=${i.tags}`:""}`)}function l(c){const{manager:d}=c,u=a(d.tags),f=d.cache.array.filter(g=>!u.slideMap.has(g.id));u.addSlides(f.map(g=>new ke().slideId(g.id).builder(()=>new Me(g)))),f.length&&u.arrange([...d.orderMap.values()].map(g=>g.id))}return[$("div").class("slide-viewer-container").self(c=>{r.on("open",()=>{c.content(a(i.tags))})}),$("div").class("content").content([$("h3").content("Artist's Commentary"),$("section").class("commentary").self(async c=>{t.on("post_switch",async d=>{const u=(await Jt.fetchMultiple(v.used,{post:{_id:d.id}})).at(0);c.content([u?[u.original_title?$("h3").content(u.original_title):null,$("pre").content(u.original_description)]:"No commentary"])})})]),new Yt().position(r).self(c=>{t.on("post_switch",d=>c.update(d))})]});class _t extends p{constructor(t,s){super("post-tile");h(this,"post");h(this,"$video");h(this,"$img");h(this,"duration$",$.state(""));h(this,"$grid");this.$grid=t,this.post=s,this.$video=this.post.isVideo?$("video").width(this.post.image_width).height(this.post.image_height).disablePictureInPicture(!0).loop(!0).muted(!0).hide(!0).on("mousedown",i=>i.preventDefault()):null,this.$img=$("img").draggable(!1).css({opacity:"0"}).width(this.post.image_width).height(this.post.image_height).src(this.post.previewURL).loading("lazy"),this.attribute("filetype",this.post.file_ext),this.durationUpdate(),this.build()}build(){var t;(t=this.$video)==null||t.on("timeupdate",(s,i)=>{this.durationUpdate()}),this.class("loading").content([this.post.isVideo?$("div").class("video-detail").content([this.post.hasSound?$("ion-icon").name("volume-medium-outline"):null,this.post.isUgoria?$("ion-icon").name("images-outline"):null,$("span").class("duration").content(this.duration$)]):null,this.post.isGif?$("div").class("gif-detail").content([$("span").content("GIF")]):null,$("a").href(this.url).preventDefault(H).content(()=>[this.$video,this.$img.on("mousedown",s=>s.preventDefault()).once("load",(s,i)=>{i.animate({opacity:[0,1]},{duration:300},()=>i.css({opacity:""})),this.removeClass("loading")})])]),this.on(["focus","mouseenter","touchstart"],()=>{var s,i;(s=this.$video)!=null&&s.isPlaying||(i=this.$video)==null||i.src(this.post.large_file_url).hide(!1).play().catch(n=>{}),this.post.isGif&&this.$img.src(this.post.large_file_url)},{passive:!0}).on(["blur","mouseleave","touchend","touchcancel"],()=>{var s;(s=this.$video)==null||s.pause().currentTime(0).hide(!0),this.post.isGif&&this.$img.src(this.post.previewURL)},{passive:!0}).on("click",()=>{if(H.value){if(innerWidth<=800)return $.open(this.url);this.attribute("focus")===""?$.open(this.url):this.trigger("$focus")}})}durationUpdate(){if(!this.$video)return;const t=bt(this.post.media_asset.duration*1e3-this.$video.currentTime()*1e3);this.duration$.set(Number(t.hh)>0?`${t.hh}:${t.mm}:${t.ss}`:`${t.mm}:${t.ss}`)}get url(){return`${this.post.pathname}${this.$grid.tags?`?q=${this.$grid.tags}`:""}`}}class Le extends qt{constructor(t){super();h(this,"$postMap",new Map);h(this,"tags");h(this,"$focus",$.focus());h(this,"posts");this.tags=t==null?void 0:t.tags,this.posts=mt.get(this.tags),this.addStaticClass("post-grid"),this.type("waterfall").gap(10),this.init()}async init(){this.posts.events.on("post_fetch",t=>{this.renderPosts()}),setInterval(async()=>{this.inDOM()&&document.documentElement.scrollTop===0&&await this.posts.fetchPosts("newer")},1e4),v.events.on("set",()=>{this.removeAll(),this.loader()}),this.on("resize",()=>this.resize()),this.loader(),this.$focus.layer(100).loop(!1).scrollThreshold($.rem(2)+60),$.keys($(window)).if(t=>{if(this.inDOM()&&!($(t.target)instanceof O))return!0}).keydown(["w","W"],t=>{t.preventDefault(),this.$focus.up()}).keydown(["s","S"],t=>{t.preventDefault(),this.$focus.down()}).keydown(["d","D"],t=>{t.preventDefault(),this.$focus.right()}).keydown(["a","A"],t=>{t.preventDefault(),this.$focus.left()}).keydown([" ","Enter"],t=>{var i;t.preventDefault();const s=(i=this.$focus.currentLayer)==null?void 0:i.currentFocus;s instanceof _t&&$.open(s.url)}).keydown(["Escape"],t=>{t.preventDefault(),this.$focus.blur()})}async loader(){if(!this.inDOM())return setTimeout(()=>this.loader(),100);for(;this.inDOM()&&document.documentElement.scrollHeight<=innerHeight*2;)if(!(await this.posts.fetchPosts("older")).length)return;document.documentElement.scrollTop+innerHeight>document.documentElement.scrollHeight-innerHeight*2&&!(await this.posts.fetchPosts("older")).length||setTimeout(()=>this.loader(),100)}resize(){const t=Math.round(this.dom.clientWidth/300);this.column(t>=2?t:2)}renderPosts(){this.$focus.layer(100).elementSet.clear();const t=[...this.posts.orderMap.values()].map(s=>{const i=this.$postMap.get(s)??new _t(this,s).on("$focus",(n,a)=>this.$focus.layer(100).focus(a));return this.$postMap.set(s,i),i.self(this.$focus.layer(100).add)});return this.content(t).render(),this}removeAll(){return this.$postMap.clear(),this.$focus.layer(100).removeAll(),this.animate({opacity:[1,0]},{duration:300,easing:"ease"},()=>this.clear().render()),this}}class Zt{static async fetch(e,t,s=20){if(!t.length)return this.searchQuery.map(a=>new Ct(a));const i=await e.fetch(`/autocomplete.json?search[query]=${t}&search[type]=tag_query&version=1&limit=${s}`);return[...t.length?this.searchQuery.filter(a=>a.value.startsWith(t)&&a.value!==t):this.searchQuery,...i].map(a=>new Ct(a))}}h(Zt,"searchQuery",[{value:"user:",label:"user:"},{value:"approver:",label:"approver:"},{value:"-approver:",label:"-approver:"},{value:"order:",label:"order:"},{value:"ordfav:",label:"ordfav:"},{value:"ordfavgroup:",label:"ordfavgroup:"},{value:"search:",label:"search:"},{value:"favgroup:",label:"favgroup:"},{value:"-favgroup:",label:"-favgroup:"},{value:"favcount:",label:"favcount:"},{value:"id:",label:"id:"},{value:"tagcount:",label:"tagcount:"},{value:"gentags:",label:"gentags:"},{value:"arttags:",label:"arttags:"},{value:"chartags:",label:"chartags:"},{value:"copytags:",label:"copytags:"},{value:"metatags:",label:"metatags:"},{value:"score:",label:"score:"},{value:"upvote:",label:"upvote:"},{value:"downvote:",label:"downvote:"},{value:"disapproved:",label:"disapproved:"},{value:"md5:",label:"md5:"},{value:"width:",label:"width:"},{value:"height:",label:"height:"},{value:"ratio:",label:"ratio:"},{value:"mpixels:",label:"mpixels:"},{value:"filesize:",label:"filesize:"},{value:"duration:",label:"duration:"},{value:"is:",label:"is:"},{value:"has:",label:"has:"},{value:"pool:",label:"pool:"},{value:"-pool:",label:"-pool:"},{value:"ordpool:",label:"ordpool:"},{value:"random:",label:"random:"},{value:"limit:",label:"limit:"},{value:"date:",label:"date:"},{value:"commenter:",label:"commenter:"},{value:"note:",label:"note:"},{value:"noter:",label:"noter:"},{value:"noteupdater:",label:"noteupdater:"},{value:"status:",label:"status:"},{value:"-status:",label:"-status:"},{value:"rating:",label:"rating:"},{value:"-rating:",label:"-rating:"},{value:"source:",label:"source:"},{value:"-source:",label:"-source:"},{value:"pixiv:",label:"pixiv:"},{value:"parent:",label:"parent:"},{value:"child:",label:"child:"},{value:"flagger:",label:"flagger:"},{value:"appealer:",label:"appealer:"},{value:"commentary:",label:"commentary:"},{value:"commentaryupdater:",label:"commentaryupdater:"}].map(e=>({type:"query",...e})));class Ct{constructor(e){Object.assign(this,e)}isTag(){return this.type==="tag"||this.type==="tag-autocorrect"||this.type==="tag-alias"||this.type==="tag-word"}isTagAutocorrect(){return this.type==="tag-autocorrect"}isTagAntecedent(){return!!this.antecedent}isTagWord(){return this.type==="tag-word"}isUser(){return this.type==="user"}}class Pe extends p{constructor(){super("searchbar");h(this,"$tagInput",new De(this));h(this,"$selectionList",new Ae);h(this,"typingTimer",null);h(this,"$filter",$("div").class("filter"));this.build(),window.addEventListener("keyup",t=>{!this.inDOM()&&t.key==="/"&&this.open(),this.inDOM()&&t.key==="Escape"&&this.close()})}build(){this.content([$("div").class("input-container").content([this.$tagInput.on("input",()=>this.inputHandler()).on("keydown",t=>this.keyHandler(t)),$("ion-icon").name("close-circle-outline").title("Clear Input").on("click",()=>this.$tagInput.clearAll())]).on("click",t=>{t.target===this.$tagInput.dom&&this.$tagInput.addTag().input()}),$("div").class("selection-list-container").content([this.$selectionList]),this.$filter.on("click",()=>{location.hash==="#search"&&this.close()})])}open(){return location.hash!=="#search"&&$.open(location.href+"#search"),this}close(){return location.hash==="#search"&&$.back(),this}activate(){return this.hide(!1),this.$filter.animate({opacity:[0,.5]},{duration:300,easing:"ease"}),this.$tagInput.input(),this}inactivate(){return this.animate({opacity:[.5,0]},{duration:300,easing:"ease"},()=>this.hide(!0)),this}keyHandler(t){var n,a;const s=()=>{t.preventDefault(),this.$tagInput.addTag().input()},i=o=>{const l=this.$tagInput.children.indexOf(this.$tagInput.$inputor);if(this.$tagInput.$input.value().at(-1)===":")return this.getSearchSuggestions();const c=this.$tagInput.children.array.at(l+1);this.$tagInput.addTag(o.value()),c?this.$tagInput.editTag(c):this.$tagInput.input()};switch(t.key){case"ArrowUp":{t.preventDefault(),this.$selectionList.focusPrevSelection(),this.$tagInput.value((n=this.$selectionList.focused)==null?void 0:n.value());break}case"ArrowDown":{t.preventDefault(),this.$selectionList.focusNextSelection(),this.$tagInput.value((a=this.$selectionList.focused)==null?void 0:a.value());break}case" ":s();break;case"Enter":{t.preventDefault(),this.$selectionList.focused?i(this.$selectionList.focused):(this.$tagInput.addTag(),this.search());break}case"Tab":{t.preventDefault();const o=this.$tagInput.children.indexOf(this.$tagInput.$inputor);if(t.shiftKey){o-1>=0&&this.$tagInput.editTag(this.$tagInput.children.array.at(o-1));break}if(this.$selectionList.focused)i(this.$selectionList.focused);else{const l=this.$tagInput.children.array.at(o+1);l?this.$tagInput.editTag(l):this.$tagInput.addTag().input()}break}case"Backspace":{const o=this.$tagInput.children.indexOf(this.$tagInput.$inputor);o!==0&&!this.$tagInput.$input.value().length&&(t.preventDefault(),this.$tagInput.editTag(this.$tagInput.children.array.at(o-1)));break}}}inputHandler(){this.typingTimer&&(clearTimeout(this.typingTimer),this.typingTimer=null),this.typingTimer=setTimeout(async()=>{this.typingTimer=null,this.getSearchSuggestions()},200)}async getSearchSuggestions(){const t=this.$tagInput.$input.value(),s=await Zt.fetch(v.used,t,20);this.$selectionList.clearSelections().addSelections(s.map(i=>new Ce().value(i.value).content([$("div").class("selection-label").content([i.isTagAntecedent()?$("span").class("tag-antecedent").self(n=>n.dom.innerHTML=i.antecedent.replaceAll(t,`${t}`)):null,$("div").class("label-container").content([i.isTagAntecedent()?$("ion-icon").name("arrow-forward-outline"):null,$("span").class("label").self(n=>n.dom.innerHTML=i.label.replaceAll(t,`${t}`))])]),i.isTag()?$("div").class("tag-detail").content([$("span").class("tag-post-count").content(rt(i.post_count)),$("span").class("tag-category").content(q[i.category])]):null,i.isUser()?$("span").class("user-level").content(i.level):null]).on("click",()=>{this.$tagInput.addTag(i.value).input()})))}search(){return $.replace(`/posts?tags=${this.$tagInput.query.replace(":","%3A")}`),this.$tagInput.clearAll(),this.inactivate(),this}checkURL(t,s){if((t==null?void 0:t.hash)==="#search"&&this.inactivate(),s.hash==="#search"&&this.activate(),`${t==null?void 0:t.pathname}${t==null?void 0:t.search}`==`${s.pathname}${s.search}`)return;const i=s.searchParams.get("tags");this.$tagInput.clearAll(),i==null||i.split(" ").forEach(n=>this.$tagInput.addTag(n))}}class Ae extends p{constructor(){super("selection-list");h(this,"focused",null);h(this,"selections",new Set)}addSelections(t){t=$.orArrayResolve(t);for(const s of t)this.selections.add(s);return this.insert(t),this}clearSelections(){return this.focused=null,this.selections.clear(),this.clear(),this}focusSelection(t){return this.blurSelection(),this.focused=t,t.focus(),t.offsetTopthis.scrollTop()+this.offsetHeight&&this.scrollTop(t.offsetTop+t.offsetHeight-this.offsetHeight),this}blurSelection(){var t;return(t=this.focused)==null||t.blur(),this.focused=null,this}focusNextSelection(){const t=this.selections.array,s=t.at(0);if(this.focused){const i=t.at(t.indexOf(this.focused)+1);i?this.focusSelection(i):s&&this.focusSelection(s)}else s&&this.focusSelection(s)}focusPrevSelection(){const t=this.selections.array;if(this.focused){const s=t.at(t.indexOf(this.focused)-1);s&&this.focusSelection(s)}else{const s=t.at(0);s&&this.focusSelection(s)}}}class Ce extends p{constructor(){super("selection");h(this,"property",{value:""})}value(t){return $.fluent(this,arguments,()=>this.property.value,()=>$.set(this.property,"value",t))}focus(){return this.addClass("active"),this}blur(){return this.removeClass("active"),this}}class De extends p{constructor(t){super("tag-input");h(this,"$input",$("input").type("text"));h(this,"$sizer",$("span").class("sizer"));h(this,"$inputor",$("div").class("input-wrapper").content([this.$sizer,this.$input.on("input",()=>{this.$sizer.content(this.$input.value())})]));h(this,"tags",new Set);h(this,"$seachbar");this.$seachbar=t}input(){return this.insert(this.$inputor),this.$input.focus(),this.$seachbar.$selectionList.clearSelections(),this.$seachbar.getSearchSuggestions(),this}addTag(t){if(t=t??this.$input.value(),!t.length)return this;const s=new Re(t);return s.on("click",()=>this.editTag(s)),this.tags.add(s),this.value(""),this.$input.inDOM()?this.$inputor.replace(s):this.insert(s),this}editTag(t){return this.addTag(),this.tags.delete(t),t.replace(this.$inputor),this.value(t.name),this.$input.focus(),this.$seachbar.getSearchSuggestions(),this}clearAll(){return this.value(""),this.tags.clear(),this.clear(),this}value(t){return t===void 0?this:(this.$input.value(t),this.$sizer.content(t),this)}focus(){return this.$input.focus(),this}get query(){return this.tags.array.map(t=>t.name).toString().replace(",","+")}}class Re extends p{constructor(t){super("tag");h(this,"name");this.name=t,this.build()}build(){this.content(this.name)}}class Ne extends p{constructor(){super("ion-icon")}name(e){return this.attribute("name",e),this}size(e){return this.attribute("size",e),this}disable(e){return this.attribute("disable",e),this}link(e,t=!1){return this.on("click",()=>t?$.replace(e):$.open(e)),this}}class He extends Tt{constructor(){super();h(this,"$icon",$("ion-icon"));h(this,"$label",$("span"));this.addStaticClass("icon"),this.build()}build(){super.content([this.$icon.hide(!0),this.$label])}content(t){return this.$label.content(t),this}icon(t){return this.$icon.name(t).hide(!1),this}link(t,s=!1){return this.on("click",()=>s?$.replace(t):$.open(t)),this}}const Fe=$("route").id("login").path("/login").builder(()=>{const[r,e]=[$.state(""),$.state("")];return[$("div").class("login-container").content([$("h1").content("Login"),$("div").class("username","input-container").content([$("label").for("username").content("Username"),$("input").type("text").id("username").value(r)]),$("div").class("api-key","input-container").content([$("label").for("api-key").content("API Key"),$("input").type("password").id("api-key").value(e)]),$("icon-button").content("Login").on("click",async()=>{await v.used.login(r.value,e.value),v.used.user&&(N.storageUserData={apiKey:e.value,username:r.value},r.set(""),e.set(""),$.replace("/"))}),$("icon-button").content("Create Account").icon("open-outline").on("click",()=>$.open("https://danbooru.donmai.us/users/new","_blank"))])]});class Ve extends p{constructor(){super("drawer");h(this,"$filter",$("div").class("filter"));h(this,"$container",$("div").class("drawer-container"));h(this,"pointers",$.pointers($(document.body)));h(this,"opened",!1);this.hide(!0),this.build()}build(){this.content([this.$container.content([$("div").class("user-info").hide(!0).self(t=>[v.events.on("login",s=>{t.content([$("div").content([$("h3").class("username").content(s.name$),$("div").class("user-detail").content([$("span").class("userid").content(`ID: ${s.id}`),$("span").class("level").content(["Level: ",s.level_string$])])]),$("div").class("user-nav").content([$("icon-button").title("Uploaded Posts").icon("image").content(s.post_upload_count$.convert(rt)).link(`/posts?tags=user:${s.name}`,!0),$("icon-button").title("Favorites").icon("heart").content(s.favorite_count$.convert(rt)).link(`/posts?tags=ordfav:${s.name}`,!0),$("icon-button").title("Forum Posts").icon("document-text").content(s.forum_post_count$.convert(rt)).hide(!0)])]).hide(!1)}).on("logout",()=>{t.clear().hide(!0)})]),$("div").class("nav").content([$("icon-button").icon("log-in-outline").content("Login").link("/login",!0).self(t=>v.events.on("login",()=>t.hide(!0)).on("logout",()=>t.hide(!1))),$("icon-button").icon("log-in-outline").content("Logout").on("dblclick",()=>v.used.logout()).hide(!0).self(t=>v.events.on("login",()=>t.hide(!1)).on("logout",()=>t.hide(!0))),$("icon-button").icon("swap-horizontal").class("switch").content("Switch Booru").on("click",()=>{v.used===xt?v.set(We):v.set(xt),this.close()})])]),this.$filter.on("click",()=>$.back())]),this.pointers.on("move",t=>{var s;(s=$(":slide-viewer"))!=null&&s.contains(t.$target)||(t.$target.parent,!(t.type!=="pen"&&t.type!=="touch")&&(t.move_y>4||t.move_y<-4||(t.move_x<=-7&&(t.delete(),this.open()),t.move_x>=7&&(t.delete(),this.close()))))})}open(){return location.hash!=="#drawer"&&$.open(location.href+"#drawer"),this}close(){return location.hash==="#drawer"&&$.back(),this}activate(){this.opened=!0,this.hide(!1),this.$container.animate({transform:["translateX(100%)","translateX(0%)"]},{fill:"both",duration:300,easing:"ease"}),this.$filter.animate({opacity:[0,1]},{fill:"both",duration:300,easing:"ease"})}inactivate(){this.opened=!1,this.$container.animate({transform:["translateX(0%)","translateX(100%)"]},{fill:"both",duration:300,easing:"ease"},()=>this.hide(!this.opened)),this.$filter.animate({opacity:[1,0]},{fill:"both",duration:300,easing:"ease"})}checkURL(t,s){(t==null?void 0:t.hash)==="#drawer"&&this.inactivate(),s.hash==="#drawer"&&this.activate()}}class Dt{static get detailPanelEnabled(){var e;return(e=this.localdata)==null?void 0:e.detailPanelEnabled}static set detailPanelEnabled(e){this.localdata={...this.localdata,detailPanelEnabled:e}}static get localdata(){const e=localStorage.getItem("local_settings_data");return e?JSON.parse(e):null}static set localdata(e){localStorage.setItem("local_settings_data",JSON.stringify(e))}}$.registerTagName("ion-icon",Ne);$.registerTagName("icon-button",He);$.registerTagName("a",Qt);const[xt,We]=[new v({origin:"https://danbooru.donmai.us",name:"Danbooru"}),new v({origin:"https://safebooru.donmai.us",name:"Safebooru"}),new v({origin:"https://testbooru.donmai.us",name:"Testbooru"})];v.set(v.manager.get(v.storageAPI??"")??xt);const ut=new Pe().hide(!0),St=new Ve,H=$.state(Dt.detailPanelEnabled??!1).on("update",({state$:r})=>Dt.detailPanelEnabled=r.value);$(document.body).content([$("nav").content([$("a").class("title").href("/").content([$("h1").class("booru-name").content(v.name$),$("h2").class("app").content([$("span").class("app-name").content("Viewer"),$("span").class("version").content("v0.13.0")])]),$("div").class("searchbar").content(["Search in ",v.name$]).self(r=>I.events.on("stateChange",({beforeURL:e,afterURL:t})=>{e.hash==="#search"&&r.hide(!1),t.hash==="#search"&&r.hide(!0)})).on("click",()=>ut.open()),$("div").class("buttons").content([$("ion-icon").class("search").name("search-outline").title("Search").self(r=>I.events.on("stateChange",({beforeURL:e,afterURL:t})=>{e.hash==="#search"&&r.hide(!1),t.hash==="#search"&&r.hide(!0)})).on("click",()=>ut.open()),$("ion-icon").class("detail-panel").name("reader-outline").title("Toggle Detail Panel").on("click",()=>H.set(!H.value)),$("a").content($("ion-icon").class("open").name("open-outline").title("Open in Original Site")).href(location.href.replace(location.origin,v.used.origin)).target("_blank"),$("ion-icon").class("copy").name("link-outline").title("Copy Page Link").hide(!1).on("click",(r,e)=>{navigator.clipboard.writeText(`${location.origin}${location.pathname}${location.search}`),e.name("checkmark-outline"),setTimeout(()=>{e.name("link-outline")},2e3)}),$("ion-icon").class("menu").name("menu-outline").title("Menu").hide(!1).self(r=>{v.events.on("login",()=>r.hide(!0)).on("logout",()=>r.hide(!1))}).on("click",()=>$.open(location.href+"#drawer")),$("div").class("account").hide(!0).title("Menu").self(r=>{v.events.on("login",e=>{r.content(e.name$.convert(t=>{var s;return((s=t.at(0))==null?void 0:s.toUpperCase())??""})).hide(!1)}).on("logout",()=>r.hide(!0))}).on("click",()=>St.open())])]),ut,St,$("router").base("/").map([$("route").id("posts").path(["/","/posts"]).builder(({$route:r,query:e})=>{const{$postGrid:t,$detail:s}=Rt(r,e);return[t,s]}),$("route").id("posts").path("/posts?tags").builder(({$route:r,query:e})=>{const{$postGrid:t,$detail:s}=Rt(r,e);return[$("header").content([$("h2").content("Posts"),$("div").class("tags").self(i=>{e.tags.split("+").forEach(n=>{i.insert($("a").class("tag").content(decodeURIComponent(n)).href(`posts?tags=${n}`))})})]),$("div").class("no-post").hide(!0).self(i=>{i.on("startLoad",()=>i.hide(!0)),t.self(()=>{t.posts.events.on("noPost",()=>i.hide(!1).content("No Posts")).on("post_error",n=>i.hide(!1).content(n))})}),t,s]}),Ee,Fe]).on("beforeSwitch",r=>{r.preventDefault();function s(){var a;$(document.documentElement).css({scrollBehavior:"auto"});const n=$.call(()=>{switch(I.navigationDirection){case K.Forward:return["translateX(2%)","translateX(0%)"];case K.Back:return["translateX(-2%)","translateX(0%)"];case K.Replace:return""}});r.$view.content(r.nextContent),r.rendered(),(a=r.nextContent.element)==null||a.class("animated").animate({opacity:[0,1],transform:n},{duration:300,easing:"ease"},()=>{var o;r.switched(),$(document.documentElement).css({scrollBehavior:""}),(o=r.nextContent.element)==null||o.removeClass("animated")})}function i(){var a,o;$(document.documentElement).css({scrollBehavior:"auto"});const n=$.call(()=>{switch(I.navigationDirection){case K.Forward:return["translateX(0%)","translateX(-2%)"];case K.Back:return["translateX(0%)","translateX(2%)"];case K.Replace:return""}});(o=(a=r.previousContent)==null?void 0:a.element)==null||o.class("animated").animate({opacity:[1,0],transform:n},{duration:300,easing:"ease"},()=>{var l,c;(c=(l=r.previousContent)==null?void 0:l.element)==null||c.removeClass("animated"),s()})}r.previousContent?i():s()})]);I.events.on("stateChange",({beforeURL:r,afterURL:e})=>te(r,e));te(void 0,new URL(location.href));function te(r,e){ut.checkURL(r,e),St.checkURL(r,e)}function Rt(r,e){const t=new Le(e),s=new Yt({preview:!0,tagsType:"name_only"}).hide(H.convert(n=>!n)).position(r);i(),H.on("update",i),v.events.on("set",()=>s.update(null));function i(){H.value?t.addClass("detail-panel-enabled"):t.removeClass("detail-panel-enabled")}return t.$focus.on("focus",({$focused:n})=>{n.inDOM()&&n instanceof _t&&s.update(n.post)}).on("blur",()=>s.update(null)),{$postGrid:t,$detail:s}}$.keys($(window)).if(r=>{if(!($(r.target)instanceof O))return!0}).keydown(["q","Q"],r=>{r.preventDefault(),I.index!==0&&$.back()}).keydown(["e","E"],r=>{r.preventDefault(),I.forwardIndex!==0&&$.forward()}).keydown("Tab",r=>{r.preventDefault(),H.set(!H.value)}); diff --git a/dist/assets/index-g15FC_9F.css b/dist/assets/index-g15FC_9F.css deleted file mode 100644 index f6298be..0000000 --- a/dist/assets/index-g15FC_9F.css +++ /dev/null @@ -1 +0,0 @@ -layout.post-grid{margin-block:1rem}layout.post-grid a{transition:.3s all ease}layout.post-grid:has(post-tile[focus]) post-tile:not([focus]) a{opacity:.5}layout.post-grid:has(post-tile[focus]) post-tile:hover a{opacity:1}post-tile{display:block;transition:.3s all ease;position:relative;transition:all .3s ease;border-radius:var(--border-radius-medium);overflow:hidden;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none;outline:transparent solid 2px;background-color:var(--secondary-color-1)}post-tile[focus]{outline:var(--secondary-color-9) solid 2px;transform:scale(1.02)}@media (hover: hover){post-tile:hover{transform:scale(1.02);z-index:1;box-shadow:0 0 10px color-mix(in srgb,var(--secondary-color-1) 50%,transparent)}}post-tile.loading{transition:none}post-tile:active{transform:scale(.95)}post-tile div.video-detail,post-tile div.gif-detail{position:absolute;background-color:var(--secondary-color-3);color:var(--primary-color);bottom:.3rem;right:.3rem;padding:.2em .4em;height:1rem;border-radius:var(--border-radius-small);font-size:12px;display:flex;align-items:center;gap:.2rem;z-index:2}post-tile div.video-detail ion-icon,post-tile div.gif-detail ion-icon{font-size:1.4rem}post-tile div.video-detail ion-icon[name=images-outline],post-tile div.gif-detail ion-icon[name=images-outline]{padding:.1rem;font-size:1rem}post-tile div.video-detail span.duration,post-tile div.gif-detail span.duration{text-transform:uppercase;z-index:2}post-tile a{background-color:transparent;padding:0;border-radius:0}post-tile a img{height:100%;width:100%;vertical-align:top;background-color:var(--secondary-color-1)}post-tile a video{height:100%;width:100%;object-fit:cover;position:absolute;z-index:1}searchbar{display:flex;align-items:center;flex-direction:column;width:100%;z-index:200;position:fixed;height:100%}searchbar div.input-container{margin-top:.4rem;background-color:color-mix(in srgb,var(--secondary-color-2) 100%,transparent);border-radius:var(--border-radius-small);font-size:1rem;width:500px;padding:.4rem;max-width:calc(100% - 2rem);box-sizing:border-box;z-index:201;display:flex;align-items:center;border:1px solid var(--secondary-color-4)}searchbar div.input-container:focus-within{outline:none}searchbar div.input-container tag-input{display:flex;gap:.4rem;width:100%;overflow:hidden;padding-inline:.4rem;box-sizing:border-box;cursor:text}searchbar div.input-container tag-input tag{display:inline-block;padding:.2rem .4rem;background-color:var(--secondary-color-4);color:var(--secondary-color-9);border-radius:var(--border-radius-small);cursor:pointer}searchbar div.input-container ion-icon{font-size:20px;color:var(--secondary-color-4);cursor:pointer}searchbar div.input-container ion-icon:hover{color:var(--secondary-color-9)}searchbar div.selection-list-container{overflow:hidden;border-radius:var(--border-radius-small);background-color:var(--secondary-color-1);z-index:201;max-width:calc(100% - 2rem);width:500px}searchbar div.selection-list-container selection-list{display:block;max-height:40vh;overflow-y:scroll;overflow-x:hidden;position:relative}searchbar div.selection-list-container selection-list::-webkit-scrollbar{width:4px}searchbar div.selection-list-container selection-list selection{display:flex;justify-content:space-between;align-items:center;padding:.4rem 1rem;cursor:pointer;gap:1rem}searchbar div.selection-list-container selection-list selection:hover{background-color:color-mix(in srgb,var(--secondary-color-3) 50%,transparent)}searchbar div.selection-list-container selection-list selection.active{background-color:var(--secondary-color-3)}searchbar div.selection-list-container selection-list selection div.selection-label{display:flex;flex-wrap:wrap;gap:.5rem}searchbar div.selection-list-container selection-list selection div.selection-label .label-container{display:flex;gap:.5rem;align-items:center}searchbar div.selection-list-container selection-list selection div.selection-label .label-container ion-icon{font-size:1rem}searchbar div.selection-list-container selection-list selection div.tag-detail{display:flex;align-items:center;gap:.5rem}searchbar div.selection-list-container selection-list selection div.tag-detail .tag-post-count{font-size:.8rem}searchbar div.selection-list-container selection-list selection .tag-category,searchbar div.selection-list-container selection-list selection .user-level{padding:.1rem .4rem;border-radius:var(--border-radius-small);font-size:.9rem;background-color:var(--secondary-color-4);color:var(--secondary-color-9)}searchbar div.filter{background-color:var(--secondary-color-1);opacity:.5;position:fixed;top:0;height:100%;width:100%;z-index:199}.input-wrapper{color:var(--primary-color);border:1px solid var(--secondary-color-9);border-radius:var(--border-radius-small);position:relative;box-sizing:border-box;line-height:1em;font-size:14px;padding:4px 8px;display:inline-block;max-width:100%;text-overflow:ellipsis}.input-wrapper span.sizer{font-family:inherit;white-space:pre;height:1em;display:inline-block;font-size:inherit;line-height:inherit;box-sizing:border-box;position:relative;opacity:0;min-width:2px;-webkit-user-select:none;user-select:none;vertical-align:top}.input-wrapper input{height:100%;text-overflow:ellipsis;font-family:inherit;background:none;color:inherit;top:0;left:0;font-size:inherit;line-height:inherit;padding:inherit;position:absolute;box-sizing:border-box;width:100%;border:none;outline:none}button.icon{display:flex;justify-content:center;align-items:center;gap:.4rem;padding:.8rem 1.2rem}button.icon ion-icon{font-size:1.4rem;color:inherit}button.icon.vertical{flex-direction:column}ion-icon{font-size:24px;color:var(--primary-color);cursor:pointer}ion-icon:hover{color:var(--secondary-color-9)}ion-icon[disable=true]{color:var(--primary-color-darker);pointer-events:none}drawer{position:fixed;top:0;left:0;display:flex;z-index:300;height:100%;width:100%;box-sizing:border-box;justify-content:end;align-items:center;padding:1rem}drawer div.drawer-container{width:300px;max-width:70%;height:100%;background-color:var(--secondary-color-1);border-radius:var(--border-radius-large);z-index:1;overflow:hidden}drawer div.drawer-container div.user-info{background-color:var(--secondary-color-2);padding:2rem}drawer div.drawer-container div.user-info .username{margin:0;color:var(--secondary-color-9);cursor:pointer}drawer div.drawer-container div.user-info div.user-detail{display:flex;gap:.5rem;margin-top:.2rem}drawer div.drawer-container div.user-info div.user-detail span{font-size:.8rem;color:var(--primary-color-dark);display:block;cursor:pointer}drawer div.drawer-container div.user-info div.user-nav{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;margin-top:1rem}drawer div.drawer-container div.nav{padding:2rem;display:flex;gap:1rem;flex-direction:column}drawer div.drawer-container div.nav button.icon{justify-content:start}drawer div.filter{position:absolute;top:0;left:0;height:100%;width:100%;background:linear-gradient(90deg,color-mix(in srgb,var(--secondary-color-1) 50%,transparent),color-mix(in srgb,var(--secondary-color-0) 70%,transparent))}detail-panel{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:1rem;--padding: 1rem;position:fixed;top:calc(var(--nav-height) + var(--padding));right:var(--padding);width:300px;height:calc(100dvh - 2rem - var(--nav-height));background-color:var(--secondary-color-1)}@media (max-width: 800px){detail-panel{position:static;width:100%;overflow:visible;height:100%;padding:1rem;box-sizing:border-box}}detail-panel span.no-content{color:var(--secondary-color-3);font-size:1.6rem;font-weight:900}detail-panel div.preview{overflow:hidden;border-radius:var(--border-radius-large);height:300px;width:300px;background-color:var(--secondary-color-0)}detail-panel div.preview img{height:100%;width:100%;object-fit:contain}detail-panel div.detail{display:flex;flex-direction:column;gap:.4rem;overflow:scroll;overflow-x:hidden;border-radius:var(--border-radius-large);height:100%;width:100%}detail-panel div.detail::-webkit-scrollbar{background-color:#000;width:0px}detail-panel div.detail::-webkit-scrollbar-thumb{background-color:#aeaeec}detail-panel h3{padding-left:1rem;margin-block:.6rem}detail-panel .post-info{background-color:#2f2f45;border-radius:var(--border-radius-large);padding:20px;display:flex;flex-direction:column;gap:.4rem}detail-panel .post-info .buttons{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;margin-top:1rem}detail-panel div.property{display:flex;gap:.6rem;align-items:center;width:100%}detail-panel div.property span.property-name{flex-shrink:0}detail-panel div.property div.property-values{display:flex;gap:.4rem;width:100%;overflow:hidden}detail-panel div.property div.property-values span.property-value{padding:.2rem .4rem;background-color:var(--secondary-color-1);color:var(--primary-color-dark);border-radius:var(--border-radius-small);justify-content:space-between;flex-shrink:1;display:flex;align-items:center;overflow:hidden}detail-panel div.property div.property-values span.property-value:has(ion-icon){flex-shrink:0}detail-panel div.property div.property-values span.property-value *{display:block;overflow:hidden;text-wrap:nowrap;text-overflow:ellipsis;flex-shrink:1}detail-panel div.property div.property-values span.property-value ion-icon{font-size:1rem;padding:4px;box-sizing:border-box}detail-panel div.inline{display:flex;gap:1rem}detail-panel div.post-tags{display:flex;flex-direction:column;gap:.2rem}detail-panel div.post-tags div.tag{align-items:center}detail-panel div.post-tags div.tag a.tag-name{word-break:break-word;text-decoration:none}detail-panel div.post-tags div.tag span.tag-post-count{background-color:var(--secondary-color-3);color:var(--secondary-color-8);padding:0 4px;border-radius:var(--border-radius-small);font-size:12px;margin-left:.4rem}detail-panel div.post-tags section.tag-name-only{display:flex;flex-wrap:wrap;column-gap:.5rem}#post{padding:0;padding-top:var(--nav-height)}#post slide-viewer{display:block;height:calc(100dvh - 2rem - var(--nav-height));background-color:#000;border-radius:var(--border-radius-large);overflow:hidden;width:calc(100vw - 300px - 4rem);margin:1rem;position:relative;transition:all .3s ease;touch-action:pan-y}@media (max-width: 800px){#post slide-viewer{width:100%;height:calc(100dvh - var(--nav-height));border-radius:0;margin:0}}#post div.viewer{height:100%;width:100%;display:flex;justify-content:center;align-items:center;background-color:#000;border-radius:var(--border-radius-large);overflow:hidden;position:relative;transition:all .3s ease}#post div.viewer img{max-width:100%;max-height:100%;object-fit:contain;transition:all .3s ease}#post div.viewer img.loading{filter:blur(5px)}#post div.viewer video{max-width:100%;max-height:100%;-webkit-user-drag:none;transition:all .3s ease}#post div.viewer div.viewer-panel{position:absolute;bottom:0;width:100%;z-index:1}#post div.viewer div.viewer-panel div.panel{width:100%;display:flex;justify-content:center;flex-direction:column;padding:1rem;gap:1rem;box-sizing:border-box}#post div.viewer div.viewer-panel div.panel video-controller{display:flex;flex-direction:column;gap:1rem;align-items:center;width:100%}#post div.viewer div.viewer-panel div.panel video-controller div.video-details{display:flex;align-items:center;justify-content:space-between;width:100%}#post div.viewer div.viewer-panel div.panel video-controller div.video-details div{display:flex;align-items:center;gap:1rem}#post div.viewer div.viewer-panel div.panel video-controller div.progressbar-container{height:2rem;width:100%;display:flex;touch-action:none;align-items:center;cursor:pointer}#post div.viewer div.viewer-panel div.panel video-controller div.progressbar-container div.progressbar{height:.4rem;width:100%;background-color:var(--secondary-color-1);flex-shrink:1}#post div.viewer div.viewer-panel div.panel video-controller div.progressbar-container div.progressbar div.progress{height:100%;background-color:var(--secondary-color-3);width:100px}#post div.viewer div.viewer-panel div.panel video-controller .play{flex-shrink:0}#post div.viewer div.viewer-panel div.panel div.buttons{width:100%;display:flex;justify-content:center;gap:2rem}#post div.viewer div.viewer-panel div.overlay{position:absolute;bottom:0;width:100%;height:200%;z-index:-1;background:linear-gradient(180deg,color-mix(in srgb,var(--secondary-color-1) 0%,transparent),color-mix(in srgb,var(--secondary-color-0) 70%,transparent))}#post div.content{width:calc(100vw - 300px - 2rem);display:flex;flex-direction:column;padding:1rem;box-sizing:border-box}@media (max-width: 800px){#post div.content{width:100%}}#post div.content::-webkit-scrollbar{background-color:#000;width:4px}#post div.content::-webkit-scrollbar-thumb{background-color:#aeaeec;border-radius:2px}#post div.content>h3{padding-left:1rem;margin-block:1rem}#post div.content section.commentary *{text-wrap:wrap;word-break:break-word}route#login{display:flex;justify-content:center;align-items:center;margin-top:5rem}route#login .login-container{padding:2rem;border:1px solid var(--secondary-color-9);border-radius:var(--border-radius-large);display:flex;flex-direction:column;justify-content:center;gap:1rem;max-width:400px;width:100%;box-sizing:border-box}route#login .login-container h1{margin:0}route#login .login-container .input-container{display:flex;flex-direction:column;gap:.5rem}route#login .login-container .input-container input{display:block}:root{--primary-color: #d1d1ee;--primary-color-dark: #9696b3;--primary-color-darker: #72728d;--secondary-color-9: #aeaeec;--secondary-color-8: #9a9ad6;--secondary-color-7: #7c7cb8;--secondary-color-6: #646497;--secondary-color-5: #545486;--secondary-color-4: #424268;--secondary-color-3: #3b3b66;--secondary-color-2: #24243b;--secondary-color-1: #1e1e2c;--secondary-color-0: #07070c;--shadow-color: #09090e50;--border-radius-small: .4rem;--border-radius-medium: .8rem;--border-radius-large: 1.2rem;--nav-height: 50px}html{overflow-x:hidden;font-size:14px;scroll-behavior:smooth}html ::-webkit-scrollbar{background-color:var(--secondary-color-1);width:8px}html ::-webkit-scrollbar-thumb{background-color:#aeaeec;border-radius:2px}body{overflow-x:hidden;background-color:var(--secondary-color-1);color:var(--primary-color);margin:0;font-family:Microsoft Yahei}nav{display:flex;width:100%;height:var(--nav-height);position:fixed;top:0;z-index:100;background-color:color-mix(in srgb,var(--secondary-color-1) 70%,transparent);justify-content:space-between;align-items:center;padding-inline:1rem;box-sizing:border-box;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}nav a.title{display:flex;align-items:center;gap:.4rem;text-decoration:none}nav a.title .booru-name{color:var(--secondary-color-9);margin:0}nav a.title .app{display:flex;align-items:center;border-radius:var(--border-radius-small);margin:0;gap:.4rem}nav a.title .app .version{color:var(--secondary-color-1);background-color:var(--secondary-color-9);padding:.2em .4em;border-radius:var(--border-radius-small);font-size:.8rem;font-weight:700}nav a.title .app .app-name{display:none;font-size:1rem;color:var(--secondary-color-9)}nav div.searchbar{padding:.4rem 10%;max-width:500px;background-color:color-mix(in srgb,var(--secondary-color-2) 30%,transparent);border:1px solid var(--primary-color-darker);border-radius:var(--border-radius-small);color:var(--primary-color-dark);transition:.3s all ease;cursor:pointer}nav div.searchbar:hover{color:var(--primary-color)}nav div.buttons{display:flex;align-items:center;gap:1rem}nav div.buttons ion-icon{transition:all .3s ease;border-radius:1rem;padding:.4rem}nav div.buttons ion-icon:hover{background-color:color-mix(in srgb,var(--secondary-color-3) 50%,transparent)}nav div.buttons ion-icon.search{display:none}nav div.buttons a{display:flex;justify-content:center;align-items:center}nav div.buttons div.account{height:2rem;width:2rem;display:flex;justify-content:center;align-items:center;border-radius:2rem;font-weight:bolder;color:var(--secondary-color-9);background-color:var(--secondary-color-4);-webkit-user-select:none;user-select:none;cursor:pointer}@media (max-width: 800px){nav div.searchbar{display:none}nav div.buttons ion-icon.search{display:inline-block}nav div.buttons ion-icon.detail-panel{display:none}}router{display:block;position:relative}router route{display:block;position:relative;padding-inline:1rem;padding-top:var(--nav-height)}route#posts header{margin-bottom:1rem}route#posts header h2{margin:0}route#posts header div.tags{display:flex;flex-wrap:wrap;gap:.5rem}route#posts .post-grid.detail-panel-enabled{width:calc(100vw - 300px - 4rem)}@media (max-width: 800px){route#posts .post-grid.detail-panel-enabled{width:100%}}@media (max-width: 800px){route#posts detail-panel{display:none}}section{background-color:#2f2f45;border-radius:var(--border-radius-large);padding:20px}button{background-color:var(--secondary-color-4);color:var(--secondary-color-9);padding:.8rem 1.2rem;border-radius:1rem;border:none;cursor:pointer}button:hover{background-color:var(--secondary-color-6);color:var(--primary-color)}a{text-decoration:none;color:var(--secondary-color-9)}input{background-color:var(--secondary-color-2);border-radius:var(--border-radius-small);border:1px solid var(--secondary-color-3);font-size:.9rem;padding:.8rem 1.2rem;color:var(--primary-color);outline:none}input:focus{border-color:var(--secondary-color-9)} diff --git a/dist/index.html b/dist/index.html deleted file mode 100644 index 15308ca..0000000 --- a/dist/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - Danbooru Viewer - - - - - - - - - - - diff --git a/dist/statics/danbooru-viewer-app-icon@2x.png b/dist/statics/danbooru-viewer-app-icon@2x.png deleted file mode 100644 index 656e338..0000000 Binary files a/dist/statics/danbooru-viewer-app-icon@2x.png and /dev/null differ diff --git a/dist/statics/danbooru-viewer-icon.png b/dist/statics/danbooru-viewer-icon.png deleted file mode 100644 index 7a6d8fe..0000000 Binary files a/dist/statics/danbooru-viewer-icon.png and /dev/null differ diff --git a/dist/statics/manifest.json b/dist/statics/manifest.json deleted file mode 100644 index 875e4b8..0000000 --- a/dist/statics/manifest.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "lang": "en", - "name": "Danbooru Viewer", - "theme_color": "#1E1E2C", - "background_color": "#1E1E2C", - "short_name": "Danbooru Viewer", - "description": "Danbooru images viewer, modern style user interface.", - "start_url": "/", - "dir": "ltr", - "orientation": "any", - "display": "standalone", - "categories": [ - "photo" - ], - "icons": [ - { - "src": "/statics/danbooru-viewer-icon.png", - "sizes": "128x128", - "type": "image/png" - }, - { - "src": "/statics/danbooru-viewer-app-icon@2x.png", - "sizes": "512x512", - "type": "image/png" - } - ] - } \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 87c6009..0000000 --- a/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - Danbooru Viewer - - - - - - - - - - - diff --git a/index.scss b/index.scss deleted file mode 100644 index 6434e77..0000000 --- a/index.scss +++ /dev/null @@ -1,246 +0,0 @@ -// components -@import '/src/component/PostGrid/$PostGrid'; -@import '/src/component/PostTile/$PostTile'; -@import '/src/component/Searchbar/$Searchbar'; -@import '/src/component/IconButton/$IconButton'; -@import '/src/component/IonIcon/$IonIcon'; -@import '/src/component/Drawer/$Drawer'; -@import '/src/component/DetailPanel/$DetailPanel'; -// routes -@import '/src/route/post/$post_route'; -@import '/src/route/login/$login_route'; - -:root { - --primary-color: #d1d1ee; - --primary-color-dark: #9696b3; - --primary-color-darker: #72728d; - --secondary-color-9: #aeaeec; - --secondary-color-8: #9a9ad6; - --secondary-color-7: #7c7cb8; - --secondary-color-6: #646497; - --secondary-color-5: #545486; - --secondary-color-4: #424268; - --secondary-color-3: #3b3b66; - --secondary-color-2: #24243b; - --secondary-color-1: #1e1e2c; // background color - --secondary-color-0: #07070c; - --shadow-color: #09090e50; - - --border-radius-small: 0.4rem; - --border-radius-medium: 0.8rem; - --border-radius-large: 1.2rem; - --nav-height: 50px; -} -html { - overflow-x: hidden; - font-size: 14px; - scroll-behavior: smooth; - ::-webkit-scrollbar { - background-color: var(--secondary-color-1); - width: 8px; - } - - ::-webkit-scrollbar-thumb { - background-color: #aeaeec; - border-radius: 2px; - } -} -body { - overflow-x: hidden; - background-color: var(--secondary-color-1); - color: var(--primary-color); - margin: 0; - font-family: Microsoft Yahei; -} - -nav { - display: flex; - width: 100%; - height: var(--nav-height); - position: fixed; - top: 0; - z-index: 100; - background-color: color-mix(in srgb, var(--secondary-color-1) 70%, transparent); - justify-content: space-between; - align-items: center; - padding-inline: 1rem; - box-sizing: border-box; - backdrop-filter: blur(3px); - - a.title { - display: flex; - align-items: center; - gap: 0.4rem; - text-decoration: none; - .booru-name { - color: var(--secondary-color-9); - margin: 0; - } - - .app { - display: flex; - align-items: center; - border-radius: var(--border-radius-small); - // border: 1px solid var(--secondary-color-9); - // padding: 0.2rem 0.4rem; - margin: 0; - gap: 0.4rem; - .version { - color: var(--secondary-color-1); - background-color: var(--secondary-color-9); - padding: 0.2em 0.4em; - border-radius: var(--border-radius-small); - font-size: 0.8rem; - font-weight: bold; - } - .app-name { - display: none; - font-size: 1rem; - color: var(--secondary-color-9); - } - } - } - div.searchbar { - padding: 0.4rem 10%; - max-width: 500px; - background-color: color-mix(in srgb, var(--secondary-color-2) 30%, transparent); - border: 1px solid var(--primary-color-darker); - border-radius: var(--border-radius-small); - color: var(--primary-color-dark); - transition: 0.3s all ease; - cursor: pointer; - &:hover { - color: var(--primary-color); - } - } - div.buttons { - display: flex; - align-items: center; - gap: 1rem; - ion-icon { - // background-color: var(--secondary-color-1); - transition: all 0.3s ease; - border-radius: 1rem; - padding: 0.4rem; - &:hover { - background-color: color-mix(in srgb, var(--secondary-color-3) 50%, transparent); - } - } - ion-icon.search { - display: none; - } - - a { - display: flex; - justify-content: center; - align-items: center; - } - - div.account { - height: 2rem; - width: 2rem; - display: flex; - justify-content: center; - align-items: center; - border-radius: 2rem; - font-weight: bolder; - color: var(--secondary-color-9); - background-color: var(--secondary-color-4); - user-select: none; - cursor: pointer; - } - } - - @media (max-width: 800px) { - div.searchbar { - display: none; - } - div.buttons ion-icon { - &.search { - display: inline-block; - } - - &.detail-panel { - display: none; - } - } - } -} - -router { - display: block; - position: relative; - - route { - display: block; - position: relative; - padding-inline: 1rem; - padding-top: var(--nav-height); - } -} - -route#posts { - - header { - margin-bottom: 1rem; - h2 { - margin: 0; - } - div.tags { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - } - } - - .post-grid.detail-panel-enabled { - width: calc(100vw - 300px - 4rem); - @media (max-width: 800px) { - width: 100%; - } - } - - @media (max-width: 800px) { - detail-panel { - display: none; - } - } -} - -section { - background-color: #2f2f45; - border-radius: var(--border-radius-large); - padding: 20px; -} - -button { - background-color: var(--secondary-color-4); - color: var(--secondary-color-9); - padding: 0.8rem 1.2rem; - border-radius: 1rem; - border: none; - cursor: pointer; - - &:hover { - background-color: var(--secondary-color-6); - color: var(--primary-color); - } -} - -a { - text-decoration: none; - color: var(--secondary-color-9); -} - -input { - background-color: var(--secondary-color-2); - border-radius: var(--border-radius-small); - border: 1px solid var(--secondary-color-3); - font-size: 0.9rem; - padding: 0.8rem 1.2rem; - color: var(--primary-color); - outline: none; - &:focus { - border-color: var(--secondary-color-9); - } -} \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 0851431..0000000 --- a/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "danbooru-viewer", - "module": "index.ts", - "type": "module", - "version": "0.13.0", - "scripts": { - "dev": "bun x vite", - "build": "bun x vite build", - "start": "bun server.ts" - }, - "devDependencies": { - "@types/bun": "latest", - "vite": "^5.4.8", - "sass": "^1.77.1", - "elexis": "../elexis", - "@elexis/layout": "../elexis-ext/layout", - "@elexis/router": "../elexis-ext/router" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "@elysiajs/cors": "^1.1.1", - "@elysiajs/html": "^1.1.1", - "cheerio": "^1.0.0", - "elysia": "^1.1.20" - } -} \ No newline at end of file diff --git a/public/statics/danbooru-viewer-app-icon@2x.png b/public/statics/danbooru-viewer-app-icon@2x.png deleted file mode 100644 index 656e338..0000000 Binary files a/public/statics/danbooru-viewer-app-icon@2x.png and /dev/null differ diff --git a/public/statics/danbooru-viewer-icon.png b/public/statics/danbooru-viewer-icon.png deleted file mode 100644 index 7a6d8fe..0000000 Binary files a/public/statics/danbooru-viewer-icon.png and /dev/null differ diff --git a/public/statics/manifest.json b/public/statics/manifest.json deleted file mode 100644 index 875e4b8..0000000 --- a/public/statics/manifest.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "lang": "en", - "name": "Danbooru Viewer", - "theme_color": "#1E1E2C", - "background_color": "#1E1E2C", - "short_name": "Danbooru Viewer", - "description": "Danbooru images viewer, modern style user interface.", - "start_url": "/", - "dir": "ltr", - "orientation": "any", - "display": "standalone", - "categories": [ - "photo" - ], - "icons": [ - { - "src": "/statics/danbooru-viewer-icon.png", - "sizes": "128x128", - "type": "image/png" - }, - { - "src": "/statics/danbooru-viewer-app-icon@2x.png", - "sizes": "512x512", - "type": "image/png" - } - ] - } \ No newline at end of file diff --git a/scripts/server.sh b/scripts/server.sh deleted file mode 100755 index 1a6394f..0000000 --- a/scripts/server.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -bun --hot index.ts \ No newline at end of file diff --git a/scripts/vite-build.sh b/scripts/vite-build.sh deleted file mode 100755 index fd4c447..0000000 --- a/scripts/vite-build.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -bunx --bun vite build \ No newline at end of file diff --git a/scripts/vite.sh b/scripts/vite.sh deleted file mode 100755 index ef03a33..0000000 --- a/scripts/vite.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -bunx --bun vite --host 127.0.0.1 \ No newline at end of file diff --git a/server.ts b/server.ts deleted file mode 100644 index fd7ff15..0000000 --- a/server.ts +++ /dev/null @@ -1,102 +0,0 @@ -import cors from "@elysiajs/cors"; -import Elysia from "elysia"; -import * as cheerio from 'cheerio'; -import html from "@elysiajs/html"; -import type { PostData } from "./src/structure/Post"; -const list_format = new Intl.ListFormat('en', {type: 'conjunction', style: 'long'}) -const app = new Elysia() - .use(cors()) - .use(html()) - .get('*', async ({path}) => { - const $ = cheerio.load(Buffer.from(await Bun.file('./dist/index.html').arrayBuffer())); - if (path.match(/posts\/(\d+)/)) { - const post_id = path.match(/posts\/(\d+)/)?.at(1); - const data = await fetch(`https://danbooru.donmai.us/posts/${post_id}.json`).then(res => res.json()) as PostData; - switch (data.file_ext) { - case 'png': - case 'webp': - case 'jpg': - case 'gif': { - $('head') - .append(og("og:image", data.file_url)) - .append(og("og:image:secure_url", data.file_url)) - .append(og('og:image:type', `image/${data.file_ext}`)) - .append(og('og:image:height', data.image_height.toString())) - .append(og('og:image:width', data.image_width.toString())) - .append(og('twitter:image', data.file_url)) - break; - } - case 'zip': $('head').append(og("og:video", data.media_asset.variants.find(v => v.file_ext === 'webm')?.url ?? '')); break; - case 'mp4': - case 'webm': { - $('head') - .append(og("og:video", data.file_url)) - .append(og("og:video:secure_url", data.file_url)) - .append(og("og:video:type", `video/${data.file_ext}`)) - .append(og("og:video:height", data.image_height.toString())) - .append(og("og:video:width", data.image_width.toString())) - .append(og("og:image", data.media_asset.variants.find(v => v.file_ext === 'webp')?.url ?? '')) - break; - } - } - const byArtist = `${list_format.format(data.tag_string_artist.split(' '))}`; - const characters = data.tag_string_character.split(' ').map(str => { - const matched = str.match(/([a-z-_]+)(?:\((\w+)\))?/) - console.debug(str) - return matched?.at(1)?.replaceAll('_', ' ') - }).filter(str => str !== undefined); - const copyrights = data.tag_string_copyright.split(' ').map(str => { - const matched = str.match(/([a-z-_]+)(?:\((\w+)\))?/) - return matched?.at(1)?.replaceAll('_', ' ') - }).filter(str => str !== undefined); - const copyright0 = copyrights.at(0); - const title = `${list_format.format(characters)}${copyright0 ? ` (${copyright0}${copyrights.length > 1 ? ` and ${copyrights.length - 1} more` : ''})` : '' }${byArtist ? ` drawn by ${byArtist}` : ''} | Danbooru Viewer`; - const description = `${data.file_ext.toUpperCase()} | ${data.image_width}x${data.image_height} | ${digitalUnit(data.file_size)}`; - $('head') - .append(og('og:title', title)) - .append(og('og:description', description)) - .append(og('og:site_name', 'Danbooru Viewer')) - .append(og('og:type', 'website')) - .append(og('og:url', `https://danbooru.defaultkavy.com/${path}`)) - .append(og('twitter:site', '@defaultkavy_dev')) - .append(og('twitter:title', title)) - .append(og('twitter:description', description)) - .append(og('twitter:card', 'summary_large_image')) - } - return $.html() - }) - .get('/assets/*', (res) => { - return Bun.file(`./dist/${res.path}`) - }) - .group('/api', app => { return app - .delete('/favorites/:id', async ({params, query}) => { - const data = await fetch(`${query.origin}/favorites/${params.id}.json?login=${query.login}&api_key=${query.api_key}`, {method: "DELETE"}).then(res => res.ok); - return data - }) - }) - .get('/statics/*', (res => { - return Bun.file(`./dist/${res.path}`) - })) - .listen(3030); -console.log('Start listening: 3030') -export type Server = typeof app; - -function og(property: string, content: string | undefined) { - return `` -} - -export function digitalUnit(bytes: number) { - if (bytes < 1000) return `${bytes}B` - const kb = bytes / 1000; - if (kb < 1000) return `${kb.toFixed(2)}kB`; - const mb = bytes / (1000 ** 2); - if (mb < 1000) return `${mb.toFixed(2)}MB`; - const gb = bytes / (1000 ** 3); - if (gb < 1000) return `${gb.toFixed(2)}GB`; - const tb = bytes / (1000 ** 4); - if (tb < 1000) return `${tb.toFixed(2)}TB`; - const pb = bytes / (1000 ** 5); - if (pb < 1000) return `${pb.toFixed(2)}PB`; - const eb = bytes / (1000 * 6); - return `${eb.toFixed(2)}EB`; -} \ No newline at end of file diff --git a/src/component/$SlideViewer.ts b/src/component/$SlideViewer.ts deleted file mode 100644 index 8f660f9..0000000 --- a/src/component/$SlideViewer.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { $Container, $Element, $Node, $Pointer, $PointerDirection, $PointerManager, type $ContainerContentType, type $ContainerEventMap, type $EventMap } from "elexis"; - -export class $SlideViewer extends $Container { - pointers = new $PointerManager(this); - $container = $('div').class('slide-container') - slideMap = new Map(); - slideId: null | string | number = null; - #pointerException?: (pointer: $Pointer, e: PointerEvent) => boolean; - constructor() { - super('slide-viewer'); - this.css({position: 'relative'}); - this.__build__(); - new ResizeObserver(() => { - if (!this.inDOM()) return; - this.__render__(); - this.trigger('resize'); - }).observe(this.dom); - } - - protected __build__() { - this.content([ this.$container ]); - this.$container.css({position: 'relative', height: '100%'}) - let containerStartLeft = 0, containerLeft = 0; - this.pointers.on('down', ($pointer, e) => { - if (this.#pointerException) { - if (!this.#pointerException($pointer, e)) return $pointer.delete(); - } - containerStartLeft = this.$container.offsetLeft; - }) - this.pointers.on('move', ($pointer, e) => { - if ($pointer.direction !== $PointerDirection.Horizontal) return; - e.preventDefault(); - containerLeft = containerStartLeft + $pointer.move_x; - if (containerLeft > containerStartLeft && this.slideList.at(0)?.slideId() === this.slideId) return; - if (containerLeft < containerStartLeft && this.slideList.at(-1)?.slideId() === this.slideId) return; - this.$container.css({left: `${containerLeft}px`}); - }) - this.pointers.on('up', ($pointer) => { - const width = this.domRect().width; - const containerMove = containerStartLeft - this.$container.offsetLeft; - if ($pointer.move_x === 0) return; - if ($pointer.movement_x < -5 || containerMove > width / 2) this.next(); - else if ($pointer.movement_x > 5 || containerMove + width < width / 2) this.prev(); - else { - containerLeft = containerStartLeft; - this.__slideAnimate__() - } - }) - } - - addSlides(slides: OrMatrix<$Slide>) { - slides = $.orArrayResolve(slides); - if (!slides.length) return; - for (const $slide of slides) { - if ($slide instanceof Array) this.addSlides($slide); - else { - this.slideMap.set($slide.slideId(), $slide); - this.$container.insert($slide) - } - } - this.__render__(); - return this; - } - - arrange(list: (string | number)[]) { - const newOrderedMap = new Map(); - list.forEach(id => { - const $slide = this.slideMap.get(id); - if (!$slide) return; - newOrderedMap.set(id, $slide); - }) - this.slideMap = newOrderedMap; - this.__render__(); - return this; - } - - switch(id: string | number | undefined) { - if (id === undefined) return this; - const $targetSlide = this.slideMap.get(id); - if (!$targetSlide) throw 'target undefined'; - if ($targetSlide.slideId() === this.slideId) return this; - this.events.fire('beforeSwitch', {prevSlide: this.currentSlide, nextSlide: $targetSlide}) - this.slideId = id; - this.__slideAnimate__(); - this.events.fire('switch', {nextSlide: $targetSlide}) - return this; - } - - protected __slideAnimate__() { - const currentIndex = this.currentSlide ? this.slideList.indexOf(this.currentSlide) : undefined; - if (currentIndex === undefined) return; - const ease = Math.abs(this.getPositionLeft(currentIndex) - this.$container.offsetLeft) === this.dom.clientWidth; - this.$container.animate({ - left: `-${this.getPositionLeft(currentIndex)}px`, - }, { - duration: 300, - easing: ease ? 'ease' : 'ease-out', - }, (animation) => { - this.$container.css({left: `-${this.getPositionLeft(currentIndex)}px`}) - this.__render__(false); - }) - } - - protected __navigation__(dir: 'next' | 'prev') { - const currentSlide = this.currentSlide; - const slideList = this.slideList; - const currentIndex = currentSlide ? slideList.indexOf(currentSlide) : undefined; - if (currentIndex === undefined) { this.switch(slideList.at(0)?.slideId()); return this } - const targetIndex = $.call(() => { - switch (dir) { - case 'next': return currentIndex === slideList.length ? currentIndex : currentIndex + 1 - case 'prev': return currentIndex === 0 ? currentIndex : currentIndex -1 - } - }) - const $targetSlide = this.slideList.at(targetIndex); - this.switch($targetSlide?.slideId()); - return this; - } - - next() { return this.__navigation__('next') } - prev() { return this.__navigation__('prev') } - - get currentSlide() { return this.slideId ? this.slideMap.get(this.slideId) : undefined; } - get slideIdList() { return Array.from(this.slideMap.keys()); } - get slideList() { return Array.from(this.slideMap.values()); } - - protected getPositionLeft(index: number) { return index * this.dom.clientWidth } - - protected __render__(positioning = true) { - let i = 0; - this.slideMap.forEach($slide => { - $slide.hide(true, false); - $slide.css({top: '0', left: `${this.getPositionLeft(i)}px`}); - i++; - }) - if (!this.currentSlide) return; - const currentIndex = this.slideList.indexOf(this.currentSlide); - this.currentSlide.build().hide(false, false); - if (currentIndex !== 0) this.slideList.at(currentIndex - 1)?.build().hide(false, false); - if (currentIndex !== this.slideList.length - 1) this.slideList.at(currentIndex + 1)?.build().hide(false, false); - this.$container.children.render(); - if (positioning) this.$container.css({left: `-${this.getPositionLeft(currentIndex)}px`}) - } - - pointerException(resolver: (pointer: $Pointer, e: PointerEvent) => boolean) { - this.#pointerException = resolver; - return this; - } - -} - -export interface $SlideViewerEventMap extends $ContainerEventMap { - switch: [{nextSlide: $Slide}]; - beforeSwitch: [{prevSlide?: $Slide, nextSlide: $Slide}]; -} - -export class $Slide extends $Container { - #builder?: () => OrMatrix<$ContainerContentType>; - builded = false; - #slideId?: string | number; - constructor() { - super('slide'); - this.css({width: '100%', height: '100%', display: 'block', position: 'absolute'}) - } - - builder(builder: () => OrMatrix<$ContainerContentType>) { - this.#builder = builder; - return this; - } - - build() { - if (!this.builded && this.#builder) { - this.content(this.#builder()); - this.builded = true; - } - return this; - } - - slideId(): string | number; - slideId(slideId: string | number): this; - slideId(slideId?: string | number) { return $.fluent(this, arguments, () => this.#slideId, () => this.#slideId = slideId) } -} \ No newline at end of file diff --git a/src/component/DetailPanel/$DetailPanel.ts b/src/component/DetailPanel/$DetailPanel.ts deleted file mode 100644 index 56369f9..0000000 --- a/src/component/DetailPanel/$DetailPanel.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { $Container, type $ContainerContentType } from "elexis"; -import type { Post } from "../../structure/Post"; -import { Booru } from "../../structure/Booru"; -import { Tag, TagCategory } from "../../structure/Tag"; -import { numberFormat } from "../../structure/Util"; -import type { $IonIcon } from "../IonIcon/$IonIcon"; -import type { $Route } from "@elexis/router"; - -export class $DetailPanel extends $Container { - post: Post | null = null; - options: $DetailPanelOptions; - constructor(options?: $DetailPanelOptions) { - super('detail-panel'); - this.options = { - preview: options?.preview ?? false, - tagsType: options?.tagsType ?? 'detail' - }; - this.build(); - - } - - private build() { - if (this.post) { - this.content([ - this.options.preview ? $('div').class('preview').content([ - $('img').src(this.post.previewURL) - ]) : null, - $('div').class('detail').content([ - $('section').class('post-info').content([ - new $Property('id').name('Post').content(`#${this.post.id}`), - new $Property('uploader').name('Uploader').content(this.post.uploader$), - new $Property('approver').name('Approver').content(this.post.approver$), - new $Property('date').name('Date').content(this.post.created_date$), - new $Property('size').name('Size').content([this.post.file_size$, this.post.dimension$]), - new $Property('file-type').name('File Type').content(this.post.file_ext$), - $('div').class('inline').content([ - new $Property('favorites').name('Favorites').content(this.post.favcount$), - new $Property('score').name('Score').content(this.post.score$) - ]), - this.post.file_url ? new $Property('file-url').name('File').content([ - $('a').href(this.post.file_url$).content(this.post.file_url$.convert((value) => value ? value.replace('https://', '') : '' )).target('_blank'), - $('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.file_url!)) - ]) : null, - new $Property('source-url').name('Source').content([ - $('a').href(this.post.source$).content(this.post.source$.convert((value) => value.replace('https://', ''))).target('_blank'), - $('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.source)) - ]), - new $Property('booru-url').name(Booru.name$).content([ - $('a').href(this.post.booruUrl$).content(this.post.booruUrl$.convert((value) => value.replace('https://', ''))).target('_blank'), - $('ion-icon').name('clipboard').on('click', (e, $ion) => this.copyButtonHandler($ion, this.post!.booruUrl)) - ]), - new $Property('webm-url').name('Webm').hide(true).self(async ($property) => { - await this.post!.ready; - if (this.post!.isUgoria) $property.content($('a').href(this.post!.webm_url$).content(this.post!.webm_url$.convert((value) => value.replace('https://', ''))).target('_blank')).hide(false); - }), - ]), - $('div').class('post-tags').content(async $tags => { - if (this.options.tagsType === 'detail') { - const tags = (await this.post!.fetchTags()).tags; - const [artist_tags, char_tags, gen_tags, meta_tags, copy_tags] = [ - tags.filter(tag => tag.category === TagCategory.Artist), - tags.filter(tag => tag.category === TagCategory.Character), - tags.filter(tag => tag.category === TagCategory.General), - tags.filter(tag => tag.category === TagCategory.Meta), - tags.filter(tag => tag.category === TagCategory.Copyright), - ] - - function $tag_category(category: string, tags: Tag[]) { - return tags.length ? [ - $('h3').content(category), - $('section').content([ - tags.map(tag => $('div').class('tag').content([ - $('a').class('tag-name').content(tag.name).href(`/posts?tags=${tag.name}`), - $('span').class('tag-post-count').content(tag.post_count$.convert(numberFormat)) - ])) - ]) - ] : null - } - - return [ - $tag_category('Artist', artist_tags), - $tag_category('Character', char_tags), - $tag_category('Copyright', copy_tags), - $tag_category('Meta', meta_tags), - $tag_category('General', gen_tags), - ] - } else { - function $tag_category(category: string, tags: string[]) { - return tags.at(0)?.length ? [ - $('h3').content(category), - $('section').class('tag-name-only').content([ - tags.map(tag => $('a').class('tag').content(tag).href(`/posts?tags=${tag}`)), - ]) - ] : null - } - return [ - $tag_category('Artist', this.post!.tag_string_artist.split(' ')), - $tag_category('Character', this.post!.tag_string_character.split(' ')), - $tag_category('Copyright', this.post!.tag_string_copyright.split(' ')), - $tag_category('Meta', this.post!.tag_string_meta.split(' ')), - $tag_category('General', this.post!.tag_string_general.split(' ')), - ] - } - }) - ]) - ]) - } else { - this.content($('span').class('no-content').content('No Selected')) - } - } - - update(post: Post | null) { - this.post = post; - this.build(); - return this; - } - - private copyButtonHandler($ion: $IonIcon, text: string) { - $ion.name('checkmark'); - navigator.clipboard.writeText(text); - setTimeout(() => $ion.name('clipboard'), 3000); - } - - position($route: $Route) { - let scrollTop = 0; - addEventListener('scroll', () => { if (this.inDOM()) scrollTop = document.documentElement.scrollTop }, {passive: true}) - $route - .on('beforeShift', () => { if (innerWidth > 800) this.css({position: `absolute`, top: `calc(${scrollTop}px + var(--nav-height) + var(--padding))`}) }) - .on('afterShift', () => this.css({position: '', top: ''})) - return this; - } -} - -export interface $DetailPanelOptions { - preview?: boolean; - tagsType?: 'detail' | 'name_only'; -} - -class $Property extends $Container { - $name = $('span').class('property-name') - $values = $('div').class('property-values') - constructor(id: string) { - super('div'); - this.staticClass('property').attribute('property-id', id); - super.content([ - this.$name, - this.$values.hide(true) - ]) - } - - name(content: $ContainerContentType) { - this.$name.content(content); - return this; - } - - content(content: OrMatrix<$ContainerContentType>) { - this.$values.hide(false); - const list = $.orArrayResolve(content); - this.$values.content(list.map($item => $('span').staticClass('property-value').content($item))); - return this; - } -} \ No newline at end of file diff --git a/src/component/DetailPanel/_$DetailPanel.scss b/src/component/DetailPanel/_$DetailPanel.scss deleted file mode 100644 index b05e424..0000000 --- a/src/component/DetailPanel/_$DetailPanel.scss +++ /dev/null @@ -1,162 +0,0 @@ -detail-panel { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 1rem; - --padding: 1rem; - position: fixed; - top: calc(var(--nav-height) + var(--padding)); - right: var(--padding); - width: 300px; - height: calc(100dvh - 2rem - var(--nav-height)); - // transition: all 0.3s ease; - background-color: var(--secondary-color-1); - - @media (max-width: 800px) { - position: static; - width: 100%; - overflow: visible; - height: 100%; - padding: 1rem; - box-sizing: border-box; - } - - span.no-content { - color: var(--secondary-color-3); - font-size: 1.6rem; - font-weight: 900; - } - - div.preview { - overflow: hidden; - border-radius: var(--border-radius-large); - height: 300px; - width: 300px; - background-color: var(--secondary-color-0); - img { - height: 100%; - width: 100%; - object-fit: contain; - } - } - - div.detail { - display: flex; - flex-direction: column; - gap: 0.4rem; - overflow: scroll; - overflow-x: hidden; - border-radius: var(--border-radius-large); - height: 100%; - width: 100%; - - &::-webkit-scrollbar { - background-color: #000000; - width: 0px; - } - - &::-webkit-scrollbar-thumb { - background-color: #aeaeec; - } - } - - - h3 { - padding-left: 1rem; - margin-block: 0.6rem; - } - .post-info { - background-color: #2f2f45; - border-radius: var(--border-radius-large); - padding: 20px; - display: flex; - flex-direction: column; - gap: 0.4rem; - - .buttons { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 1rem; - margin-top: 1rem; - } - } - - div.property { - display: flex; - gap: 0.6rem; - align-items: center; - width: 100%; - span.property-name { - flex-shrink: 0; - } - div.property-values { - display: flex; - gap: 0.4rem; - width: 100%; - overflow: hidden; - span.property-value { - padding: 0.2rem 0.4rem; - background-color: var(--secondary-color-1); - color: var(--primary-color-dark); - border-radius: var(--border-radius-small); - justify-content: space-between; - flex-shrink: 1; - display: flex; - align-items: center; - overflow: hidden; - - &:has(ion-icon) { - flex-shrink: 0; - } - - * { - display: block; - overflow: hidden; - text-wrap: nowrap; - text-overflow: ellipsis; - flex-shrink: 1; - } - - ion-icon { - font-size: 1rem; - padding: 4px; - box-sizing: border-box; - } - } - } - } - div.inline { - display: flex; - gap: 1rem; - } - div.post-tags { - display: flex; - flex-direction: column; - gap: 0.2rem; - div.tag { - align-items: center; - a.tag-name { - word-break: break-word; - text-decoration: none; - } - span.tag-post-count { - background-color: var(--secondary-color-3); - color: var(--secondary-color-8); - padding: 0px 4px; - border-radius: var(--border-radius-small); - font-size: 12px; - margin-left: 0.4rem; - } - } - - section.tag-name-only { - display: flex; - flex-wrap: wrap; - column-gap: 0.5rem; - a.tag { - - } - } - } -} \ No newline at end of file diff --git a/src/component/Drawer/$Drawer.ts b/src/component/Drawer/$Drawer.ts deleted file mode 100644 index 1eede63..0000000 --- a/src/component/Drawer/$Drawer.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { $Container } from "elexis"; -import { Booru } from "../../structure/Booru"; -import { numberFormat } from "../../structure/Util"; -import { danbooru, safebooru } from "../../main"; - -export class $Drawer extends $Container { - $filter = $('div').class('filter'); - $container = $('div').class('drawer-container') - pointers = $.pointers($(document.body)); - protected opened = false; - constructor() { - super('drawer'); - this.hide(true); - this.build(); - } - - private build() { - this.content([ - this.$container.content([ - $('div').class('user-info').hide(true).self(($div) => [ - Booru.events - .on('login', (user) => { - $div.content([ - $('div').content([ - $('h3').class('username').content(user.name$), - $('div').class('user-detail').content([ - $('span').class('userid').content(`ID: ${user.id}`), - $('span').class('level').content(['Level: ', user.level_string$]) - ]) - ]),//.on('click', () => $.replace(user.url)), - $('div').class('user-nav').content([ - $('icon-button').title('Uploaded Posts').icon('image').content(user.post_upload_count$.convert(numberFormat)).link(`/posts?tags=user:${user.name}`, true), - $('icon-button').title('Favorites').icon('heart').content(user.favorite_count$.convert(numberFormat)).link(`/posts?tags=ordfav:${user.name}`, true), - $('icon-button').title('Forum Posts').icon('document-text').content(user.forum_post_count$.convert(numberFormat)).hide(true), - ]) - ]).hide(false); - }) - .on('logout', () => { - $div.clear().hide(true); - }) - ]), - $('div').class('nav').content([ - $('icon-button').icon('log-in-outline').content('Login').link('/login', true) - .self(($div => Booru.events.on('login', () => $div.hide(true)).on('logout', () => $div.hide(false)))), - $('icon-button').icon('log-in-outline').content('Logout').on('dblclick', () => Booru.used.logout()).hide(true) - .self(($div => Booru.events.on('login', () => $div.hide(false)).on('logout', () => $div.hide(true)))), - - $('icon-button').icon('swap-horizontal').class('switch').content('Switch Booru') - .on('click', () => { - if (Booru.used === danbooru) Booru.set(safebooru); - else Booru.set(danbooru); - this.close(); - }), - ]) - ]), - this.$filter.on('click', () => $.back()) - ]) - - this.pointers.on('move', pointer => { - if ($(':slide-viewer')?.contains(pointer.$target)) return; - pointer.$target.parent - if (pointer.type !== 'pen' && pointer.type !== 'touch') return; - if (pointer.move_y > 4 || pointer.move_y < -4) return; - if (pointer.move_x <= -7) { pointer.delete(); this.open(); } - if (pointer.move_x >= 7) { pointer.delete(); this.close(); } - }) - } - - open() { if (location.hash !== '#drawer') $.open(location.href + '#drawer'); return this; } - close() { if (location.hash === '#drawer') $.back(); return this; } - - private activate() { - this.opened = true; - this.hide(false); - this.$container.animate({ - transform: [`translateX(100%)`, `translateX(0%)`] - }, { - fill: 'both', - duration: 300, - easing: 'ease' - }) - this.$filter.animate({ - opacity: [0, 1] - }, { - fill: 'both', - duration: 300, - easing: 'ease' - }) - } - - private inactivate() { - this.opened = false - this.$container.animate({ - transform: [`translateX(0%)`, `translateX(100%)`] - }, { - fill: 'both', - duration: 300, - easing: 'ease' - }, () => this.hide(!this.opened)) - this.$filter.animate({ - opacity: [1, 0] - }, { - fill: 'both', - duration: 300, - easing: 'ease' - }) - } - - checkURL(beforeURL: URL | undefined, afterURL: URL) { - if (beforeURL?.hash === '#drawer') this.inactivate(); - if (afterURL.hash === '#drawer') this.activate(); - } -} \ No newline at end of file diff --git a/src/component/Drawer/_$Drawer.scss b/src/component/Drawer/_$Drawer.scss deleted file mode 100644 index cc78f57..0000000 --- a/src/component/Drawer/_$Drawer.scss +++ /dev/null @@ -1,75 +0,0 @@ -drawer { - position: fixed; - top: 0; - left: 0; - display: flex; - z-index: 300; - height: 100%; - width: 100%; - box-sizing: border-box; - justify-content: end; - align-items: center; - padding: 1rem; - - div.drawer-container { - width: 300px; - max-width: 70%; - height: 100%; - background-color: var(--secondary-color-1); - border-radius: var(--border-radius-large); - z-index: 1; - overflow: hidden; - - div.user-info { - background-color: var(--secondary-color-2); - padding: 2rem; - - .username { - margin: 0; - color: var(--secondary-color-9); - cursor: pointer; - } - - div.user-detail { - display: flex; - gap: 0.5rem; - margin-top: 0.2rem; - span { - font-size: 0.8rem; - color: var(--primary-color-dark); - display: block; - cursor: pointer; - } - } - - div.user-nav { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 1rem; - margin-top: 1rem; - } - } - - div.nav { - padding: 2rem; - display: flex; - gap: 1rem; - flex-direction: column; - button.icon { - justify-content: start; - } - } - } - - div.filter { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - background: linear-gradient(90deg, - color-mix(in srgb, var(--secondary-color-1) 50%, transparent) 0%, - color-mix(in srgb, var(--secondary-color-0) 70%, transparent) 100% - ); - } -} \ No newline at end of file diff --git a/src/component/IconButton/$IconButton.ts b/src/component/IconButton/$IconButton.ts deleted file mode 100644 index 41651f1..0000000 --- a/src/component/IconButton/$IconButton.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { $Button, type $ContainerContentBuilder } from "elexis"; - -export class $IconButton extends $Button { - $icon = $('ion-icon'); - $label = $('span'); - constructor() { - super(); - this.addStaticClass('icon') - this.build(); - } - - private build() { - super.content([ - this.$icon.hide(true), - this.$label - ]) - } - - content(children: $ContainerContentBuilder): this { - this.$label.content(children); - return this; - } - - icon(name: string) { - this.$icon.name(name).hide(false); - return this; - } - - link(url: string, replace = false) { - this.on('click', () => replace ? $.replace(url) : $.open(url)); - return this; - } -} \ No newline at end of file diff --git a/src/component/IconButton/_$IconButton.scss b/src/component/IconButton/_$IconButton.scss deleted file mode 100644 index 583fd67..0000000 --- a/src/component/IconButton/_$IconButton.scss +++ /dev/null @@ -1,15 +0,0 @@ -button.icon { - display: flex; - justify-content: center; - align-items: center; - gap: 0.4rem; - padding: 0.8rem 1.2rem; - ion-icon { - font-size: 1.4rem; - color: inherit; - } - - &.vertical { - flex-direction: column; - } -} \ No newline at end of file diff --git a/src/component/IonIcon/$IonIcon.ts b/src/component/IonIcon/$IonIcon.ts deleted file mode 100644 index ccabdda..0000000 --- a/src/component/IonIcon/$IonIcon.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { $Container } from "elexis"; - -export class $IonIcon extends $Container { - constructor() { - super('ion-icon'); - } - - name(name: string) { - this.attribute('name', name); - return this; - } - - size(size: 'small' | 'large') { - this.attribute('size', size); - return this; - } - - disable(disable: boolean) { - this.attribute('disable', disable); - return this; - } - - link(url: string, replace = false) { - this.on('click', () => replace ? $.replace(url) : $.open(url)); - return this; - } -} \ No newline at end of file diff --git a/src/component/IonIcon/_$IonIcon.scss b/src/component/IonIcon/_$IonIcon.scss deleted file mode 100644 index 709e349..0000000 --- a/src/component/IonIcon/_$IonIcon.scss +++ /dev/null @@ -1,14 +0,0 @@ -ion-icon { - font-size: 24px; - color: var(--primary-color); - cursor: pointer; - - &:hover { - color: var(--secondary-color-9); - } - - &[disable="true"] { - color: var(--primary-color-darker); - pointer-events: none; - } -} \ No newline at end of file diff --git a/src/component/PostGrid/$PostGrid.ts b/src/component/PostGrid/$PostGrid.ts deleted file mode 100644 index df06a26..0000000 --- a/src/component/PostGrid/$PostGrid.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { $Layout, type $LayoutEventMap } from "@elexis/layout"; -import { Booru } from "../../structure/Booru"; -import { Post } from "../../structure/Post"; -import { $PostTile } from "../PostTile/$PostTile"; -import { $Input } from "elexis/lib/node/$Input"; -import { PostManager } from "../../structure/PostManager"; - -interface $PostGridOptions { - tags?: string -} -export class $PostGrid extends $Layout { - $postMap = new Map(); - tags?: string; - $focus = $.focus(); - posts: PostManager; - constructor(options?: $PostGridOptions) { - super(); - this.tags = options?.tags; - this.posts = PostManager.get(this.tags); - this.addStaticClass('post-grid'); - this.type('waterfall').gap(10); - this.init(); - } - - protected async init() { - this.posts.events.on('post_fetch', (posts) => { this.renderPosts() }) - setInterval(async () => { if (this.inDOM() && document.documentElement.scrollTop === 0) await this.posts.fetchPosts('newer'); }, 10000); - Booru.events.on('set', () => { - this.removeAll(); - this.loader(); - }) - this.on('resize', () => this.resize()) - // this.on('afterRender', () => { - // this.$focus.currentLayer?.focus(this.$focus.currentLayer.currentFocus); - // }) - this.loader(); - this.$focus.layer(100).loop(false).scrollThreshold($.rem(2) + 60); - - $.keys($(window)) - .if(e => { - if (!this.inDOM()) return; - if ($(e.target) instanceof $Input) return; - return true; - }) - // .keydown('Tab', e => { - // e.preventDefault(); - // if (e.shiftKey) this.$focus.prev(); - // else this.$focus.next(); - // }) - .keydown(['w', 'W'], e => { e.preventDefault(); this.$focus.up(); }) - .keydown(['s', 'S'], e => { e.preventDefault(); this.$focus.down(); }) - .keydown(['d', 'D'], e => { e.preventDefault(); this.$focus.right(); }) - .keydown(['a', 'A'], e => { e.preventDefault(); this.$focus.left(); }) - .keydown([' ', 'Enter'], e => { - e.preventDefault(); - const focused = this.$focus.currentLayer?.currentFocus; - if (focused instanceof $PostTile) $.open(focused.url); - }) - .keydown(['Escape'], e => { e.preventDefault(); this.$focus.blur(); }) - } - - protected async loader() { - if (!this.inDOM()) return setTimeout(() => this.loader(), 100);; - while (this.inDOM() && document.documentElement.scrollHeight <= innerHeight * 2) { - const posts = await this.posts.fetchPosts('older'); - if (!posts.length) return; - } - if (document.documentElement.scrollTop + innerHeight > document.documentElement.scrollHeight - innerHeight * 2) { - const posts = await this.posts.fetchPosts('older'); - if (!posts.length) return; - } - setTimeout(() => this.loader(), 100); - } - - protected resize() { - const col = Math.round(this.dom.clientWidth / 300); - this.column(col >= 2 ? col : 2); - } - - renderPosts() { - this.$focus.layer(100).elementSet.clear(); - const $postList = [...this.posts.orderMap.values()].map(post => { - const $post = this.$postMap.get(post) ?? new $PostTile(this, post).on('$focus', (e, $post) => this.$focus.layer(100).focus($post)); - this.$postMap.set(post, $post) - return $post.self(this.$focus.layer(100).add) - }); - this.content($postList).render(); - return this; - } - - removeAll() { - this.$postMap.clear(); - this.$focus.layer(100).removeAll(); - this.animate({opacity: [1, 0]}, {duration: 300, easing: 'ease'}, () => this.clear().render()) - return this; - } - -} \ No newline at end of file diff --git a/src/component/PostGrid/_$PostGrid.scss b/src/component/PostGrid/_$PostGrid.scss deleted file mode 100644 index dccd82b..0000000 --- a/src/component/PostGrid/_$PostGrid.scss +++ /dev/null @@ -1,19 +0,0 @@ -layout.post-grid { - margin-block: 1rem; - - a { - transition: 0.3s all ease; - } - &:has(post-tile[focus]) { - post-tile:not([focus]) { - a { - opacity: 0.5; - } - } - post-tile:hover { - a { - opacity: 1; - } - } - } -} \ No newline at end of file diff --git a/src/component/PostTile/$PostTile.ts b/src/component/PostTile/$PostTile.ts deleted file mode 100644 index 57c8bd7..0000000 --- a/src/component/PostTile/$PostTile.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { $Container, $Image, $State, $Video } from "elexis"; -import type { Post } from "../../structure/Post"; -import { time } from "../../structure/Util"; -import { detailPanelEnable$ } from "../../main"; -import type { $PostGrid } from "../PostGrid/$PostGrid"; -export class $PostTile extends $Container { - post: Post; - $video: $Video | null; - $img: $Image; - duration$ = $.state(``); - $grid: $PostGrid; - constructor($grid: $PostGrid, post: Post) { - super('post-tile'); - this.$grid = $grid; - this.post = post; - this.$video = this.post.isVideo ? $('video').width(this.post.image_width).height(this.post.image_height).disablePictureInPicture(true).loop(true).muted(true).hide(true).on('mousedown', (e) => e.preventDefault()) : null; - this.$img = $('img').draggable(false).css({opacity: '0'}).width(this.post.image_width).height(this.post.image_height).src(this.post.previewURL).loading('lazy'); - this.attribute('filetype', this.post.file_ext); - this.durationUpdate(); - this.build(); - } - - build() { - this.$video?.on('timeupdate', (e, $video) => { - this.durationUpdate(); - }) - this.class('loading').content([ - // Video Detail - this.post.isVideo - ? $('div').class('video-detail').content([ - this.post.hasSound ? $('ion-icon').name('volume-medium-outline') : null, - this.post.isUgoria ? $('ion-icon').name('images-outline') : null, - $('span').class('duration').content(this.duration$) - ]) : null, - // Gif - this.post.isGif - ? $('div').class('gif-detail').content([ - $('span').content('GIF') - ]) : null, - // Tile - $('a').href(this.url).preventDefault(detailPanelEnable$).content(() => [ - this.$video, - this.$img.on('mousedown', (e) => e.preventDefault()) - .once('load', (e, $img) => { - $img.animate({opacity: [0, 1]}, {duration: 300}, () => $img.css({opacity: ''})); - this.removeClass('loading'); - }) - ]) - ]) - this.on(['focus', 'mouseenter', 'touchstart'], () => { - if (!this.$video?.isPlaying) { - this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined) - } - if (this.post.isGif) { this.$img.src(this.post.large_file_url) } - }, {passive: true} ) - .on(['blur', 'mouseleave', 'touchend', 'touchcancel'], () => { - this.$video?.pause().currentTime(0).hide(true); - if (this.post.isGif) { this.$img.src(this.post.previewURL) } - }, {passive: true} ) - .on('click', () => { - if (!detailPanelEnable$.value) return; - if (innerWidth <= 800) return $.open(this.url); - if (this.attribute('focus') === '') $.open(this.url); - else this.trigger('$focus'); - }) - } - - durationUpdate() { - if (!this.$video) return; - const t = time(this.post.media_asset.duration * 1000 - this.$video.currentTime() * 1000) - this.duration$.set(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`) - } - - get url() { return `${this.post.pathname}${this.$grid.tags ? `?q=${this.$grid.tags}` : ''}` } -} \ No newline at end of file diff --git a/src/component/PostTile/_$PostTile.scss b/src/component/PostTile/_$PostTile.scss deleted file mode 100644 index 19ba991..0000000 --- a/src/component/PostTile/_$PostTile.scss +++ /dev/null @@ -1,81 +0,0 @@ -post-tile { - display: block; - transition: 0.3s all ease; - position: relative; - transition: all 0.3s ease; - border-radius: var(--border-radius-medium); - overflow: hidden; - -webkit-tap-highlight-color: transparent; - user-select: none; - outline: transparent solid 2px; - background-color: var(--secondary-color-1); - - &[focus] { - outline: var(--secondary-color-9) solid 2px; - transform: scale(1.02); - } - @media (hover: hover) { - &:hover { - transform: scale(1.02); - z-index: 1; - box-shadow: 0 0 10px color-mix(in srgb, var(--secondary-color-1) 50%, transparent) - } - } - - &.loading { - transition: none; - } - - &:active { - transform: scale(0.95); - } - div.video-detail, div.gif-detail { - position: absolute; - background-color: var(--secondary-color-3);//color-mix(in srgb, var(--secondary-color-3) 80%, transparent); - color: var(--primary-color); - bottom: 0.3rem; - right: 0.3rem; - padding: 0.2em 0.4em; - height: 1rem; - border-radius: var(--border-radius-small); - font-size: 12px; - display: flex; - align-items: center; - gap: 0.2rem; - z-index: 2; - // text-shadow: 0 0 0.5em var(--secondary-color-1); - - ion-icon { - font-size: 1.4rem; - - &[name="images-outline"] { - padding: 0.1rem; - font-size: 1rem; - } - } - - span.duration { - text-transform: uppercase; - z-index: 2; - } - } - - a { - background-color: transparent; - padding: 0; - border-radius: 0; - img { - height: 100%; - width: 100%; - vertical-align: top; - background-color: var(--secondary-color-1); - } - video { - height: 100%; - width: 100%; - object-fit: cover; - position: absolute; - z-index: 1; - } - } -} \ No newline at end of file diff --git a/src/component/PostViewer/$PostViewer.ts b/src/component/PostViewer/$PostViewer.ts deleted file mode 100644 index d6aed47..0000000 --- a/src/component/PostViewer/$PostViewer.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { $Container } from "elexis"; -import type { Post } from "../../structure/Post"; -import { Booru } from "../../structure/Booru"; -import { ClientUser } from "../../structure/ClientUser"; -import { $VideoController } from "../VideoController/$VideoController"; -import { $Input } from "elexis/lib/node/$Input"; - -export class $PostViewer extends $Container { - $video = $('video'); - post: Post; - constructor(post: Post) { - super('div'); - this.post = post - this.class('viewer'); - this.build(); - } - - async build() { - await this.post.ready; - this.events.on('video_play_pause', () => { if (this.$video.isPlaying) this.$video.pause(); else this.$video.play() }) - this.content([ - $('div').class('viewer-panel').hide(false).content($viewerPanel => { - this.events.on('viewerPanel_hide', () => $viewerPanel.hide(true)) - .on('viewerPanel_show', () => $viewerPanel.hide(false)) - .on('viewerPanel_switch', () => { $viewerPanel.hide(!$viewerPanel.hide()) }) - return [ - $('div').class('panel').content([ - this.post.isVideo ? new $VideoController(this.$video, this, this.post) : null, - $('div').class('buttons').content([ - $('ion-icon').title('Favorite').name('heart-outline').self($heart => { - ClientUser.events.on('favoriteUpdate', (user) => { - if (user.favorites.has(this.post.id)) $heart.name('heart'); - else $heart.name('heart-outline'); - }) - if (Booru.used.user?.favorites.has(this.post.id)) $heart.name('heart'); - $heart.on('click', () => { - if (Booru.used.user?.favorites.has(this.post.id)) this.post.deleteFavorite(); - else this.post.createFavorite(); - }) - }), - $('ion-icon').title('Original Size').name('resize-outline').self($original => { - $original.on('click', () => { this.events.fire('original_size'); $original.disable(true); }) - if (!this.post.isLargeFile || this.post.isVideo) $original.disable(true); - }) - ]) - ]), - $('div').class('overlay') - ] - }), - this.post.isVideo - ? this.$video.height(this.post.image_height).width(this.post.image_width).src(this.post.file_ext === 'zip' ? this.post.large_file_url : this.post.file_url) - .controls(false).loop(true).disablePictureInPicture(true) - : $('img').height(this.post.image_height).width(this.post.image_width).self($img => { - $img.once('load', () => - $img.once('load', () => $img.removeClass('loading')).src(this.post.isLargeFile ? this.post.large_file_url : this.post.file_url) - ).src(this.post.preview_file_url) - if (!$img.complete) $img.class('loading') - this.events.on('original_size', () => $img.src(this.post.file_url)) - }) - ]) - this.on('pointerleave', (e) => { - if (e.pointerType === 'touch') return; - this.events.fire('viewerPanel_hide'); - }) - this.on('pointermove', (e) => { - if (e.pointerType === 'mouse' || e.pointerType === 'pen') this.events.fire('viewerPanel_show'); - }) - let doubleTap: Timer | null = null; - $.pointers(this) - .on('up', pointer => { - if ( this.$(':.viewer-panel .panel')?.contains($(pointer.$target)) ) return; - if (pointer.type === 'mouse') this.events.fire('video_play_pause'); - else { - if (doubleTap !== null) { - this.events.fire('video_play_pause'); - } - doubleTap = setTimeout(() => { - doubleTap = null; - }, 300); - this.events.fire('viewerPanel_switch'); - } - }) - $.keys($(window)).self($keys => $keys - .if(e => { - if ($(e.target) instanceof $Input) return; - if (!this.inDOM()) return; - return true; - }) - .keydown(' ', e => { - e.preventDefault(); - if (this.$video.isPlaying) this.$video.pause(); - else this.$video.play(); - }) - ) - } -} - -export interface $PostViewerEventMap { - viewerPanel_hide: [], - viewerPanel_show: [], - viewerPanel_switch: [], - original_size: [], - video_play_pause: [], -} \ No newline at end of file diff --git a/src/component/Searchbar/$Searchbar.ts b/src/component/Searchbar/$Searchbar.ts deleted file mode 100644 index 2348a88..0000000 --- a/src/component/Searchbar/$Searchbar.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { $Container } from "elexis"; -import { Tag, TagCategory } from "../../structure/Tag"; -import { Booru } from "../../structure/Booru"; -import { Autocomplete } from "../../structure/Autocomplete"; -import { numberFormat } from "../../structure/Util"; - -export class $Searchbar extends $Container { - $tagInput = new $TagInput(this); - $selectionList = new $SelectionList(); - typingTimer: Timer | null = null; - $filter = $('div').class('filter'); - constructor() { - super('searchbar'); - this.build(); - window.addEventListener('keyup', (e) => { - if (!this.inDOM() && e.key === '/') this.open(); - if (this.inDOM() && e.key === 'Escape') this.close(); - }) - } - - private build() { - this - .content([ - $('div').class('input-container') - .content([ - this.$tagInput - .on('input', () => this.inputHandler()) - .on('keydown', (e) => this.keyHandler(e)), - $('ion-icon').name('close-circle-outline').title('Clear Input') - .on('click', () => this.$tagInput.clearAll()) - ]) - .on('click', (e) => { - if (e.target === this.$tagInput.dom) this.$tagInput.addTag().input(); - }), - $('div').class('selection-list-container').content([ - this.$selectionList - ]), - this.$filter.on('click', () => { - if (location.hash === '#search') this.close(); - }) - ]) - } - - open() { if (location.hash !== '#search') $.open(location.href + '#search'); return this; } - close() { if (location.hash === '#search') $.back(); return this; } - - activate() { - this.hide(false); - this.$filter - .animate({ - opacity: [0, 0.5] - }, { duration: 300, easing: 'ease'}) - this.$tagInput.input(); - return this; - } - - inactivate() { - this.animate({ - opacity: [0.5, 0] - }, { duration: 300, easing: 'ease'}, () => this.hide(true)) - return this; - } - - private keyHandler(e: KeyboardEvent) { - const addTag = () => {e.preventDefault(); this.$tagInput.addTag().input()} - const addSelectedTag = ($selection: $Selection) => { - const inputIndex = this.$tagInput.children.indexOf(this.$tagInput.$inputor); - if (this.$tagInput.$input.value().at(-1) === ':') return this.getSearchSuggestions(); - const nextTag = this.$tagInput.children.array.at(inputIndex + 1) as $Tag; - this.$tagInput.addTag($selection.value()); - if (nextTag) this.$tagInput.editTag(nextTag); - else this.$tagInput.input(); - } - switch (e.key) { - case 'ArrowUp': { - e.preventDefault(); - this.$selectionList.focusPrevSelection(); - this.$tagInput.value(this.$selectionList.focused?.value()); - break; - } - case 'ArrowDown': { - e.preventDefault(); - this.$selectionList.focusNextSelection(); - this.$tagInput.value(this.$selectionList.focused?.value()); - break; - } - case ' ': addTag(); break; - case 'Enter': { - e.preventDefault(); - if (this.$selectionList.focused) addSelectedTag(this.$selectionList.focused); - else { - this.$tagInput.addTag(); - this.search(); - } - break; - } - case 'Tab': { - e.preventDefault(); - const inputIndex = this.$tagInput.children.indexOf(this.$tagInput.$inputor) - if (e.shiftKey) { - if (inputIndex - 1 >= 0) this.$tagInput.editTag(this.$tagInput.children.array.at(inputIndex - 1) as $Tag) - break; - } - if (this.$selectionList.focused) addSelectedTag(this.$selectionList.focused); - else { - const nextTag = this.$tagInput.children.array.at(inputIndex + 1) as $Tag; - if (nextTag) this.$tagInput.editTag(nextTag); - else this.$tagInput.addTag().input(); - } - break; - } - case 'Backspace': { - const inputIndex = this.$tagInput.children.indexOf(this.$tagInput.$inputor) - if (inputIndex !== 0 && !this.$tagInput.$input.value().length) { - e.preventDefault(); - this.$tagInput.editTag(this.$tagInput.children.array.at(inputIndex - 1) as $Tag) - } - break; - } - } - } - - private inputHandler() { - if (this.typingTimer) { - clearTimeout(this.typingTimer); - this.typingTimer = null; - } - this.typingTimer = setTimeout(async() => { - this.typingTimer = null; - this.getSearchSuggestions(); - }, 200); - } - - async getSearchSuggestions() { - const input = this.$tagInput.$input.value(); - const results = await Autocomplete.fetch(Booru.used, input, 20); - this.$selectionList - .clearSelections() - .addSelections(results.map(data => new $Selection().value(data.value) - .content([ - $('div').class('selection-label').content([ - data.isTagAntecedent() ? $('span').class('tag-antecedent').self($span => $span.dom.innerHTML = data.antecedent.replaceAll(input, `${input}`)) : null, - $('div').class('label-container').content([ - data.isTagAntecedent() ? $('ion-icon').name('arrow-forward-outline') : null, - $('span').class('label').self($span => $span.dom.innerHTML = data.label.replaceAll(input, `${input}`)) - ]) - ]), - data.isTag() ? $('div').class('tag-detail').content([ - $('span').class('tag-post-count').content(numberFormat(data.post_count)), - $('span').class('tag-category').content(TagCategory[data.category]) - ]) : null, - data.isUser() ? $('span').class('user-level').content(data.level) : null - ]) - .on('click', () => {this.$tagInput.addTag(data.value).input()}) - )) - } - - search() { - $.replace(`/posts?tags=${this.$tagInput.query.replace(':', '%3A')}`); - this.$tagInput.clearAll(); - this.inactivate(); - return this; - } - - checkURL(beforeURL: URL | undefined, afterURL: URL) { - if (beforeURL?.hash === '#search') this.inactivate(); - if (afterURL.hash === '#search') this.activate(); - if (`${beforeURL?.pathname}${beforeURL?.search}` === `${afterURL.pathname}${afterURL.search}`) return; - const tags_string = afterURL.searchParams.get('tags'); - this.$tagInput.clearAll(); - tags_string?.split(' ').forEach(tag => this.$tagInput.addTag(tag)); - } -} - -class $SelectionList extends $Container { - focused: $Selection | null = null; - selections = new Set<$Selection>(); - constructor() { - super('selection-list'); - } - - addSelections(selections: OrArray<$Selection>) { - selections = $.orArrayResolve(selections); - for (const $selection of selections) { - this.selections.add($selection); - } - this.insert(selections); - return this; - } - - clearSelections() { - this.focused = null; - this.selections.clear(); - this.clear(); - return this; - } - - focusSelection($selection: $Selection) { - this.blurSelection(); - this.focused = $selection; - $selection.focus(); - if ($selection.offsetTop < this.scrollTop()) this.scrollTop($selection.offsetTop); - if ($selection.offsetTop + $selection.offsetHeight > this.scrollTop() + this.offsetHeight) this.scrollTop($selection.offsetTop + $selection.offsetHeight - this.offsetHeight); - return this; - } - - blurSelection() { - this.focused?.blur(); - this.focused = null; - return this; - } - - focusNextSelection() { - const selections = this.selections.array; - const first = selections.at(0); - if (this.focused) { - const next = selections.at(selections.indexOf(this.focused) + 1); - if (next) this.focusSelection(next); - else if (first) this.focusSelection(first); - } else if (first) this.focusSelection(first); - } - - focusPrevSelection() { - const selections = this.selections.array; - if (this.focused) { - const next = selections.at(selections.indexOf(this.focused) - 1); - if (next) this.focusSelection(next); - } else { - const next = selections.at(0); - if (next) this.focusSelection(next); - } - } -} - -class $Selection extends $Container { - private property = { - value: '' - } - constructor() { - super('selection'); - } - - value(): string; - value(value: string): this; - value(value?: string) { return $.fluent(this, arguments, () => this.property.value, () => $.set(this.property, 'value', value))} - - focus() { - this.addClass('active'); - return this; - } - - blur() { - this.removeClass('active'); - return this; - } -} - -class $TagInput extends $Container { - $input = $('input').type('text'); - $sizer = $('span').class('sizer'); - $inputor = $('div').class('input-wrapper').content([ - this.$sizer, - this.$input - .on('input', () => { - this.$sizer.content(this.$input.value()); - }) - ]) - tags = new Set<$Tag>(); - $seachbar: $Searchbar - constructor($seachbar: $Searchbar) { - super('tag-input'); - this.$seachbar = $seachbar; - } - - input() { - this.insert(this.$inputor); - this.$input.focus(); - this.$seachbar.$selectionList.clearSelections(); - this.$seachbar.getSearchSuggestions(); - return this; - } - - addTag(tagName?: string) { - tagName = tagName ?? this.$input.value(); - if (!tagName.length) return this; - const $tag = new $Tag(tagName); - $tag.on('click', () => this.editTag($tag)) - this.tags.add($tag); - this.value(''); - if (this.$input.inDOM()) this.$inputor.replace($tag); - else this.insert($tag); - return this; - } - - editTag($tag: $Tag) { - this.addTag(); - this.tags.delete($tag); - $tag.replace(this.$inputor); - this.value($tag.name); - this.$input.focus(); - this.$seachbar.getSearchSuggestions(); - return this; - } - - clearAll() { - this.value(''); - this.tags.clear(); - this.clear(); - return this; - } - - value(value?: string) { - if (value === undefined) return this; - this.$input.value(value); - this.$sizer.content(value); - return this; - } - - focus() { - this.$input.focus(); - return this; - } - - get query() { return this.tags.array.map(tag => tag.name).toString().replace(',', '+') } -} - -class $Tag extends $Container { - name: string; - constructor(name: string) { - super('tag'); - this.name = name; - this.build(); - } - - private build() { - this.content(this.name) - } -} \ No newline at end of file diff --git a/src/component/Searchbar/_$Searchbar.scss b/src/component/Searchbar/_$Searchbar.scss deleted file mode 100644 index eca4c0e..0000000 --- a/src/component/Searchbar/_$Searchbar.scss +++ /dev/null @@ -1,184 +0,0 @@ -searchbar { - display: flex; - // justify-content: center; - align-items: center; - flex-direction: column; - width: 100%; - z-index: 200; - position: fixed; - height: 100%; - - div.input-container { - margin-top: 0.4rem; - background-color: color-mix(in srgb, var(--secondary-color-2) 100%, transparent); - border-radius: var(--border-radius-small); - font-size: 1rem; - width: 500px; - padding: 0.4rem 0.4rem; - max-width: calc(100% - 2rem); - box-sizing: border-box; - z-index: 201; - display: flex; - align-items: center; - border: 1px solid var(--secondary-color-4); - - &:focus-within { - outline: none; - // border-color: var(--primary-color-dark); - } - - tag-input { - display: flex; - gap: 0.4rem; - width: 100%; - overflow: hidden; - padding-inline: 0.4rem; - box-sizing: border-box; - cursor: text; - - tag { - display: inline-block; - padding: 0.2rem 0.4rem; - background-color: var(--secondary-color-4); - color: var(--secondary-color-9); - border-radius: var(--border-radius-small); - cursor: pointer; - } - } - - ion-icon { - font-size: 20px; - color: var(--secondary-color-4); - cursor: pointer; - &:hover { - color: var(--secondary-color-9); - } - } - } - - div.selection-list-container { - overflow: hidden; - border-radius: var(--border-radius-small); - background-color: var(--secondary-color-1); - z-index: 201; - max-width: calc(100% - 2rem); - width: 500px; - - selection-list { - display: block; - max-height: 40vh; - overflow-y: scroll; - overflow-x: hidden; - position: relative; - - &::-webkit-scrollbar { - width: 4px; - } - - selection { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.4rem 1rem; - cursor: pointer; - gap: 1rem; - - &:hover { - background-color: color-mix(in srgb, var(--secondary-color-3) 50%, transparent); - } - &.active { - background-color: var(--secondary-color-3); - } - div.selection-label { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - .label-container { - display: flex; - gap: 0.5rem; - align-items: center; - - ion-icon { - font-size: 1rem; - } - } - } - - div.tag-detail { - display: flex; - align-items: center; - gap: 0.5rem; - .tag-post-count { - font-size: 0.8rem; - } - } - .tag-category, .user-level { - padding: 0.1rem 0.4rem; - border-radius: var(--border-radius-small); - font-size: 0.9rem; - background-color: var(--secondary-color-4); - color: var(--secondary-color-9); - } - - } - } - } - - div.filter { - background-color: var(--secondary-color-1); - opacity: 0.5; - position: fixed; - top: 0; - height: 100%; - width: 100%; - z-index: 199; - } -} - -.input-wrapper { - color: var(--primary-color); - border: 1px solid var(--secondary-color-9); - border-radius: var(--border-radius-small); - position: relative; - box-sizing: border-box; - line-height: 1em; - font-size: 14px; - padding: 4px 8px; - display: inline-block; - max-width: 100%; - text-overflow: ellipsis; - // overflow: hidden; - - span.sizer { - font-family: inherit; - white-space: pre; - height: 1em; - display: inline-block; - font-size: inherit; - line-height: inherit; - box-sizing: border-box; - position: relative; - opacity: 0; - min-width: 2px; - user-select: none; - vertical-align: top; - } - input { - color: inherit; - height: 100%; - text-overflow: ellipsis; - font-family: inherit; - background: none; - color: inherit; - top: 0; - left: 0; - font-size: inherit; - line-height: inherit; - padding: inherit; - position: absolute; - box-sizing: border-box; - width: 100%; - border: none; - outline: none; - } -} \ No newline at end of file diff --git a/src/component/VideoController/$VideoController.ts b/src/component/VideoController/$VideoController.ts deleted file mode 100644 index 10465da..0000000 --- a/src/component/VideoController/$VideoController.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { $Container, $Node, type $Video } from "elexis"; -import { time } from "../../structure/Util"; -import type { Post } from "../../structure/Post"; -import type { $PostViewer } from "../PostViewer/$PostViewer"; - -export class $VideoController extends $Container { - $video: $Video; - $viewer: $Container; - duration$ = $.state('00:00'); - post: Post; - constructor($video: $Video, $viewer: $PostViewer, post: Post) { - super('video-controller') - this.$video = $video - this.$viewer = $viewer; - this.post = post; - this.build(); - } - - protected build() { - const events = $.events<{ - progressChange: [number] - }>(); - this.$video.on('timeupdate', () => this.durationUpdate()) - this.content([ - $('div').class('video-details').content([ - $('div').class('left').content([ - $('ion-icon').class('play').title('Play').name('play').self($play => { - this.$video.on('play', () => $play.name('pause')) - .on('pause', () => $play.name('play')) - $play.on('click', () => this.$video.isPlaying ? this.$video.pause() : this.$video.play()) - }), - $('div').class('duration').content([ - $('span').class('current-time').content(this.duration$), - $('span').content('/'), - $('span').class('total-time').content('00:00').self($time => { - this.$video.on('loadeddata', () => { - const t = time(this.$video.duration * 1000); - $time.content(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`); - }) - }), - ]), - ]), - $('div').class('right').content([ - $('ion-icon').class('volume').title('Volume').name('volume-high').disable(!this.post.hasSound).self($volume => { - const check = () => { - if (this.$video.muted()) $volume.name('volume-mute'); - else $volume.name('volume-high'); - } - $volume.on('click', () => { - this.$video.muted(!this.$video.muted()) - check(); - }) - }), - $('ion-icon').class('full-screen').title('Full-Screen').name('scan').self($fullscreen => { - $fullscreen.on('click', () => { - if (document.fullscreenElement) document.exitFullscreen() - else this.$viewer.dom.requestFullscreen() - }) - }) - ]) - ]), - $('div').class('progressbar-container').content([ - $('div').class('progressbar').content([ - $('div').class('progress').self($progress => { - this.$video.on('timeupdate', e => { - $progress.css({width: `${(this.$video.currentTime() / this.$video.duration) * 100}%`}) - }) - events.on('progressChange', percentage => { - $progress.css({width: `${percentage * 100}%`}) - }) - }) - ]) - ]).self($bar => { - const pointers = $.pointers($(document.body)); - let isPlaying = false; - pointers.on('down', (pointer, e) => { - if (!$bar.contains(pointer.$target)) return pointer.delete(); - e.preventDefault() - if (this.$video.isPlaying) { - isPlaying = true; - this.$video.pause(); - } - const percentage = (pointer.x - $bar.domRect().x) / $bar.offsetWidth; - this.$video.currentTime(percentage * this.$video.duration); - }) - pointers.on('move', (pointer, e) => { - e.preventDefault() - const percentage = (pointer.x - $bar.domRect().x) / $bar.offsetWidth; - this.$video.currentTime(percentage * this.$video.duration); - events.fire('progressChange', percentage) - }) - pointers.on('up', (pointer, e) => { - if (isPlaying) this.$video.play(); - isPlaying = false; - }) - }) - ]) - } - - durationUpdate() { - const t = time(this.$video.currentTime() * 1000) - this.duration$.set(Number(t.hh) > 0 ? `${t.hh}:${t.mm}:${t.ss}` : `${t.mm}:${t.ss}`) - } -} \ No newline at end of file diff --git a/src/env.d.ts b/src/env.d.ts deleted file mode 100644 index 05904ad..0000000 --- a/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare const __APP_VERSION__: string; \ No newline at end of file diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index b6edd5a..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -type timestamp = number; -type ISOString = string; -type id = number; -type username = string; -type DateType = `${number}-${number}-${number}`; -type PeriodType = `${number}${PeriodUnit}` -type PeriodUnit = 'seconds' | 's' | 'minutes' | 'mi' | 'hours' | 'h' | 'days' | 'd' | 'weeks' | 'w' | 'months' | 'mo' | 'years' | 'y' -type UserSyntax = username | 'any' | 'none' -type Rating = 'explicit' | 'e' | 'questionable' | 'q' | 'sensitive' | 's' | 'general' | 'g' -type Source = 'http' | `https://${string}` | `*${string}*` | 'none' -type Ratio = `${numebr}:${numebr}` | `${number}/${number}` | number -type FileSize = `${number}${FileSizeUnit}` -type FileSizeUnit = 'b' | 'kb' | 'm' -type FileType = 'jpg' | 'png' | 'gif' | 'swf' | 'webm' | 'mp4' | 'zip' | 'webp' -type seconds = number; -type poolname = string; - -type NumericSymbols = '<' | '>' | '<=' | '>=' -type NumericBasicSyntax = N | N[] | `${N}` | `${NumericSymbols}${N}` | `${N}..` | `..${N}` | `${N}...${N}` -type NumericSyntax = NumericBasicSyntax | NumericSyntaxComparisons; -type NumericSyntaxComparisons = - { _not: NumericBasicSyntax } | - { _eq: NumericBasicSyntax } | - { _not_eq: NumericBasicSyntax } | - { _gt: NumericBasicSyntax } | - { _gteq: NumericBasicSyntax } | - { _lt: NumericBasicSyntax } | - { _lteq: NumericBasicSyntax} - -type TextSyntax = T | TextSyntaxComparisons; -type TextSyntaxComparisons = - { _eq: T } | - { _not_eq: T } | - { _like: T } | - { _ilike: T } | - { _not_like: T } | - { _not_ilike: T } | - { _regex: string } | - { _not_regex: string } | - { _array: string } | - { _comma: string } | - { _space: string } | - { _lower_array: string } | - { _lower_comma: string } | - { _lower_space: string } - -type UserSyntax = { _id: id } | { _name: username }; -type ChainingSyntax = {_id: id} | {has_: boolean}; -type PostSyntax = {_id: id} | {_tags_match: string}; -interface APIError { - success: false; - error: string; - message: string; -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index b7fc1dc..0000000 --- a/src/main.ts +++ /dev/null @@ -1,210 +0,0 @@ -import 'elexis'; -import '@elexis/layout'; -import '@elexis/router'; -import { Booru } from './structure/Booru'; -import { post_route } from './route/post/$post_route'; -import { $PostGrid } from './component/PostGrid/$PostGrid'; -import { $Route, $Router, $RouterAnchor, $RouterNavigationDirection } from '@elexis/router'; -import { $Searchbar } from './component/Searchbar/$Searchbar'; -import { $IonIcon } from './component/IonIcon/$IonIcon'; -import { $IconButton } from './component/IconButton/$IconButton'; -import { $login_route } from './route/login/$login_route'; -import { $Drawer } from './component/Drawer/$Drawer'; -import { $Input } from 'elexis/lib/node/$Input'; -import { $DetailPanel } from './component/DetailPanel/$DetailPanel'; -import { $PostTile } from './component/PostTile/$PostTile'; -import { LocalSettings } from './structure/LocalSettings'; -// declare elexis module -declare module 'elexis' { - export namespace $ { - export interface TagNameElementMap { - 'ion-icon': typeof $IonIcon; - 'icon-button': typeof $IconButton; - 'a': typeof $RouterAnchor; - } - } -} -$.registerTagName('ion-icon', $IonIcon) -$.registerTagName('icon-button', $IconButton) -$.registerTagName('a', $RouterAnchor) -// settings -export const [danbooru, safebooru]: Booru[] = [ - new Booru({ origin: 'https://danbooru.donmai.us', name: 'Danbooru' }), - new Booru({ origin: 'https://safebooru.donmai.us', name: 'Safebooru' }), - new Booru({ origin: 'https://testbooru.donmai.us', name: 'Testbooru' }), -] -Booru.set(Booru.manager.get(Booru.storageAPI ?? '') ?? danbooru); -const $searchbar = new $Searchbar().hide(true); -const $drawer = new $Drawer(); -export const detailPanelEnable$ = $.state(LocalSettings.detailPanelEnabled ?? false).on('update', ({state$}) => LocalSettings.detailPanelEnabled = state$.value) - -// render -$(document.body).content([ - // Navigation Bar - $('nav').content([ - // Title - $('a').class('title').href('/').content([ - $('h1').class('booru-name').content(Booru.name$), - $('h2').class('app').content([ - $('span').class('app-name').content(`Viewer`), - $('span').class('version').content(`v${__APP_VERSION__}`) - ]) - ]), - // Searchbar - $('div').class('searchbar').content(['Search in ', Booru.name$]) - .self($self => $Router.events.on('stateChange', ({beforeURL, afterURL}) => {if (beforeURL.hash === '#search') $self.hide(false); if (afterURL.hash === '#search') $self.hide(true)})) - .on('click', () => $searchbar.open()), - // Buttons - $('div').class('buttons').content([ - // Search Icon - $('ion-icon').class('search').name('search-outline').title('Search') - .self($self => $Router.events.on('stateChange', ({beforeURL, afterURL}) => {if (beforeURL.hash === '#search') $self.hide(false); if (afterURL.hash === '#search') $self.hide(true)})) - .on('click', () => $searchbar.open()), - // Detail Panel Button - $('ion-icon').class('detail-panel').name('reader-outline').title('Toggle Detail Panel').on('click', () => detailPanelEnable$.set(!detailPanelEnable$.value)), - // Open Booru - $('a').content($('ion-icon').class('open').name('open-outline').title('Open in Original Site')).href(location.href.replace(location.origin, Booru.used.origin)).target('_blank'), - // Copy Button - $('ion-icon').class('copy').name('link-outline').title('Copy Page Link').hide(false) - .on('click', (e, $copy) => { - navigator.clipboard.writeText(`${location.origin}${location.pathname}${location.search}`) - $copy.name('checkmark-outline'); - setTimeout(() => { - $copy.name('link-outline') - }, 2000); - }), - // Menu Button - $('ion-icon').class('menu').name('menu-outline').title('Menu').hide(false) - .self(($icon) => { Booru.events.on('login', () => $icon.hide(true)).on('logout', () => $icon.hide(false)) }) - .on('click', () => $.open(location.href + '#drawer')), - // Account Menu - $('div').class('account').hide(true).title('Menu') - .self(($account) => { - Booru.events - .on('login', user => { $account.content(user.name$.convert(value => value.at(0)?.toUpperCase() ?? '')).hide(false); }) - .on('logout', () => $account.hide(true)) - }) - .on('click', () => $drawer.open()) - ]) - ]), - // Searchbar - $searchbar, - // Drawer - $drawer, - // Base Router - $('router').base('/').map([ - // Home Page - $('route').id('posts').path(['/', '/posts']).builder(({$route, query}) => { - const { $postGrid, $detail } = $postsPageComponents($route, query); - return [ $postGrid, $detail ] - }), - // Posts Page - $('route').id('posts').path('/posts?tags').builder(({$route, query}) => { - const { $postGrid, $detail } = $postsPageComponents($route, query) - return [ - $('header').content([ - $('h2').content('Posts'), - $('div').class('tags').self($div => { - query.tags.split('+').forEach(tag => { - $div.insert($('a').class('tag').content(decodeURIComponent(tag)).href(`posts?tags=${tag}`)) - }) - }) - ]), - $('div').class('no-post').hide(true).self($div => { - $div.on('startLoad', () => $div.hide(true)) - $postGrid.self(() => { - $postGrid.posts.events - .on('noPost', () => $div.hide(false).content('No Posts')) - .on('post_error', message => $div.hide(false).content(message)) - }) - }), - $postGrid, - $detail - ] - }), - // Post Page - post_route, - // Login Page - $login_route - ]).on('beforeSwitch', (e) => { - const DURATION = 300; - const TX = 2; - e.preventDefault(); - function intro() { - $(document.documentElement).css({scrollBehavior: 'auto'}); - const transform = $.call(() => { - switch ($Router.navigationDirection) { - case $RouterNavigationDirection.Forward: return [`translateX(${TX}%)`, `translateX(0%)`]; - case $RouterNavigationDirection.Back: return [`translateX(-${TX}%)`, `translateX(0%)`]; - case $RouterNavigationDirection.Replace: return ''; - } - }) - e.$view.content(e.nextContent); - e.rendered(); - e.nextContent.element?.class('animated').animate({ - opacity: [0, 1], - transform - }, { - duration: DURATION, - easing: 'ease' - }, () => { - e.switched(); - $(document.documentElement).css({scrollBehavior: ''}); - e.nextContent.element?.removeClass('animated') - }) - } - function outro() { - $(document.documentElement).css({scrollBehavior: 'auto'}); - const transform = $.call(() => { - switch ($Router.navigationDirection) { - case $RouterNavigationDirection.Forward: return [`translateX(0%)`, `translateX(-${TX}%)`]; - case $RouterNavigationDirection.Back: return [`translateX(0%)`, `translateX(${TX}%)`]; - case $RouterNavigationDirection.Replace: return ''; - } - }) - - e.previousContent?.element?.class('animated').animate({ - opacity: [1, 0], - transform - }, { - duration: DURATION, - easing: 'ease' - }, () => { - e.previousContent?.element?.removeClass('animated'); - intro(); - }) - } - - if (e.previousContent) outro(); - else intro(); - }) -]) - -$Router.events.on('stateChange', ({beforeURL, afterURL}) => componentState(beforeURL, afterURL)) -componentState(undefined, new URL(location.href)) - -function componentState(beforeURL: URL | undefined, afterURL: URL) { - $searchbar.checkURL(beforeURL, afterURL); $drawer.checkURL(beforeURL, afterURL) -} - -function $postsPageComponents($route: $Route, query: {tags?: string}) { - const $postGrid = new $PostGrid(query); - const $detail = new $DetailPanel({preview: true, tagsType: 'name_only'}).hide(detailPanelEnable$.convert(bool => !bool)).position($route); - detailPanelCheck(); - detailPanelEnable$.on('update', detailPanelCheck); - Booru.events.on('set', () => $detail.update(null)); - function detailPanelCheck() { detailPanelEnable$.value ? $postGrid.addClass('detail-panel-enabled') : $postGrid.removeClass('detail-panel-enabled') } - $postGrid.$focus - .on('focus', ({$focused: $target}) => {if ($target.inDOM() && $target instanceof $PostTile) $detail.update($target.post) }) - .on('blur', () => $detail.update(null)) - return { $postGrid, $detail }; -} - -$.keys($(window)) - .if(e => { - if ($(e.target) instanceof $Input) return; - return true; - }) - .keydown(['q', 'Q'], e => { e.preventDefault(); if ($Router.index !== 0) $.back(); }) - .keydown(['e', 'E'], e => { e.preventDefault(); if ($Router.forwardIndex !== 0) $.forward(); }) - .keydown('Tab', e => { e.preventDefault(); detailPanelEnable$.set(!detailPanelEnable$.value) }) \ No newline at end of file diff --git a/src/route/login/$login_route.ts b/src/route/login/$login_route.ts deleted file mode 100644 index 4d2861d..0000000 --- a/src/route/login/$login_route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Booru } from "../../structure/Booru" -import { ClientUser } from "../../structure/ClientUser"; - -export const $login_route = $('route').id('login').path('/login').builder(() => { - const [username$, apiKey$] = [$.state(''), $.state('')] - return [ - $('div').class('login-container').content([ - $('h1').content('Login'), - $('div').class('username', 'input-container').content([ - $('label').for('username').content('Username'), - $('input').type('text').id('username').value(username$) - ]), - $('div').class('api-key', 'input-container').content([ - $('label').for('api-key').content('API Key'), - $('input').type('password').id('api-key').value(apiKey$) - ]), - $('icon-button').content('Login').on('click', async () => { - await Booru.used.login(username$.value, apiKey$.value); - if (Booru.used.user) { - ClientUser.storageUserData = { apiKey: apiKey$.value, username: username$.value } - // Clear input - username$.set(''); - apiKey$.set(''); - $.replace('/'); - }; - }), - $('icon-button').content('Create Account').icon('open-outline').on('click', () => $.open('https://danbooru.donmai.us/users/new', '_blank')), - ]) - ] -}) \ No newline at end of file diff --git a/src/route/login/_$login_route.scss b/src/route/login/_$login_route.scss deleted file mode 100644 index 1b14a29..0000000 --- a/src/route/login/_$login_route.scss +++ /dev/null @@ -1,31 +0,0 @@ -route#login { - display: flex; - justify-content: center; - align-items: center; - margin-top: 5rem; - - .login-container { - padding: 2rem; - border: 1px solid var(--secondary-color-9); - border-radius: var(--border-radius-large); - display: flex; - flex-direction: column; - justify-content: center; - gap: 1rem; - max-width: 400px; - width: 100%; - box-sizing: border-box; - - h1 { - margin: 0; - } - .input-container { - display: flex; - flex-direction: column; - gap: 0.5rem; - input { - display: block; - } - } - } -} \ No newline at end of file diff --git a/src/route/post/$post_route.ts b/src/route/post/$post_route.ts deleted file mode 100644 index a21b545..0000000 --- a/src/route/post/$post_route.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Post } from "../../structure/Post"; -import { ArtistCommentary } from "../../structure/Commentary"; -import { Booru } from "../../structure/Booru"; -import { $Input } from "elexis/lib/node/$Input"; -import { $DetailPanel } from "../../component/DetailPanel/$DetailPanel"; -import { PostManager } from "../../structure/PostManager"; -import { $PostViewer } from "../../component/PostViewer/$PostViewer"; -import { $Slide, $SlideViewer } from "../../component/$SlideViewer"; -import { $Video } from "elexis"; - -export const post_route = $('route').path('/posts/:id?q').id('post').static(false).builder(({$route, params}) => { - if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND'); - const events = $.events<{ - post_switch: [Post] - }>(); - let post: Post, posts: PostManager; - $.keys($(window)).self($keys => $keys - .if(e => { - if ($(e.target) instanceof $Input) return; - if (!$route.inDOM()) return; - return true; - }) - .keydown(['f', 'F'], e => { - if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite(); - else post.createFavorite(); - }) - .keydown(['a', 'A'], e => navPost('prev') ) - .keydown(['d', 'D'], e => { navPost('next') }) - ) - const $slideViewerMap = new Map(); - $route.on('open', async ({params, query}) => { - posts = PostManager.get(query.q); - post = Post.get(Booru.used, +params.id); - posts.events.on('post_fetch', slideViewerHandler); - if (!posts.orderMap.size || !posts.cache.has(post)) { - await post.ready - posts.addPosts(post); - posts.orderMap.set(post.id, post); - posts.fetchPosts('newer'); - posts.fetchPosts('older'); - } else { - const ordered = [...posts.orderMap.values()]; - const index = ordered.indexOf(post); - if (!posts.finished && index === ordered.length - 1) { - posts.fetchPosts('older'); - } else if (index === 0) { - posts.fetchPosts('newer'); - } - } - slideViewerHandler({manager: posts}); - const $slideViewer = $getSlideViewer(posts.tags) - $slideViewer.switch(post.id); - events.fire('post_switch', post); - }) - - function $getSlideViewer(q: string | undefined) { - const $slideViewer = $slideViewerMap.get(q) ?? - new $SlideViewer() - .pointerException((pointer) => { - if ($slideViewer.currentSlide?.$('::.progressbar-container').find($div => $div.contains(pointer.$target))) return false; - if (pointer.type === 'mouse') return false; - return true; - }) - .on('switch', ({nextSlide: $target}) => { - $.replace(`/posts/${$target.slideId()}${q ? `?q=${q}` : ''}`); - }).on('beforeSwitch', ({prevSlide, nextSlide}) => { - const $prevVideo = prevSlide?.$<$Video>(':video'); - if ($prevVideo?.isPlaying) $prevVideo.pause(); - const $nextVideo = nextSlide.$<$Video>(':video'); - if ($nextVideo?.isPlaying === false) $nextVideo.play(); - }) - $slideViewerMap.set(q, $slideViewer); - return $slideViewer; - } - - function navPost(dir: 'next' | 'prev') { - const orderList = [...posts.orderMap.values()]; - const index = orderList.indexOf(post); - if (dir === 'prev' && index === 0) return; - const targetPost = orderList.at(dir === 'next' ? index + 1 : index - 1); - if (!targetPost) return; - $.replace(`/posts/${targetPost.id}${posts.tags ? `?q=${posts.tags}` : ''}`); - } - - function slideViewerHandler(params: {manager: PostManager}) { - const { manager: posts } = params; - const $slideViewer = $getSlideViewer(posts.tags); - const postList = posts.cache.array.filter(post => !$slideViewer.slideMap.has(post.id)); - $slideViewer.addSlides(postList.map(post => new $Slide().slideId(post.id).builder(() => new $PostViewer(post)))); - if (postList.length) $slideViewer.arrange([...posts.orderMap.values()].map(post => post.id)); - } - - return [ - $('div').class('slide-viewer-container').self($div => { - $route.on('open', () => { - $div.content($getSlideViewer(posts.tags)) - }) - }), - $('div').class('content').content([ - $('h3').content(`Artist's Commentary`), - $('section').class('commentary').self(async ($comentary) => { - events.on('post_switch', async post => { - const commentary = (await ArtistCommentary.fetchMultiple(Booru.used, {post: {_id: post.id}})).at(0); - $comentary.content([ - commentary ? [ - commentary.original_title ? $('h3').content(commentary.original_title) : null, - $('pre').content(commentary.original_description) - ] : 'No commentary' - ]) - }) - }) - ]), - new $DetailPanel().position($route).self($detail => { - events.on('post_switch', (post) => $detail.update(post)) - }) - ] -}) \ No newline at end of file diff --git a/src/route/post/_$post_route.scss b/src/route/post/_$post_route.scss deleted file mode 100644 index 3587042..0000000 --- a/src/route/post/_$post_route.scss +++ /dev/null @@ -1,176 +0,0 @@ -#post { - padding: 0; - padding-top: var(--nav-height); - - slide-viewer { - display: block; - height: calc(100dvh - 2rem - var(--nav-height)); - background-color: #000000; - border-radius: var(--border-radius-large); - overflow: hidden; - width: calc(100vw - 300px - 4rem); - margin: 1rem; - position: relative; - transition: all 0.3s ease; - touch-action: pan-y; - - @media (max-width: 800px) { - width: 100%; - height: calc(100dvh - var(--nav-height)); - border-radius: 0; - margin:0; - } - } - - div.viewer { - height: 100%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - background-color: #000000; - border-radius: var(--border-radius-large); - overflow: hidden; - position: relative; - transition: all 0.3s ease; - - img { - max-width: 100%; - max-height: 100%; - object-fit: contain; - transition: all 0.3s ease; - - &.loading { - filter: blur(5px); - } - } - - video { - max-width: 100%; - max-height: 100%; - -webkit-user-drag: none; - transition: all 0.3s ease; - } - - div.viewer-panel { - position: absolute; - bottom: 0; - width: 100%; - z-index: 1; - - div.panel { - width: 100%; - display: flex; - justify-content: center; - flex-direction: column; - padding: 1rem; - gap: 1rem; - box-sizing: border-box; - - video-controller { - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; - width: 100%; - - div.video-details { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - - div { - display: flex; - align-items: center; - gap: 1rem; - } - } - div.progressbar-container { - height: 2rem; - width: 100%; - display: flex; - touch-action: none; - align-items: center; - cursor: pointer; - - div.progressbar { - height: 0.4rem; - width: 100%; - background-color: var(--secondary-color-1); - flex-shrink: 1; - - div.progress { - height: 100%; - background-color: var(--secondary-color-3); - width: 100px; - } - } - } - - .play { - flex-shrink: 0; - } - } - - div.buttons { - width: 100%; - display: flex; - justify-content: center; - gap: 2rem; - } - } - - div.overlay { - position: absolute; - bottom: 0; - width: 100%; - height: 200%; - z-index: -1; - background: linear-gradient(180deg, - color-mix(in srgb, var(--secondary-color-1) 0%, transparent) 0%, - color-mix(in srgb, var(--secondary-color-0) 70%, transparent) 100% - ); - } - } - } - - div.content { - width: calc(100vw - 300px - 2rem); - display: flex; - flex-direction: column; - padding: 1rem; - box-sizing: border-box; - - @media (max-width: 800px) { - width: 100%; - } - - &::-webkit-scrollbar { - background-color: #000000; - width: 4px; - } - - &::-webkit-scrollbar-thumb { - background-color: #aeaeec; - border-radius: 2px; - } - - & > h3 { - padding-left: 1rem; - margin-block: 1rem; - } - - section.commentary { - * { - text-wrap: wrap; - word-break: break-word; - } - } - } -} - -// animated resolver -// .animated div.sidebar { -// top: 0 !important; -// } \ No newline at end of file diff --git a/src/structure/Autocomplete.ts b/src/structure/Autocomplete.ts deleted file mode 100644 index 483e651..0000000 --- a/src/structure/Autocomplete.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { Booru } from "./Booru"; -import type { TagCategory } from "./Tag"; -import type { UserLevel } from "./User"; - -export class Autocomplete { - static async fetch(booru: Booru, query: string, limit: number = 20) { - if (!query.length) return this.searchQuery.map(data => new AutocompleteResult(data)) - const res = await booru.fetch(`/autocomplete.json?search[query]=${query}&search[type]=tag_query&version=1&limit=${limit}`); - const searchQueryResult = query.length ? this.searchQuery.filter(sq => sq.value.startsWith(query) && sq.value !== query) : this.searchQuery - return [...searchQueryResult, ...res].map(data => new AutocompleteResult(data)); - } - - static searchQuery: AutocompleteSearchQueryData[] = [ - {value: 'user:', label: 'user:'}, - {value: 'approver:', label: 'approver:'}, - {value: '-approver:', label: '-approver:'}, - {value: 'order:', label: 'order:'}, - {value: 'ordfav:', label: 'ordfav:'}, - {value: 'ordfavgroup:', label: 'ordfavgroup:'}, - {value: 'search:', label: 'search:'}, - {value: 'favgroup:', label: 'favgroup:'}, - {value: '-favgroup:', label: '-favgroup:'}, - {value: 'favcount:', label: 'favcount:'}, - {value: 'id:', label: 'id:'}, - {value: 'tagcount:', label: 'tagcount:'}, - {value: 'gentags:', label: 'gentags:'}, - {value: 'arttags:', label: 'arttags:'}, - {value: 'chartags:', label: 'chartags:'}, - {value: 'copytags:', label: 'copytags:'}, - {value: 'metatags:', label: 'metatags:'}, - {value: 'score:', label: 'score:'}, - {value: 'upvote:', label: 'upvote:'}, - {value: 'downvote:', label: 'downvote:'}, - {value: 'disapproved:', label: 'disapproved:'}, - {value: 'md5:', label: 'md5:'}, - {value: 'width:', label: 'width:'}, - {value: 'height:', label: 'height:'}, - {value: 'ratio:', label: 'ratio:'}, - {value: 'mpixels:', label: 'mpixels:'}, - {value: 'filesize:', label: 'filesize:'}, - {value: 'duration:', label: 'duration:'}, - {value: 'is:', label: 'is:'}, - {value: 'has:', label: 'has:'}, - {value: 'pool:', label: 'pool:'}, - {value: '-pool:', label: '-pool:'}, - {value: 'ordpool:', label: 'ordpool:'}, - {value: 'random:', label: 'random:'}, - {value: 'limit:', label: 'limit:'}, - {value: 'date:', label: 'date:'}, - {value: 'commenter:', label: 'commenter:'}, - {value: 'note:', label: 'note:'}, - {value: 'noter:', label: 'noter:'}, - {value: 'noteupdater:', label: 'noteupdater:'}, - {value: 'status:', label: 'status:'}, - {value: '-status:', label: '-status:'}, - {value: 'rating:', label: 'rating:'}, - {value: '-rating:', label: '-rating:'}, - {value: 'source:', label: 'source:'}, - {value: '-source:', label: '-source:'}, - {value: 'pixiv:', label: 'pixiv:'}, - {value: 'parent:', label: 'parent:'}, - {value: 'child:', label: 'child:'}, - {value: 'flagger:', label: 'flagger:'}, - {value: 'appealer:', label: 'appealer:'}, - {value: 'commentary:', label: 'commentary:'}, - {value: 'commentaryupdater:', label: 'commentaryupdater:'}, - ].map(data => ({type: 'query', ...data})) -} - -export interface AutocompleteResult extends AutocompleteBaseData {} -export class AutocompleteResult { - constructor(data: AutocompleteData) { - Object.assign(this, data); - } - - isTag(): this is AutocompleteResult & (AutocompleteTagData | AutocompleteTagAliasData | AutocompleteTagAutocorrectData) { - return this.type === 'tag' || this.type === 'tag-autocorrect' || this.type === 'tag-alias' || this.type === 'tag-word'; - } - - isTagAutocorrect(): this is AutocompleteResult & AutocompleteTagAutocorrectData { - return this.type === 'tag-autocorrect'; - } - - isTagAntecedent(): this is Autocomplete & AutocompleteTagAutocorrectData { - //@ts-expect-error - return !!this['antecedent' as any] - } - - isTagWord(): this is AutocompleteResult & AutocompleteTagWordData { - return this.type === 'tag-word' - } - - isUser(): this is AutocompleteResult & AutocompleteUserData { - return this.type === 'user'; - } -} - -type AutocompleteData = AutocompleteBaseData & (AutocompleteUserData | AutocompleteTagData | AutocompleteTagAutocorrectData | AutocompleteTagAliasData | AutocompleteSearchQueryData); - -interface AutocompleteBaseData { - type: 'user' | 'tag' | 'tag-autocorrect' | 'tag-alias' | 'tag-word' | 'query'; - label: string; - value: string; -} - -interface AutocompleteUserData { - type: 'user'; - id: number; - level: Lowercase; -} -interface AutocompleteTagData { - type: 'tag'; - category: TagCategory; - post_count: number; -} -interface AutocompleteTagAutocorrectData { - type: 'tag-autocorrect'; - category: TagCategory; - post_count: number; - antecedent: string; -} -interface AutocompleteTagAliasData { - type: 'tag-alias'; - category: TagCategory; - post_count: number; - antecedent: string; -} -interface AutocompleteTagWordData{ - type: 'tag-word'; - category: TagCategory; - post_count: number; - antecedent: string; -} - -interface AutocompleteSearchQueryData {type: 'query', value: string, label: string} \ No newline at end of file diff --git a/src/structure/Booru.ts b/src/structure/Booru.ts deleted file mode 100644 index 427faf8..0000000 --- a/src/structure/Booru.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { $EventManager, type $EventMap } from "elexis"; -import type { Post } from "./Post"; -import type { Tag } from "./Tag"; -import { ClientUser, type ClientUserData } from "./ClientUser"; -import type { User } from "./User"; -import type { Favorite } from "./Favorite"; - -export interface BooruOptions { - origin: string; - name: string; -} -export interface Booru extends BooruOptions {} -export class Booru { - static used: Booru; - static events = new $EventManager(); - static name$ = $.state(this.name); - static manager = new Map() - user?: ClientUser; - posts = new Map(); - tags = new Map(); - users = new Map(); - favorites = new Map(); - constructor(options: BooruOptions) { - Object.assign(this, options); - if (this.origin.endsWith('/')) this.origin = this.origin.slice(0, -1); - Booru.manager.set(this.name, this); - } - - static set(booru: Booru) { - this.used = booru; - this.name$.set(booru.name); - this.storageAPI = booru.name; - const userdata = ClientUser.storageUserData; - if (userdata) booru.login(userdata.username, userdata.apiKey); - this.events.fire('set'); - return this; - } - - static get storageAPI() { return localStorage.getItem('booru_api'); } - static set storageAPI(name: string | null) { if (name) localStorage.setItem('booru_api', name); else localStorage.removeItem('booru_api') } - - async fetch(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET') { - const auth = this.user ? `${endpoint.includes('?') ? '&' : '?'}login=${this.user.name}&api_key=${this.user.apiKey}` : ''; - const data = await fetch(`${this.origin}${endpoint}${auth}`, { - method: method, - }).then(res => res.json()) as any; - if (data.success === false) throw data.message; - return data as T; - } - - - async login(username: string, apiKey: string) { - const data = await this.fetch(`/profile.json?login=${username}&api_key=${apiKey}`); - this.user = new ClientUser(this, apiKey, data); - this.user.init(); - Booru.events.fire('login', this.user); - return this.user; - } - - logout() { - this.user = undefined; - ClientUser.storageUserData = null; - Booru.events.fire('logout'); - return this - } - -} - -interface BooruStaticEventMap extends $EventMap { - set: []; - login: [user: ClientUser]; - logout: []; -} - -interface BooruEventMap extends $EventMap { -} \ No newline at end of file diff --git a/src/structure/ClientUser.ts b/src/structure/ClientUser.ts deleted file mode 100644 index af9e9bd..0000000 --- a/src/structure/ClientUser.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { $EventManager } from "elexis"; -import type { Booru } from "./Booru"; -import { Favorite, type FavoriteData } from "./Favorite"; -import { User, type UserData } from "./User"; -import type { Post } from "./Post"; - -export interface ClientUser extends ClientUserData {} -export class ClientUser extends User { - apiKey: string; - favorite_count$ = $.state(0); - forum_post_count$ = $.state(0); - static events = new $EventManager() - constructor(booru: Booru, apiKey: string, data: ClientUserData) { - super(booru, data, false); - this.apiKey = apiKey; - this.update$(); - } - - update$() { - super.update$(); - this.forum_post_count$?.set(this.forum_post_count); - this.favorite_count$?.set(this.favorite_count); - } - - async init() { - await this.fetchFavorites(); - - } - - async fetchFavorites() { - const oldestId = Array.from(this.favorites.keys()).at(-1); - const list = await Favorite.fetchUserFavorites(this.booru, this, ``, 1000, oldestId ? `b${oldestId}` : 1); - ClientUser.events.fire('favoriteUpdate', this); - if (list.length >= 1000) this.fetchFavorites(); - return list; - } - - static get storageUserData() { const data = localStorage.getItem('user_data'); return data ? JSON.parse(data) as ClientUserStoreData : null } - static set storageUserData(data: ClientUserStoreData | null) { localStorage.setItem('user_data', JSON.stringify(data)) } -} -export interface ClientUserData extends UserData { - "last_logged_in_at": ISOString, - "last_forum_read_at": ISOString, - "comment_threshold": number, - "updated_at": ISOString, - "default_image_size": "large" | "original", - "favorite_tags": null | string, - "blacklisted_tags": string, - "time_zone": string, - "favorite_count": number, - "per_page": number, - "custom_style": string, - "theme": "auto" | "light" | "dark", - "receive_email_notifications": boolean, - "new_post_navigation_layout": boolean, - "enable_private_favorites": boolean, - "show_deleted_children": boolean, - "disable_categorized_saved_searches": boolean, - "disable_tagged_filenames": boolean, - "disable_mobile_gestures": boolean, - "enable_safe_mode": boolean, - "enable_desktop_mode": boolean, - "disable_post_tooltips": boolean, - "requires_verification": boolean, - "is_verified": boolean, - "show_deleted_posts": boolean, - "statement_timeout": number, - "favorite_group_limit": 10 | 100, - "tag_query_limit": 2 | 6, - "max_saved_searches": 250, - "wiki_page_version_count": number, - "artist_version_count": number, - "artist_commentary_version_count": number, - "pool_version_count": number | null, - "forum_post_count": number, - "comment_count": number, - "favorite_group_count": number, - "appeal_count": number, - "flag_count": number, - "positive_feedback_count": number, - "neutral_feedback_count": number, - "negative_feedback_count": number -} - -export interface ClientUserStoreData { - username: string; - apiKey: string; -} - -export interface ClientUserEventMap { - favoriteUpdate: [user: ClientUser] -} \ No newline at end of file diff --git a/src/structure/Commentary.ts b/src/structure/Commentary.ts deleted file mode 100644 index 513d47e..0000000 --- a/src/structure/Commentary.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Booru } from "./Booru"; - -export interface ArtistCommentary extends ArtistCommentaryData {} -export class ArtistCommentary { - static manager = new Map(); - constructor(data: ArtistCommentaryData) { - Object.assign(this, data); - } - - static async fetch(booru: Booru, id: id) { - const data = await booru.fetch(`/artist_commentaries/${id}.json`); - const post = new this(data); - return post; - } - - static async fetchMultiple(booru: Booru, search?: Partial, limit = 200) { - let searchQuery = ''; - if (search) { - for (const [key, val] of Object.entries(search)) { - if (val instanceof Array) searchQuery += `&search[${key}]=${val}`; - else if (val instanceof Object) { - for (const [ckey, cval] of Object.entries(val)) { - searchQuery += `&search[${key}${ckey}]=${cval}` - } - } - else searchQuery += `&search[${key}]=${val}` - } - } - const dataArray = await booru.fetch(`/artist_commentaries.json?limit=${limit}${searchQuery}`); - const list = dataArray.map(data => { - const instance = new this(data); - this.manager.set(instance.id, instance); - return instance; - }); - return list; - } -} - -export interface ArtistCommentaryData { - "id": id, - "post_id": id, - "original_title": string, - "original_description": string, - "translated_title": string, - "translated_description": string, - "created_at": ISOString, - "updated_at": ISOString -} - -export interface ArtistCommentarySearchParams { - id: NumericSyntax; - created_at: NumericSyntax; - updated_at: NumericSyntax; - original_title: TextSyntax; - original_description: TextSyntax; - translated_title: TextSyntax; - translated_description: TextSyntax; - post: PostSyntax; - text_matches: string; - original_present: boolean; - translated_present: boolean; - is_deleted: 'yes' | 'no'; -} \ No newline at end of file diff --git a/src/structure/Favorite.ts b/src/structure/Favorite.ts deleted file mode 100644 index 6ee65be..0000000 --- a/src/structure/Favorite.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Booru } from "./Booru"; -import type { ClientUser } from "./ClientUser"; -import type { Post } from "./Post"; -import type { User } from "./User"; - -export interface Favorite extends FavoriteData {} -export class Favorite { - booru: Booru; - constructor(booru: Booru, data: FavoriteData) { - Object.assign(this, data); - this.booru = booru; - } - - static async fetchUserFavorites(booru: Booru, user: User, query: string, limit: number = 100, page: number | string) { - const dataArray = await booru.fetch(`/favorites.json?${query}&${`search[user_id]=${user.id}`}&limit=${limit}&page=${page}`); - return dataArray.map(data => { - user.favorites.add(data.post_id); - return data.post_id; - }) - } - - update(data: FavoriteData) { - Object.assign(this, data) - return this; - } -} - -export interface FavoriteData { - id: id; - post_id: id; - user_id: id; -} \ No newline at end of file diff --git a/src/structure/LocalSettings.ts b/src/structure/LocalSettings.ts deleted file mode 100644 index b2911aa..0000000 --- a/src/structure/LocalSettings.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class LocalSettings { - static get detailPanelEnabled() { return this.localdata?.detailPanelEnabled } - static set detailPanelEnabled(boolean: boolean | undefined) { this.localdata = {...this.localdata, detailPanelEnabled: boolean }} - - static get localdata() { const data = localStorage.getItem('local_settings_data'); return data ? JSON.parse(data) as LocalSettingsStoreData : null } - static set localdata(data: LocalSettingsStoreData | null) { localStorage.setItem('local_settings_data', JSON.stringify(data)) } -} - -export interface LocalSettingsStoreData { - detailPanelEnabled?: boolean; -} \ No newline at end of file diff --git a/src/structure/Post.ts b/src/structure/Post.ts deleted file mode 100644 index b869e1c..0000000 --- a/src/structure/Post.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { $, $EventManager } from "elexis"; -import { Booru } from "./Booru"; -import { Tag } from "./Tag"; -import { User } from "./User"; -import { dateFrom, digitalUnit } from "./Util"; -import { ClientUser } from "./ClientUser"; - -const LOADING_STRING = '...' - -export interface PostOptions {} -export interface Post extends PostData {} -export class Post extends $EventManager<{update: []}> { - uploader$ = $.state(LOADING_STRING); - approver$ = $.state(LOADING_STRING); - created_date$ = $.state(LOADING_STRING); - favcount$ = $.state(0); - score$ = $.state(0); - file_size$ = $.state(LOADING_STRING); - file_ext$ = $.state(LOADING_STRING); - file_url$ = $.state(LOADING_STRING); - source$ = $.state(LOADING_STRING); - dimension$ = $.state(LOADING_STRING); - booruUrl$ = $.state(LOADING_STRING); - createdDate = new Date(this.created_at); - ready?: Promise; - webm_url$ = $.state(LOADING_STRING); - - booru: Booru; - constructor(booru: Booru, id: id, data?: PostData) { - super(); - this.booru = booru; - this.id = id; - booru.posts.set(this.id, this); - if (data) this.update(data); - else this.ready = this.fetch(); - } - - static get(booru: Booru, id: id) { - return booru.posts.get(id) ?? new Post(booru, id); - } - - async fetch() { - const data = await this.booru.fetch(`/posts/${this.id}.json`); - this.update(data); - User.fetchMultiple(this.booru, {id: [this.uploader_id, this.approver_id].detype(null)}).then(() => this.update$()); - return this; - } - - static async fetchMultiple(booru: Booru, tags?: Partial | string, limit = 20, page?: string | number) { - let tagsQuery = ''; - if (tags) { - if (typeof tags === 'string') tagsQuery = tags; - else { - for (const [key, val] of Object.entries(tags)) { - if (val === undefined) continue; - if (key === 'tags') { tagsQuery += `${val}`; continue; } - if (tagsQuery.at(-1) !== '=') tagsQuery += ' '; // add space between tags - tagsQuery += `${key}:${val}` - } - } - } - const dataArray = await booru.fetch(`/posts.json?limit=${limit}&tags=${tagsQuery}${page ? `&page=${page}` : ''}&_method=get`); - if (dataArray instanceof Array === false) return []; - const tagnameSet = new Set(); - const list = dataArray.map(data => { - const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data); - booru.posts.set(instance.id, instance); - instance.tag_string.split(' ').forEach(tag_name => tagnameSet.add(tag_name)) - return instance; - }); - if (!list.length) return list; - const userIds = [...new Set(dataArray.map(data => [data.approver_id, data.uploader_id].detype(null)).flat())]; - // Tag.fetchMultiple(booru, {name: tagnameSet.array.toString().replaceAll(',', ' ')}); - User.fetchMultiple(booru, {id: userIds}).then(() => list.forEach(post => post.update$())); - return list; - } - - update$() { - this.uploader$.set(this.uploader?.name$ ?? this.uploader_id?.toString()); - this.approver$.set(this.approver?.name$ ?? this.approver_id?.toString() ?? 'None'); - this.created_date$.set(dateFrom(+new Date(this.created_at))); - this.favcount$.set(this.fav_count); - this.score$.set(this.score); - this.file_size$.set(digitalUnit(this.file_size)); - this.file_ext$.set(this.file_ext as any); - this.file_url$.set(this.file_url); - this.source$.set(this.source); - this.dimension$.set(`${this.image_width}x${this.image_height}`); - this.booruUrl$.set(`${this.booruUrl}`); - if (this.isUgoria) this.webm_url$.set(this.large_file_url); - this.createdDate = new Date(this.created_at); - this.fire('update'); - } - - update(data: PostData) { - Object.assign(this, data); - this.update$(); - return this; - } - - async fetchTags() { - await this.ready; - const uncached_tags = this.tag_string.split(' ').filter(tag_name => !Tag.get(this.booru, tag_name)); - if (!uncached_tags.length) return this; - await Tag.fetchMultiple(this.booru, {name: {_space: uncached_tags.toString().replaceAll(',', ' ')}}); - return this; - } - - async createFavorite() { - if (!this.booru.user) return; - const data = await this.booru.fetch(`/favorites.json?post_id=${this.id}`, 'POST') - this.update(data); - this.booru.user.favorites.add(data.id); - ClientUser.events.fire('favoriteUpdate', this.booru.user); - return data.id; - } - - async deleteFavorite() { - if (!this.booru.user) return; - const data = await fetch(`/api/favorites/${this.id}?login=${this.booru.user.name}&api_key=${this.booru.user.apiKey}&origin=${this.booru.origin}`, {method: 'DELETE'}).then(res => res.json()) as boolean; - if (data === false) return; - this.fav_count--; - this.favcount$.set(this.fav_count); - this.booru.user.favorites.delete(this.id); - ClientUser.events.fire('favoriteUpdate', this.booru.user); - return; - } - - get pathname() { return `/posts/${this.id}` } - get uploader() { return this.booru.users.get(this.uploader_id); } - get approver() { if (this.approver_id) return this.booru.users.get(this.approver_id); else return null } - get isVideo() { return this.file_ext === 'mp4' || this.file_ext === 'webm' || this.file_ext === 'zip' } - get isGif() { return this.file_ext === 'gif' } - get isUgoria() { return this.file_ext === 'zip' } - get hasSound() { return this.tag_string_meta.includes('sound') } - get tags() { - const tag_list = this.tag_string.split(' '); - return [...this.booru.tags.values()].filter(tag => tag_list.includes(tag.name)) - } - get previewURL() { return this.media_asset?.variants?.find(variant => variant.file_ext === 'webp')?.url ?? this.large_file_url } - get booruUrl() { return `${this.booru.origin}/posts/${this.id}` } - get url() { return `https://danbooru.defaultkavy.com/posts/${this.id}` } - get isFileSource() { return this.source.startsWith('file://') } - get isLargeFile() { return this.file_size > 5_000_000 } // || this.image_height > innerHeight || this.image_width > innerWidth -} - -export interface PostData extends PostOptions { - "id": id, - "created_at": ISOString, - "uploader_id": id, - "score": number, - "source": string, - "md5": string, - "last_comment_bumped_at": timestamp | null, - "rating": 'g' | 's' | 'q' | 'e' | null, - "image_width": number, - "image_height": number, - "tag_string": string, - "fav_count": number, - "file_ext": FileType, - "last_noted_at": null | timestamp, - "parent_id": null | id, - "has_children": boolean, - "approver_id": null | id, - "tag_count_general": number, - "tag_count_artist": number, - "tag_count_character": number, - "tag_count_copyright": number, - "file_size": number, - "up_score": number, - "down_score": number, - "is_pending": boolean, - "is_flagged": boolean, - "is_deleted": boolean, - "tag_count": number, - "updated_at": ISOString, - "is_banned": boolean, - "pixiv_id": null | id, - "last_commented_at": null | timestamp, - "has_active_children": boolean, - "bit_flags": number, - "tag_count_meta": number, - "has_large": boolean, - "has_visible_children": boolean, - "media_asset": MediaAssetData, - "tag_string_general": string, - "tag_string_character": string, - "tag_string_copyright": string, - "tag_string_artist": string, - "tag_string_meta": string, - "file_url"?: string, - "large_file_url": string, - "preview_file_url": string -} - -export interface MediaAssetData { - "id": id, - "created_at": ISOString, - "updated_at": ISOString, - "md5": string, - "file_ext": FileType, - "file_size": number, - "image_width": number, - "image_height": number, - "duration": number, - "status": "active", - "file_key": string, - "is_public": boolean, - "pixel_hash": string, - "variants": MediaAssetVariant[]; -} - -export interface MediaAssetVariant { - "type": "original" | "720x720" | "360x360" | "180x180", - "url": string, - "width": number, - "height": number, - "file_ext": FileType -} - -export interface MetaTags { - /** Search tags */ - 'tags': string; - /** Search for posts uploaded by the user */ - 'user': username; - /** Search for posts not uploaded by the user */ - '-user': username; - /** Search for posts favorited by the user */ - 'fav': username; - /** Search for posts not favorited by the user */ - '-fav': username; - /** Search for posts favorited by the user ordered in the order they were favorited in, instead of by the date they were uploaded. */ - 'ordfav': username; - /** Search for posts with at least favorites. */ - 'favcount': NumericBasicSyntax; - /** Order search results. */ - 'order': 'favcount' | 'comm' | 'comment' | 'comment_bumped' | 'note' | 'artcomm' | 'id' | 'id_asc' | 'id_desc' | 'custom' | 'score' | 'score_asc' | 'rank' | 'downvotes' | 'upvotes' | 'changes' | 'md5' | 'landscape' | 'protrait' | 'mpixels' | 'mpixels_asc' | 'filesize'; - /** Search for posts that were approved by the user. */ - 'approver': UserSyntax; - /** Search for posts that were not approved by the user. */ - '-approver': username; - /** Search for posts that were commented on by the user. */ - 'commenter': UserSyntax; - /** Search for posts that were commented on by the user. */ - 'comm': UserSyntax; - /** Search for posts with comments saying string */ - 'comment': string; - /** Search for posts that have had notes created by the user. */ - 'noter': UserSyntax; - /** Search for posts that have had notes updated by the user. */ - 'notepdater': username; - /** Search for posts with notes saying string. */ - 'note': string; - /** Search for posts by status. */ - 'status': PostStatus; - '-status': PostStatus; - /** Search for posts that have ever been flagged by user (mod only; normal users may only search for flags created by themselves). */ - 'flagger': UserSyntax; - 'appeals': UserSyntax; - 'commentary': boolean | 'translated' | 'untranslated' | string - 'commentaryupdater': username; - 'favgroup': string; - '-favgroup': string; - 'ordfavgroup': string; - /** Search for posts in the saved search named string. */ - 'search': string; - 'id': NumericBasicSyntax; - 'date': NumericBasicSyntax; - 'age': NumericBasicSyntax; - 'rating': Rating | Rating[]; - '-rating': Rating | Rating[]; - 'source': Source; - 'pixiv': NumericBasicSyntax | 'any'; - 'parent': id | `any`; - '-parent': id; - 'child': 'none' | 'any'; - 'tagcount': NumericBasicSyntax; - 'gentags': NumericBasicSyntax; - 'arttags': NumericBasicSyntax; - 'chartags': NumericBasicSyntax; - 'copytags': NumericBasicSyntax; - 'metatags': NumericBasicSyntax; - 'score': NumericBasicSyntax; - 'downvotes': NumericBasicSyntax; - 'upvotes': NumericBasicSyntax; - 'disapproved': 'disinterest' | 'breaks_rules' | 'poor_quality' | username; - 'md5': string; - 'width': NumericBasicSyntax; - 'height': NumericBasicSyntax; - 'ratio': NumericBasicSyntax; - 'mpixels': NumericBasicSyntax; - 'filesize': FileSize; - 'filetype': FileType; - 'duration': seconds; - 'is': 'parent' | 'child' | - 'general' | 'sensitive' | 'questionable' | 'explicit' | 'sfw' | 'nsfw' | - 'active' | 'deleted' | 'ending' | 'unmoderated' | 'modqueue' | 'banned' | 'appealed' | 'flagged' | - 'jpg' | 'png' | 'gif' | 'mp4' | 'webm' | 'swf' | 'zip' - 'has': 'children' | 'parent' | 'source' | 'appeals' | 'flags' | 'replacements' | 'comments' | 'commentary' | 'notes' | 'pools'; - 'pool': poolname | id | 'any' | 'series' | 'collection'; - '-pool': poolname | id | 'any' | 'series' | 'collection'; - 'ordpool': poolname; - 'upvote': username; - 'downvote': username; - 'random': number; - 'limit': number; - // 'general': string; - // 'gen': string; - // 'artist': string; - // 'art': string; - // 'character': string; - // 'char': string; - // 'copyright': string; -} - -export type PostStatus = 'flagged' | 'deleted' | 'any' | 'all' | 'pending' | 'unmoderated' | 'banned'; \ No newline at end of file diff --git a/src/structure/PostManager.ts b/src/structure/PostManager.ts deleted file mode 100644 index f59b991..0000000 --- a/src/structure/PostManager.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { $EventManager } from "elexis"; -import { Post } from "./Post"; -import { Booru } from "./Booru"; - -export class PostManager { - static managers = new Map(); - orderMap = new Map(); - cache = new Set(); - limit = 100; - tags?: string; - finished = false; - events = new $EventManager(); - constructor(tags?: string) { - this.tags = tags; - PostManager.managers.set(this.tags, this); - Booru.events.on('set', () => { - this.clear(); - if (this.finished) { - this.finished = false; - } - }) - } - - static get(tags: string | undefined) { - const manager = this.managers.get(tags) ?? new PostManager(tags); - this.managers.set(manager.tags, manager); - return manager; - } - - clear() { - this.orderMap.clear(); - this.cache.clear(); - } - - addPosts(posts: OrArray) { - posts = $.orArrayResolve(posts); - for (const post of posts) { - if (!post.file_url) continue; - if (this.cache.has(post)) continue; - this.cache.add(post); - } - return this; - } - - async fetchPosts(direction: 'newer' | 'older'): Promise { - const tags = this.tags ? decodeURIComponent(this.tags).split('+') : undefined; - const generalTags: string[] = []; - const orderTags: string[] = []; - let limit: number = this.limit; - let posts: Post[] = []; - if (tags) for (const tag of tags) { - if (tag.startsWith('ordfav:')) orderTags.push(tag); - else if (tag.startsWith('order:')) orderTags.push(tag); - else if (tag.startsWith('limit:')) limit = Number(tag.split(':')[1]); - else generalTags.push(tag); - } - if (orderTags.length) { - if (orderTags.length > 1) { - this.events.fire('post_error', `Error: These query can't be used together [${orderTags}].`) - return []; - } - const orderTag = orderTags[0]; - if (orderTag.startsWith('ordfav:')) { - const username = orderTag.split(':')[1]; - const match_tags = generalTags.length ? `&search[post_tags_match]=${generalTags.toString().replaceAll(',', '+')}` : ''; - const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `&search[id]=>${this.orderKeyList.at(0)}` : `&search[id]=<${this.orderKeyList.at(-1)}` : undefined; - const favoritesDataList = await Booru.used.fetch(`/favorites.json?search[user_name]=${username}${beforeAfter ?? ''}${match_tags}&limit=${limit}`); - posts = await Post.fetchMultiple(Booru.used, {tags: `id:${favoritesDataList.map(data => data.post_id).toString()}`}); - const newPostOrderMap = new Map(); - for (const fav of favoritesDataList) { - const post = posts.find(post => post.id === fav.post_id); - if (!post) continue; - if (!post.file_url) continue; - newPostOrderMap.set(fav.id, post); - } - this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]); - this.events.fire('post_fetch', {manager: this, postList: posts}); - return posts; - } - - if (orderTag.startsWith('order:')) { - const page = this.orderKeyList.length ? direction === 'newer' ? 1 : (this.orderMap.size / limit) + 1 : undefined; - posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, page); - const newPostOrderMap = new Map(posts.filter(post => post.file_url).map(post => [post.id, post])); - newPostOrderMap.forEach((post, id) => { if (this.orderMap.has(id)) newPostOrderMap.delete(id) }); - this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]) - this.events.fire('post_fetch', {manager: this, postList: posts}); - } - } else { - const beforeAfter = this.orderKeyList.length ? direction === 'newer' ? `a${this.orderKeyList.at(0)}` : `b${this.orderKeyList.at(-1)}` : undefined; - posts = await Post.fetchMultiple(Booru.used, {tags: this.tags}, limit, beforeAfter); - const newPostOrderMap = new Map(posts.filter(post => post.file_url).map(post => [post.id, post])); - this.orderMap = new Map(direction === 'newer' ? [...newPostOrderMap, ...this.orderMap] : [...this.orderMap, ...newPostOrderMap]) - } - - if (!posts.length) { - this.finished = true; - if (!this.cache.size) this.events.fire('noPost'); - else this.events.fire('endPost') - } - - this.events.fire('post_fetch', {manager: this, postList: posts}); - this.addPosts(posts); - return posts - } - - get orderKeyList() { return [...this.orderMap.keys()]} -} - -interface PostManagerEventMap { - startLoad: []; - noPost: []; - endPost: []; - post_error: [message: string]; - post_fetch: [{manager: PostManager, postList: Post[]}]; -} - -interface FavoritesData { - id: id; - post_id: id; - user_id: id; -} \ No newline at end of file diff --git a/src/structure/Tag.ts b/src/structure/Tag.ts deleted file mode 100644 index 011300f..0000000 --- a/src/structure/Tag.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Booru } from "./Booru"; - -export interface TagOptions {} -export interface Tag extends TagData {} -export class Tag { - post_count$ = $.state(0); - name$ = $.state(''); - booru: Booru; - constructor(booru: Booru, data: TagData) { - this.booru = booru; - Object.assign(this, data); - this.$update(); - } - - static async fetch(booru: Booru, id: id) { - const data = await booru.fetch(`/tags/${id}.json`); - const instance = booru.tags.get(data.id)?.update(data) ?? new this(booru, data); - booru.tags.set(instance.id, instance); - return instance; - } - - static async fetchMultiple(booru: Booru, search?: Partial, limit = 1000) { - let searchQuery = ''; - if (search) { - for (const [key, val] of Object.entries(search)) { - if (val instanceof Array) searchQuery += `&search[${key}]=${val}`; - else if (val instanceof Object) { - for (const [ckey, cval] of Object.entries(val)) { - searchQuery += `&search[${key}${ckey}]=${cval}` - } - } - else searchQuery += `&search[${key}]=${val}` - } - } - const dataArray = await booru.fetch(`/tags.json?limit=${limit}${searchQuery}`); - const list = dataArray.map(data => { - const instance = booru.tags.get(data.id)?.update(data) ?? new this(booru, data); - booru.tags.set(instance.id, instance); - return instance; - }); - return list; - } - - static get(booru: Booru, name: string) { - return [...booru.tags.values()].find(tag => tag.name === name); - } - - update(data: TagData) { - Object.assign(this, data); - this.$update(); - return this; - } - - $update() { - this.post_count$.set(this.post_count); - this.name$.set(this.name); - } -} - -export interface TagData { - "id": id, - "name": string, - "post_count": number, - "category": number, - "created_at": ISOString, - "updated_at": ISOString, - "is_deprecated": boolean, - "words": string[]; -} - -export interface TagSearchParams { - id: NumericSyntax; - category: NumericSyntax; - post_count: NumericSyntax; - created_at: NumericSyntax; - updated_at: NumericSyntax; - name: TextSyntax; - is_deprecated: boolean; - fuzzy_name_matches: string; - name_matches: string; - name_normalize: string; - name_or_alias_matches: string; - hide_empty: boolean; - order: 'name' | 'date' | 'count' | 'similarity' -} - -export enum TagCategory { - General = 0, - Artist = 1, - Copyright = 3, - Character = 4, - Meta = 5 -} \ No newline at end of file diff --git a/src/structure/User.ts b/src/structure/User.ts deleted file mode 100644 index 1c79bd9..0000000 --- a/src/structure/User.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { Booru } from "./Booru"; - -export class UserOptions {} -export interface User extends UserOptions, UserData {} -export class User { - name$ = $.state('...'); - post_upload_count$ = $.state(0); - level$ = $.state(10); - level_string$ = $.state('...'); - booru: Booru; - favorites = new Set(); - constructor(booru: Booru, data: UserData, update$: boolean = true) { - this.booru = booru; - Object.assign(this, data); - if (update$) this.update$(); - } - - static async fetch(booru: Booru, id: username): Promise; - static async fetch(booru: Booru, id: id): Promise; - static async fetch(booru: Booru, id: id | username) { - let data: UserData; - if (typeof id === 'string') { - const res = (await booru.fetch(`/users.json?search[name]=${id}`)).at(0); - if (!res) throw 'User Not Found'; - return data = res; - } else data = await booru.fetch(`/users/${id}.json`); - const instance = booru.users.get(data.id)?.update(data) ?? new this(booru, data); - booru.users.set(instance.id, instance); - return instance; - } - - static async fetchMultiple(booru: Booru, search?: Partial, limit = 200) { - let searchQuery = ''; - if (search) { - for (const [key, val] of Object.entries(search)) { - if (val instanceof Array) searchQuery += `&search[${key}]=${val}`; - else if (val instanceof Object) { - for (const [ckey, cval] of Object.entries(val)) { - searchQuery += `&search[${key}${ckey}]=${cval}` - } - } - else searchQuery += `&search[${key}]=${val}` - } - } - const dataArray = await booru.fetch(`/users.json?limit=${limit}${searchQuery}`); - const list = dataArray.map(data => { - const instance = new this(booru, data); - booru.users.set(instance.id, instance); - return instance; - }); - return list; - } - - update(data: UserData) { - Object.assign(this, data); - this.update$(); - return this; - } - - update$() { - this.name$.set(this.name); - this.post_upload_count$.set(this.post_upload_count); - this.level$.set(this.level); - this.level_string$.set(this.level_string); - } - - get booruURL() { return `${this.booru.origin}/users/${this.id}`} - get url() { return `/users/${this.id}`} -} - -export enum UserLevel { - Restricted = 10, - Member = 20, - Gold = 30, - Platinum = 31, - Builder = 32, - Contributor = 35, - Approver = 37, - Moderater = 40, - Admin = 50 -} -export interface UserData { - "id": id, - "name": username, - "level": UserLevel, - "inviter_id": id, - "created_at": ISOString, - "post_update_count": number, - "note_update_count": number, - "post_upload_count": number, - "is_deleted": boolean, - "level_string": keyof UserLevel, - "is_banned": boolean, -} - -export interface UserSearchParam { - id: NumericSyntax; - level: NumericSyntax; - post_upload_count: NumericSyntax; - post_update_count: NumericSyntax; - note_update_count: NumericSyntax; - favorite_count: NumericSyntax; - created_at: NumericSyntax; - updated_at: NumericSyntax; - name: TextSyntax; - inviter: UserSyntax; - name_matches: string; - min_level: UserLevel; - max_level: UserLevel; - current_user_first: boolean; - order: 'name' | 'post_upload_count' | 'post_update_count' | 'note_update_count'; -} \ No newline at end of file diff --git a/src/structure/Util.ts b/src/structure/Util.ts deleted file mode 100644 index aacdfed..0000000 --- a/src/structure/Util.ts +++ /dev/null @@ -1,55 +0,0 @@ -const SECOND_MS = 1000; -const MINUTE_MS = SECOND_MS * 60; -const HOUR_MS = MINUTE_MS * 60; -const DAY_MS = HOUR_MS * 24; -const WEEK_MS = DAY_MS * 7; -const INTL_RELATIVE_TIME = new Intl.RelativeTimeFormat('en', {style: 'long'}) - -export function time(timestamp: number) { - timestamp = Math.floor(timestamp) - const seconds = timestamp / SECOND_MS; - const minutes = timestamp / MINUTE_MS; - const hours = timestamp / HOUR_MS; - const days = timestamp / DAY_MS; - - const mil = timestamp % 1000; - const s = Math.floor(timestamp % 60_000 / 1000); - const min = Math.floor(timestamp % 3600_000 / 60_000); - const h = Math.floor(timestamp % (3600_000 * 24) / 3600_000) - const ss = s.toString().padStart(2, '0'); - const mm = min.toString().padStart(2, '0'); - const hh = h.toString().padStart(2, '0'); - return {seconds, minutes, hours, days, mil, s, min, h, ss, mm, hh} -} - -export function dateFrom(timestamp: number, from = Date.now()) { - const diff = timestamp - from; - const diff_abs = Math.abs(diff); - if (diff_abs < MINUTE_MS) return INTL_RELATIVE_TIME.format(Math.round(diff / SECOND_MS), 'second'); - if (diff_abs < HOUR_MS) return INTL_RELATIVE_TIME.format(Math.round(diff / MINUTE_MS), 'minute'); - if (diff_abs < DAY_MS) return INTL_RELATIVE_TIME.format(Math.round(diff / HOUR_MS), 'hour'); - if (diff_abs < WEEK_MS) return INTL_RELATIVE_TIME.format(Math.round(diff / DAY_MS), 'day'); - const date = new Date(timestamp); - return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${(date.getDate().toString().padStart(2, '0'))}`; -} - -export function digitalUnit(bytes: number) { - if (bytes < 1000) return `${bytes}B` - const kb = bytes / 1000; - if (kb < 1000) return `${kb.toFixed(2)}kB`; - const mb = bytes / (1000 ** 2); - if (mb < 1000) return `${mb.toFixed(2)}MB`; - const gb = bytes / (1000 ** 3); - if (gb < 1000) return `${gb.toFixed(2)}GB`; - const tb = bytes / (1000 ** 4); - if (tb < 1000) return `${tb.toFixed(2)}TB`; - const pb = bytes / (1000 ** 5); - if (pb < 1000) return `${pb.toFixed(2)}PB`; - const eb = bytes / (1000 * 6); - return `${eb.toFixed(2)}EB`; -} - -const NUMBER_FORMAT = new Intl.NumberFormat('en', {notation: 'compact'}) -export function numberFormat(number: number) { - return NUMBER_FORMAT.format(number) -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 72012eb..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - }, - "exclude": ["dist"] -} diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 102e8c7..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'vite'; -export default defineConfig({ - server: { - proxy: { - '/api': { - target: 'http://localhost:3030', - changeOrigin: true - }, - }, - watch: { - usePolling: true - } - }, - define: { - __APP_VERSION__: JSON.stringify(process.env.npm_package_version) - } -}) \ No newline at end of file