switch to bulma

This commit is contained in:
CJ_Clippy 2025-08-12 21:43:26 -08:00
parent db3977940f
commit 7f871b6b0a
22 changed files with 1277 additions and 654 deletions

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Vtuber` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Vtuber_slug_key" ON "Vtuber"("slug");

View File

@ -96,7 +96,7 @@ model Vod {
model Vtuber {
id String @id @default(cuid(2))
image String?
slug String?
slug String? @unique
displayName String?
chaturbate String?
twitter String?

View File

@ -0,0 +1,117 @@
import { PrismaClient } from '../generated/prisma';
import pg from 'pg';
const prisma = new PrismaClient();
const v1 = new pg.Pool({
host: process.env.V1_DB_HOST || 'localhost',
port: +(process.env.V1_DB_PORT || '5444'),
user: process.env.V1_DB_USER || 'postgres',
password: process.env.V1_DB_PASS || 'password',
database: process.env.V1_DB_NAME || 'restoredb'
});
// Set this to an existing user ID in v2
const DEFAULT_UPLOADER_ID = process.env.DEFAULT_UPLOADER_ID || 'REPLACE_WITH_V2_USER_ID';
async function migrateVtubers() {
console.log('Migrating vtubers...');
const res = await v1.query(`SELECT * FROM vtubers`);
for (const vt of res.rows) {
await prisma.vtuber.create({
data: {
slug: vt.slug,
image: vt.image,
displayName: vt.display_name,
chaturbate: vt.chaturbate,
twitter: vt.twitter,
patreon: vt.patreon,
twitch: vt.twitch,
tiktok: vt.tiktok,
onlyfans: vt.onlyfans,
youtube: vt.youtube,
linktree: vt.linktree,
carrd: vt.carrd,
fansly: vt.fansly,
pornhub: vt.pornhub,
discord: vt.discord,
reddit: vt.reddit,
throne: vt.throne,
instagram: vt.instagram,
facebook: vt.facebook,
merch: vt.merch,
description: `${vt.description_1 ?? ''}\n${vt.description_2 ?? ''}`.trim() || null,
themeColor: vt.theme_color,
uploaderId: DEFAULT_UPLOADER_ID
}
});
}
console.log(`Migrated ${res.rows.length} vtubers`);
}
async function migrateVods() {
console.log('Migrating vods...');
const vods = await v1.query(`SELECT * FROM vods`);
for (const vod of vods.rows) {
// Get linked vtubers
const vtuberLinks = await v1.query(
`SELECT vtuber_id FROM vods_vtuber_links WHERE vod_id = $1`,
[vod.id]
);
let vtuberSlugs: string[] = [];
if (vtuberLinks.rows.length > 0) {
const vtuberRes = await v1.query(
`SELECT slug FROM vtubers WHERE id = ANY($1)`,
[vtuberLinks.rows.map(r => r.vtuber_id)]
);
vtuberSlugs = vtuberRes.rows.map(v => v.slug).filter(Boolean);
}
// Get thumbnail
const thumbLink = await v1.query(
`SELECT b2.cdn_url, b2.url FROM vods_thumbnail_links vtl
JOIN b2_files b2 ON vtl.b_2_file_id = b2.id
WHERE vtl.vod_id = $1 LIMIT 1`,
[vod.id]
);
// Get source video
const videoSrcLink = await v1.query(
`SELECT b2.cdn_url, b2.url FROM vods_video_src_b_2_links vsl
JOIN b2_files b2 ON vsl.b_2_file_id = b2.id
WHERE vsl.vod_id = $1 LIMIT 1`,
[vod.id]
);
await prisma.vod.create({
data: {
uploaderId: DEFAULT_UPLOADER_ID,
streamDate: vod.date ?? new Date(),
notes: vod.note,
sourceVideo: videoSrcLink.rows[0]?.cdn_url || videoSrcLink.rows[0]?.url || null,
thumbnail: thumbLink.rows[0]?.cdn_url || thumbLink.rows[0]?.url || null,
vtubers: {
connect: vtuberSlugs.map(slug => ({ slug }))
}
}
});
}
console.log(`Migrated ${vods.rows.length} vods`);
}
async function main() {
try {
await migrateVtubers();
await migrateVods();
} catch (err) {
console.error(err);
} finally {
await v1.end();
await prisma.$disconnect();
}
}
main();

View File

@ -0,0 +1,269 @@
// 2025-08-12-migrate-from-futureporn.ts
//
// we are migrating from v1 futureporn.net (a strapi site) to v2 future.porn (a fastify site with prisma)
//
//
// the idea is to read a local backup of the strapi site from a .sql
// and use the data contained within to populate the database on the v2 site using prisma.
//
// here's the schema from the v1 site
```sql
CREATE TABLE "public"."b2_files" (
"id" SERIAL,
"url" VARCHAR(255) NULL,
"key" VARCHAR(255) NULL,
"upload_id" VARCHAR(255) NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"cdn_url" VARCHAR(255) NULL,
CONSTRAINT "b2_files_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "public"."mux_assets" (
"id" SERIAL,
"playback_id" VARCHAR(255) NULL,
"asset_id" VARCHAR(255) NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"deletion_queued_at" TIMESTAMP NULL,
CONSTRAINT "mux_assets_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "public"."vods" (
"id" SERIAL,
"video_src_hash" VARCHAR(255) NULL,
"video_720_hash" VARCHAR(255) NULL,
"video_480_hash" VARCHAR(255) NULL,
"video_360_hash" VARCHAR(255) NULL,
"video_240_hash" VARCHAR(255) NULL,
"thin_hash" VARCHAR(255) NULL,
"thicc_hash" VARCHAR(255) NULL,
"announce_title" VARCHAR(255) NULL,
"announce_url" VARCHAR(255) NULL,
"note" TEXT NULL,
"date" TIMESTAMP NULL,
"spoilers" TEXT NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"published_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"title" VARCHAR(255) NULL,
"chat_log" TEXT NULL,
"date_2" VARCHAR(255) NULL,
"cuid" VARCHAR(255) NULL,
"archive_status" VARCHAR(255) NULL,
CONSTRAINT "vods_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "public"."vods_mux_asset_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"mux_asset_id" INTEGER NULL,
CONSTRAINT "vods_mux_asset_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_mux_asset_links_unique" UNIQUE ("vod_id", "mux_asset_id")
);
CREATE TABLE "public"."vods_thumbnail_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"b_2_file_id" INTEGER NULL,
CONSTRAINT "vods_thumbnail_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_thumbnail_links_unique" UNIQUE ("vod_id", "b_2_file_id")
);
CREATE TABLE "public"."vods_video_src_b_2_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"b_2_file_id" INTEGER NULL,
CONSTRAINT "vods_video_src_b_2_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_video_src_b_2_links_unique" UNIQUE ("vod_id", "b_2_file_id")
);
CREATE TABLE "public"."vods_vtuber_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"vtuber_id" INTEGER NULL,
"vod_order" DOUBLE PRECISION NULL,
CONSTRAINT "vods_vtuber_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_vtuber_links_unique" UNIQUE ("vod_id", "vtuber_id")
);
CREATE TABLE "public"."vtubers" (
"id" SERIAL,
"chaturbate" VARCHAR(255) NULL,
"twitter" VARCHAR(255) NULL,
"patreon" VARCHAR(255) NULL,
"twitch" VARCHAR(255) NULL,
"tiktok" VARCHAR(255) NULL,
"onlyfans" VARCHAR(255) NULL,
"youtube" VARCHAR(255) NULL,
"linktree" VARCHAR(255) NULL,
"carrd" VARCHAR(255) NULL,
"fansly" VARCHAR(255) NULL,
"pornhub" VARCHAR(255) NULL,
"discord" VARCHAR(255) NULL,
"reddit" VARCHAR(255) NULL,
"throne" VARCHAR(255) NULL,
"instagram" VARCHAR(255) NULL,
"facebook" VARCHAR(255) NULL,
"merch" VARCHAR(255) NULL,
"slug" VARCHAR(255) NULL,
"image" VARCHAR(255) NULL,
"display_name" VARCHAR(255) NULL,
"description_1" TEXT NULL,
"description_2" TEXT NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"published_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"theme_color" VARCHAR(255) NULL,
"image_blur" VARCHAR(255) NULL,
CONSTRAINT "vtubers_pkey" PRIMARY KEY ("id")
);
```
// here's the schema from the v2 site
```prisma
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
patreonId String @unique
patreonFullName String?
imageUrl String?
roles Role[]
vods Vod[]
Vtuber Vtuber[]
}
enum RoleName {
user
supporterTier1
supporterTier2
supporterTier3
supporterTier4
supporterTier5
supporterTier6
moderator
admin
}
model Role {
id String @id @default(cuid(2))
name String @unique
users User[]
}
model RateLimiterFlexible {
key String @id
points Int
expire DateTime?
}
model Stream {
id String @id @default(cuid(2))
date DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
announcementUrl String?
vods Vod[]
@@map("stream_entity")
}
enum VodStatus {
ordering
pending
approved
rejected
processing
processed
}
model Vod {
id String @id @default(cuid(2))
streamId String?
stream Stream? @relation(fields: [streamId], references: [id])
uploaderId String // previously in Upload
uploader User @relation(fields: [uploaderId], references: [id])
streamDate DateTime
notes String?
segmentKeys Json?
sourceVideo String?
hlsPlaylist String?
thumbnail String?
asrVttKey String?
slvttSheetKeys Json?
slvttVTTKey String?
magnetLink String?
status VodStatus @default(pending)
sha256sum String?
cidv1 String?
funscript String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vtubers Vtuber[]
}
model Vtuber {
id String @id @default(cuid(2))
image String?
slug String?
displayName String?
chaturbate String?
twitter String?
patreon String?
twitch String?
tiktok String?
onlyfans String?
youtube String?
linktree String?
carrd String?
fansly String?
pornhub String?
discord String?
reddit String?
throne String?
instagram String?
facebook String?
merch String?
description String?
themeColor String?
fanslyId String?
chaturbateId String?
twitterId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vods Vod[]
uploaderId String
uploader User @relation(fields: [uploaderId], references: [id])
}
```

View File

@ -0,0 +1,22 @@
# scripts
This directory is for **DATA MIGRATIONS ONLY**.
Data migrations are **not** the same as schema migrations.
- **Schema migrations** (handled by Prisma Migrate) change the database structure — adding/removing columns, altering data types, creating new tables, etc.
- **Data migrations** modify the *contents* of the database to align with business logic changes, clean up old data, or backfill new fields without altering the schema.
Examples of data migrations:
- Populating a new column with default or computed values.
- Normalizing inconsistent text formats (e.g., fixing casing, trimming whitespace).
- Converting old enum/string values to new ones.
- Moving data between tables after a structural change has already been applied.
Guidelines:
1. **Idempotent when possible** — running the script twice should not break data integrity.
2. **Version-controlled** — keep a clear history of changes.
3. **Document assumptions** — include comments explaining why the migration is needed and what it affects.
4. **Run after schema changes** — if both schema and data changes are required, update the schema first.
> ⚠️ Always back up your database before running data migrations in production.

View File

@ -181,11 +181,11 @@ export function buildApp() {
})
// @todo we are going to need to use redis or similar https://github.com/fastify/fastify-caching
app.register(fastifyCaching, {
expiresIn: 300,
privacy: 'private',
serverExpiresIn: 300,
})
// app.register(fastifyCaching, {
// expiresIn: 300,
// privacy: 'private',
// serverExpiresIn: 300,
// })
app.register(graphileWorker)
app.register(prismaPlugin)

View File

@ -29,9 +29,12 @@ export default async function vodsRoutes(
fastify: FastifyInstance,
): Promise<void> {
fastify.get('/vods', async function (request, reply) {
const { format } = request.query as { format: 'rss' | 'html' };
const userId = request.session.get('userId');
let user = null
if (userId !== undefined) {
user = await prisma.user.findUnique({
@ -65,6 +68,34 @@ export default async function vodsRoutes(
},
});
// RSS branch
if (format === 'rss') {
const items = vods.map(vod => {
const vtuberNames = vod.vtubers.map(v => v.displayName || v.slug).join(', ');
return {
title: `${vtuberNames} stream on ${vod.streamDate.toDateString()}`,
link: `${env.ORIGIN}/vods/${vod.id}`,
guid: `${env.ORIGIN}/vods/${vod.id}`,
pubDate: vod.streamDate.toUTCString(),
description: vod.notes
? `${vod.notes}\n\nFeaturing: ${vtuberNames}`
: `Featuring: ${vtuberNames}`,
};
});
return reply
.type('application/rss+xml')
.view('/feed.hbs', {
title: 'future.porn - VODs',
description: 'All VODs and their featured VTubers',
link: `${env.ORIGIN}/vods`,
items,
}, { layout: 'layouts/xml.hbs' });
}
// rss branch
return reply.viewAsync('vods.hbs', {
user,
vods,

View File

@ -1,4 +1,4 @@
import { FastifyInstance } from "fastify"
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"
import { constants } from "../config/constants";
import { PrismaClient, Vtuber } from '../../generated/prisma'
@ -12,12 +12,14 @@ const prisma = new PrismaClient().$extends(withAccelerate())
const hexColorRegex = /^#([0-9a-fA-F]{6})$/;
export default async function vtubersRoutes(
fastify: FastifyInstance,
): Promise<void> {
fastify.get('/vtubers', async function (request, reply) {
const vtuberIndexHandler = async (request: FastifyRequest, reply: FastifyReply) => {
const userId = request.session.get('userId')
console.log(`userId=${userId}`)
@ -39,10 +41,18 @@ export default async function vtubersRoutes(
vtubers,
site: constants.site
}, { layout: 'layouts/main.hbs' });
});
};
['/vtubers', '/vt'].forEach(path => {
fastify.route({
method: ['GET'], // you could define multiple methods
url: path,
handler: vtuberIndexHandler
})
})
fastify.get('/vtubers/new', async function (request, reply) {
fastify.get('/vt/new', async function (request, reply) {
const userId = request.session.get('userId');
const user = await prisma.user.findFirst({
@ -64,7 +74,7 @@ export default async function vtubersRoutes(
}, { layout: 'layouts/main.hbs' })
})
fastify.post('/vtubers/create', async function (request, reply) {
fastify.post('/vt/create', async function (request, reply) {
const {
displayName,
themeColor,
@ -123,7 +133,7 @@ export default async function vtubersRoutes(
if (!uppyResult) {
request.flash('error', '❌ missing uppyResult')
reply.redirect('/vtubers/new')
reply.redirect('/vt/new')
// return reply.status(400).view('vtubers/new.hbs', {
// message: '❌ Missing uppyResult',
@ -135,7 +145,7 @@ export default async function vtubersRoutes(
if (!themeColor) {
request.flash('error', '❌ Missing themeColor')
reply.redirect('/vtubers/new')
reply.redirect('/vt/new')
// return reply.status(400).view('vtubers/new.hbs', {
// message: '❌ Missing themeColor',
// vtubers,
@ -240,14 +250,15 @@ export default async function vtubersRoutes(
// successful upload
request.flash('info', `✅ Successfully created vtuber <a href="/vtubers/${vtuber.id}">${vtuber.id}</a>`)
return reply.redirect('/vtubers/new')
request.flash('info', `✅ Successfully created vtuber <a href="/vt/${vtuber.id}">${vtuber.id}</a>`)
return reply.redirect('/vt/new')
})
fastify.get('/vtubers/:idOrSlug', async function (request, reply) {
fastify.get('/vt/:idOrSlug', async function (request, reply) {
const { idOrSlug } = request.params as { idOrSlug: string };
const userId = request.session.get('userId');
@ -257,7 +268,7 @@ export default async function vtubersRoutes(
});
if (!idOrSlug) {
return reply.status(400).send({ error: 'Invalid VTuber identifier' });
return reply.redirect('/vt')
}
// Determine if it's a CUID (starts with "c" and length of 24)
@ -273,7 +284,11 @@ export default async function vtubersRoutes(
],
},
include: {
vods: true,
vods: {
orderBy: {
streamDate: 'desc'
}
}
},
});
@ -289,14 +304,21 @@ export default async function vtubersRoutes(
});
fastify.get('/vtubers/:idOrSlug/rss', async function (request, reply) {
fastify.get('/vt/:idOrSlug/vods', async function (request, reply) {
const { idOrSlug } = request.params as { idOrSlug: string };
const { format } = request.query as { format: 'rss' | 'html' };
if (!idOrSlug) {
return reply.status(400).send({ error: 'Invalid VTuber identifier' });
return reply.status(400).send({ error: 'Invalid VTuber identifier ~' });
}
if (format !== 'rss') {
return reply.status(404).send({ error: 'the only available format for this endpoint is rss' });
}
// Determine if it's a CUID (starts with "c" and length of 24)
const isCuid = /^c[a-z0-9]{23}$/i.test(idOrSlug);
@ -337,7 +359,7 @@ export default async function vtubersRoutes(
.view('/feed.hbs', {
title,
description: vtuber.description || title,
link: `${env.ORIGIN}/vtubers/${vtuber.slug || vtuber.id}`,
link: `${env.ORIGIN}/vt/${vtuber.slug || vtuber.id}`,
items,
}, { layout: 'layouts/xml.hbs' });
});

View File

@ -1,22 +1,21 @@
{{#> main}}
<!-- Header -->
<header class="container">
<hgroup>
<h1>{{ site.title }}</h1>
<p>{{ site.description }}</p>
</hgroup>
<header>
{{> navbar}}
</header>
<!-- ./ Header -->
<!-- Main -->
<main class="container pico">
<main class="container">
<!-- Latest Vods -->
<section id="tables">
<h2>Latest VODs</h2>
<div class="overflow-auto">
<table class="striped">
<section class="hero">
<div class="hero-body">
<h1 class="title is-1">{{ site.title }}</h1>
<p class="subtitle">{{ site.description }}</p>
</div>
</section>
<section class="section">
<h2 class="title is-2">Latest VODs</h2>
<table class="table striped">
<thead>
<tr>
<th scope="col">ID</th>
@ -40,16 +39,12 @@
{{/each}}
</tbody>
</table>
</div>
</section>
<!-- ./ Latest Vods -->
<!-- Latest VTubers -->
<section id="tables">
<h2>Latest VTubers</h2>
<div class="overflow-auto">
<table class="striped">
<section class="section">
<h2 class="title is-2">Latest VTubers</h2>
<table class="table striped">
<thead>
<tr>
<th scope="col">ID</th>
@ -61,7 +56,7 @@
<tbody>
{{#each vtubers}}
<tr>
<td><a href="/vtubers/{{this.id}}">{{this.id}}</a></td>
<td><a href="/vt/{{this.id}}">{{this.id}}</a></td>
<td>
{{this.displayName}}
</td>
@ -70,17 +65,14 @@
{{/each}}
</tbody>
</table>
</div>
</section>
<!-- ./ Latest Streams -->
<!-- Latest Streams -->
<section id="tables">
<h2>Latest Streams</h2>
{{!--
<section>
<h2 class="title is-2">Latest Streams</h2>
<div class="overflow-auto">
<table class="striped">
<table class="title striped">
<thead>
<tr>
<th scope="col">🚧 ID</th>
@ -121,13 +113,11 @@
</div>
</section>
<!-- ./ Latest Streams -->
<!-- Latest Tags -->
<section id="tables">
<h2>Latest Tags</h2>
<section>
<h2 class="title is-2">Latest Tags</h2>
<div class="overflow-auto">
<table class="striped">
<table class="title striped">
<thead>
<tr>
<th scope="col">🚧 ID</th>
@ -166,8 +156,7 @@
</tbody>
</table>
</div>
</section>
<!-- ./ Latest Tags -->
</section> --}}
{{>footer}}
</main>

View File

@ -11,8 +11,9 @@
<meta name="twitter:creator" content="@cj_clippy" />
<meta name="twitter:title" content="Futureporn.net" />
<meta name="twitter:description" content="{{site.description}}" />
<link rel="stylesheet" href="/css/pico.conditional.pink.min.css">
<style>
{{!-- <link rel="stylesheet" href="/css/pico.conditional.pink.min.css"> --}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
{{!-- <style>
.logo {
display: flex;
justify-content: center;
@ -33,14 +34,14 @@
img.avatar {
height: 24px;
}
</style>
</style> --}}
</head>
<body>
{{{body}}}
<script src="/js/htmx.min.js"></script>
<script>
@ -56,6 +57,31 @@
</script>
<script src="/js/alpine/cdn.min.js"></script>
{{!-- JS for Bulma's navbar --}}
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
</body>

View File

@ -1,45 +1,55 @@
<div class="pico">
<nav>
<ul>
<li>
<a href="/">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<div class="logo">
<img class="logo" src="/favicon.ico">
<img class="logo" src="/favicon.ico" alt="Logo">
</div>
</a>
</li>
<li><a href="/vods">VODs</a></li>
<li><a href="/streams"><s>🚧 Streams</s></a></li>
<li><a href="/vtubers">VTubers</a></li>
<li><a href="/perks">Perks</a></li>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navMenu" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/vods">VODs</a>
{{!-- <a class="navbar-item" href="/streams"><s>🚧 Streams</s></a> @todo --}}
<a class="navbar-item" href="/vt">VTubers</a>
<a class="navbar-item" href="/perks">Perks</a>
{{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
<li><a href="/uploads">Uploads</a></li>
<a class="navbar-item" href="/uploads">Uploads</a>
{{/if}}
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
<li>
<a href="/upload">
<a class="navbar-item" href="/upload">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M6 20q-.825 0-1.412-.587T4 18v-2q0-.425.288-.712T5 15t.713.288T6 16v2h12v-2q0-.425.288-.712T19 15t.713.288T20 16v2q0 .825-.587 1.413T18 20zm5-12.15L9.125 9.725q-.3.3-.712.288T7.7 9.7q-.275-.3-.288-.7t.288-.7l3.6-3.6q.15-.15.325-.212T12 4.425t.375.063t.325.212l3.6 3.6q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L13 7.85V15q0 .425-.288.713T12 16t-.712-.288T11 15z" />
</svg>
</span>
Upload
</a>
</li>
<li>
<a href="/vtubers/new">
<a class="navbar-item" href="/vt/new">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" />
</svg>
</span>
Add VTuber
</a>
</li>
{{/if}}
{{#if user.id}}
<li><a href="/profile">Profile</a></li>
{{else}}
<li><a href="/auth/patreon">Log in via Patreon</a></li>
{{/if}}
</ul>
</nav>
</div>
{{#if user.id}}
<a class="navbar-item" href="/profile">Profile</a>
{{else}}
<a class="navbar-item" href="/auth/patreon">Log in via Patreon</a>
{{/if}}
</div>
</div>
</nav>

View File

@ -1,18 +1,19 @@
{{#> main}}
<header class="container">
<header>
{{> navbar}}
</header>
<main class="container pico">
<main class="container">
<section id="perks">
<h2>Perks</h2>
<h2 class="title is-1">Perks</h2>
<p>future.porn is free to use, but to keep the site running we need your help! In return, we offer extra perks
<p class="subtitle">We need your help to keep the site running! In return, we offer
extra perks
to supporters.</p>
<table>
<table class="table striped">
<thead>
<tr>
<th>Feature</th>
@ -24,78 +25,90 @@
<tbody>
<tr>
<td>View</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>RSS</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>API</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>Torrent Downloads</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>CDN Downloads</td>
<td></td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Ad-Free</td>
<td></td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Upload</td>
<td></td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><abbr title="Sex toy playback syncronization">Funscripts</abbr></td>
<td></td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Closed Captions</td>
<td></td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
<td></td>
</tr>
{{!--
@todo add these things
<tr>
<td><abbr title="Closed Captions">CC</abbr> Search</td>
<td></td>
<td>✔️</td>
<td>✔️</td>
<td></td>
<td></td>
</tr>
<tr>
<td>CSV Export</td>
<td></td>
<td></td>
<td>✔️</td>
<td></td>
</tr>
<tr>
<td>SQL Export</td>
<td></td>
<td></td>
<td>✔️</td>
<td></td>
</tr>
<tr>
<td>vibeui PyTorch Model</td>
<td></td>
<td></td>
<td>✔️</td>
<td></td>
</tr>
--}}
</tbody>
</table>
<article>
<article class="notification">
<p>Become a patron at <a target="_blank" href="https://patreon.com/CJ_Clippy">patreon.com/CJ_Clippy</a></p>
</article>

View File

@ -1,21 +1,23 @@
{{#> main}}
<header class="container">
<header>
{{> navbar}}
</header>
<main class="container pico">
<main class="container">
<section id="tables">
<h2>Profile</h2>
<p><strong>ID:</strong> <small>{{user.id}}</small></p>
<section class="section">
<h2 class="title is-1">Profile</h2>
<div class="box">
<p><strong>Identicon:</strong> {{{identicon user.id 48}}}</p>
<p><strong>ID:</strong> <small>{{user.id}}</small></p>
<p><strong>Roles:</strong> {{#each user.roles}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
</p>
<p><strong>Perks:</strong> @todo
{{!-- @todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}} --}}
</p>
</div>
{{!-- <p><strong>Perks:</strong> @todo
@todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
</p> --}}
</section>
<a href="/logout">Logout</a>

View File

@ -8,7 +8,7 @@
<!-- Main -->
<main class="container">
<section id="tables">
<section>
<h1>
{{#each vod.vtubers}}
{{this.displayName}}{{#unless @last}}, {{/unless}}

View File

@ -4,25 +4,25 @@
crossorigin="anonymous" referrerpolicy="no-referrer">
<header class="container">
<header>
{{> navbar }}
</header>
<main class="container pico">
<main class="main">
{{#each info}}
<article id="article">
<section id="article">
<p>
{{{this}}}
</p>
</article>
</section>
{{/each}}
<section id="tables">
<h2>Uploads</h2>
<section class="section">
<h2 class="title is-1">Uploads</h2>
<div class="overflow-auto">
<table class="striped">
<table class="table striped">
<thead>
<tr>
<th>Upload ID</th>

View File

@ -4,52 +4,71 @@
crossorigin="anonymous" referrerpolicy="no-referrer">
<header class="container">
<header>
{{> navbar }}
</header>
<main class="container">
<h1>Upload Lewdtuber VODs here.</h1>
<h1 class="title is-1">Upload</h1>
<p class="subtitle">Upload Lewdtuber VODs here.</p>
<article>
<article class="notification">
<p><strong>Instructions:</strong> Enter the metadata first. Vtuber name and original stream date is required. Then
upload the vod
segment(s). Wait for the vod segment(s) to fully upload before pressing the submit button.</p>
</article>
<form id="details" method="POST" action="/upload">
<div class="pico">
<!-- VTuber select input -->
<label for="vtuberIds">VTuber(s)</label>
<div class="field">
<label class="label" for="vtuberIds">VTuber(s)</label>
<div class="control">
<div class="select is-multiple is-fullwidth">
<select id="vtuberIds" name="vtuberIds" multiple required>
{{#each vtubers}}
<option value="{{this.id}}">{{this.displayName}}</option>
{{/each}}
</select>
</div>
</div>
</div>
<!-- Original stream date -->
<label for="streamDate">Original Stream Datetime</label>
<input type="datetime-local" id="streamDate" name="streamDate" required />
<div class="field">
<label class="label" for="streamDate">Original Stream Datetime</label>
<div class="control">
<input class="input" type="datetime-local" id="streamDate" name="streamDate" required>
</div>
</div>
<!-- Notes textarea -->
<label for="notes">Notes (optional)</label>
<textarea id="notes" name="notes" rows="4" placeholder="Any notes about the upload…"></textarea>
<div class="field">
<label class="label" for="notes">Notes (optional)</label>
<div class="control">
<textarea class="textarea" id="notes" name="notes" rows="4"
placeholder="Any notes about the upload…"></textarea>
</div>
</div>
<div id="uploader"></div>
<h3></h3>
<!-- File uploader -->
<div class="field mb-5" id="uploader"></div>
<!-- Message display -->
{{#if message}}
<p class="help is-info" id="messages" hx-swap-oob="true">{{message}}</p>
{{/if}}
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
<div class="pico">
<button>Submit</button>
<!-- Submit button -->
<div class="field mb-5">
<div class="control">
<button class="button is-primary">Submit</button>
</div>
</div>
</form>
{{> footer }}
</main>
@ -60,6 +79,7 @@
import ImageEditor from 'https://esm.sh/@uppy/image-editor@3.3.3'
import AwsS3 from 'https://esm.sh/@uppy/aws-s3@4.2.3'
import Form from 'https://esm.sh/@uppy/form@4.1.1'
//import Link from 'https://esm.sh/@uppy/url@4.3.2' // requires companion instance 💢
window.vtuberOptions = {{ safeJson vtubers }}
@ -93,6 +113,7 @@
// 'Authorization': `Bearer @todo`
// }
//})
// .use(Link) requires companion instance 💢
.use(AwsS3, {
id: 's3Plugin',
endpoint: '/',

View File

@ -15,7 +15,7 @@
</style>
<main class="container pico">
<main class="container">
<section>

View File

@ -1,5 +1,5 @@
{{#> main}}
<!-- Header -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video-js.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/videojs-vtt-thumbnails@0.0.13/dist/videojs-vtt-thumbnails.min.css">
@ -35,14 +35,12 @@
}
</style>
<header class="container">
<header>
{{> navbar}}
</header>
<!-- ./ Header -->
<main class="container" x-data="{}">
<div class="section" x-data="{}">
<section>
{{#if vod.hlsPlaylist}}
<div class="video-container">
@ -63,15 +61,15 @@
{{else}}
<video id="player" class="hidden"></video>
<div class="pico">
<article>
<div class="section">
<div class="notification">
{{icon "processing" 24}} HTTP Live Streaming is processing.
</article>
</div>
</div>
{{/if}}
</section>
<section id="tables" class="pico">
<div class="container">
{{!--
<h2>Details</h2>
@ -98,32 +96,38 @@
type="application/x-mpegURL">
</video> --}}
<h1>
<nav class="level">
<div class="level-left">
<div class="level-item">
{{#each vod.vtubers}}
<a href="/vtubers/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
<a href="/vt/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
{{/each}}
- {{formatDate vod.streamDate}}
</h1>
</div>
<div class="level-item">{{formatDate vod.streamDate}}</div>
</div>
</nav>
<div class="overflow-auto">
{{#if vod.notes}}
<h2>Notes</h2>
<article>
<p class="breaklines">{{vod.notes}}</p>
<footer>
{{{identicon vod.uploader.id 24}}}
<h2 id="notes" class="title is-4 mb-5">Notes</h2>
<div class="card">
<div class="card-content">
<div class="content breaklines">{{vod.notes}}</div>
</div>
<footer class="card-footer">
<div class="card-footer-item">— {{{identicon vod.uploader.id 24}}} ❦</div>
</footer>
</article>
</div>
{{/if}}
<h2>Downloads</h2>
<h2 class="title is-4">Downloads</h2>
<h3>VOD</h3>
<article>
<h3 class="title is-5">VOD</h3>
<div class="box">
{{#if vod.sourceVideo}}
{{#if (hasRole "supporterTier1" user)}}
@ -133,7 +137,7 @@
target="_blank">{{icon "download" 24}} Download</a>
</p>
{{else}}
<p>
<p class="mb-3">
<a href="/perks">{{icon "patreon" 24}}</a>
<del>
CDN Download
@ -145,22 +149,22 @@
{{#if vod.cidv1}}
<p><b>IPFS cidv1</b> {{vod.cidv1}}</p>
{{else}}
<article>
<article class="notification">
IPFS CID is processing.
</article>
{{/if}}
{{#if vod.magnetLink}}
<p><a href="{{vod.magnetLink}}">{{icon "magnet" 24}} Magnet Link</a></p>
{{else}}
<article>
<article class="notification">
Magnet Link is processing.
</article>
{{/if}}
</article>
</div>
<h4>Raw Segments</h4>
<article>
<h4 class="title is-5">Raw Segments</h4>
<div class="mb-5">
{{#if vod.segmentKeys}}
{{#if (hasRole "supporterTier1" user)}}
<ul>
@ -181,28 +185,28 @@
{{/if}}
{{else}}
<article>
<div class="notification">
This VOD has no file segments.
</article>
</div>
{{/if}} {{!-- end of raw segments --}}
</article>
</div>
{{else}}
<article>
<div class="notification">
Video Source is processing.
</article>
</div>
{{/if}} {{!-- end of vod.sourceVideo --}}
<h4>HLS Playlist</h4>
<article>
<h4 class="title is-5">HLS Playlist</h4>
<div class="mb-5">
{{#if vod.hlsPlaylist}}
{{#if (hasRole "supporterTier1" user)}}
<a href="{{signedHlsUrl vod.hlsPlaylist}}">{{signedHlsUrl vod.hlsPlaylist}}</a>
@ -215,26 +219,28 @@
</p>
{{/if}}
{{else}}
<article>
<div class="notification">
HLS Playlist is processing.
</article>
</div>
{{/if}}
</article>
</div>
<h4>Thumbnail Image</h4>
<div class="mb-5">
<h4 class="title is-5">Thumbnail Image</h4>
{{#if vod.thumbnail}}
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail">
<div class="mx-5"></div>
{{else}}
<article>
<div class="notification">
Thumbnail is processing.
</article>
</div>
{{/if}}
</div>
<h4>Funscripts (sex toy sync)</h4>
<article>
<div class="mb-5">
<h4 class="title is-5">Funscripts (sex toy sync)</h4>
{{#if vod.funscript}}
{{#if (hasRole "supporterTier1" user)}}
@ -259,13 +265,14 @@
{{/if}}
<div class="mx-5"></div>
{{else}}
<article>
<div class="notification">
Funscript file is processing.
</article>
</div>
{{/if}}
</article>
</div>
<h4>Closed Captions / Subtitles</h4>
<div class="mb-5">
<h4 class="title is-5">Closed Captions / Subtitles</h4>
<article>
{{#if vod.asrVttKey}}
@ -282,12 +289,13 @@
</p>
{{/if}}
</article>
</div>
{{else}}
<article>
<div class="notification">
Closed captions are processing.
</article>
</div>
{{/if}}
@ -295,23 +303,30 @@
{{#if (isModerator user)}}
<article>
<h2>Moderator section</h2>
<article class="mb-5">
<h2 class="title is-3">Moderator section</h2>
<div class="box">
<h3>Storyboard Images</h3>
<div class="mb-5">
<h3 class="title is-4">Storyboard Images</h3>
{{#if vod.slvttVTTKey}}
<a id="slvtt" data-url="{{getCdnUrl vod.slvttVTTKey}}" data-file-name="{{basename vod.slvttVTTKey}}"
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.slvttVTTKey}}"
alt="slvttVTTKey">{{icon "download" 24}} slvttVTTKey</a>
{{else}}
<article>
<article class="notification">
Storyboard Images are processing
</article>
{{/if}}
</div>
<h3>Controls</h3>
<button hx-post="/vods/{{vod.id}}/process" hx-target="body">{{icon "processing" 24}} Re-Schedule Vod
<h3 class="title is-4">Controls</h3>
<button class="button" hx-post="/vods/{{vod.id}}/process" hx-target="body"><span
class="icon mr-2">{{icon "processing" 24}}</span>
Re-Schedule
Vod
Processing</button>
</div>
</article>
{{/if}}
@ -322,11 +337,11 @@
{{!-- <h2>Comments</h2>
{{>commentForm}} --}}
</div>
</section>
</div>
{{>footer}}
</main>
</div>
<script src=" https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video.min.js "></script>
<script>

View File

@ -1,14 +1,18 @@
{{#> main}}
<header class="container">
<link rel="alternate" type="application/rss+xml" href="/vods?format=rss" />
<header>
{{> navbar}}
</header>
<main class="container pico">
<section id="tables">
<h2>VODs</h2>
<div class="overflow-auto">
<table class="striped">
<section>
<h2 class="title is-1">VODs
<a href="/vods?format=rss" alt="RSS feed for all VODs">
{{icon "rss" 32}}</a>
</h2>
<table class="table striped">
<thead>
<tr>
<th>Upload Date</th>
@ -38,7 +42,6 @@
{{/each}}
</tbody>
</table>
</div>
</section>

View File

@ -1,10 +1,10 @@
{{#> main}}
<header class="container">
<header>
{{> navbar}}
</header>
<main class="container pico">
<main class="container">
{{#each info}}
<article id="article">
@ -15,10 +15,10 @@
{{/each}}
<section id="tables">
<h2>VTubers</h2>
<section>
<h2 class="title is-1">VTubers</h2>
<div class="overflow-auto">
<table class="striped">
<table class="table striped">
<thead>
<tr>
<th>Name</th>
@ -30,7 +30,7 @@
<tbody>
{{#each vtubers}}
<tr>
<td><a href="/vtubers/{{this.slug}}">{{this.displayName}}</a></td>
<td><a href="/vt/{{this.slug}}">{{this.displayName}}</a></td>
<td>{{#if this.image}}<img src="{{getCdnUrl this.image}}" alt="{{this.displayName}}" class="avatar">{{/if}}
</td>
<td>

View File

@ -8,7 +8,7 @@
</header>
<main class="container">
<h1>Add a new VTuber</h1>
<h1 class="title is-1">Add a new VTuber</h1>
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
{{#each info}}
@ -27,74 +27,172 @@
{{/each}}
<h3></h3>
<form id="details" method="POST" action="/vtubers/create">
<div class="pico">
<label for="displayName">VTuber Name <span style="color: red;">*</span></label>
<input type="text" id="displayName" name="displayName" required>
<form id="details" method="POST" action="/vt/create">
<label for="themeColor">Theme Color <span style="color: red;">*</span></label>
<input type="color" id="themeColor" name="themeColor" required>
<label for="chaturbate">Chaturbate URL</label>
<input type="url" id="chaturbate" name="chaturbate">
<label for="twitter">Twitter URL</label>
<input type="url" id="twitter" name="twitter">
<label for="patreon">Patreon URL</label>
<input type="url" id="patreon" name="patreon">
<label for="twitch">Twitch URL</label>
<input type="url" id="twitch" name="twitch">
<label for="tiktok">TikTok URL</label>
<input type="url" id="tiktok" name="tiktok">
<label for="onlyfans">OnlyFans URL</label>
<input type="url" id="onlyfans" name="onlyfans">
<label for="youtube">YouTube URL</label>
<input type="url" id="youtube" name="youtube">
<label for="linktree">Linktree URL</label>
<input type="url" id="linktree" name="linktree">
<label for="carrd">Carrd URL</label>
<input type="url" id="carrd" name="carrd">
<label for="fansly">Fansly URL</label>
<input type="url" id="fansly" name="fansly">
<label for="pornhub">Pornhub URL</label>
<input type="url" id="pornhub" name="pornhub">
<label for="discord">Discord URL</label>
<input type="url" id="discord" name="discord">
<label for="reddit">Reddit URL</label>
<input type="url" id="reddit" name="reddit">
<label for="throne">Throne URL</label>
<input type="url" id="throne" name="throne">
<label for="instagram">Instagram URL</label>
<input type="url" id="instagram" name="instagram">
<label for="facebook">Facebook URL</label>
<input type="url" id="facebook" name="facebook">
<label for="merch">Merch URL</label>
<input type="url" id="merch" name="merch">
{{!-- VTuber Name --}}
<div class="field">
<label class="label" for="displayName">VTuber Name <span class="has-text-danger">*</span></label>
<div class="control">
<input class="input" type="text" id="displayName" name="displayName" required>
</div>
</div>
<div id="uploader"></div>
{{!-- Theme Color --}}
<div class="field">
<label class="label" for="themeColor">Theme Color <span class="has-text-danger">*</span></label>
<div class="control">
<input class="input" type="color" id="themeColor" name="themeColor" required>
</div>
</div>
<h3></h3>
<div class="pico">
<button type="submit">Submit</button>
{{!-- Social / Platform URLs --}}
{{!-- Chaturbate --}}
<div class="field">
<label class="label" for="chaturbate">Chaturbate URL</label>
<div class="control">
<input class="input" type="url" id="chaturbate" name="chaturbate">
</div>
</div>
{{!-- Twitter --}}
<div class="field">
<label class="label" for="twitter">Twitter URL</label>
<div class="control">
<input class="input" type="url" id="twitter" name="twitter">
</div>
</div>
{{!-- Patreon --}}
<div class="field">
<label class="label" for="patreon">Patreon URL</label>
<div class="control">
<input class="input" type="url" id="patreon" name="patreon">
</div>
</div>
{{!-- Twitch --}}
<div class="field">
<label class="label" for="twitch">Twitch URL</label>
<div class="control">
<input class="input" type="url" id="twitch" name="twitch">
</div>
</div>
{{!-- TikTok --}}
<div class="field">
<label class="label" for="tiktok">TikTok URL</label>
<div class="control">
<input class="input" type="url" id="tiktok" name="tiktok">
</div>
</div>
{{!-- OnlyFans --}}
<div class="field">
<label class="label" for="onlyfans">OnlyFans URL</label>
<div class="control">
<input class="input" type="url" id="onlyfans" name="onlyfans">
</div>
</div>
{{!-- YouTube --}}
<div class="field">
<label class="label" for="youtube">YouTube URL</label>
<div class="control">
<input class="input" type="url" id="youtube" name="youtube">
</div>
</div>
{{!-- Linktree --}}
<div class="field">
<label class="label" for="linktree">Linktree URL</label>
<div class="control">
<input class="input" type="url" id="linktree" name="linktree">
</div>
</div>
{{!-- Carrd --}}
<div class="field">
<label class="label" for="carrd">Carrd URL</label>
<div class="control">
<input class="input" type="url" id="carrd" name="carrd">
</div>
</div>
{{!-- Fansly --}}
<div class="field">
<label class="label" for="fansly">Fansly URL</label>
<div class="control">
<input class="input" type="url" id="fansly" name="fansly">
</div>
</div>
{{!-- Pornhub --}}
<div class="field">
<label class="label" for="pornhub">Pornhub URL</label>
<div class="control">
<input class="input" type="url" id="pornhub" name="pornhub">
</div>
</div>
{{!-- Discord --}}
<div class="field">
<label class="label" for="discord">Discord URL</label>
<div class="control">
<input class="input" type="url" id="discord" name="discord">
</div>
</div>
{{!-- Reddit --}}
<div class="field">
<label class="label" for="reddit">Reddit URL</label>
<div class="control">
<input class="input" type="url" id="reddit" name="reddit">
</div>
</div>
{{!-- Throne --}}
<div class="field">
<label class="label" for="throne">Throne URL</label>
<div class="control">
<input class="input" type="url" id="throne" name="throne">
</div>
</div>
{{!-- Instagram --}}
<div class="field">
<label class="label" for="instagram">Instagram URL</label>
<div class="control">
<input class="input" type="url" id="instagram" name="instagram">
</div>
</div>
{{!-- Facebook --}}
<div class="field">
<label class="label" for="facebook">Facebook URL</label>
<div class="control">
<input class="input" type="url" id="facebook" name="facebook">
</div>
</div>
{{!-- Merch --}}
<div class="field">
<label class="label" for="merch">Merch URL</label>
<div class="control">
<input class="input" type="url" id="merch" name="merch">
</div>
</div>
{{!-- File uploader --}}
<div class="field" id="uploader"></div>
{{!-- Submit button --}}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Submit</button>
</div>
</div>
</form>
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
{{>footer}}

View File

@ -1,16 +1,16 @@
{{#> main}}
<link rel="alternate" type="application/rss+xml" href="/vtubers/{{vtuber.slug}}/rss" />
<link rel="alternate" type="application/rss+xml" href="/vt/{{vtuber.slug}}/vods?format=rss" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/video.js@8.22.0/dist/video-js.min.css">
<header class="container">
<header>
{{> navbar}}
</header>
<main class="container">
<section id="tables" class="pico">
<section class="">
@ -39,7 +39,7 @@
type="application/x-mpegURL">
</video> --}}
<h1>
<h1 class="title is-1">
{{vtuber.displayName}}
</h1>
@ -59,10 +59,11 @@
<section>
<h2>Socials</h2>
<div class="grid">
<div class="mb-5">
<h2 class="title is-2">Socials</h2>
<div class="columns is-mobile is-multiline">
{{#if vtuber.chaturbate}}
<p>
<div class="column">
<a href="{{vtuber.chaturbate}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210.63 189.77" width="20" class="icon_icon__aycE9">
<path fill="currentColor"
@ -71,90 +72,90 @@
</path>
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.twitter}}
<p>
<div class="column">
<a href="{{vtuber.twitter}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="m17.687 3.063l-4.996 5.711l-4.32-5.711H2.112l7.477 9.776l-7.086 8.099h3.034l5.469-6.25l4.78 6.25h6.102l-7.794-10.304l6.625-7.571zm-1.064 16.06L5.654 4.782h1.803l10.846 14.34z" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.patreon}}
<p>
<div class="column">
<a href="{{vtuber.patreon}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482c-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46c3.765.047 4.326-4.804 6.068-7.141c1.24-1.662 2.836-2.132 4.801-2.618c3.376-.836 5.678-3.501 5.673-7.031" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.twitch}}
<p>
<div class="column">
<a href="{{vtuber.twitch}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M11.64 5.93h1.43v4.28h-1.43m3.93-4.28H17v4.28h-1.43M7 2L3.43 5.57v12.86h4.28V22l3.58-3.57h2.85L20.57 12V2m-1.43 9.29l-2.85 2.85h-2.86l-2.5 2.5v-2.5H7.71V3.43h11.43Z" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.tiktok}}
<p>
<div class="column">
<a href="{{vtuber.tiktok}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M16.6 5.82s.51.5 0 0A4.28 4.28 0 0 1 15.54 3h-3.09v12.4a2.59 2.59 0 0 1-2.59 2.5c-1.42 0-2.6-1.16-2.6-2.6c0-1.72 1.66-3.01 3.37-2.48V9.66c-3.45-.46-6.47 2.22-6.47 5.64c0 3.33 2.76 5.7 5.69 5.7c3.14 0 5.69-2.55 5.69-5.7V9.01a7.35 7.35 0 0 0 4.3 1.38V7.3s-1.88.09-3.24-1.48" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.onlyfans}}
<p>
<div class="column">
<a href="{{vtuber.onlyfans}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M24 4.003h-4.015c-3.45 0-5.3.197-6.748 1.957a7.996 7.996 0 1 0 2.103 9.211c3.182-.231 5.39-2.134 6.085-5.173c0 0-2.399.585-4.43 0c4.018-.777 6.333-3.037 7.005-5.995M5.61 11.999A2.391 2.391 0 0 1 9.28 9.97a2.966 2.966 0 0 1 2.998-2.528h.008c-.92 1.778-1.407 3.352-1.998 5.263A2.392 2.392 0 0 1 5.61 12Zm2.386-7.996a7.996 7.996 0 1 0 7.996 7.996a7.996 7.996 0 0 0-7.996-7.996m0 10.394A2.399 2.399 0 1 1 10.395 12a2.396 2.396 0 0 1-2.399 2.398Z" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.youtube}}
<p>
<div class="column">
<a href="{{vtuber.youtube}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="m10 15l5.19-3L10 9zm11.56-7.83c.13.47.22 1.1.28 1.9c.07.8.1 1.49.1 2.09L22 12c0 2.19-.16 3.8-.44 4.83c-.25.9-.83 1.48-1.73 1.73c-.47.13-1.33.22-2.65.28c-1.3.07-2.49.1-3.59.1L12 19c-4.19 0-6.8-.16-7.83-.44c-.9-.25-1.48-.83-1.73-1.73c-.13-.47-.22-1.1-.28-1.9c-.07-.8-.1-1.49-.1-2.09L2 12c0-2.19.16-3.8.44-4.83c.25-.9.83-1.48 1.73-1.73c.47-.13 1.33-.22 2.65-.28c1.3-.07 2.49-.1 3.59-.1L12 5c4.19 0 6.8.16 7.83.44c.9.25 1.48.83 1.73 1.73" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.linktree}}
<p>
<div class="column">
<a href="{{vtuber.linktree}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="m13.736 5.853l4.005-4.117l2.325 2.38l-4.2 4.005h5.908v3.305h-5.937l4.229 4.108l-2.325 2.334l-5.74-5.769l-5.741 5.769l-2.325-2.325l4.229-4.108H2.226V8.121h5.909l-4.2-4.004l2.324-2.381l4.005 4.117V0h3.472zm-3.472 10.306h3.472V24h-3.472z" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.carrd}}
<p>
<div class="column">
<a href="{{vtuber.carrd}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M21.254 18.42L9.746 23.948a.5.5 0 0 1-.23.053a.55.55 0 0 1-.284-.08a.53.53 0 0 1-.247-.45v-5.474l-6.217-2.602a.53.53 0 0 1-.327-.49V.531c0-.181.093-.354.248-.45A.54.54 0 0 1 3.202.05l11.964 5.743l5.632-2.703a.53.53 0 0 1 .513.03a.53.53 0 0 1 .248.452v14.37a.54.54 0 0 1-.305.479M3.503 1.378V14.55l5.482 2.297V14.2l-3.447-1.39a.537.537 0 0 1-.296-.69a.533.533 0 0 1 .69-.296l3.053 1.23V10.88L5.538 9.492a.537.537 0 0 1-.296-.69a.534.534 0 0 1 .69-.297l3.053 1.23v-.632c0-.204.115-.39.3-.478l.788-.38l-4.562-2.076a.534.534 0 0 1-.265-.703a.536.536 0 0 1 .704-.266l5.367 2.447L13.93 6.39zm16.99 3.04L10.047 9.435v13.193l10.446-5.022zm-8.45 6.867l5.985-2.894a.53.53 0 0 1 .708.248a.527.527 0 0 1-.247.708l-5.987 2.894a.55.55 0 0 1-.23.053a.53.53 0 0 1-.23-1.01m0 3.318l5.985-2.893a.53.53 0 0 1 .708.248a.527.527 0 0 1-.247.707l-5.987 2.894a.55.55 0 0 1-.23.053a.53.53 0 0 1-.23-1.009m0 3.314l5.985-2.893a.53.53 0 0 1 .708.247a.527.527 0 0 1-.247.708L12.5 18.872a.55.55 0 0 1-.23.053a.53.53 0 0 1-.23-1.009" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.fansly}}
<p>
<div class="column">
<a href="{{vtuber.fansly}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" baseProfile="tiny" viewBox="0 0 394.7 324.7"
width="20" height="20">
@ -166,10 +167,10 @@
</path>
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.pornhub}}
<p>
<div class="column">
<a href="{{vtuber.pornhub}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="visible" viewBox="0 0 79.6 84.5"
width="20" height="20" class="icon_icon__aycE9">
@ -187,20 +188,20 @@
</g>
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.discord}}
<p>
<div class="column">
<a href="{{vtuber.discord}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.reddit}}
<p>
<div class="column">
<a href="{{vtuber.reddit}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
@ -209,10 +210,10 @@
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m5.8 11.33c.02.14.03.29.03.44c0 2.24-2.61 4.06-5.83 4.06s-5.83-1.82-5.83-4.06c0-.15.01-.3.03-.44c-.51-.23-.86-.74-.86-1.33a1.455 1.455 0 0 1 2.47-1.05c1.01-.73 2.41-1.19 3.96-1.24l.74-3.49c.01-.07.05-.13.11-.16c.06-.04.13-.05.2-.04l2.42.52a1.04 1.04 0 1 1 .93 1.5c-.56 0-1.01-.44-1.04-.99l-2.17-.46l-.66 3.12c1.53.05 2.9.52 3.9 1.24a1.455 1.455 0 1 1 1.6 2.38" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.throne}}
<p>
<div class="column">
<a href="{{vtuber.throne}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 58" fill="none" width="20" height="20">
<path fill="currentColor"
@ -229,100 +230,76 @@
</defs>
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.instagram}}
<p>
<div class="column">
<a href="{{vtuber.instagram}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8C4 18.39 5.61 20 7.6 20h8.8a3.6 3.6 0 0 0 3.6-3.6V7.6C20 5.61 18.39 4 16.4 4zm9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8A1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5a5 5 0 0 1-5 5a5 5 0 0 1-5-5a5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.facebook}}
<p>
<div class="column">
<a href="{{vtuber.facebook}}" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95" />
</svg>
</a>
</p>
</div>
{{/if}}
{{#if vtuber.merch}}
<p>
<div class="column">
<a href="{{vtuber.merch}}" target="_blank">
<svg focusable="false" data-prefix="fas" data-icon="bag-shopping" class="svg-inline--fa fas fa-bag-shopping"
role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="24" height="24">
<svg focusable="false" data-prefix="fas" data-icon="bag-shopping"
class="svg-inline--fa fas fa-bag-shopping" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512" width="24" height="24">
<path fill="currentColor"
d="M160 112c0-35.3 28.7-64 64-64s64 28.7 64 64v48H160V112zm-48 48H48c-26.5 0-48 21.5-48 48V416c0 53 43 96 96 96H352c53 0 96-43 96-96V208c0-26.5-21.5-48-48-48H336V112C336 50.1 285.9 0 224 0S112 50.1 112 112v48zm24 48a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm152 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z">
</path>
</svg>
</a>
</p>
</div>
{{/if}}
</div>
</div>
</section>
<section>
<h2>VODs</h2>
<h2 class="title is-2">VODs <a href="/vt/{{vtuber.slug}}/vods?format=rss"
alt="RSS feed for {{vtuber.displayName}}">
{{icon "rss" 32}}</a>
</h2>
<div class="grid">
{{#each vtuber.vods}}
<p>
<div>
<a href="/vods/{{this.id}}">
{{#if this.thumbnail}}
<img src="{{getCdnUrl this.thumbnail}}">
{{/if}}
<span>{{formatDate this.streamDate}}</span>
</a>
</p>
</div>
{{/each}}
</div>
</section>
{{!--
@todo implement
<h2><s>🚧Streams</s></h2>
<p>🚧</p>
<p>🚧</div>
<h2><s>🚧Toys</s></h2>
<p>🚧</p>
<h2>Feeds</h2>
<p>
<a href="/vtubers/{{vtuber.slug}}/rss" alt="RSS feed for {{vtuber.displayName}}">RSS {{icon "rss" 32}}</a>
</p>
<div class="overflow-auto">
{{!-- <h2>Thumbnail Image</h2>
{{#if vod.thumbnail}}
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{this.vtuber.displayName}} thumbnail">
<div class="mx-5"></div>
{{else}}
<article>
Thumbnail is still processing.
</article>
{{/if}} --}}
{{!-- {{#if (isModerator user)}}
<h2>Moderator Controls</h2>
<button hx-post="/vods/{{vod.id}}/process" hx-target="body"><svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24" viewBox="0 0 2048 2048">
<path fill="currentColor"
d="M1930 630q0 22-2 43t-8 43l123 51l-49 118l-124-51q-46 74-120 120l51 125l-118 49l-52-124q-21 5-42 7t-43 3q-22 0-43-2t-43-8l-23 56l-111-67l16-39q-74-46-120-120l-125 51l-49-118l124-51q-5-21-7-42t-3-44q0-22 2-43t8-42l-124-52l49-118l125 52q23-37 53-67t67-54l-51-124l118-49l51 123q21-5 42-7t44-3q22 0 43 2t42 8l52-123l118 49l-51 124q74 46 120 120l124-51l49 118l-123 52q5 21 7 42t3 43m-384 256q53 0 99-20t82-55t55-81t20-100q0-53-20-99t-55-82t-81-55t-100-20q-53 0-99 20t-82 55t-55 81t-20 100q0 53 20 99t55 82t81 55t100 20m-577 220l139-58l44 106v15l-133 55q7 27 11 54t4 56q0 28-4 55t-11 55l133 55v15l-44 106l-139-58q-29 48-68 87t-87 69l58 139l-119 49l-57-139q-27 7-54 11t-56 4q-28 0-55-4t-55-11l-58 139l-118-49l58-140q-97-58-155-155l-140 58l-48-118l138-58q-7-27-11-54t-4-56q0-28 4-55t11-55l-138-57l48-119l140 58q58-97 155-155l-58-139l118-49l58 138q27-7 54-11t56-4q28 0 55 4t55 11l57-138l119 49l-58 139q97 58 155 155m-383 548q66 0 124-25t101-68t69-102t26-125t-25-124t-69-101t-102-69t-124-26t-124 25t-102 69t-69 102t-25 124t25 124t68 102t102 69t125 25m694 394v-896l747 448zm128-670v444l370-222z" />
</svg> Re-Schedule VTuber Processing</button>
{{/if}} --}}
<p>🚧</div> --}}
{{!-- this is feature creep-- we will add this once we get the basic site running --}}
{{!-- <h2>Comments</h2>
{{>commentForm}} --}}
</div>
</section>