add vrm
1
.github/workflows/cf-pages.yml
vendored
@@ -31,4 +31,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
wranglerVersion: '4'
|
||||||
command: pages deploy dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
command: pages deploy dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||||
|
|||||||
32
lexicons/ai.syui.vrm.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$type": "com.atproto.lexicon.schema",
|
||||||
|
"lexicon": 1,
|
||||||
|
"id": "ai.syui.vrm",
|
||||||
|
"defs": {
|
||||||
|
"main": {
|
||||||
|
"type": "record",
|
||||||
|
"key": "literal:self",
|
||||||
|
"description": "VRM game score collection",
|
||||||
|
"record": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["item", "createdAt", "updatedAt"],
|
||||||
|
"properties": {
|
||||||
|
"item": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "cp", "cid"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer", "description": "Crown type: 1=gold, 2=silver, 3=bronze" },
|
||||||
|
"cp": { "type": "integer", "description": "Score at acquisition" },
|
||||||
|
"cid": { "type": "string", "description": "Unique instance ID" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createdAt": { "type": "string", "format": "datetime" },
|
||||||
|
"updatedAt": { "type": "string", "format": "datetime" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,23 +72,6 @@
|
|||||||
"45yvfu6vqnmv4",
|
"45yvfu6vqnmv4",
|
||||||
"3r7eq4inllheg",
|
"3r7eq4inllheg",
|
||||||
"3pxakq4gd75sv",
|
"3pxakq4gd75sv",
|
||||||
"3mg3rw27axw3b",
|
|
||||||
"3mg3rq6ij6437",
|
|
||||||
"3mg3rhouk6f35",
|
|
||||||
"3mg3rapmgsl33",
|
|
||||||
"3mg3r6s5p7z2z",
|
|
||||||
"3mg3r3gri7r2x",
|
|
||||||
"3mg3qqz3dot2v",
|
|
||||||
"3mg3qpeskxq2t",
|
|
||||||
"3mg3qfakfy62r",
|
|
||||||
"3mg3qckhoty2p",
|
|
||||||
"3mg3q7kmfuh2n",
|
|
||||||
"3mg3q6rk64a2l",
|
|
||||||
"3mg3phljj7a2j",
|
|
||||||
"3mg3pcyma2q2h",
|
|
||||||
"3mg3pc6z6ul2f",
|
|
||||||
"3mg3opcvv2m2d",
|
|
||||||
"3mg3fsxyzmc23",
|
|
||||||
"3mftdjlirgw2j",
|
"3mftdjlirgw2j",
|
||||||
"3mftdemshj52h",
|
"3mftdemshj52h",
|
||||||
"3mftdbillyb2f",
|
"3mftdbillyb2f",
|
||||||
@@ -138,11 +121,21 @@
|
|||||||
"2ks46vomw4s2i",
|
"2ks46vomw4s2i",
|
||||||
"2ivbc5b4um5bu",
|
"2ivbc5b4um5bu",
|
||||||
"255k3bskheo6j",
|
"255k3bskheo6j",
|
||||||
"3mg3gbuc2al25",
|
"3mg3fsxyzmc23",
|
||||||
"3mg3gdhs3rx27",
|
"3mg3opcvv2m2d",
|
||||||
"3mg3gey7tos2b",
|
"3mg3pc6z6ul2f",
|
||||||
"3mg3ntffa7z23",
|
"3mg3pcyma2q2h",
|
||||||
"3mg3nwdq3px25",
|
"3mg3phljj7a2j",
|
||||||
"3mg3nwu2d7l27",
|
"3mg3q6rk64a2l",
|
||||||
"3mg3nzxv5cz2b"
|
"3mg3q7kmfuh2n",
|
||||||
|
"3mg3qckhoty2p",
|
||||||
|
"3mg3qfakfy62r",
|
||||||
|
"3mg3qpeskxq2t",
|
||||||
|
"3mg3qqz3dot2v",
|
||||||
|
"3mg3r3gri7r2x",
|
||||||
|
"3mg3r6s5p7z2z",
|
||||||
|
"3mg3rapmgsl33",
|
||||||
|
"3mg3rhouk6f35",
|
||||||
|
"3mg3rq6ij6437",
|
||||||
|
"3mg3rw27axw3b"
|
||||||
]
|
]
|
||||||
@@ -122,13 +122,6 @@
|
|||||||
"3mftdemsfzl2g",
|
"3mftdemsfzl2g",
|
||||||
"3mftdjliqoc2i",
|
"3mftdjliqoc2i",
|
||||||
"3mg3fsxyyss22",
|
"3mg3fsxyyss22",
|
||||||
"3mg3gbubzqr24",
|
|
||||||
"3mg3gdhs3d326",
|
|
||||||
"3mg3gey7t322a",
|
|
||||||
"3mg3ntff62p22",
|
|
||||||
"3mg3nwdq3a524",
|
|
||||||
"3mg3nwu2cu726",
|
|
||||||
"3mg3nzxv3ro2a",
|
|
||||||
"3mg3opcvul42c",
|
"3mg3opcvul42c",
|
||||||
"3mg3pc6z5zp2e",
|
"3mg3pc6z5zp2e",
|
||||||
"3mg3pcym7pc2g",
|
"3mg3pcym7pc2g",
|
||||||
|
|||||||
1
public/icon/crown.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M345 151.2C354.2 143.9 360 132.6 360 120C360 97.9 342.1 80 320 80C297.9 80 280 97.9 280 120C280 132.6 285.9 143.9 295 151.2L226.6 258.8C216.6 274.5 195.3 278.4 180.4 267.2L120.9 222.7C125.4 216.3 128 208.4 128 200C128 177.9 110.1 160 88 160C65.9 160 48 177.9 48 200C48 221.8 65.5 239.6 87.2 240L119.8 457.5C124.5 488.8 151.4 512 183.1 512L456.9 512C488.6 512 515.5 488.8 520.2 457.5L552.8 240C574.5 239.6 592 221.8 592 200C592 177.9 574.1 160 552 160C529.9 160 512 177.9 512 200C512 208.4 514.6 216.3 519.1 222.7L459.7 267.3C444.8 278.5 423.5 274.6 413.5 258.9L345 151.2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 802 B |
1
public/icon/play.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320zM252.3 211.1C244.7 215.3 240 223.4 240 232L240 408C240 416.7 244.7 424.7 252.3 428.9C259.9 433.1 269.1 433 276.6 428.4L420.6 340.4C427.7 336 432.1 328.3 432.1 319.9C432.1 311.5 427.7 303.8 420.6 299.4L276.6 211.4C269.2 206.9 259.9 206.7 252.3 210.9z"/></svg>
|
||||||
|
After Width: | Height: | Size: 594 B |
19
public/service/ai.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||||
|
<path fill-rule="evenodd" fill="#F6E717" d="
|
||||||
|
M 619,232
|
||||||
|
L 512,7
|
||||||
|
L 405,232
|
||||||
|
A 300,300 0 0,0 216,559
|
||||||
|
L 75,765
|
||||||
|
L 323,745
|
||||||
|
A 300,300 0 0,0 701,745
|
||||||
|
L 949,765
|
||||||
|
L 808,559
|
||||||
|
A 300,300 0 0,0 619,232
|
||||||
|
Z
|
||||||
|
M 512,337
|
||||||
|
A 175,175 0 0,0 512,687
|
||||||
|
A 175,175 0 0,0 512,337
|
||||||
|
Z
|
||||||
|
"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 413 B |
BIN
public/service/ai.syui.app.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 45 KiB |
BIN
public/service/ai.syui.gpt.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/service/ai.syui.log.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
public/service/ai.syui.os.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/service/ai.syui.shell.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/service/ai.syui.ue.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/service/ai.syui.vrm.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
public/service/ai.syui.vrma.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
19
public/service/ai_black.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||||
|
<path fill-rule="evenodd" fill="#000000" d="
|
||||||
|
M 619,232
|
||||||
|
L 512,7
|
||||||
|
L 405,232
|
||||||
|
A 300,300 0 0,0 216,559
|
||||||
|
L 75,765
|
||||||
|
L 323,745
|
||||||
|
A 300,300 0 0,0 701,745
|
||||||
|
L 949,765
|
||||||
|
L 808,559
|
||||||
|
A 300,300 0 0,0 619,232
|
||||||
|
Z
|
||||||
|
M 512,337
|
||||||
|
A 175,175 0 0,0 512,687
|
||||||
|
A 175,175 0 0,0 512,337
|
||||||
|
Z
|
||||||
|
"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 413 B |
19
public/service/ai_white.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||||
|
<path fill-rule="evenodd" fill="#FFFFFF" d="
|
||||||
|
M 619,232
|
||||||
|
L 512,7
|
||||||
|
L 405,232
|
||||||
|
A 300,300 0 0,0 216,559
|
||||||
|
L 75,765
|
||||||
|
L 323,745
|
||||||
|
A 300,300 0 0,0 701,745
|
||||||
|
L 949,765
|
||||||
|
L 808,559
|
||||||
|
A 300,300 0 0,0 619,232
|
||||||
|
Z
|
||||||
|
M 512,337
|
||||||
|
A 175,175 0 0,0 512,687
|
||||||
|
A 175,175 0 0,0 512,337
|
||||||
|
Z
|
||||||
|
"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 413 B |
67
public/service/syui.svg
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
syui
|
||||||
|
</metadata>
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M3660 4460 c-11 -11 -33 -47 -48 -80 l-29 -60 -12 38 c-27 88 -58 92
|
||||||
|
-98 11 -35 -70 -73 -159 -73 -169 0 -6 -5 -10 -10 -10 -6 0 -15 -10 -21 -22
|
||||||
|
-33 -73 -52 -92 -47 -48 2 26 -1 35 -14 38 -16 3 -168 -121 -168 -138 0 -5
|
||||||
|
-13 -16 -28 -24 -24 -13 -35 -12 -87 0 -221 55 -231 56 -480 56 -219 1 -247
|
||||||
|
-1 -320 -22 -44 -12 -96 -26 -115 -30 -57 -13 -122 -39 -200 -82 -8 -4 -31
|
||||||
|
-14 -50 -23 -41 -17 -34 -13 -146 -90 -87 -59 -292 -252 -351 -330 -63 -83
|
||||||
|
-143 -209 -143 -225 0 -10 -7 -23 -15 -30 -8 -7 -15 -17 -15 -22 0 -5 -13 -37
|
||||||
|
-28 -71 -16 -34 -36 -93 -45 -132 -9 -38 -24 -104 -34 -145 -13 -60 -17 -121
|
||||||
|
-17 -300 1 -224 1 -225 36 -365 24 -94 53 -175 87 -247 28 -58 51 -108 51
|
||||||
|
-112 0 -3 13 -24 28 -48 42 -63 46 -79 22 -85 -11 -3 -20 -9 -20 -14 0 -5 -4
|
||||||
|
-9 -10 -9 -5 0 -22 -11 -37 -25 -16 -13 -75 -59 -133 -100 -58 -42 -113 -82
|
||||||
|
-123 -90 -9 -8 -22 -15 -27 -15 -6 0 -10 -6 -10 -13 0 -8 -11 -20 -25 -27 -34
|
||||||
|
-18 -34 -54 0 -48 14 3 25 2 25 -1 0 -3 -43 -31 -95 -61 -52 -30 -95 -58 -95
|
||||||
|
-62 0 -5 -5 -8 -11 -8 -19 0 -84 -33 -92 -47 -4 -7 -15 -13 -22 -13 -14 0 -17
|
||||||
|
-4 -19 -32 -1 -8 15 -15 37 -18 l38 -5 -47 -48 c-56 -59 -54 -81 9 -75 30 3
|
||||||
|
45 0 54 -11 9 -13 16 -14 43 -4 29 11 30 10 18 -5 -7 -9 -19 -23 -25 -30 -7
|
||||||
|
-7 -13 -20 -13 -29 0 -12 8 -14 38 -9 20 4 57 8 82 9 25 2 54 8 66 15 18 10
|
||||||
|
23 8 32 -13 17 -38 86 -35 152 6 27 17 50 34 50 38 0 16 62 30 85 19 33 -15
|
||||||
|
72 -2 89 30 8 15 31 43 51 62 35 34 38 35 118 35 77 0 85 2 126 33 24 17 52
|
||||||
|
32 61 32 9 0 42 18 73 40 30 22 61 40 69 40 21 0 88 -26 100 -38 7 -7 17 -12
|
||||||
|
24 -12 7 0 35 -11 62 -25 66 -33 263 -84 387 -101 189 -25 372 -12 574 41 106
|
||||||
|
27 130 37 261 97 41 20 80 37 85 39 6 2 51 31 100 64 166 111 405 372 489 534
|
||||||
|
10 20 22 43 27 51 5 8 12 22 15 30 3 8 17 40 31 70 54 115 95 313 108 520 13
|
||||||
|
200 -43 480 -134 672 -28 58 -51 108 -51 112 0 3 -13 24 -29 48 -15 24 -34 60
|
||||||
|
-40 80 -19 57 3 142 50 193 10 11 22 49 28 85 6 36 16 67 21 68 18 6 31 53 25
|
||||||
|
83 -4 18 -17 33 -36 41 -16 7 -29 15 -29 18 1 10 38 50 47 50 5 0 20 11 33 25
|
||||||
|
18 19 22 31 17 61 -3 20 -14 45 -23 55 -16 18 -16 20 6 44 15 16 21 32 18 49
|
||||||
|
-3 15 1 34 8 43 32 43 7 73 -46 55 l-30 -11 0 85 c0 74 -2 84 -18 84 -21 0
|
||||||
|
-53 -33 -103 -104 l-34 -48 -5 74 c-7 102 -35 133 -80 88z m-870 -740 c36 -7
|
||||||
|
75 -14 88 -16 21 -4 23 -9 16 -37 -3 -18 -14 -43 -24 -57 -10 -14 -20 -35 -24
|
||||||
|
-46 -4 -12 -16 -32 -27 -45 -12 -13 -37 -49 -56 -79 -20 -30 -52 -73 -72 -96
|
||||||
|
-53 -60 -114 -133 -156 -189 -21 -27 -44 -54 -52 -58 -7 -4 -13 -14 -13 -22 0
|
||||||
|
-7 -18 -33 -40 -57 -22 -23 -40 -46 -40 -50 0 -5 -19 -21 -42 -38 -47 -35 -85
|
||||||
|
-38 -188 -15 -115 25 -173 20 -264 -23 -45 -22 -106 -46 -136 -56 -48 -15 -77
|
||||||
|
-25 -140 -50 -70 -28 -100 -77 -51 -84 14 -2 34 -10 45 -17 12 -7 53 -16 91
|
||||||
|
-20 90 -9 131 -22 178 -57 20 -16 52 -35 70 -43 18 -7 40 -22 49 -32 16 -18
|
||||||
|
15 -22 -24 -88 -23 -39 -47 -74 -53 -80 -7 -5 -23 -26 -36 -45 -26 -39 -92
|
||||||
|
-113 -207 -232 -4 -4 -37 -36 -73 -71 l-66 -64 -20 41 c-58 119 -105 240 -115
|
||||||
|
301 -40 244 -35 409 20 595 8 30 21 66 28 80 7 14 24 54 38 89 15 35 35 75 46
|
||||||
|
89 11 13 20 31 20 38 0 8 3 14 8 14 4 0 16 16 27 36 24 45 221 245 278 281 23
|
||||||
|
15 44 30 47 33 20 20 138 78 250 123 61 24 167 50 250 61 60 7 302 -1 370 -14z
|
||||||
|
m837 -661 c52 -101 102 -279 106 -379 2 -42 0 -45 -28 -51 -16 -4 -101 -7
|
||||||
|
-187 -8 -166 -1 -229 10 -271 49 -19 19 -19 19 14 49 22 21 44 31 65 31 41 0
|
||||||
|
84 34 84 66 0 30 12 55 56 112 19 25 37 65 44 95 11 51 53 111 74 104 6 -2 25
|
||||||
|
-32 43 -68z m-662 -810 c17 -10 40 -24 53 -30 12 -7 22 -16 22 -20 0 -4 17
|
||||||
|
-13 38 -19 20 -7 44 -18 52 -24 8 -7 33 -21 55 -31 22 -11 42 -23 45 -26 11
|
||||||
|
-14 109 -49 164 -58 62 -11 101 -7 126 14 15 14 38 18 78 16 39 -2 26 -41 -49
|
||||||
|
-146 -78 -109 -85 -118 -186 -219 -61 -61 -239 -189 -281 -203 -17 -5 -73 -29
|
||||||
|
-104 -44 -187 -92 -605 -103 -791 -21 -42 19 -47 24 -37 41 5 11 28 32 51 48
|
||||||
|
22 15 51 38 64 51 13 12 28 22 33 22 17 0 242 233 242 250 0 6 5 10 10 10 6 0
|
||||||
|
10 6 10 14 0 25 50 55 100 62 59 8 56 6 115 83 50 66 74 117 75 162 0 14 7 40
|
||||||
|
16 57 18 38 52 41 99 11z" fill="#EF454A"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -42,6 +42,16 @@ export function getServiceLinks(handle: string, collections: string[]): ServiceL
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VRM
|
||||||
|
if (collections.includes('ai.syui.vrm')) {
|
||||||
|
services.push({
|
||||||
|
name: 'VRM',
|
||||||
|
icon: '/service/ai.syui.vrm.png',
|
||||||
|
url: `/@${handle}/at/vrm`,
|
||||||
|
collection: 'ai.syui.vrm'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Link
|
// Link
|
||||||
if (collections.includes('ai.syui.at.link')) {
|
if (collections.includes('ai.syui.at.link')) {
|
||||||
services.push({
|
services.push({
|
||||||
|
|||||||
170
src/web/components/vrm.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// VRM display component for ai.syui.vrm collection
|
||||||
|
|
||||||
|
export interface VrmItem {
|
||||||
|
id: number
|
||||||
|
cp: number
|
||||||
|
cid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VrmCollection {
|
||||||
|
item: VrmItem[]
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VrmTier {
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
bgmUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const VRM_TIERS: Record<number, VrmTier> = {
|
||||||
|
1: { name: 'Gold', color: '#FFD700', bgmUrl: 'https://vrm.syui.ai/music/gold.mp3' },
|
||||||
|
2: { name: 'Silver', color: '#C0C0C0', bgmUrl: 'https://vrm.syui.ai/music/silver.mp3' },
|
||||||
|
3: { name: 'Bronze', color: '#CD7F32', bgmUrl: 'https://vrm.syui.ai/music/bronze.mp3' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCrownSvg(color: string): string {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" class="vrm-crown" fill="${color}"><path d="M345 151.2C354.2 143.9 360 132.6 360 120C360 97.9 342.1 80 320 80C297.9 80 280 97.9 280 120C280 132.6 285.9 143.9 295 151.2L226.6 258.8C216.6 274.5 195.3 278.4 180.4 267.2L120.9 222.7C125.4 216.3 128 208.4 128 200C128 177.9 110.1 160 88 160C65.9 160 48 177.9 48 200C48 221.8 65.5 239.6 87.2 240L119.8 457.5C124.5 488.8 151.4 512 183.1 512L456.9 512C488.6 512 515.5 488.8 520.2 457.5L552.8 240C574.5 239.6 592 221.8 592 200C592 177.9 574.1 160 552 160C529.9 160 512 177.9 512 200C512 208.4 514.6 216.3 519.1 222.7L459.7 267.3C444.8 278.5 423.5 274.6 413.5 258.9L345 151.2z"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVrmItem(item: VrmItem): string {
|
||||||
|
const tier = VRM_TIERS[item.id] || VRM_TIERS[3]
|
||||||
|
const audioId = `vrm-audio-${item.id}-${item.cid}`
|
||||||
|
const filename = tier.bgmUrl.split('/').pop()?.replace('.mp3', '') || ''
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="vrm-item" data-tier="${item.id}">
|
||||||
|
<div class="vrm-crown-wrapper">
|
||||||
|
${renderCrownSvg(tier.color)}
|
||||||
|
</div>
|
||||||
|
<div class="vrm-info">
|
||||||
|
<span class="vrm-track-name">${filename}</span>
|
||||||
|
<span class="vrm-cp">${item.cp}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vrm-actions">
|
||||||
|
<button class="vrm-play-btn" data-audio-id="${audioId}" data-src="${tier.bgmUrl}" title="play">
|
||||||
|
${playIcon('currentColor')}
|
||||||
|
</button>
|
||||||
|
<audio id="${audioId}" preload="none"></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderVrmPage(
|
||||||
|
collection: VrmCollection | null,
|
||||||
|
handle: string
|
||||||
|
): string {
|
||||||
|
const jsonUrl = `/@${handle}/at/collection/ai.syui.vrm/self`
|
||||||
|
|
||||||
|
if (!collection || !collection.item || collection.item.length === 0) {
|
||||||
|
return `
|
||||||
|
<div class="vrm-page">
|
||||||
|
<div class="vrm-header">
|
||||||
|
<h2>VRM</h2>
|
||||||
|
<a href="${jsonUrl}" class="json-btn">json</a>
|
||||||
|
</div>
|
||||||
|
<p class="no-cards">No VRM data found for @${handle}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [...collection.item].sort((a, b) => a.id - b.id)
|
||||||
|
const totalCp = items.reduce((sum, i) => sum + i.cp, 0)
|
||||||
|
|
||||||
|
const itemsHtml = items.map(item => renderVrmItem(item)).join('')
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="vrm-page">
|
||||||
|
<div class="vrm-header">
|
||||||
|
<div class="vrm-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-value">${items.length}</span>
|
||||||
|
<span class="stat-label">Items</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-value">${totalCp}</span>
|
||||||
|
<span class="stat-label">CP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vrm-actions-header">
|
||||||
|
<a href="${jsonUrl}" class="json-btn">json</a>
|
||||||
|
</div>
|
||||||
|
<div class="vrm-list">${itemsHtml}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLAY_SVG_PATH = 'M64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320zM252.3 211.1C244.7 215.3 240 223.4 240 232L240 408C240 416.7 244.7 424.7 252.3 428.9C259.9 433.1 269.1 433 276.6 428.4L420.6 340.4C427.7 336 432.1 328.3 432.1 319.9C432.1 311.5 427.7 303.8 420.6 299.4L276.6 211.4C269.2 206.9 259.9 206.7 252.3 210.9z'
|
||||||
|
|
||||||
|
function playIcon(fill: string): string {
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" class="vrm-play-icon" fill="${fill}"><path d="${PLAY_SVG_PATH}"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVrmItem(button: HTMLButtonElement): HTMLElement | null {
|
||||||
|
return button.closest('.vrm-item')
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateItem(btn: HTMLButtonElement): void {
|
||||||
|
btn.innerHTML = playIcon('currentColor')
|
||||||
|
const item = getVrmItem(btn)
|
||||||
|
if (item) item.classList.remove('vrm-active')
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateItem(btn: HTMLButtonElement, color: string): void {
|
||||||
|
btn.innerHTML = playIcon(color)
|
||||||
|
const item = getVrmItem(btn)
|
||||||
|
if (item) {
|
||||||
|
item.style.setProperty('--vrm-tier-color', color)
|
||||||
|
item.classList.add('vrm-active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupVrmPage(): void {
|
||||||
|
let currentAudio: HTMLAudioElement | null = null
|
||||||
|
let currentBtn: HTMLButtonElement | null = null
|
||||||
|
|
||||||
|
document.querySelectorAll('.vrm-play-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const button = btn as HTMLButtonElement
|
||||||
|
const audioId = button.dataset.audioId!
|
||||||
|
const src = button.dataset.src!
|
||||||
|
const audio = document.getElementById(audioId) as HTMLAudioElement
|
||||||
|
const item = getVrmItem(button)
|
||||||
|
const tier = item?.dataset.tier || '3'
|
||||||
|
const tierColors: Record<string, string> = { '1': '#FFD700', '2': '#C0C0C0', '3': '#CD7F32' }
|
||||||
|
const color = tierColors[tier] || '#C0C0C0'
|
||||||
|
|
||||||
|
if (currentAudio && currentAudio === audio && !audio.paused) {
|
||||||
|
audio.pause()
|
||||||
|
audio.currentTime = 0
|
||||||
|
deactivateItem(button)
|
||||||
|
currentAudio = null
|
||||||
|
currentBtn = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAudio && currentAudio !== audio) {
|
||||||
|
currentAudio.pause()
|
||||||
|
currentAudio.currentTime = 0
|
||||||
|
if (currentBtn) deactivateItem(currentBtn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!audio.src || audio.src === '') {
|
||||||
|
audio.src = src
|
||||||
|
}
|
||||||
|
audio.play()
|
||||||
|
activateItem(button, color)
|
||||||
|
currentAudio = audio
|
||||||
|
currentBtn = button
|
||||||
|
|
||||||
|
audio.onended = () => {
|
||||||
|
deactivateItem(button)
|
||||||
|
currentAudio = null
|
||||||
|
currentBtn = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -669,6 +669,52 @@ export async function getRse(did: string): Promise<RseCollection | null> {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VRM collection type
|
||||||
|
export interface VrmItem {
|
||||||
|
id: number
|
||||||
|
cp: number
|
||||||
|
cid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VrmCollection {
|
||||||
|
item: VrmItem[]
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's VRM collection (ai.syui.vrm)
|
||||||
|
export async function getVrm(did: string): Promise<VrmCollection | null> {
|
||||||
|
const collection = 'ai.syui.vrm'
|
||||||
|
|
||||||
|
// Try local first
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/at/${did}/${collection}/self.json`)
|
||||||
|
if (res.ok && isJsonResponse(res)) {
|
||||||
|
const record = await res.json()
|
||||||
|
return record.value as VrmCollection
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Try remote
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote fallback
|
||||||
|
const pds = await getPds(did)
|
||||||
|
if (!pds) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const host = pds.replace('https://', '')
|
||||||
|
const url = `${xrpcUrl(host, comAtprotoRepo.getRecord)}?repo=${did}&collection=${collection}&rkey=self`
|
||||||
|
const res = await fetch(url)
|
||||||
|
if (res.ok) {
|
||||||
|
const record = await res.json()
|
||||||
|
return record.value as VrmCollection
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Failed
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// Link item type
|
// Link item type
|
||||||
export interface LinkItem {
|
export interface LinkItem {
|
||||||
service: 'github' | 'youtube' | 'x'
|
service: 'github' | 'youtube' | 'x'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Route {
|
export interface Route {
|
||||||
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'chat-edit' | 'card' | 'card-old' | 'rse' | 'link'
|
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'chat-edit' | 'card' | 'card-old' | 'rse' | 'link' | 'vrm'
|
||||||
handle?: string
|
handle?: string
|
||||||
rkey?: string
|
rkey?: string
|
||||||
service?: string
|
service?: string
|
||||||
@@ -75,6 +75,12 @@ export function parseRoute(): Route {
|
|||||||
return { type: 'link', handle: linkMatch[1] }
|
return { type: 'link', handle: linkMatch[1] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VRM page: /@handle/at/vrm
|
||||||
|
const vrmMatch = path.match(/^\/@([^/]+)\/at\/vrm\/?$/)
|
||||||
|
if (vrmMatch) {
|
||||||
|
return { type: 'vrm', handle: vrmMatch[1] }
|
||||||
|
}
|
||||||
|
|
||||||
// Chat edit: /@handle/at/chat/{rkey}/edit
|
// Chat edit: /@handle/at/chat/{rkey}/edit
|
||||||
const chatEditMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)\/edit$/)
|
const chatEditMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)\/edit$/)
|
||||||
if (chatEditMatch) {
|
if (chatEditMatch) {
|
||||||
@@ -133,6 +139,8 @@ export function navigate(route: Route): void {
|
|||||||
path = `/@${route.handle}/at/chat/${route.rkey}/edit`
|
path = `/@${route.handle}/at/chat/${route.rkey}/edit`
|
||||||
} else if (route.type === 'link' && route.handle) {
|
} else if (route.type === 'link' && route.handle) {
|
||||||
path = `/@${route.handle}/at/link`
|
path = `/@${route.handle}/at/link`
|
||||||
|
} else if (route.type === 'vrm' && route.handle) {
|
||||||
|
path = `/@${route.handle}/at/vrm`
|
||||||
}
|
}
|
||||||
|
|
||||||
window.history.pushState({}, '', path)
|
window.history.pushState({}, '', path)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import './styles/main.css'
|
import './styles/main.css'
|
||||||
import './styles/card.css'
|
import './styles/card.css'
|
||||||
import './styles/card-migrate.css'
|
import './styles/card-migrate.css'
|
||||||
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getRseAdmin, getLinks } from './lib/api'
|
import './styles/vrm.css'
|
||||||
|
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getRseAdmin, getLinks, getVrm } from './lib/api'
|
||||||
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
|
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
|
||||||
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks, getAgent } from './lib/auth'
|
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks, getAgent } from './lib/auth'
|
||||||
import { validateRecord } from './lib/lexicon'
|
import { validateRecord } from './lib/lexicon'
|
||||||
@@ -16,6 +17,7 @@ import { renderChatListPage, renderChatThreadPage, renderChatEditForm } from './
|
|||||||
import { renderCardPage } from './components/card'
|
import { renderCardPage } from './components/card'
|
||||||
import { renderRsePage } from './components/rse'
|
import { renderRsePage } from './components/rse'
|
||||||
import { renderLinkPage, renderLinkButtons } from './components/link'
|
import { renderLinkPage, renderLinkButtons } from './components/link'
|
||||||
|
import { renderVrmPage, setupVrmPage } from './components/vrm'
|
||||||
import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate'
|
import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate'
|
||||||
import { showLoading, hideLoading } from './components/loading'
|
import { showLoading, hideLoading } from './components/loading'
|
||||||
|
|
||||||
@@ -282,6 +284,12 @@ async function render(route: Route): Promise<void> {
|
|||||||
html += `<div id="content">${renderLinkPage(links, handle, isOwner)}</div>`
|
html += `<div id="content">${renderLinkPage(links, handle, isOwner)}</div>`
|
||||||
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
|
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
|
||||||
|
|
||||||
|
} else if (route.type === 'vrm') {
|
||||||
|
// VRM page
|
||||||
|
const vrmData = await getVrm(did)
|
||||||
|
html += `<div id="content">${renderVrmPage(vrmData, handle)}</div>`
|
||||||
|
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
|
||||||
|
|
||||||
} else if (route.type === 'chat') {
|
} else if (route.type === 'chat') {
|
||||||
// Chat list page - show threads started by this user
|
// Chat list page - show threads started by this user
|
||||||
if (!config.bot) {
|
if (!config.bot) {
|
||||||
@@ -451,6 +459,11 @@ async function render(route: Route): Promise<void> {
|
|||||||
setupRecordMerge()
|
setupRecordMerge()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup VRM page audio controls
|
||||||
|
if (route.type === 'vrm') {
|
||||||
|
setupVrmPage()
|
||||||
|
}
|
||||||
|
|
||||||
// Setup card migration button
|
// Setup card migration button
|
||||||
if (route.type === 'card-old' && cardMigrationState?.oldApiUser && cardMigrationState?.oldApiCards) {
|
if (route.type === 'card-old' && cardMigrationState?.oldApiUser && cardMigrationState?.oldApiCards) {
|
||||||
setupMigrationButton(
|
setupMigrationButton(
|
||||||
|
|||||||
240
src/web/styles/vrm.css
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
/* VRM Display Styles */
|
||||||
|
|
||||||
|
.vrm-page {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-actions-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-secondary, #f5f5f5);
|
||||||
|
border-radius: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-stats .stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 50px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-stats .stat-value {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-stats .stat-label {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VRM List */
|
||||||
|
.vrm-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-secondary, #f5f5f5);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-item:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Playing state - rotating border */
|
||||||
|
.vrm-item.vrm-active {
|
||||||
|
border-color: transparent;
|
||||||
|
background-origin: border-box;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-item.vrm-active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2px;
|
||||||
|
background: conic-gradient(
|
||||||
|
from var(--vrm-border-angle, 0deg),
|
||||||
|
transparent 0%,
|
||||||
|
var(--vrm-tier-color, #C0C0C0) 25%,
|
||||||
|
transparent 50%,
|
||||||
|
var(--vrm-tier-color, #C0C0C0) 75%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
animation: vrm-border-spin 3s linear infinite;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes vrm-border-spin {
|
||||||
|
to {
|
||||||
|
--vrm-border-angle: 360deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --vrm-border-angle {
|
||||||
|
syntax: '<angle>';
|
||||||
|
initial-value: 0deg;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Crown icon */
|
||||||
|
.vrm-crown-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-crown {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tier-specific glow */
|
||||||
|
.vrm-item[data-tier="1"] .vrm-crown {
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-item[data-tier="2"] .vrm-crown {
|
||||||
|
filter: drop-shadow(0 0 8px rgba(192, 192, 192, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-item[data-tier="3"] .vrm-crown {
|
||||||
|
filter: drop-shadow(0 0 8px rgba(205, 127, 50, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info */
|
||||||
|
.vrm-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-tier-name {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-track-name {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-cp {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Play button */
|
||||||
|
.vrm-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-play-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-play-btn:hover {
|
||||||
|
background: var(--bg-secondary, #f0f0f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-play-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-play-btn:hover .vrm-play-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.vrm-stats {
|
||||||
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-stats .stat-value {
|
||||||
|
color: var(--text-primary, #e0e0e0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-item {
|
||||||
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-play-btn:hover {
|
||||||
|
background: var(--bg-secondary, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-play-icon {
|
||||||
|
color: var(--text-primary, #e0e0e0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.vrm-item {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-crown-wrapper {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-crown {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vrm-stats .stat-value {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||