This commit is contained in:
defaultkavy 2024-03-30 18:39:39 +08:00
commit a79c1c599c
30 changed files with 2368 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "msedge",
"request": "launch",
"name": "Launch Edge against localhost",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}"
}
]
}

34
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,34 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "server",
"type": "shell",
"command": "${workspaceFolder}/scripts/server.sh"
},
{
"label": "vite",
"type": "shell",
"command": "${workspaceFolder}/scripts/vite.sh",
"problemMatcher": []
},
{
"label": "caddy",
"type": "npm",
"script": "caddy-dev"
},
{
"label": "vite-build",
"type": "shell",
"command": "${workspaceFolder}/scripts/vite-build.sh"
},
{
"label": "dev-startup",
"dependsOn": [
"server",
"vite"
],
"problemMatcher": []
}
]
}

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# YouTube Chat Designer V1.0
A simple css editor for YouTube live chat.

BIN
bun.lockb Executable file

Binary file not shown.

49
dist/assets/index-C9J66gnR.js vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

BIN
dist/avatar.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

13
dist/index.html vendored Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube Chat Designer</title>
<script type="module" crossorigin src="/assets/index-C9J66gnR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-mL8GJvpZ.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/src/index.scss">
<title>YouTube Chat Designer</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "ytchat",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"sass": "^1.72.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"colortranslator": "^4.1.0",
"fluentx": "../fluentx",
"vite": "^5.2.6"
}
}

BIN
public/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

3
scripts/vite-build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
cd web
bunx --bun vite build

3
scripts/vite.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
cd web
bunx --bun vite --host 127.0.0.1

View File

@ -0,0 +1,30 @@
import { InputComponent } from "./InputComponent";
export class ColorInput extends InputComponent {
$color = $('input').type('color').class('color');
constructor(id: string) {
super(id);
this.addStaticClass('color');
this.$color.id(id);
this.layout();
}
layout() {
this.content([
this.$label,
$('div').content([
this.$value,
this.$color,
])
])
this.$value.on('input', e => this.$color.value(this.$value.value()))
this.$color.on('input', e => this.$value.value(this.$color.value()))
}
value(value: string | undefined) {
super.value(value);
this.$color.value(value);
return this;
}
}

View File

@ -0,0 +1,40 @@
import { $Container } from "fluentx";
export class InputComponent extends $Container {
$value = $('input').class('value');
$unit = $('span').staticClass('unit');
$label = $('label').hide(true);
constructor(id: string) {
super('div');
this.staticClass('input-component', id);
this.$value.id(id);
this.$label.for(id);
}
unit(unit: string) {
this.$unit.content(unit);
return this;
}
value(number: number | string | undefined) {
if (number === undefined) return this;
this.$value.value(number.toString());
return this;
}
label(label: string) {
if (label) this.$label.hide(false);
this.$label.content(label);
return this;
}
min(number: number) {
this.$value.min(number);
return this;
}
max(number: number) {
this.$value.max(number);
return this;
}
}

View File

@ -0,0 +1,54 @@
import { InputComponent } from "./InputComponent";
export class RangeInput extends InputComponent {
$range = $('input').type('range').class('range');
constructor(id: string) {
super(id);
this.addStaticClass('range');
this.$range.id(id);
this.$value;
this.layout();
}
layout() {
this.content([
this.$label,
$('div').content([
this.$value,
this.$range,
this.$unit
])
])
this.$range.on('input', e => {
this.$value.value(`${this.$range.value()}`)
})
this.$value.on('input', e => {
this.$range.value(this.$value.value())
})
}
value(): string;
value(value: string | undefined): this;
value(value?: string) {
if (!arguments.length) return this.value();
if (value === undefined) return this;
if (value.match(/[a-zA-Z]/)) value = value.replaceAll(/[a-zA-Z]/g, '')
super.value(value);
this.$range.value(value);
return this;
}
min(number: number) {
super.min(number);
this.$range.min(number);
return this;
}
max(number: number) {
super.max(number);
this.$range.max(number);
return this;
}
}

View File

@ -0,0 +1,31 @@
import type { $SelectContentType } from "fluentx";
import { InputComponent } from "./InputComponent";
export class SelectInput extends InputComponent {
$select = $('select');
constructor(id: string) {
super(id);
this.addStaticClass('select');
this.$select.id(id);
this.layout();
}
layout() {
this.content([
this.$label,
this.$select
])
}
add(option: $SelectContentType | OrMatrix<$SelectContentType>) {
this.$select.add(option);
return this;
}
value(value: string | undefined) {
super.value(value);
this.$select.value(value);
return this;
}
}

View File

@ -0,0 +1,86 @@
.input-component {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background-color: #ffffff15;
border-radius: 5px;
input.value {
font-family: Noto Sans Mono;
}
.unit {
display: none;
}
select {
height: 2rem;
width: 100%;
max-width: 100px;
border: none;
padding: 0.4rem 0.6rem;
background-color: #ffffff20;
border-radius: 5px;
color: white;
option {
background-color: #ffffff20;
color: black;
}
}
&.range {
& > div {
display: flex;
gap: 0.2rem;
align-items: center;
justify-content: end;
input.value {
background-color: transparent;
color: white;
border: none;
text-align: right;
padding-top: 0.2rem;
width: 2rem;
}
input.range {
appearance: none;
height: 5px;
background-color: #ffffff50;
border-radius: 10px;
outline: none;
&::-webkit-slider-thumb, &::-moz-range-thumb {
background-color: #000000;
}
}
}
}
&.color {
& > div {
display: flex;
gap: 0.2rem;
justify-content: end;
}
input.value {
background-color: transparent;
color: white;
border: none;
text-align: right;
padding-top: 0.2rem;
width: 4rem;
}
input.color {
padding: 0;
appearance: none;
border: none;
background-color: #00000000;
width: 30px;
}
}
}

944
src/data/defaultStyle.ts Normal file
View File

@ -0,0 +1,944 @@
const element = {
"fontSize": "16px",
"color": "#000000",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
}
const image = {
...element,
height: '60px',
width: '60px'
}
const style : {[key:string]:Partial<CSSStyleDeclaration>} = {
"Message": element,
"Name": element,
"Badge": element,
"Avatar": image,
"Author Area": element,
"Content Area": element,
"Outer Area": element,
}
export const defaultStyle: {
[key: string]: {[key: string]: Partial<CSSStyleDeclaration>}
} = {
"Normal": {
"Message": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#F0F0F0FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "4px",
"marginBottom": "0px",
"marginLeft": "2px",
"marginRight": "2px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Name": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#FFFFFFFF",
"backgroundColor": "#00000099",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "4px",
"paddingBottom": "4px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "block"
},
"Badge": {
"fontSize": "16px",
"color": "#000000",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Avatar": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block",
"height": "60px",
"width": "60px"
},
"Author Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "flex"
},
"Content Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "10px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Outer Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#3D3D3D80",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "10px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "8px",
"paddingBottom": "8px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "flex"
}
},
"Member": {
"Message": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#F0F0F0FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "4px",
"marginBottom": "0px",
"marginLeft": "2px",
"marginRight": "2px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Name": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#0AFFFBFF",
"backgroundColor": "#527F8099",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "4px",
"paddingBottom": "4px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "block"
},
"Badge": {
"fontSize": "16px",
"color": "#000000",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Avatar": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block",
"height": "60px",
"width": "60px"
},
"Author Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "flex"
},
"Content Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "10px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Outer Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#3D3D3D80",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "10px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "8px",
"paddingBottom": "8px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "flex"
}
},
"Moderator": {
"Message": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#F0F0F0FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "4px",
"marginBottom": "0px",
"marginLeft": "2px",
"marginRight": "2px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Name": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#B8CFFFFF",
"backgroundColor": "#2E58FF99",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "4px",
"paddingBottom": "4px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "block"
},
"Badge": {
"fontSize": "16px",
"color": "#000000",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Avatar": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block",
"height": "60px",
"width": "60px"
},
"Author Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "flex"
},
"Content Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "10px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Outer Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#3D3D3D80",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "10px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "8px",
"paddingBottom": "8px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "flex"
}
},
"Owner": {
"Message": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#F0F0F0FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "4px",
"marginBottom": "0px",
"marginLeft": "2px",
"marginRight": "2px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Name": {
"fontSize": "16px",
"fontWeight": "400",
"color": "#FFEB6BFF",
"backgroundColor": "#00000099",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "4px",
"paddingBottom": "4px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "block"
},
"Badge": {
"fontSize": "16px",
"color": "#000000",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Avatar": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block",
"height": "60px",
"width": "60px"
},
"Author Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "flex"
},
"Content Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#00000000",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "0px",
"borderTopRightRadius": "0px",
"borderBottomLeftRadius": "0px",
"borderBottomRightRadius": "0px",
"marginTop": "0px",
"marginBottom": "0px",
"marginLeft": "10px",
"marginRight": "0px",
"paddingTop": "0px",
"paddingBottom": "0px",
"paddingLeft": "0px",
"paddingRight": "0px",
"opacity": "1",
"display": "block"
},
"Outer Area": {
"fontSize": "16px",
"color": "#000000FF",
"backgroundColor": "#3D3D3D80",
"borderTopStyle": "solid",
"borderTopColor": "#000000",
"borderTopWidth": "0px",
"borderBottomStyle": "solid",
"borderBottomColor": "#000000",
"borderBottomWidth": "0px",
"borderLeftStyle": "solid",
"borderLeftColor": "#000000",
"borderLeftWidth": "0px",
"borderRightStyle": "solid",
"borderRightColor": "#000000",
"borderRightWidth": "0px",
"borderTopLeftRadius": "10px",
"borderTopRightRadius": "10px",
"borderBottomLeftRadius": "10px",
"borderBottomRightRadius": "10px",
"marginTop": "0px",
"marginBottom": "10px",
"marginLeft": "0px",
"marginRight": "0px",
"paddingTop": "8px",
"paddingBottom": "8px",
"paddingLeft": "8px",
"paddingRight": "8px",
"opacity": "1",
"display": "flex"
}
}
}
//{"Normal": style,"Member": style,"Moderator": style,"Owner": style}

44
src/data/ytchat.ts Normal file
View File

@ -0,0 +1,44 @@
export const ytchat_css = `yt-live-chat-renderer yt-live-chat-header-renderer,
yt-live-chat-renderer yt-live-chat-ticker-renderer,
yt-live-chat-renderer yt-live-chat-message-input-renderer,
yt-live-chat-renderer yt-reaction-control-panel-overlay-view-model,
yt-live-chat-viewer-engagement-message-renderer,
yt-live-chat-banner-manager,
yt-live-chat-docked-message {
display: none !important;
}
yt-live-chat-text-message-renderer {
position: relative;
overflow: hidden;
}
yt-live-chat-text-message-renderer #author-photo {
overflow: hidden;
flex-shrink: 0;
}
yt-live-chat-text-message-renderer #author-photo img {
height: 100%;
width: 100%;
}
yt-live-chat-text-message-renderer #content {
width: 100%;
}
yt-live-chat-text-message-renderer #menu {
display: none;
}
yt-live-chat-text-message-renderer #chat-badges {
display: flex !important;
align-items: center !important;
gap: 0.2rem !important;
}
yt-live-chat-text-message-renderer yt-live-chat-author-chip {
align-items: unset !important;
}
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
display: block;
height: 16px;
width: 16px;
fill: #5e84f1;
}
`

370
src/index.scss Normal file
View File

@ -0,0 +1,370 @@
@import './component/$InputComponent';
:root {
--background-color: #131313;
--font-family : system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
display: flex;
justify-content: center;
font-family: var(--font-family);
overflow-y: scroll;
background-color: var(--background-color);
color: #ffffff;
font-size: 14px;
}
html {
::-webkit-scrollbar {
background-color: var(--background-color);
width: 4px;
}
::-webkit-scrollbar-thumb {
background-color: #ffffff20;
}
::-webkit-scrollbar-button {
height: 0;
width: 0;
}
}
button {
padding: 10px;
background-color: #00000000;
border: 1px solid #ffffff20;
border-radius: 10px;
color: white;
height: 100%;
cursor: pointer;
&:hover {
background-color: #ffffff10;
}
}
app {
max-width: 1200px;
width: 100%;
h1 {
span {
font-size: 14px;
color: #ffffff90;
font-weight: 100;
letter-spacing: 0.1rem;
margin-left: 10px;
}
}
.content {
display: flex;
gap: 1rem;
.console {
width: 100%;
max-width: 60%;
label, button {
user-select: none;
}
.menu {
display: flex;
flex-direction: column;
gap: 1rem;
position: sticky;
top: 0;
background-color: #13131390;
backdrop-filter: blur(10px);
padding-block: 1rem;
.action-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
.role-list {
display: flex;
border: 1px solid #ffffff20;
border-radius: 10px;
width: fit-content;
overflow: hidden;
span {
padding: 10px;
color: #ffffffaa;
font-weight: 700;
}
& > div {
display: flex;
.role {
cursor: pointer;
display: flex;
align-items: center;
input {
display: none;
}
label {
cursor: pointer;
padding: 10px;
}
&:has(input:checked) {
background-color: #ffffff20;
}
&:hover {
background-color: #ffffff10;
}
}
}
}
.button-list {
display: flex;
height: 100%;
gap: 0.6rem;
}
}
.element-list {
display: flex;
gap: 0.1rem;
button {
flex: 1;
background-color: transparent;
white-space: nowrap;
text-overflow: ellipsis;
border: none;
font-size: 16px;
padding-block: 0.6rem 0.4rem;
padding-inline: 1rem;
cursor: pointer;
color: #ffffff66;
border-radius: 5px;
&.active {
background-color: #ffffff20;
color: white;
}
&:hover {
background-color: #ffffff10;
}
}
}
}
.style-panel {
display: flex;
flex-direction: column;
gap: 0.6rem;
padding-block: 1rem;
section {
display: flex;
flex-direction: column;
gap: 0.3rem;
h3 {
font-size: 14px;
margin-block: 0.5em;
font-weight: 200;
letter-spacing: 0.2rem;
text-transform: uppercase;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
}
& > div {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.3rem;
}
section {
display: flex;
flex-direction: column;
gap: 0.4rem;
border: 2px solid #ffffff15;
border-radius: 5px;
padding: 1rem;
h4 {
margin: 0;
font-size: 14px;
font-weight: 200;
}
& > div {
display: flex;
flex-direction: column;
background-color: #ffffff15;
border-radius: 5px;
.input-component {
background-color: #00000000;
}
}
}
}
}
}
.preview {
border: 1px solid #ffffff20;
border-radius: 10px;
padding: 1rem;
width: 400px;
height: 80%;
position: fixed;
left: calc(50vw + 150px);
display: flex;
flex-direction: column;
header {
margin-block: 0.6rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
h2 {
margin-block: 0;
}
}
ytchat {
flex: 5;
overflow-y: scroll;
padding-right: 2px;
}
.input-panel {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding-top: 1rem;
div {
display: flex;
gap: 0.4rem;
align-items: center;
}
select {
height: 2rem;
width: 100%;
max-width: 100px;
border: none;
padding: 0.4rem 0.6rem;
background-color: #ffffff20;
border-radius: 5px;
color: white;
option {
background-color: #ffffff20;
color: black;
}
}
textarea, input {
background-color: #ffffff20;
border: none;
padding: 10px;
height: 1rem;
color: white;
border-radius: 10px;
resize: none;
flex: 5;
font-family: var(--font-family);
overflow: hidden;
}
button {
}
}
}
}
}
yt-live-chat-renderer {
yt-live-chat-header-renderer,
yt-live-chat-ticker-renderer,
yt-live-chat-message-input-renderer,
yt-reaction-control-panel-overlay-view-model {
display: none !important;
}
}
yt-live-chat-text-message-renderer {
position: relative;
overflow: hidden;
&:hover {
.overlay {
display: flex;
}
}
.overlay {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
background: linear-gradient(270deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.2) 100%);
user-select: none;
pointer-events: none;
display: none;
justify-content: end;
align-items: center;
color: white;
padding-right: 10px;
box-sizing: border-box;
}
#author-photo {
overflow: hidden;
flex-shrink: 0;
img {
height: 100%;
width: 100%;
}
}
#content {
width: 100%;
#timestamp {}
yt-live-chat-author-chip {
align-items: unset !important;
#prepend-chat-badges {}
#author-name {
#chip-badges {}
}
#chat-badges {
display: flex;
align-items: center;
gap: 0.2rem;
yt-live-chat-author-badge-renderer[type="moderator"] {
display: block;
height: 16px;
width: 16px;
fill: #5e84f1;
svg {
height: 100%;
width: 100%;
}
}
}
}
#message {
}
#deleted-state {}
}
#menu {
display: none;
}
}

188
src/main.ts Normal file
View File

@ -0,0 +1,188 @@
import { $, $Button, $Select, $Textarea } from 'fluentx';
import { StylePanel } from './structure/StylePanel';
import { StyleModel } from './structure/StyleModel';
import { defaultStyle } from './data/defaultStyle';
import { YouTubeChat } from './structure/YouTubeChat';
import { $Input } from 'fluentx/lib/$Input';
import { ColorInput } from './component/ColorInput';
import { ytchat_css } from './data/ytchat';
export const ROLE_MODEL_MAP = new Map<string, Map<string, StyleModel>>();
const ROLE_LIST = ['Normal', 'Member', 'Moderator', 'Owner'];
const ELEMENT_LIST = ['Message', 'Name', 'Badge', 'Avatar', 'Author Area', 'Content Area', 'Outer Area'];
const IS_TEXT_ELEMENT = ['Message', 'Name', 'Timestamp'];
const IS_IMAGE_ELEMENT = ['Badge', 'Avatar'];
const PANEL_MAP = new Map<string, StylePanel>();
const $view = $('view');
export const $chat = new YouTubeChat().css({backgroundColor: '#131313'})
.send({name: 'Normal User', message: 'Hover mouse on the message will show the author role info.', role: 'Normal'})
.send({name: 'Member User', message: 'You can use Shift + Left Click on Role list to select multiple role!', role: 'Member'})
.send({name: 'Moderator User', message: 'yoyo', role: 'Moderator'})
.send({name: 'Owner User', message: 'Using the input panel to send message for testing.', role: 'Owner'})
init();
function init() {
ROLE_MODEL_MAP.clear();
for (const role of ROLE_LIST) {
const STYLE_MAP = new Map<string, StyleModel>()
ROLE_MODEL_MAP.set(role, STYLE_MAP)
for (const element of ELEMENT_LIST) {
const model = new StyleModel(defaultStyle[role][element]);
STYLE_MAP.set(element, model)
$chat.updateStyle(element, model, [role])
}
}
for (const element of ELEMENT_LIST) PANEL_MAP.set(element, new StylePanel(element, IS_TEXT_ELEMENT.includes(element) ? 'text' : IS_IMAGE_ELEMENT.includes(element) ? 'image' : 'element'))
}
function exportJson() {
const json = {};
for (const [role, element_model_map] of ROLE_MODEL_MAP.entries()) {
const element_json = {};
for (const [element, model]of element_model_map.entries()) {
element_json[element] = model.data
}
json[role] = element_json;
}
console.debug(json);
return json;
}
$('app').content([
$('h1').content(['YouTube Chat Designer v1.0', $('span').content('DEFAULTKAVY')]),
$('div').class('content').content([
$('div').class('console').content([
$('div').class('menu').content([
$('div').class('action-row').content([
$('div').class('role-list').content([
$('span').content('Role'),
$('div').content([
ROLE_LIST.map(id => [
$('div').class('role').content($div => [
$('input').class('role-checkbox').type('checkbox').value(id).id(id.toLowerCase()).on('input', refreshPanel),
$('label').content(id).for(id.toLowerCase())
.on('click', (e) => {
const checkboxes = $<$Input>('::.role-checkbox')
if (e.shiftKey) return;
checkboxes.forEach($input => {if ($input.id() !== id.toLowerCase()) $input.checked(false)})
})
])
])
])
]),
$('div').class('button-list').content([
$('button').content('Select All').on('click', (e, $button) => {
const $input_list = $<$Input>('::.role-checkbox');
const IS_ALL_CHECKED = !$input_list.find($input => $input.checked() === false);
$input_list
.slice(IS_ALL_CHECKED ? 1 : 0)
.forEach($input => $input.checked(!IS_ALL_CHECKED))
refreshPanel();
}),
$('button').content('Export JSON').on('click', () => exportJson()),
$('button').content('Export CSS').on('click', () => exportCSS())
]),
]),
$('div').class('element-list').content($content => [
ELEMENT_LIST.map(id => {
$view.set(id, PANEL_MAP.get(id)!)
return $('button').staticClass('element-button').content(id).on('click', (e, $button) => $view.switch(id))
.self($button => {
$view.event.on('switch', content_id => {
if (content_id !== id) $button.removeClass('active');
else $button.addClass('active')
})
})
.on('mouseenter', e => {
$chat.showHint(id)
})
})
]).on('mouseleave', e => {
$chat.hideHint();
}),
]),
$view
]),
$('div').class('preview').content([
$('header').content([
$('h2').content('YouTube Chat Preview'),
new ColorInput('ytchat-background-color').label('Background Color').value('#131313').on('input', (e, $input) => {
$chat.css({backgroundColor: $input.$color.value()})
})
]),
$chat,
$('div').class('input-panel').content([
$('div').content([
$('select').id('role-select').add([
ROLE_LIST.map(role => $('option').content(role).value(role))
]),
$('input').id('username').placeholder('User Name')
]),
$('div').content([
$('textarea').id('message-input').attribute('placeholder', 'Type here...').on('keydown', e => {
if (e.key === 'Enter') {e.preventDefault(); send();}
}),
$('button').content('Send').on('click', (e) => {
send();
})
])
])
])
])
]).self($app => document.body.append($app.dom))
load();
exportCSS();
function load() {
$view.switch('Message');
$<$Input>(':#normal')?.checked(true);
refreshPanel();
}
function refreshPanel() {
PANEL_MAP.forEach(panel => panel.layout())
}
function send() {
const message = $<$Textarea>(':#message-input')!.value().trim();
if (message === '') return;
$chat.send({
name: $<$Input>(':#username')!.value(),
message: message,
role: $<$Select>(':#role-select')!.value() as any
})
$chat.dom.scrollTop = $chat.dom.scrollHeight;
}
function exportCSS() {
let css = ytchat_css;
for (const [ROLE, MODEL_MAP] of ROLE_MODEL_MAP) {
for (const [ELEMENT, MODEL] of MODEL_MAP) {
let selector = ROLE === 'Normal' ? `yt-live-chat-text-message-renderer` : `yt-live-chat-text-message-renderer[author-type="${ROLE.toLowerCase()}"]`
switch (ELEMENT) {
case 'Message': selector += ' #message'; break;
case 'Name': selector += ' #author-name '; break;
case 'Badge': selector += ' #chat-bagdes'; break;
case 'Avatar': selector += ' #author-photo'; break;
case 'Author Area': selector += ' yt-live-chat-author-clip'; break;
case 'Content Area': selector += ' #content'; break;
case 'Outer Area': break;
}
let stylesheet = '';
for (const [key, value] of Object.entries(MODEL.data)) {
stylesheet += ` ${toCssProp(key)}: ${value} !important;\n`
}
css += `${selector} {\n${stylesheet}\n}\n\n`
}
}
console.debug(css)
function toCssProp(str: string) {
return str.replaceAll(/[A-Z]/g, $1 => `-${$1.toLowerCase()}`)
}
}

View File

@ -0,0 +1,90 @@
import { $Input } from "fluentx/lib/$Input";
import { StylePanel } from "./StylePanel";
import { ColorTranslator } from "colortranslator";
import { $Select } from "fluentx";
import { firstCap } from "./util";
export class StyleModel {
data: Partial<CSSStyleDeclaration>;
constructor(data: Partial<CSSStyleDeclaration>) {
this.data = data;
}
update(panel: StylePanel) {
const filterMultitype = (value: string | undefined, recover: any, unit = false) => {
if (value === '??' || value === undefined) {
return recover;
} else {
if (unit) return value + 'px';
return value;
}
}
const border = (dir: 'top' | 'right' | 'left' | 'bottom') => {
return {
[`border${firstCap(dir)}Style`]: filterMultitype(panel.$<$Select>(`#border-${dir}-style`)?.value(), this.data[`boder${firstCap(dir)}Style`]),
[`border${firstCap(dir)}Color`]: filterMultitype(panel.$<$Input>(`#border-${dir}-color`)?.value(), this.data[`boder${firstCap(dir)}Color`]),
[`border${firstCap(dir)}Width`]: filterMultitype(panel.$<$Input>(`#border-${dir}-width`)?.value(), this.data[`boder${firstCap(dir)}Color`], true)
}
}
const dir = (prop: 'margin' | 'padding') => {
return {
[`${prop}Top`]: filterMultitype(panel.$<$Input>(`#${prop}-top`)?.value(), this.data[`${prop}Top`], true),
[`${prop}Bottom`]: filterMultitype(panel.$<$Input>(`#${prop}-bottom`)?.value(), this.data[`${prop}Bottom`], true),
[`${prop}Left`]: filterMultitype(panel.$<$Input>(`#${prop}-left`)?.value(), this.data[`${prop}Left`], true),
[`${prop}Right`]: filterMultitype(panel.$<$Input>(`#${prop}-right`)?.value(), this.data[`${prop}Right`], true),
}
}
const data: Partial<CSSStyleDeclaration> = {
fontSize: filterMultitype(panel.$<$Input>('#font-size')?.value(), this.data.fontSize, true),
fontWeight: filterMultitype(panel.$<$Input>('#font-weight')?.value(), this.data.fontWeight),
color:
new ColorTranslator({
...new ColorTranslator(filterMultitype(panel.$<$Input>('#font-color')?.value(), this.data.color)).RGBObject,
A: filterMultitype(panel.$<$Input>('#font-color-transparent')?.value(), new ColorTranslator(this.data.color!).A)
}).HEXA,
backgroundColor:
new ColorTranslator({
...new ColorTranslator(filterMultitype(panel.$<$Input>('#background-color')?.value(), this.data.backgroundColor)).RGBObject,
A: filterMultitype(panel.$<$Input>('#background-color-transparent')?.value(), new ColorTranslator(this.data.backgroundColor!).A)
}).HEXA,
...border('top'),
...border('bottom'),
...border('left'),
...border('right'),
borderTopLeftRadius: filterMultitype(panel.$<$Input>('#border-top-left-radius')?.value(), this.data.borderTopLeftRadius, true),
borderTopRightRadius: filterMultitype(panel.$<$Input>('#border-top-right-radius')?.value(), this.data.borderTopRightRadius, true),
borderBottomLeftRadius: filterMultitype(panel.$<$Input>('#border-bottom-left-radius')?.value(), this.data.borderBottomLeftRadius, true),
borderBottomRightRadius: filterMultitype(panel.$<$Input>('#border-bottom-right-radius')?.value(), this.data.borderBottomRightRadius, true),
...dir('margin'),
...dir('padding'),
opacity: filterMultitype(panel.$<$Input>('#opacity')?.value(), this.data.opacity),
display: filterMultitype(panel.$<$Select>('#display')?.value(), this.data.display),
height: filterMultitype(panel.$<$Input>('#height')?.value(), this.data.height, true),
width: filterMultitype(panel.$<$Input>('#width')?.value(), this.data.width, true),
}
this.data = data;
return this;
}
cssObject() {
const json = {};
const convert = (passKey: string | null, data: Object) => {
for (let [key, value] of Object.entries(data)) {
key = passKey ? passKey + key.charAt(0).toUpperCase() + key.slice(1) : key;
if (value instanceof Object === false) {
if (typeof value === 'number' && key !== 'opacity') value = `${value}px`;
Object.assign(json, {[`${key}`]: value});
continue;
}
convert(key, value);
}
}
convert(null, this.data);
return json
}
css() {
}
}

152
src/structure/StylePanel.ts Normal file
View File

@ -0,0 +1,152 @@
import { $Container } from "fluentx";
import { ColorInput } from "../component/ColorInput";
import { RangeInput } from "../component/RangeInput";
import { SelectInput } from "../component/SelectInput";
import { $chat, ROLE_MODEL_MAP } from "../main";
import { ColorTranslator } from 'colortranslator';
import { $Input } from "fluentx/lib/$Input";
import { firstCap, propCap } from "./util";
export class StylePanel extends $Container {
type: StyleType;
name: string;
constructor(name: string, type: StyleType) {
super('div');
this.staticClass('style-panel');
this.type = type;
this.name = name;
this.layout();
this.on('input', e => {
this.role_model_list.forEach(([role, model]) => {
model.update(this)
$chat.updateStyle(this.name, model, [role]);
});
})
}
layout() {
if (!this.roles.length) return this.clear();
const backgroundColor = this.data.backgroundColor === '??' ? {HEX: '??', A: '??'} : new ColorTranslator(this.data.backgroundColor!);
const color = this.data.color === '??' ? {HEX: '??', A: '??'} : new ColorTranslator(this.data.color!);
this.content([
$('section').content([
$('h3').content('Properties'),
$('div').content([
new SelectInput('display').label('Display').add([
['block', 'inline', 'flex', 'none'].map(value => $('option').content(value).value(value))
]),
new RangeInput('opacity').value(this.data.opacity).unit('px').min(0).max(1).label('Opacity').self($input => {$input.$range.step(0.01); $input.$value.step(0.1)}),
])
]),
this.type === 'text' ? $('section').content([
$('h3').content('Font'),
$('div').content([
new RangeInput('font-size').value(this.data.fontSize).unit('px').min(1).label('Size'),
new RangeInput('font-weight').min(100).max(900).label('Weight').value(this.data.fontWeight).self($input => $input.$range.step(100)),
new ColorInput('font-color').value(this.data.color).label('Color'),
new RangeInput('font-color-transparent').value(color.A.toString()).unit('px').min(0).max(1).label('Transparent').self($input => {$input.$range.step(0.01); $input.$value.step(0.1)}),
])
]) : undefined,
this.type === 'image' ? $('section').content([
$('h3').content('Dimension'),
$('div').content([
new RangeInput('height').value(this.data.height).unit('px').min(1).label('Height'),
new RangeInput('width').value(this.data.width).unit('px').min(1).label('Width'),
])
]) : undefined,
$('section').content([
$('h3').content('Background'),
$('div').content([
new ColorInput('background-color').value(backgroundColor.HEX).label('Color'),
new RangeInput('background-color-transparent').value(backgroundColor.A.toString()).unit('px').min(0).max(1).label('Transparent').self($input => {$input.$range.step(0.01); $input.$value.step(0.1)}),
])
]),
$('section').content([
$('h3').content('Padding'),
$('div').content([
['left', 'top', 'right', 'bottom'].map(dir => new RangeInput(`padding-${dir}`).value(this.data[`padding${firstCap(dir)}`]).unit('px').label(firstCap(dir)))
])
]),
$('section').content([
$('h3').content('Margin'),
$('div').content([
['left', 'top', 'right', 'bottom'].map(dir => new RangeInput(`margin-${dir}`).value(this.data[`margin${firstCap(dir)}`]).unit('px').label(firstCap(dir)))
])
]),
$('section').content([
$('header').content([
$('h3').content('Border'),
$('div').content([
$('label').content('Link').for('border-link'),
$('input').id('border-link').type('checkbox').checked(true)
])
]),
$('div').content([
['left', 'top', 'right', 'bottom'].map(dir =>
$('section').content([
$('h4').content(firstCap(dir)),
$('div').content([
new RangeInput(`border-${dir}-width`).value(this.data[`border${firstCap(dir)}Width`]).unit('px').label('Width'),
new SelectInput(`border-${dir}-style`).label('Style').add([
['solid', 'dashed', 'doubled', 'dotted', 'groove', 'outset', 'inset', 'ridge', 'hidden'].map(value => $('option').value(value).content(value).id(value))
]).value(this.data[`border${firstCap(dir)}Style`]),
new ColorInput(`border-${dir}-color`).value(this.data[`border${firstCap(dir)}Color`]).label('Color')
]).on('input', (e, $div) => {
if (!$<$Input>(':#border-link')?.checked()) return;
['left', 'top', 'right', 'bottom'].forEach(d => {
if (d === dir) return;
const id = $(e.target)?.id()
if (id?.includes('width')) $<RangeInput>(`:div.border-${d}-width`)?.value($div.$<$Input>(`#border-${dir}-width`)?.value())
if (id?.includes('style')) $<SelectInput>(`:div.border-${d}-style`)?.value($div.$<$Input>(`#border-${dir}-style`)?.value())
if (id?.includes('color')) $<ColorInput>(`:div.border-${d}-color`)?.value($div.$<$Input>(`#border-${dir}-color`)?.value())
})
})
])),
]),
]),
$('section').content([
$('h3').content('Border Radius'),
['top-left', 'top-right', 'bottom-left', 'bottom-right'].map(corner =>
new RangeInput(`border-${corner}-radius`).value(this.data[`border${propCap(corner)}Radius`]).unit('px').label(`${corner.split('-').map(str => str.charAt(0).toUpperCase() + str.slice(1)).toString().replace(',', ' ')}`) )
]),
])
}
get models() {
return this.roles.map(role => ROLE_MODEL_MAP.get(role)!.get(this.name)!);
}
get role_model_list() {
return this.roles.map(role => [role, ROLE_MODEL_MAP.get(role)!.get(this.name)!] as const);
}
get data() {
if (this.roles.length > 1) {
function multidata<T extends Object>(object: T, list: T[]) {
let data = {};
for (const [key, value] of Object.entries(object)) {
data[key] = value;
for (const model of list) {
if (model[key] !== value) {
data[key] = '??';
break;
}
}
}
return data;
}
return multidata(this.models[0].data, this.models.map(model => model.data)) as Partial<CSSStyleDeclaration>
} else return this.models[0].data
}
get roles() {
return $<$Input>('::.role-checkbox').map($input => {
if ($input.checked()) return $input.value();
}).detype()
}
}
export type StyleType = 'text' | 'element' | 'image'

View File

@ -0,0 +1,42 @@
import { $Container } from "fluentx";
import { YouTubeMessage, type YouTubeMessageData } from "./YouTubeMessage";
import { StyleModel } from "./StyleModel";
import { $Input } from "fluentx/lib/$Input";
export class YouTubeChat extends $Container {
messageList = new Set<YouTubeMessage>();
constructor() {
super('ytchat')
}
send(data: YouTubeMessageData) {
const message = new YouTubeMessage(data);
this.messageList.add(message);
this.insert(message);
return this;
}
updateStyle(element: string, model: StyleModel, roles: string[]) {
this.messageList.forEach(message => {
if (roles.includes(message.data.role)) message.updateStyle(element, model);
})
}
showHint(element: string) {
this.messageList.forEach(message => {
if (this.roles.includes(message.data.role)) message.hint(element);
})
}
hideHint() {
this.messageList.forEach(message => {
message.$hint.css({display: 'none'})
})
}
get roles() {
return $<$Input>('::.role-checkbox').map($input => {
if ($input.checked()) return $input.value();
}).detype()
}
}

View File

@ -0,0 +1,101 @@
import { $Container, $Element } from "fluentx";
import { StyleModel } from "./StyleModel";
import { ROLE_MODEL_MAP } from "../main";
export interface YouTubeMessageData {
name: string;
message: string;
role: 'Normal' | 'Member' | 'Moderator' | 'Owner';
}
export class YouTubeMessage extends $Container {
data: YouTubeMessageData;
$content = $('div').id('content');
$message = $('span').id('message');
$name = $('span').id('author-name');
$author_area = $('yt-live-chat-author-chip');
$timestamp = $('span').id('timestamp')
$avatar = $('yt-img-shadow').id('author-photo');
$overlay = $('div').class('overlay');
$hint = $('div').class('hint').css({display: 'none'});
constructor(data: YouTubeMessageData) {
super('yt-live-chat-text-message-renderer')
this.data = data;
this.build();
this.init();
}
init() {
ROLE_MODEL_MAP.get(this.data.role)?.forEach((model, element) => this.updateStyle(element, model))
}
build() {
this.content([
this.$overlay.content([
`Role: ${this.data.role}`
]),
this.$hint,
this.$avatar.content([
$('img').src('/avatar.png')
]),
this.$content.content([
this.$timestamp,
this.$author_area.content([
this.$name.content(this.data.name),
$('span').id('chat-badges').content([
$('yt-live-chat-author-badge-renderer').content([
$('div').id('image').content([
$('img')
])
]),
this.data.role === 'Moderator' ?
$('yt-live-chat-author-badge-renderer').attribute('type', 'moderator').content([
$('div').id('image').self($div => {
$div.dom.innerHTML =
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" focusable="false" style="pointer-events: none; display: inherit; width: 100%; height: 100%;"><path d="M9.64589146,7.05569719 C9.83346524,6.562372 9.93617022,6.02722257 9.93617022,5.46808511 C9.93617022,3.00042984 7.93574038,1 5.46808511,1 C4.90894765,1 4.37379823,1.10270499 3.88047304,1.29027875 L6.95744681,4.36725249 L4.36725255,6.95744681 L1.29027875,3.88047305 C1.10270498,4.37379824 1,4.90894766 1,5.46808511 C1,7.93574038 3.00042984,9.93617022 5.46808511,9.93617022 C6.02722256,9.93617022 6.56237198,9.83346524 7.05569716,9.64589147 L12.4098057,15 L15,12.4098057 L9.64589146,7.05569719 Z"></path></svg>`
})
]) : undefined
]),
]),
this.$message.content(this.data.message),
$('span').id('deleted-state')
])
])
}
updateStyle(element: string, model: StyleModel) {
switch (element) {
case 'Message': this.$message.css(model.data); break;
case 'Name': this.$name.css(model.data); break;
case 'Avatar': this.$avatar.css(model.data); break;
case 'Content Area': this.$content.css(model.data); break;
case 'Author Area': this.$author_area.css(model.data); break;
case 'Outer Area': this.css(model.data); break;
}
}
hint(element: string) {
switch (element) {
case 'Message': this.hintPosition(this.$message); break;
case 'Name': this.hintPosition(this.$name); break;
case 'Avatar': this.hintPosition(this.$avatar); break;
case 'Content Area': this.hintPosition(this.$content); break;
case 'Author Area': this.hintPosition(this.$author_area); break;
case 'Outer Area': this.hintPosition(this); break;
}
}
private hintPosition($ele: $Element) {
const rect = $ele.dom.getBoundingClientRect();
const this_rect = this.dom.getBoundingClientRect();
this.$hint.css({
position: 'absolute',
top: `${rect.top - this_rect.top}px`,
left: `${rect.left - this_rect.left}px`,
height: `${rect.height}px`,
width: `${rect.width}px`,
backgroundColor: '#ff000030',
display: 'block'
})
}
}

7
src/structure/util.ts Normal file
View File

@ -0,0 +1,7 @@
export function firstCap(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function propCap(str: string) {
return str.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).toString().replaceAll(',', '');
}

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"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,
"experimentalDecorators": true
},
"exclude": ["dist"]
}

8
vite.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'es2022'
},
})