4 Commits
v1.0.0 ... main

Author SHA1 Message Date
ad376772e9 fix broken badge
All checks were successful
release / release (push) Successful in 1m2s
2024-09-29 07:45:33 +08:00
209cd0e208 add top layout
All checks were successful
release / release (push) Successful in 36s
2024-06-14 13:30:40 +08:00
c575059570 enhance youtube gift
All checks were successful
release / release (push) Successful in 37s
2024-06-14 04:30:45 +08:00
8a8f0979fb fix release file 2024-06-12 17:24:45 +08:00
18 changed files with 63073 additions and 63 deletions

View File

@@ -23,4 +23,4 @@ jobs:
- uses: akkuman/gitea-release-action@v1
with:
files: |-
.out/**
.out/*.zip

BIN
bun.lockb

Binary file not shown.

View File

@@ -13,8 +13,11 @@
"@onecomme.com/onesdk": "^5.2.1",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2",
"@types/color": "^3.0.6",
"@types/node": "^20.14.2",
"autoprefixer": "^10.4.16",
"color": "^4.2.3",
"husky": "^8.0.0",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
"prettier": "^3.3.2",
@@ -25,7 +28,6 @@
"tailwindcss": "^3.3.6",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^5.1.4",
"husky": "^8.0.0"
"vite": "^5.1.4"
}
}

12
preview.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/preview/main.ts"></script>
</body>
</html>

View File

@@ -1,13 +1,16 @@
<script lang="ts">
import type { Comment } from '@onecomme.com/onesdk/types/Comment';
import { chats } from './chats';
import { chats } from './lib/chats';
import { type TransitionConfig } from 'svelte/transition';
import { quadOut as easing } from 'svelte/easing';
import CommentRenderer from './components/CommentRenderer.svelte';
import { url } from './lib/url';
const child = new WeakMap<Node, number>();
let container: HTMLDivElement;
let queued = false;
$: top = $url.searchParams.get('layout') === 'top';
function queue() {
if (queued) return;
function fn() {
@@ -17,7 +20,7 @@
container.childNodes.forEach((node) => {
n += child.get(node) ?? 0;
});
container.scrollTo({ top: -n });
container.scrollTo({ top: top ? n : -n });
}
requestAnimationFrame(fn);
}
@@ -35,73 +38,24 @@
}
};
}
function authorColor(comment: Comment) {
if (comment.service === 'youtube') {
if (comment.data.isModerator) return 'text-blue-300';
if (comment.data.isMember) return 'text-green-300';
}
return 'text-zinc-200';
}
</script>
<div
class="main flex flex-col-reverse h-screen w-screen overflow-hidden px-3 py-2"
class="flex h-screen w-screen overflow-hidden px-3 py-2"
class:bottom={!top}
class:top
bind:this={container}
>
{#each [...$chats].reverse() as chat (`${chat.service}/${chat.data.id}`)}
{@const author = chat.data.displayName ?? chat.data.name}
<div class="flex flex-row gap-4 pt-3 pb-1 box-border origin-top-left" in:scrollEffect>
<div class="w-10 min-w-10 relative">
<img
src={chat.data.originalProfileImage}
alt={author}
class="w-10 h-10 rounded-full absolute -top-3 bg-slate-200 border-2 border-slate-400 shadow"
/>
</div>
<div
class="bubble shadow relative flex-grow flex flex-col gap-1 bg-slate-800 bg-opacity-95 text-gray-50 px-4 py-2 rounded-b-xl rounded-tr-xl"
>
<div class="text-xs font-bold gap-0.5 items-center {authorColor(chat)}">
{author}
{#each chat.data.badges as badge}
<img
class="h-3 w-3 inline-block mx-0.5"
src={badge.url}
alt={badge.label}
/>
{/each}
</div>
<div class="comment-container">
{@html chat.data.comment}
</div>
</div>
<div class="origin-top-left" in:scrollEffect>
<CommentRenderer comment={chat} />
</div>
{/each}
</div>
<style lang="postcss">
.comment-container {
:global(img) {
@apply mx-0.5 h-6 w-6;
display: inline-block;
}
}
.bubble {
&::before {
@apply -left-2 top-0 block h-2 w-2 bg-slate-800 bg-opacity-95;
content: ' ';
position: absolute;
mask-image: linear-gradient(
45deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 50%,
rgba(0, 0, 0, 1) 50%,
rgba(0, 0, 0, 1) 100%
);
}
}
.main {
.bottom {
@apply flex-col-reverse;
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
@@ -110,4 +64,14 @@
rgba(0, 0, 0, 1) 100%
);
}
.top {
@apply flex-col;
mask-image: linear-gradient(
0deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.25) 15%,
rgba(0, 0, 0, 1) 30%,
rgba(0, 0, 0, 1) 100%
);
}
</style>

View File

@@ -8,3 +8,12 @@
background-color: transparent;
}
}
@layer components {
.comment-container {
img {
@apply mx-0.5 h-6 w-6;
display: inline-block;
}
}
}

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import type { Comment } from '@onecomme.com/onesdk/types/Comment';
import { authorColor } from '../lib/utils';
export let comment: Comment;
$: author = comment.data.displayName ?? comment.data.name;
</script>
<div class="flex flex-row gap-4 pt-3 pb-1">
<div class="w-10 min-w-10 relative">
<img
src={comment.data.originalProfileImage}
alt={author}
class="w-10 h-10 rounded-full absolute -top-3 bg-slate-200 border-2 border-slate-400 shadow"
/>
</div>
<div
class="bubble shadow relative flex-grow flex flex-col gap-1 bg-slate-800 bg-opacity-95 text-gray-50 px-4 py-2 rounded-b-xl rounded-tr-xl"
>
<div class="text-xs font-bold gap-0.5 items-center {authorColor(comment)}">
{author}
{#each comment.data.badges as badge}
{#if badge.url}
<img class="h-3 w-3 inline-block mx-0.5" src={badge.url} alt={badge.label} />
{/if}
{/each}
</div>
<div class="comment-container">
{@html comment.data.comment}
</div>
</div>
</div>
<style lang="postcss">
.bubble {
&::before {
@apply -left-2 top-0 block h-2 w-2 bg-inherit;
content: ' ';
position: absolute;
mask-image: linear-gradient(
45deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 50%,
rgba(0, 0, 0, 1) 50%,
rgba(0, 0, 0, 1) 100%
);
}
}
</style>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { Comment } from '@onecomme.com/onesdk/types/Comment';
import BaseComment from './BaseComment.svelte';
import ReceivedMember from './youtube/ReceivedMember.svelte';
import Gift from './youtube/Gift.svelte';
export let comment: Comment;
function determineComponent(comment: Comment) {
if (comment.service === 'youtube') {
if (comment.data.hasGift) {
switch (comment.data.giftType) {
case 'giftreceived':
return ReceivedMember;
default:
return Gift;
}
}
}
return BaseComment;
}
</script>
<svelte:component this={determineComponent(comment)} {comment} />

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import type { YouTubeComment } from '@onecomme.com/onesdk/types/Comment';
import { authorColor } from '../../lib/utils';
import Color from 'color';
export let comment: YouTubeComment;
$: author = comment.data.displayName ?? comment.data.name;
function bgColor(comment: YouTubeComment) {
const color = Color(comment.data.colors?.bodyBackgroundColor)
.darken(0.6)
.saturate(0.4)
.alpha(0.95);
return color.rgb();
}
</script>
<div class="flex flex-row gap-4 pt-3 pb-1" style="--sc-bg-color: {bgColor(comment)}">
<div class="w-10 min-w-10 relative">
<img
src={comment.data.originalProfileImage}
alt={author}
class="w-10 h-10 rounded-full absolute -top-3 bg-slate-200 border-2 border-slate-400 shadow"
/>
</div>
<div
class="bubble shadow relative flex-grow flex flex-col gap-1 bg-slate-800 bg-opacity-95 text-gray-50 px-4 py-2 rounded-b-xl rounded-tr-xl"
>
<div class="flex flex-row items-center gap-1">
<div class="text-xs font-bold items-center {authorColor(comment)}">
{author}
{#each comment.data.badges as badge}
<img class="h-3 w-3 inline-block mx-0.5" src={badge.url} alt={badge.label} />
{/each}
</div>
{#if comment.data.giftType === 'milestonechat'}
<div class="text-xs font-bold ml-auto">
{comment.data.membership?.primary}
</div>
{:else if comment.data.paidText}
<div class="text-xs font-bold ml-auto">
{comment.data.paidText}
</div>
{/if}
</div>
{#if comment.data.giftType === 'supersticker'}
{@html comment.data.comment}
{:else if comment.data.giftType === 'sponsorgift'}
<div class="text-sm font-semibold italic">
{@html comment.data.comment}
</div>
{:else if comment.data.comment}
<div class="comment-container">
{@html comment.data.comment}
</div>
{/if}
</div>
</div>
<style lang="postcss">
.bubble {
background-color: var(--sc-bg-color);
&::before {
@apply -left-2 top-0 block h-2 w-2 bg-inherit;
content: ' ';
position: absolute;
mask-image: linear-gradient(
45deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 50%,
rgba(0, 0, 0, 1) 50%,
rgba(0, 0, 0, 1) 100%
);
}
}
</style>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import type { YouTubeComment } from '@onecomme.com/onesdk/types/Comment';
export let comment: YouTubeComment;
$: name = comment.data.displayName ?? comment.data.name;
</script>
<div class="text-center text-white font-semibold shadow-slate-600 py-1 text-sm">
<span class="text-green-300">{name}</span>
{comment.data.comment.replace(comment.data.name, '')}
</div>
<style lang="postcss">
div {
text-shadow:
var(--tw-shadow-color) 2px 0px 0px,
var(--tw-shadow-color) 1.75517px 0.958851px 0px,
var(--tw-shadow-color) 1.0806px 1.68294px 0px,
var(--tw-shadow-color) 0.141474px 1.99499px 0px,
var(--tw-shadow-color) -0.832294px 1.81859px 0px,
var(--tw-shadow-color) -1.60229px 1.19694px 0px,
var(--tw-shadow-color) -1.97998px 0.28224px 0px,
var(--tw-shadow-color) -1.87291px -0.701566px 0px,
var(--tw-shadow-color) -1.30729px -1.5136px 0px,
var(--tw-shadow-color) -0.421592px -1.95506px 0px,
var(--tw-shadow-color) 0.567324px -1.91785px 0px,
var(--tw-shadow-color) 1.41734px -1.41108px 0px,
var(--tw-shadow-color) 1.92034px -0.558831px 0px;
}
</style>

3
src/lib/url.ts Normal file
View File

@@ -0,0 +1,3 @@
import { readable } from 'svelte/store';
export const url = readable(new URL(window.location.href));

10
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { Comment } from '@onecomme.com/onesdk/types/Comment';
export function authorColor(comment: Comment) {
if (comment.service === 'youtube') {
if (comment.data.isOwner) return 'text-yellow-300';
if (comment.data.isModerator) return 'text-blue-300';
if (comment.data.isMember) return 'text-green-300';
}
return 'text-zinc-200';
}

View File

@@ -1,7 +1,7 @@
import './app.pcss';
import App from './App.svelte';
import type { Comment } from '@onecomme.com/onesdk/types/Comment';
import { chats } from './chats';
import { chats } from './lib/chats';
if (import.meta.env.DEV) {
globalThis.OneSDK = (await import('@onecomme.com/onesdk')).default;

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import type { Comment } from '@onecomme.com/onesdk/types/Comment';
import CommentRenderer from '../components/CommentRenderer.svelte';
import data from './chats.json';
import { state } from './states';
$: filtered = ($state.giftOnly ? data.filter((c) => c.data.hasGift) : data) as Comment[];
$: chats = filtered.slice($state.skip, $state.skip + $state.commentLimit);
</script>
<div class="p-4">
<div class="bg-white sticky top-0 z-50 flex flex-col gap-2 py-2">
<div class="grid grid-cols-2 gap-2">
<label>
<input type="checkbox" bind:checked={$state.giftOnly} />
Gift Only
</label>
<label>
<input type="checkbox" bind:checked={$state.showJson} />
Show JSON
</label>
</div>
<label class="flex flex-row items-center gap-1">
Limit ({$state.commentLimit})
<input type="range" bind:value={$state.commentLimit} class="flex-grow" min="10" />
</label>
<label class="flex flex-row items-center gap-1">
Skip ({$state.skip})
<input
type="range"
bind:value={$state.skip}
class="flex-grow"
min={0}
max={filtered.length - $state.commentLimit}
/>
</label>
</div>
<div class="flex flex-col py-3">
{#each chats as comment}
{#if $state.showJson}
<pre class="text-xs whitespace-pre-wrap">{JSON.stringify(comment, null, 2)}</pre>
{/if}
<CommentRenderer {comment} />
{/each}
</div>
</div>

62765
src/preview/chats.json Normal file

File diff suppressed because it is too large Load Diff

8
src/preview/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import '../app.pcss';
import Preview from './Preview.svelte';
const app = new Preview({
target: document.getElementById('app')!
});
export default app;

8
src/preview/states.ts Normal file
View File

@@ -0,0 +1,8 @@
import { writable } from 'svelte/store';
export const state = writable({
giftOnly: false,
commentLimit: 30,
skip: 0,
showJson: false
});