v0.3.5
new: danbooru account login with api is available now. new: class Client User adjust: [css] route element padding inline. new: [css] color variable. new: interface APIError. new: [page] /login route. new: [$nav] menu button and account button. new: class $Drawer, with user account basic detail, and login/logout button. new: function numberFormat. optimize: [$IconButton] $icon default is hiding, method icon will set $icon visible. new: [$IconButton] method link, set 'click' listener with open link.
This commit is contained in:
parent
e9f5e1808d
commit
a992af35c2
1
dist/assets/index-BkbpsU_v.css
vendored
Normal file
1
dist/assets/index-BkbpsU_v.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-DaDrA_5o.js
vendored
1
dist/assets/index-DaDrA_5o.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-DeBFHjT4.css
vendored
1
dist/assets/index-DeBFHjT4.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-La3x4wRW.js
vendored
Normal file
1
dist/assets/index-La3x4wRW.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -7,8 +7,8 @@
|
|||||||
<title>Danbooru Viewer v0.2.5</title>
|
<title>Danbooru Viewer v0.2.5</title>
|
||||||
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
|
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
|
||||||
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
|
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
|
||||||
<script type="module" crossorigin src="/assets/index-DaDrA_5o.js"></script>
|
<script type="module" crossorigin src="/assets/index-La3x4wRW.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DeBFHjT4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BkbpsU_v.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
@import '/src/component/PostTile/$PostTile';
|
@import '/src/component/PostTile/$PostTile';
|
||||||
@import '/src/component/Searchbar/$Searchbar';
|
@import '/src/component/Searchbar/$Searchbar';
|
||||||
@import '/src/component/IconButton/$IconButton';
|
@import '/src/component/IconButton/$IconButton';
|
||||||
|
@import '/src/component/Drawer/$Drawer';
|
||||||
// routes
|
// routes
|
||||||
@import '/src/route/post/$post_route';
|
@import '/src/route/post/$post_route';
|
||||||
@import '/src/route/login/$login_route';
|
@import '/src/route/login/$login_route';
|
||||||
@ -11,12 +12,15 @@
|
|||||||
--background-color: #1e1e2c;
|
--background-color: #1e1e2c;
|
||||||
--background-color-lighter: #3b3b66;
|
--background-color-lighter: #3b3b66;
|
||||||
--background-color-light: #24243b;
|
--background-color-light: #24243b;
|
||||||
|
--background-color-dark: #12121f;
|
||||||
|
--background-color-darker: #07070c;
|
||||||
--primary-color: #d1d1ee;
|
--primary-color: #d1d1ee;
|
||||||
--primary-color-dark: #9696b3;
|
--primary-color-dark: #9696b3;
|
||||||
--primary-color-darker: #72728d;
|
--primary-color-darker: #72728d;
|
||||||
--secondary-color: #aeaeec;
|
--secondary-color: #aeaeec;
|
||||||
--secondary-color-dark: #6d6da1;
|
--secondary-color-dark: #6d6da1;
|
||||||
--secondary-color-darker: #424268;
|
--secondary-color-darker: #424268;
|
||||||
|
--shadow-color: #09090e50;
|
||||||
|
|
||||||
--border-radius-small: 0.4rem;
|
--border-radius-small: 0.4rem;
|
||||||
--border-radius-medium: 0.8rem;
|
--border-radius-medium: 0.8rem;
|
||||||
@ -154,7 +158,7 @@ router {
|
|||||||
route {
|
route {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-inline: 10px;
|
padding-inline: 1rem;
|
||||||
padding-top: var(--nav-height);
|
padding-top: var(--nav-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "danbooru-viewer",
|
"name": "danbooru-viewer",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.3.4",
|
"version": "0.3.5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"vite": "^5.4.8"
|
"vite": "^5.4.8"
|
||||||
|
91
src/component/Drawer/$Drawer.ts
Normal file
91
src/component/Drawer/$Drawer.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { $Container } from "elexis";
|
||||||
|
import { Booru } from "../../structure/Booru";
|
||||||
|
import { numberFormat } from "../../modules";
|
||||||
|
|
||||||
|
export class $Drawer extends $Container {
|
||||||
|
$filter = $('div').class('filter');
|
||||||
|
$container = $('div').class('drawer-container')
|
||||||
|
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([
|
||||||
|
$('div').class('login')
|
||||||
|
.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)))),
|
||||||
|
$('div').class('logout').hide(true)
|
||||||
|
.content([ $('icon-button').icon('log-in-outline').content('Logout').on('dblclick', () => Booru.used.logout()) ])
|
||||||
|
.self(($div => Booru.events.on('login', () => $div.hide(false)).on('logout', () => $div.hide(true)))),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
this.$filter.on('click', () => $.back())
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$container.animate({
|
||||||
|
transform: [`translateX(0%)`, `translateX(100%)`]
|
||||||
|
}, {
|
||||||
|
fill: 'both',
|
||||||
|
duration: 300,
|
||||||
|
easing: 'ease'
|
||||||
|
}, () => this.hide(true))
|
||||||
|
this.$filter.animate({
|
||||||
|
opacity: [1, 0]
|
||||||
|
}, {
|
||||||
|
fill: 'both',
|
||||||
|
duration: 300,
|
||||||
|
easing: 'ease'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkURL(beforeURL: URL | undefined, afterURL: URL) {
|
||||||
|
if (beforeURL?.hash === '#drawer') this.close();
|
||||||
|
if (afterURL.hash === '#drawer') this.open();
|
||||||
|
}
|
||||||
|
}
|
75
src/component/Drawer/_$Drawer.scss
Normal file
75
src/component/Drawer/_$Drawer.scss
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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(--background-color);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
div.user-info {
|
||||||
|
background-color: var(--background-color-light);
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.filter {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
color-mix(in srgb, var(--background-color-dark) 50%, transparent) 0%,
|
||||||
|
color-mix(in srgb, var(--background-color-darker) 70%, transparent) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ export class $IconButton extends $Button {
|
|||||||
|
|
||||||
private build() {
|
private build() {
|
||||||
super.content([
|
super.content([
|
||||||
this.$icon,
|
this.$icon.hide(true),
|
||||||
this.$label
|
this.$label
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@ -22,7 +22,12 @@ export class $IconButton extends $Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
icon(name: string) {
|
icon(name: string) {
|
||||||
this.$icon.name(name);
|
this.$icon.name(name).hide(false);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
link(url: string, replace = false) {
|
||||||
|
this.on('click', () => replace ? $.replace(url) : $.open(url));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { $Container } from "elexis";
|
import { $Container } from "elexis";
|
||||||
import { Tag, TagCategory } from "../../structure/Tag";
|
import { Tag, TagCategory } from "../../structure/Tag";
|
||||||
import { Booru } from "../../structure/Booru";
|
import { Booru } from "../../structure/Booru";
|
||||||
import { User } from "../../structure/User";
|
|
||||||
import { Autocomplete } from "../../structure/Autocomplete";
|
import { Autocomplete } from "../../structure/Autocomplete";
|
||||||
|
import { numberFormat } from "../../modules";
|
||||||
|
|
||||||
export class $Searchbar extends $Container {
|
export class $Searchbar extends $Container {
|
||||||
$tagInput = new $TagInput(this);
|
$tagInput = new $TagInput(this);
|
||||||
@ -143,7 +143,7 @@ export class $Searchbar extends $Container {
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
data.isTag() ? $('div').class('tag-detail').content([
|
data.isTag() ? $('div').class('tag-detail').content([
|
||||||
$('span').class('tag-post-count').content(new Intl.NumberFormat('en', {notation: 'compact'}).format(data.post_count)),
|
$('span').class('tag-post-count').content(numberFormat(data.post_count)),
|
||||||
$('span').class('tag-category').content(TagCategory[data.category])
|
$('span').class('tag-category').content(TagCategory[data.category])
|
||||||
]) : null,
|
]) : null,
|
||||||
data.isUser() ? $('span').class('user-level').content(data.level) : null
|
data.isUser() ? $('span').class('user-level').content(data.level) : null
|
||||||
@ -160,8 +160,8 @@ export class $Searchbar extends $Container {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkURL(beforeURL: URL, afterURL: URL) {
|
checkURL(beforeURL: URL | undefined, afterURL: URL) {
|
||||||
if (beforeURL.hash === '#search') this.inactivate();
|
if (beforeURL?.hash === '#search') this.inactivate();
|
||||||
if (afterURL.hash === '#search') this.activate();
|
if (afterURL.hash === '#search') this.activate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src/index.d.ts
vendored
9
src/index.d.ts
vendored
@ -45,7 +45,10 @@ type TextSyntaxComparisons<T> =
|
|||||||
{ _lower_space: string }
|
{ _lower_space: string }
|
||||||
|
|
||||||
type UserSyntax = { _id: id } | { _name: username };
|
type UserSyntax = { _id: id } | { _name: username };
|
||||||
|
|
||||||
type ChainingSyntax = {_id: id} | {has_: boolean};
|
type ChainingSyntax = {_id: id} | {has_: boolean};
|
||||||
|
type PostSyntax = {_id: id} | {_tags_match: string};
|
||||||
type PostSyntax = {_id: id} | {_tags_match: string};
|
interface APIError {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
30
src/main.ts
30
src/main.ts
@ -8,6 +8,8 @@ import { $Router, $RouterNavigationDirection } from '@elexis/router';
|
|||||||
import { $Searchbar } from './component/Searchbar/$Searchbar';
|
import { $Searchbar } from './component/Searchbar/$Searchbar';
|
||||||
import { $IonIcon } from './component/IonIcon/$IonIcon';
|
import { $IonIcon } from './component/IonIcon/$IonIcon';
|
||||||
import { $IconButton } from './component/IconButton/$IconButton';
|
import { $IconButton } from './component/IconButton/$IconButton';
|
||||||
|
import { $login_route } from './route/login/$login_route';
|
||||||
|
import { $Drawer } from './component/Drawer/$Drawer';
|
||||||
// declare elexis module
|
// declare elexis module
|
||||||
declare module 'elexis' {
|
declare module 'elexis' {
|
||||||
export namespace $ {
|
export namespace $ {
|
||||||
@ -28,7 +30,8 @@ export const [danbooru, safebooru]: Booru[] = [
|
|||||||
]
|
]
|
||||||
Booru.set(Booru.manager.get(Booru.storageAPI ?? '') ?? danbooru);
|
Booru.set(Booru.manager.get(Booru.storageAPI ?? '') ?? danbooru);
|
||||||
const $searchbar = new $Searchbar().hide(true);
|
const $searchbar = new $Searchbar().hide(true);
|
||||||
if (location.hash === '#search') $searchbar.activate();
|
const $drawer = new $Drawer();
|
||||||
|
|
||||||
// render
|
// render
|
||||||
$(document.body).content([
|
$(document.body).content([
|
||||||
// Navigation Bar
|
// Navigation Bar
|
||||||
@ -60,10 +63,24 @@ $(document.body).content([
|
|||||||
// Open Booru
|
// Open Booru
|
||||||
$('ion-icon').class('open').name('open-outline').title('Open in Original Site')
|
$('ion-icon').class('open').name('open-outline').title('Open in Original Site')
|
||||||
.on('click', () => $.open(location.href.replace(location.origin, Booru.used.origin))),
|
.on('click', () => $.open(location.href.replace(location.origin, Booru.used.origin))),
|
||||||
|
// 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', () => $.open(location.href + '#drawer'))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
// Searchbar
|
// Searchbar
|
||||||
$searchbar,
|
$searchbar,
|
||||||
|
// Drawer
|
||||||
|
$drawer,
|
||||||
// Base Router
|
// Base Router
|
||||||
$('router').base('/').map([
|
$('router').base('/').map([
|
||||||
// Home Page
|
// Home Page
|
||||||
@ -71,7 +88,9 @@ $(document.body).content([
|
|||||||
// Posts Page
|
// Posts Page
|
||||||
$('route').id('posts').path('/posts?tags').builder(({query}) => new $PostGrid({tags: query.tags})),
|
$('route').id('posts').path('/posts?tags').builder(({query}) => new $PostGrid({tags: query.tags})),
|
||||||
// Post Page
|
// Post Page
|
||||||
post_route
|
post_route,
|
||||||
|
// Login Page
|
||||||
|
$login_route
|
||||||
]).on('beforeSwitch', (e) => {
|
]).on('beforeSwitch', (e) => {
|
||||||
const DURATION = 300;
|
const DURATION = 300;
|
||||||
const TX = 2;
|
const TX = 2;
|
||||||
@ -123,4 +142,9 @@ $(document.body).content([
|
|||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
$Router.events.on('stateChange', ({beforeURL, afterURL}) => { $searchbar.checkURL(beforeURL, afterURL) })
|
$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)
|
||||||
|
}
|
4
src/modules.ts
Normal file
4
src/modules.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const NUMBER_FORMAT = new Intl.NumberFormat('en', {notation: 'compact'})
|
||||||
|
export function numberFormat(number: number) {
|
||||||
|
return NUMBER_FORMAT.format(number)
|
||||||
|
}
|
30
src/route/login/$login_route.ts
Normal file
30
src/route/login/$login_route.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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')),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
27
src/route/login/_$login_route.scss
Normal file
27
src/route/login/_$login_route.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
route#login {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid var(--secondary-color);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
input {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { Tag, TagCategory } from "../../structure/Tag";
|
|||||||
import { ArtistCommentary } from "../../structure/Commentary";
|
import { ArtistCommentary } from "../../structure/Commentary";
|
||||||
import { Booru } from "../../structure/Booru";
|
import { Booru } from "../../structure/Booru";
|
||||||
import type { $IonIcon } from "../../component/IonIcon/$IonIcon";
|
import type { $IonIcon } from "../../component/IonIcon/$IonIcon";
|
||||||
|
import { numberFormat } from "../../modules";
|
||||||
|
|
||||||
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
|
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
|
||||||
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
|
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
|
||||||
@ -87,7 +88,7 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
|||||||
$('section').content([
|
$('section').content([
|
||||||
tags.map(tag => $('div').class('tag').content([
|
tags.map(tag => $('div').class('tag').content([
|
||||||
$('a').class('tag-name').content(tag.name).href(`/posts?tags=${tag.name}`),
|
$('a').class('tag-name').content(tag.name).href(`/posts?tags=${tag.name}`),
|
||||||
$('span').class('tag-post-count').content(tag.post_count$)
|
$('span').class('tag-post-count').content(tag.post_count$.convert(numberFormat))
|
||||||
]))
|
]))
|
||||||
])
|
])
|
||||||
] : null
|
] : null
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { $EventManager, type $EventMap } from "elexis";
|
import { $EventManager, type $EventMap } from "elexis";
|
||||||
import type { Post } from "./Post";
|
import type { Post } from "./Post";
|
||||||
import type { Tag } from "./Tag";
|
import type { Tag } from "./Tag";
|
||||||
|
import { ClientUser, type ClientUserData } from "./ClientUser";
|
||||||
|
|
||||||
export interface BooruOptions {
|
export interface BooruOptions {
|
||||||
origin: string;
|
origin: string;
|
||||||
@ -9,9 +10,10 @@ export interface BooruOptions {
|
|||||||
export interface Booru extends BooruOptions {}
|
export interface Booru extends BooruOptions {}
|
||||||
export class Booru {
|
export class Booru {
|
||||||
static used: Booru;
|
static used: Booru;
|
||||||
static events = new $EventManager<BooruEventMap>();
|
static events = new $EventManager<BooruStaticEventMap>();
|
||||||
static name$ = $.state(this.name);
|
static name$ = $.state(this.name);
|
||||||
static manager = new Map<string, Booru>()
|
static manager = new Map<string, Booru>()
|
||||||
|
user?: ClientUser;
|
||||||
posts = new Map<id, Post>();
|
posts = new Map<id, Post>();
|
||||||
tags = new Map<id, Tag>();
|
tags = new Map<id, Tag>();
|
||||||
constructor(options: BooruOptions) {
|
constructor(options: BooruOptions) {
|
||||||
@ -24,6 +26,8 @@ export class Booru {
|
|||||||
this.used = booru;
|
this.used = booru;
|
||||||
this.name$.set(booru.name);
|
this.name$.set(booru.name);
|
||||||
this.storageAPI = booru.name;
|
this.storageAPI = booru.name;
|
||||||
|
const userdata = ClientUser.storageUserData;
|
||||||
|
if (userdata) booru.login(userdata.username, userdata.apiKey);
|
||||||
this.events.fire('set');
|
this.events.fire('set');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -31,8 +35,33 @@ export class Booru {
|
|||||||
static get storageAPI() { return localStorage.getItem('booru_api'); }
|
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') }
|
static set storageAPI(name: string | null) { if (name) localStorage.setItem('booru_api', name); else localStorage.removeItem('booru_api') }
|
||||||
|
|
||||||
|
async fetch<T>(endpoint: string) {
|
||||||
|
const data = await fetch(`${this.origin}${endpoint}`).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<ClientUserData>(`/profile.json?login=${username}&api_key=${apiKey}`);
|
||||||
|
this.user = new ClientUser(this, apiKey, data);
|
||||||
|
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 {
|
interface BooruEventMap extends $EventMap {
|
||||||
set: []
|
|
||||||
}
|
}
|
71
src/structure/ClientUser.ts
Normal file
71
src/structure/ClientUser.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import type { Booru } from "./Booru";
|
||||||
|
import { User, type UserData } from "./User";
|
||||||
|
|
||||||
|
export interface ClientUser extends ClientUserData {}
|
||||||
|
export class ClientUser extends User {
|
||||||
|
apiKey: string;
|
||||||
|
favorite_count$ = $.state(0);
|
||||||
|
forum_post_count$ = $.state(0);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -113,6 +113,7 @@ export class Post extends $EventManager<{update: []}> {
|
|||||||
}
|
}
|
||||||
get previewURL() { return this.media_asset.variants?.find(variant => variant.file_ext === 'webp')?.url ?? this.large_file_url }
|
get previewURL() { return this.media_asset.variants?.find(variant => variant.file_ext === 'webp')?.url ?? this.large_file_url }
|
||||||
get url() { return `${this.booru.origin}/posts/${this.id}` }
|
get url() { return `${this.booru.origin}/posts/${this.id}` }
|
||||||
|
get isFileSource() { return this.source.startsWith('file://') }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostData extends PostOptions {
|
export interface PostData extends PostOptions {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import type { Booru } from "./Booru";
|
import type { Booru } from "./Booru";
|
||||||
|
|
||||||
const INTL_number = new Intl.NumberFormat('en', {notation: 'compact'})
|
|
||||||
export interface TagOptions {}
|
export interface TagOptions {}
|
||||||
export interface Tag extends TagData {}
|
export interface Tag extends TagData {}
|
||||||
export class Tag {
|
export class Tag {
|
||||||
post_count$ = $.state(0, {format: (value) => `${INTL_number.format(value)}`});
|
post_count$ = $.state(0);
|
||||||
name$ = $.state('');
|
name$ = $.state('');
|
||||||
booru: Booru;
|
booru: Booru;
|
||||||
constructor(booru: Booru, data: TagData) {
|
constructor(booru: Booru, data: TagData) {
|
||||||
|
@ -4,15 +4,20 @@ export class UserOptions {}
|
|||||||
export interface User extends UserOptions, UserData {}
|
export interface User extends UserOptions, UserData {}
|
||||||
export class User {
|
export class User {
|
||||||
static manager = new Map<id, User>();
|
static manager = new Map<id, User>();
|
||||||
name$ = $.state('loding...');
|
name$ = $.state('...');
|
||||||
constructor(data: UserData) {
|
post_upload_count$ = $.state(0);
|
||||||
|
level$ = $.state(10);
|
||||||
|
level_string$ = $.state('...');
|
||||||
|
booru: Booru;
|
||||||
|
constructor(booru: Booru, data: UserData, update$: boolean = true) {
|
||||||
|
this.booru = booru;
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
this.update$();
|
if (update$) this.update$();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetch(booru: Booru, id: id) {
|
static async fetch(booru: Booru, id: id) {
|
||||||
const data = await fetch(`${booru.origin}/users/${id}.json`).then(async data => await data.json()) as UserData;
|
const data = await fetch(`${booru.origin}/users/${id}.json`).then(async data => await data.json()) as UserData;
|
||||||
const instance = this.manager.get(data.id)?.update(data) ?? new this(data);
|
const instance = this.manager.get(data.id)?.update(data) ?? new this(booru, data);
|
||||||
this.manager.set(instance.id, instance);
|
this.manager.set(instance.id, instance);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@ -33,7 +38,7 @@ export class User {
|
|||||||
const req = await fetch(`${booru.origin}/users.json?limit=${limit}${searchQuery}`);
|
const req = await fetch(`${booru.origin}/users.json?limit=${limit}${searchQuery}`);
|
||||||
const dataArray: UserData[] = await req.json();
|
const dataArray: UserData[] = await req.json();
|
||||||
const list = dataArray.map(data => {
|
const list = dataArray.map(data => {
|
||||||
const instance = new this(data);
|
const instance = new this(booru, data);
|
||||||
this.manager.set(instance.id, instance);
|
this.manager.set(instance.id, instance);
|
||||||
return instance;
|
return instance;
|
||||||
});
|
});
|
||||||
@ -48,9 +53,26 @@ export class User {
|
|||||||
|
|
||||||
update$() {
|
update$() {
|
||||||
this.name$.set(this.name);
|
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 {
|
export interface UserData {
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": username,
|
"name": username,
|
||||||
@ -61,57 +83,10 @@ export interface UserData {
|
|||||||
"note_update_count": number,
|
"note_update_count": number,
|
||||||
"post_upload_count": number,
|
"post_upload_count": number,
|
||||||
"is_deleted": boolean,
|
"is_deleted": boolean,
|
||||||
"level_string": UserLevelString,
|
"level_string": keyof UserLevel,
|
||||||
"is_banned": boolean,
|
"is_banned": boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserLevel = 10 | 20 | 30 | 31 | 32 | 40 | 50;
|
|
||||||
export type UserLevelString = "Member" | "Gold" | "Platinum" | "Admin" | "Contributor" | "Builder" | "Approver";
|
|
||||||
|
|
||||||
export interface UserProfileData 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 UserSearchParam {
|
export interface UserSearchParam {
|
||||||
id: NumericSyntax<id>;
|
id: NumericSyntax<id>;
|
||||||
level: NumericSyntax<UserLevel>;
|
level: NumericSyntax<UserLevel>;
|
||||||
|
Loading…
Reference in New Issue
Block a user