v0.6.0
new: post favorite. new: User.favorites Map. new: ClientUser.init(), fetch client user fav data on start up.
This commit is contained in:
parent
a58e5d4b95
commit
5605aa94d2
1
dist/assets/index-8i15BK3r.js
vendored
1
dist/assets/index-8i15BK3r.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-B0cSv4EX.js
vendored
Normal file
1
dist/assets/index-B0cSv4EX.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-C3nfTvuP.css
vendored
1
dist/assets/index-C3nfTvuP.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-GbkvssuE.css
vendored
Normal file
1
dist/assets/index-GbkvssuE.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -16,8 +16,8 @@
|
||||
|
||||
gtag('config', 'G-59HBGP98WR');
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-8i15BK3r.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C3nfTvuP.css">
|
||||
<script type="module" crossorigin src="/assets/index-B0cSv4EX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-GbkvssuE.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "danbooru-viewer",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.0",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"vite": "^5.4.8"
|
||||
@ -13,7 +13,11 @@
|
||||
"dependencies": {
|
||||
"@elexis/layout": "../elexis-ext/layout",
|
||||
"@elexis/router": "../elexis-ext/router",
|
||||
"@elysiajs/cors": "^1.1.1",
|
||||
"@elysiajs/eden": "^1.1.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"elexis": "../elexis",
|
||||
"elysia": "^1.1.20",
|
||||
"sass": "^1.77.1"
|
||||
}
|
||||
}
|
20
server.ts
Normal file
20
server.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import cors from "@elysiajs/cors";
|
||||
import Elysia from "elysia";
|
||||
const app = new Elysia()
|
||||
.use(cors())
|
||||
.get('*', async ({path}) => {
|
||||
return Bun.file('./dist/index.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);
|
||||
console.debug(data)
|
||||
return data
|
||||
})
|
||||
})
|
||||
.listen(3030);
|
||||
console.log('Start listening: 3030')
|
||||
export type Server = typeof app;
|
@ -14,4 +14,9 @@ export class $IonIcon extends $Container {
|
||||
this.attribute('size', size);
|
||||
return this;
|
||||
}
|
||||
|
||||
link(url: string, replace = false) {
|
||||
this.on('click', () => replace ? $.replace(url) : $.open(url));
|
||||
return this;
|
||||
}
|
||||
}
|
@ -5,17 +5,47 @@ import { ArtistCommentary } from "../../structure/Commentary";
|
||||
import { Booru } from "../../structure/Booru";
|
||||
import type { $IonIcon } from "../../component/IonIcon/$IonIcon";
|
||||
import { numberFormat } from "../../modules";
|
||||
import { ClientUser } from "../../structure/ClientUser";
|
||||
|
||||
export const post_route = $('route').path('/posts/:id').id('post').builder(({$route, params}) => {
|
||||
if (!Number(params.id)) return $('h1').content('404: POST NOT FOUND');
|
||||
const post = Post.get(Booru.used, +params.id);
|
||||
const $viewerPanel =
|
||||
$('div').class('viewer-panel').content([
|
||||
$('div').class('panel').content([
|
||||
$('ion-icon').name('heart-outline').self($heart => {
|
||||
ClientUser.events.on('favoriteUpdate', (user) => {
|
||||
if (user.favorites.has(post.id)) $heart.name('heart');
|
||||
else $heart.name('heart-outline');
|
||||
})
|
||||
if (Booru.used.user?.favorites.has(post.id)) $heart.name('heart');
|
||||
}).on('click', () => {
|
||||
if (Booru.used.user?.favorites.has(post.id)) post.deleteFavorite();
|
||||
else post.createFavorite();
|
||||
})
|
||||
]),
|
||||
$('div').class('overlay')
|
||||
]).hide(true);
|
||||
return [
|
||||
$('div').class('viewer').content(async () => {
|
||||
await post.ready;
|
||||
return post.isVideo
|
||||
return [
|
||||
$viewerPanel,
|
||||
post.isVideo
|
||||
? $('video').height(post.image_height).width(post.image_width).src(post.file_ext === 'zip' ? post.large_file_url : post.file_url).controls(true).autoplay(true).loop(true).disablePictureInPicture(true)
|
||||
: $('img').src(post.large_file_url)//.once('load', (e, $img) => { $img.src(post.file_url)})
|
||||
}),
|
||||
]
|
||||
})
|
||||
.on('pointermove', (e) => {
|
||||
if (e.pointerType === 'mouse' || e.pointerType === 'pen') $viewerPanel.hide(false);
|
||||
})
|
||||
.on('pointerup', (e) => {
|
||||
console.debug(e.movementX)
|
||||
if (e.pointerType === 'touch') $viewerPanel.hide(!$viewerPanel.hide());
|
||||
})
|
||||
.on('mouseleave', () => {
|
||||
$viewerPanel.hide(true);
|
||||
}),
|
||||
$('div').class('content').content([
|
||||
$('h3').content(`Artist's Commentary`),
|
||||
$('section').class('commentary').content(async ($comentary) => {
|
||||
@ -45,7 +75,7 @@ export const post_route = $('route').path('/posts/:id').id('post').builder(({$ro
|
||||
new $Property('size').name('Size').content([post.file_size$, post.dimension$]),
|
||||
new $Property('file-type').name('File Type').content(post.file_ext$),
|
||||
$('div').class('inline').content([
|
||||
new $Property('favorites').name('Favorites').content(post.favorites$),
|
||||
new $Property('favorites').name('Favorites').content(post.favcount$),
|
||||
new $Property('score').name('Score').content(post.score$)
|
||||
]),
|
||||
new $Property('file-url').name('File').content([
|
||||
|
@ -17,6 +17,7 @@
|
||||
overflow: hidden;
|
||||
width: calc(100vw - 300px - 4rem);
|
||||
margin: 1rem;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
width: 100%;
|
||||
@ -37,6 +38,32 @@
|
||||
-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;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -3,6 +3,7 @@ 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;
|
||||
@ -18,6 +19,7 @@ export class Booru {
|
||||
posts = new Map<id, Post>();
|
||||
tags = new Map<id, Tag>();
|
||||
users = new Map<id, User>();
|
||||
favorites = new Map<id, Favorite>();
|
||||
constructor(options: BooruOptions) {
|
||||
Object.assign(this, options);
|
||||
if (this.origin.endsWith('/')) this.origin = this.origin.slice(0, -1);
|
||||
@ -37,16 +39,20 @@ export class Booru {
|
||||
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<T>(endpoint: string) {
|
||||
async fetch<T>(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}`).then(res => res.json()) as any;
|
||||
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<ClientUserData>(`/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;
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
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<ClientUserEventMap>()
|
||||
constructor(booru: Booru, apiKey: string, data: ClientUserData) {
|
||||
super(booru, data, false);
|
||||
this.apiKey = apiKey;
|
||||
@ -18,6 +22,19 @@ export class ClientUser extends User {
|
||||
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)) }
|
||||
}
|
||||
@ -68,4 +85,8 @@ export interface ClientUserData extends UserData {
|
||||
export interface ClientUserStoreData {
|
||||
username: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface ClientUserEventMap {
|
||||
favoriteUpdate: [user: ClientUser]
|
||||
}
|
32
src/structure/Favorite.ts
Normal file
32
src/structure/Favorite.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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<FavoriteData[]>(`/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;
|
||||
}
|
@ -3,6 +3,8 @@ import { Booru } from "./Booru";
|
||||
import { Tag } from "./Tag";
|
||||
import { User } from "./User";
|
||||
import { dateFrom, digitalUnit } from "./Util";
|
||||
import { ClientUser } from "./ClientUser";
|
||||
import type { FavoriteData } from "./Favorite";
|
||||
|
||||
const LOADING_STRING = '...'
|
||||
|
||||
@ -12,7 +14,7 @@ export class Post extends $EventManager<{update: []}> {
|
||||
uploader$ = $.state(LOADING_STRING);
|
||||
approver$ = $.state(LOADING_STRING);
|
||||
created_date$ = $.state(LOADING_STRING);
|
||||
favorites$ = $.state(0);
|
||||
favcount$ = $.state(0);
|
||||
score$ = $.state(0);
|
||||
file_size$ = $.state(LOADING_STRING);
|
||||
file_ext$ = $.state(LOADING_STRING);
|
||||
@ -75,7 +77,7 @@ export class Post extends $EventManager<{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.favorites$.set(this.fav_count);
|
||||
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);
|
||||
@ -99,6 +101,26 @@ export class Post extends $EventManager<{update: []}> {
|
||||
return await Tag.fetchMultiple(this.booru, {name: {_space: this.tag_string}});
|
||||
}
|
||||
|
||||
async createFavorite() {
|
||||
if (!this.booru.user) return;
|
||||
const data = await this.booru.fetch<Post>(`/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 }
|
||||
|
@ -8,6 +8,7 @@ export class User {
|
||||
level$ = $.state(10);
|
||||
level_string$ = $.state('...');
|
||||
booru: Booru;
|
||||
favorites = new Set<id>();
|
||||
constructor(booru: Booru, data: UserData, update$: boolean = true) {
|
||||
this.booru = booru;
|
||||
Object.assign(this, data);
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { defineConfig } from 'vite';
|
||||
export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3030',
|
||||
changeOrigin: true
|
||||
},
|
||||
}
|
||||
},
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user