fix: event passive.
remove: Post sidebar copy link buttons.
fix: page transition transform value undefined.
enhance: $PostGrid loader will stop when no more post can fetched.
enhance: reset $PostGrid loader when booru switched.
enhance: searchbar autocomplete result same as official site.
new: class Autocomplete.
new: post sidebar add File, Booru, Webm, Source url.
This commit is contained in:
defaultkavy 2024-10-06 23:12:53 +08:00
parent 86c2d5aa9f
commit 315246a37a
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
17 changed files with 224 additions and 72 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-DaDrA_5o.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/index-DeBFHjT4.css vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@ -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-C21fHnwq.js"></script> <script type="module" crossorigin src="/assets/index-DaDrA_5o.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B240dcNf.css"> <link rel="stylesheet" crossorigin href="/assets/index-DeBFHjT4.css">
</head> </head>
<body> <body>
</body> </body>

View File

@ -156,4 +156,9 @@ ion-icon {
&:hover { &:hover {
color: var(--secondary-color); color: var(--secondary-color);
} }
}
a {
color: var(--secondary-color);
text-decoration: none;
} }

View File

@ -2,7 +2,7 @@
"name": "danbooru-viewer", "name": "danbooru-viewer",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.3.3", "version": "0.3.4",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"vite": "^5.4.8" "vite": "^5.4.8"

View File

@ -10,6 +10,7 @@ export class $PostGrid extends $Layout {
posts = new Set<Post>(); posts = new Set<Post>();
$posts = new Set<$PostTile>(); $posts = new Set<$PostTile>();
tags?: string; tags?: string;
finished = false;
constructor(options?: $PostGridOptions) { constructor(options?: $PostGridOptions) {
super(); super();
this.tags = options?.tags; this.tags = options?.tags;
@ -20,7 +21,13 @@ export class $PostGrid extends $Layout {
protected async init() { protected async init() {
setInterval(() => { if (this.inDOM() && document.documentElement.scrollTop === 0) this.updateNewest(); }, 10000); setInterval(() => { if (this.inDOM() && document.documentElement.scrollTop === 0) this.updateNewest(); }, 10000);
Booru.events.on('set', () => { this.removeAll(); }) Booru.events.on('set', () => {
this.removeAll();
if (this.finished) {
this.finished = false;
this.loader();
}
})
this.on('resize', () => this.resize()) this.on('resize', () => this.resize())
this.loader(); this.loader();
} }
@ -28,10 +35,13 @@ export class $PostGrid extends $Layout {
protected async loader() { protected async loader() {
if (!this.inDOM()) return setTimeout(() => this.loader(), 100);; if (!this.inDOM()) return setTimeout(() => this.loader(), 100);;
while (this.inDOM() && document.documentElement.scrollHeight <= innerHeight * 2) { while (this.inDOM() && document.documentElement.scrollHeight <= innerHeight * 2) {
await this.getPosts(); const posts = await this.getPosts();
if (!this.posts.size) return; if (!posts.length) return this.finished = true;
}
if (document.documentElement.scrollTop + innerHeight > document.documentElement.scrollHeight - innerHeight * 2) {
const posts = await this.getPosts();
if (!posts.length) return this.finished = true;
} }
if (document.documentElement.scrollTop + innerHeight > document.documentElement.scrollHeight - innerHeight * 2) await this.getPosts();
setTimeout(() => this.loader(), 100); setTimeout(() => this.loader(), 100);
} }
@ -65,14 +75,14 @@ export class $PostGrid extends $Layout {
const latestPost = this.sortedPosts.at(0); const latestPost = this.sortedPosts.at(0);
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: latestPost ? `>${latestPost.id}` : undefined}, 100); const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: latestPost ? `>${latestPost.id}` : undefined}, 100);
this.addPost(posts); this.addPost(posts);
return this; return posts;
} }
async getPosts() { async getPosts() {
const oldestPost = this.sortedPosts.at(-1); const oldestPost = this.sortedPosts.at(-1);
const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: oldestPost ? `<${oldestPost.id}` : undefined}, 100); const posts = await Post.fetchMultiple(Booru.used, {tags: this.tags, id: oldestPost ? `<${oldestPost.id}` : undefined}, 100);
this.addPost(posts); this.addPost(posts);
return this; return posts;
} }
get sortedPosts() { return this.posts.array.sort((a, b) => +b.createdDate - +a.createdDate); } get sortedPosts() { return this.posts.array.sort((a, b) => +b.createdDate - +a.createdDate); }

View File

@ -46,14 +46,14 @@ export class $PostTile extends $Container {
.once('load', (e, $img) => { .once('load', (e, $img) => {
$img $img
.src(this.post.previewURL) .src(this.post.previewURL)
.on(['mouseenter', 'touchstart'], () => { if (this.post.isGif) { $img.src(this.post.large_file_url) } }) .on(['mouseenter', 'touchstart'], () => { if (this.post.isGif) { $img.src(this.post.large_file_url) } }, {passive: true})
.on(['mouseleave', 'touchend', 'touchcancel'], () => { if (this.post.isGif) { $img.src(this.post.previewURL) } }) .on(['mouseleave', 'touchend', 'touchcancel'], () => { if (this.post.isGif) { $img.src(this.post.previewURL) } }, {passive: true})
.animate({opacity: [0, 1]}, {duration: 300, fill: 'both'}); .animate({opacity: [0, 1]}, {duration: 300, fill: 'both'});
this.removeClass('loading'); this.removeClass('loading');
}) })
]) ])
.on(['mouseenter', 'touchstart'], () => { if (!this.$video?.isPlaying) { this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined) } }) .on(['mouseenter', 'touchstart'], () => { if (!this.$video?.isPlaying) { this.$video?.src(this.post.large_file_url).hide(false).play().catch(err => undefined) } }, {passive: true})
.on(['mouseleave', 'touchend', 'touchcancel'], () => { this.$video?.pause().currentTime(0).hide(true); }) .on(['mouseleave', 'touchend', 'touchcancel'], () => { this.$video?.pause().currentTime(0).hide(true); }, {passive: true})
]) ])
} }

View File

@ -1,6 +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";
export class $Searchbar extends $Container { export class $Searchbar extends $Container {
$tagInput = new $TagInput(this); $tagInput = new $TagInput(this);
@ -128,21 +130,31 @@ export class $Searchbar extends $Container {
async getSearchSuggestions() { async getSearchSuggestions() {
const input = this.$tagInput.$input.value() const input = this.$tagInput.$input.value()
if (!input.length) return this.$selectionList.clearSelections(); if (!input.length) return this.$selectionList.clearSelections();
const tags = await Tag.fetchMultiple(Booru.used, {fuzzy_name_matches: input, order: 'similarity'}); const results = await Autocomplete.fetch(Booru.used, input, 20);
this.$selectionList this.$selectionList
.clearSelections() .clearSelections()
.addSelections(tags.map(tag => new $Selection().value(tag.name) .addSelections(results.map(data => new $Selection().value(data.value)
.content([ .content([
$('span').class('tag-name').content(tag.name), $('div').class('selection-label').content([
$('span').class('tag-category').content(TagCategory[tag.category]) data.isTagAntecedent() ? $('span').class('tag-antecedent').self($span => $span.dom.innerHTML = data.antecedent.replaceAll(input, `<b>${input}</b>`)) : 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, `<b>${input}</b>`))
])
]),
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-category').content(TagCategory[data.category])
]) : null,
data.isUser() ? $('span').class('user-level').content(data.level) : null
]) ])
.on('click', () => {this.$tagInput.addTag(tag.name).input()}) .on('click', () => {this.$tagInput.addTag(data.label).input()})
)) ))
if (!this.$tagInput.$input.value().length) this.$selectionList.clearSelections(); if (!this.$tagInput.$input.value().length) this.$selectionList.clearSelections();
} }
search() { search() {
$.replace(`/posts?tags=${this.$tagInput.query}`); $.replace(`/posts?tags=${this.$tagInput.query.replace(':', '%3A')}`);
this.$tagInput.clearAll(); this.$tagInput.clearAll();
this.inactivate(); this.inactivate();
return this; return this;
@ -177,10 +189,12 @@ class $SelectionList extends $Container {
return this; return this;
} }
focusSelection(selection: $Selection) { focusSelection($selection: $Selection) {
this.blurSelection(); this.blurSelection();
this.focused = selection; this.focused = $selection;
selection.focus(); $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; return this;
} }

View File

@ -81,6 +81,7 @@ searchbar {
align-items: center; align-items: center;
padding: 0.4rem 1rem; padding: 0.4rem 1rem;
cursor: pointer; cursor: pointer;
gap: 1rem;
&:hover { &:hover {
background-color: color-mix(in srgb, var(--background-color-lighter) 50%, transparent); background-color: color-mix(in srgb, var(--background-color-lighter) 50%, transparent);
@ -88,9 +89,30 @@ searchbar {
&.active { &.active {
background-color: var(--background-color-lighter); background-color: var(--background-color-lighter);
} }
.tag-name { 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;
}
}
} }
.tag-category {
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; padding: 0.1rem 0.4rem;
border-radius: 0.4rem; border-radius: 0.4rem;
font-size: 0.9rem; font-size: 0.9rem;

View File

@ -81,7 +81,7 @@ $(document.body).content([
switch ($Router.navigationDirection) { switch ($Router.navigationDirection) {
case $RouterNavigationDirection.Forward: return [`translateX(${TX}%)`, `translateX(0%)`]; case $RouterNavigationDirection.Forward: return [`translateX(${TX}%)`, `translateX(0%)`];
case $RouterNavigationDirection.Back: return [`translateX(-${TX}%)`, `translateX(0%)`]; case $RouterNavigationDirection.Back: return [`translateX(-${TX}%)`, `translateX(0%)`];
case $RouterNavigationDirection.Replace: return undefined; case $RouterNavigationDirection.Replace: return '';
} }
}) })
e.$view.content(e.nextContent); e.$view.content(e.nextContent);
@ -102,7 +102,7 @@ $(document.body).content([
switch ($Router.navigationDirection) { switch ($Router.navigationDirection) {
case $RouterNavigationDirection.Forward: return [`translateX(0%)`, `translateX(-${TX}%)`]; case $RouterNavigationDirection.Forward: return [`translateX(0%)`, `translateX(-${TX}%)`];
case $RouterNavigationDirection.Back: return [`translateX(0%)`, `translateX(${TX}%)`]; case $RouterNavigationDirection.Back: return [`translateX(0%)`, `translateX(${TX}%)`];
case $RouterNavigationDirection.Replace: return undefined; case $RouterNavigationDirection.Replace: return '';
} }
}) })

View File

@ -29,53 +29,60 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
$('div').class('sidebar') $('div').class('sidebar')
.self($sidebar => { .self($sidebar => {
let scrollTop = 0; let scrollTop = 0;
addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop }) addEventListener('scroll', () => { if ($sidebar.inDOM()) scrollTop = document.documentElement.scrollTop }, {passive: true})
$route $route
.on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `calc(${scrollTop}px + var(--nav-height) + var(--padding))`}) }) .on('beforeShift', () => { if (innerWidth > 800) $sidebar.css({position: `absolute`, top: `calc(${scrollTop}px + var(--nav-height) + var(--padding))`}) })
.on('afterShift', () => $sidebar.css({position: '', top: ''})) .on('afterShift', () => $sidebar.css({position: '', top: ''}))
}) })
.content([ .content([
$('section').class('post-info').content([ $('section').class('post-info').content([
new $Property('id').name('Post').value(`#${params.id}`), new $Property('id').name('Post').content(`#${params.id}`),
new $Property('uploader').name('Uploader').value(post.uploader$), new $Property('uploader').name('Uploader').content(post.uploader$),
new $Property('approver').name('Approver').value(post.approver$), new $Property('approver').name('Approver').content(post.approver$),
new $Property('date').name('Date').value(post.created_date$), new $Property('date').name('Date').content(post.created_date$),
new $Property('size').name('Size').value([post.file_size$, post.dimension$]), new $Property('size').name('Size').content([post.file_size$, post.dimension$]),
new $Property('file').name('File Type').value(post.file_ext$), new $Property('file-type').name('File Type').content(post.file_ext$),
$('div').class('inline').content([ $('div').class('inline').content([
new $Property('favorites').name('Favorites').value(post.favorites$), new $Property('favorites').name('Favorites').content(post.favorites$),
new $Property('score').name('Score').value(post.score$) new $Property('score').name('Score').content(post.score$)
]),
$('div').class('buttons').content([
$('icon-button').class('vertical').icon('link-outline').content(Booru.name$)
.on('click', (e, $button) => {
e.preventDefault();
navigator.clipboard.writeText(`${Booru.used.origin}${location.pathname}`);
$button.content('Copied!');
setTimeout(() => {
$button.content(Booru.name$)
}, 2000);
}),
$('icon-button').class('vertical').icon('link-outline').content(`File`)
.on('click', (e, $button) => {
e.preventDefault();
navigator.clipboard.writeText(post.file_url);
$button.content('Copied!');
setTimeout(() => {
$button.content('File')
}, 2000);
}),
$('icon-button').class('vertical').icon('link-outline').content(`Webm`)
.on('click', (e, $button) => {
e.preventDefault();
navigator.clipboard.writeText(post.previewURL);
$button.content('Copied!');
setTimeout(() => {
$button.content('Webm')
}, 2000);
})
.hide(true).self(async ($button) => { await post.ready; if (post.file_ext === 'zip') $button.hide(false) })
]), ]),
new $Property('file-url').name('File').content($('a').href(post.file_url$).content(post.file_url$.convert((value) => value.replace('https://', ''))).target('_blank')),
new $Property('source-url').name('Source').content($('a').href(post.source$).content(post.source$.convert((value) => value.replace('https://', ''))).target('_blank')),
new $Property('booru-url').name(Booru.name$).content($('a').href(post.url$).content(post.url$.convert((value) => value.replace('https://', ''))).target('_blank')),
new $Property('booru-url').name('Webm').hide(true).self(async ($property) => {
await post.ready;
if (post.isUgoria) $property.content($('a').href(post.webm_url$).content(post.webm_url$.convert((value) => value.replace('https://', ''))).target('_blank')).hide(false);
}),
// $('div').class('buttons').content([
// $('icon-button').class('vertical').icon('link-outline').content(Booru.name$)
// .on('click', (e, $button) => {
// e.preventDefault();
// navigator.clipboard.writeText(`${Booru.used.origin}${location.pathname}`);
// $button.content('Copied!');
// setTimeout(() => {
// $button.content(Booru.name$)
// }, 2000);
// }),
// $('icon-button').class('vertical').icon('link-outline').content(`File`)
// .on('click', (e, $button) => {
// e.preventDefault();
// navigator.clipboard.writeText(post.file_url);
// $button.content('Copied!');
// setTimeout(() => {
// $button.content('File')
// }, 2000);
// }),
// $('icon-button').class('vertical').icon('link-outline').content(`Webm`)
// .on('click', (e, $button) => {
// e.preventDefault();
// navigator.clipboard.writeText(post.previewURL);
// $button.content('Copied!');
// setTimeout(() => {
// $button.content('Webm')
// }, 2000);
// })
// .hide(true).self(async ($button) => { await post.ready; if (post.file_ext === 'zip') $button.hide(false) })
// ]),
]), ]),
$('div').class('post-tags').content(async $tags => { $('div').class('post-tags').content(async $tags => {
const tags = await post.fetchTags(); const tags = await post.fetchTags();
@ -116,9 +123,9 @@ class $Property extends $Container {
constructor(id: string) { constructor(id: string) {
super('div'); super('div');
this.staticClass('property').attribute('property-id', id); this.staticClass('property').attribute('property-id', id);
this.content([ super.content([
this.$name, this.$name,
this.$values this.$values.hide(true)
]) ])
} }
@ -127,7 +134,8 @@ class $Property extends $Container {
return this; return this;
} }
value(content: OrMatrix<$ContainerContentType>) { content(content: OrMatrix<$ContainerContentType>) {
this.$values.hide(false);
const list = $.orArrayResolve(content); const list = $.orArrayResolve(content);
this.$values.content(list.map($item => $('span').staticClass('property-value').content($item))); this.$values.content(list.map($item => $('span').staticClass('property-value').content($item)));
return this; return this;

View File

@ -129,14 +129,22 @@
display: flex; display: flex;
gap: 0.6rem; gap: 0.6rem;
align-items: center; align-items: center;
width: 100%;
span.property-name {
flex-shrink: 0;
}
div.property-values { div.property-values {
display: flex; display: flex;
gap: 0.4rem; gap: 0.4rem;
overflow: hidden;
span.property-value { span.property-value {
background-color: var(--secondary-color-dark); background-color: var(--secondary-color-dark);
color: var(--secondary-color); color: var(--secondary-color);
padding: 2px 4px; padding: 2px 4px;
border-radius: 4px; border-radius: 4px;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
} }
} }
} }

View File

@ -0,0 +1,75 @@
import type { Booru } from "./Booru";
import type { TagCategory } from "./Tag";
import type { UserLevelString } from "./User";
export class Autocomplete {
static async fetch(booru: Booru, query: string, limit: number = 20) {
const res = await fetch(`${booru.origin}/autocomplete.json?search[query]=${query}&search[type]=tag_query&version=1&limit=${limit}`).then(res => res.json()) as AutocompleteData[];
return res.map(data => new AutocompleteResult(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);
interface AutocompleteBaseData {
type: 'user' | 'tag' | 'tag-autocorrect' | 'tag-alias' | 'tag-word';
label: string;
value: string;
}
interface AutocompleteUserData {
type: 'user';
id: number;
level: Lowercase<UserLevelString>;
}
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;
}

View File

@ -16,9 +16,13 @@ export class Post extends $EventManager<{update: []}> {
score$ = $.state(0); score$ = $.state(0);
file_size$ = $.state(LOADING_STRING); file_size$ = $.state(LOADING_STRING);
file_ext$ = $.state(LOADING_STRING); file_ext$ = $.state(LOADING_STRING);
file_url$ = $.state(LOADING_STRING);
source$ = $.state(LOADING_STRING);
dimension$ = $.state(LOADING_STRING); dimension$ = $.state(LOADING_STRING);
url$ = $.state(LOADING_STRING);
createdDate = new Date(this.created_at); createdDate = new Date(this.created_at);
ready?: Promise<this>; ready?: Promise<this>;
webm_url$ = $.state(LOADING_STRING);
booru: Booru; booru: Booru;
constructor(booru: Booru, id: id, data?: PostData) { constructor(booru: Booru, id: id, data?: PostData) {
@ -56,6 +60,7 @@ export class Post extends $EventManager<{update: []}> {
} }
const req = await fetch(`${booru.origin}/posts.json?limit=${limit}&tags=${tagsQuery}&_method=get`); const req = await fetch(`${booru.origin}/posts.json?limit=${limit}&tags=${tagsQuery}&_method=get`);
const dataArray: PostData[] = await req.json(); const dataArray: PostData[] = await req.json();
if (dataArray instanceof Array === false) return [];
const list = dataArray.map(data => { const list = dataArray.map(data => {
const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data); const instance = booru.posts.get(data.id)?.update(data) ?? new this(booru, data.id, data);
booru.posts.set(instance.id, instance); booru.posts.set(instance.id, instance);
@ -75,7 +80,11 @@ export class Post extends $EventManager<{update: []}> {
this.score$.set(this.score); this.score$.set(this.score);
this.file_size$.set(digitalUnit(this.file_size)); this.file_size$.set(digitalUnit(this.file_size));
this.file_ext$.set(this.file_ext as any); 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.dimension$.set(`${this.image_width}x${this.image_height}`);
this.url$.set(`${this.url}`);
if (this.isUgoria) this.webm_url$.set(this.large_file_url);
this.createdDate = new Date(this.created_at); this.createdDate = new Date(this.created_at);
this.fire('update'); this.fire('update');
} }
@ -102,7 +111,8 @@ export class Post extends $EventManager<{update: []}> {
const tag_list = this.tag_string.split(' '); const tag_list = this.tag_string.split(' ');
return [...this.booru.tags.values()].filter(tag => tag_list.includes(tag.name)) 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 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}` }
} }
export interface PostData extends PostOptions { export interface PostData extends PostOptions {

View File

@ -66,7 +66,7 @@ export interface UserData {
} }
export type UserLevel = 10 | 20 | 30 | 31 | 32 | 40 | 50; export type UserLevel = 10 | 20 | 30 | 31 | 32 | 40 | 50;
export type UserLevelString = "Member" | "Gold" | "Platinum" | "Admin"; export type UserLevelString = "Member" | "Gold" | "Platinum" | "Admin" | "Contributor" | "Builder" | "Approver";
export interface UserProfileData extends UserData { export interface UserProfileData extends UserData {
"last_logged_in_at": ISOString, "last_logged_in_at": ISOString,