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 { model Vtuber {
id String @id @default(cuid(2)) id String @id @default(cuid(2))
image String? image String?
slug String? slug String? @unique
displayName String? displayName String?
chaturbate String? chaturbate String?
twitter 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 // @todo we are going to need to use redis or similar https://github.com/fastify/fastify-caching
app.register(fastifyCaching, { // app.register(fastifyCaching, {
expiresIn: 300, // expiresIn: 300,
privacy: 'private', // privacy: 'private',
serverExpiresIn: 300, // serverExpiresIn: 300,
}) // })
app.register(graphileWorker) app.register(graphileWorker)
app.register(prismaPlugin) app.register(prismaPlugin)

View File

@ -29,9 +29,12 @@ export default async function vodsRoutes(
fastify: FastifyInstance, fastify: FastifyInstance,
): Promise<void> { ): Promise<void> {
fastify.get('/vods', async function (request, reply) { fastify.get('/vods', async function (request, reply) {
const { format } = request.query as { format: 'rss' | 'html' };
const userId = request.session.get('userId'); const userId = request.session.get('userId');
let user = null let user = null
if (userId !== undefined) { if (userId !== undefined) {
user = await prisma.user.findUnique({ 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', { return reply.viewAsync('vods.hbs', {
user, user,
vods, vods,

View File

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

View File

@ -1,86 +1,78 @@
{{#> main}} {{#> main}}
<!-- Header -->
<header class="container"> <header>
<hgroup>
<h1>{{ site.title }}</h1>
<p>{{ site.description }}</p>
</hgroup>
{{> navbar}} {{> navbar}}
</header> </header>
<!-- ./ Header -->
<!-- Main --> <main class="container">
<main class="container pico">
<!-- Latest Vods --> <section class="hero">
<section id="tables"> <div class="hero-body">
<h2>Latest VODs</h2> <h1 class="title is-1">{{ site.title }}</h1>
<div class="overflow-auto"> <p class="subtitle">{{ site.description }}</p>
<table class="striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Vtubers</th>
<th scope="col">Uploader</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{{#each vods}}
<tr>
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
<td>
{{#each this.vtubers}}
{{this.displayName}}
{{/each}}
</td>
<td>{{{identicon this.upload.user.id 24}}}</td>
<td>{{this.status}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div> </div>
</section> </section>
<!-- ./ Latest Vods -->
<section class="section">
<!-- Latest VTubers --> <h2 class="title is-2">Latest VODs</h2>
<section id="tables"> <table class="table striped">
<h2>Latest VTubers</h2> <thead>
<div class="overflow-auto"> <tr>
<table class="striped"> <th scope="col">ID</th>
<thead> <th scope="col">Vtubers</th>
<tr> <th scope="col">Uploader</th>
<th scope="col">ID</th> <th scope="col">Status</th>
<th scope="col">Name</th> </tr>
<th scope="col">Image</th> </thead>
<tbody>
</tr> {{#each vods}}
</thead> <tr>
<tbody> <td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
{{#each vtubers}} <td>
<tr> {{#each this.vtubers}}
<td><a href="/vtubers/{{this.id}}">{{this.id}}</a></td> {{this.displayName}}
<td> {{/each}}
{{this.displayName}} </td>
</td> <td>{{{identicon this.upload.user.id 24}}}</td>
<td><img class="avatar" src="{{getCdnUrl this.image}}"></td> <td>{{this.status}}</td>
</tr> </tr>
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
</div>
</section> </section>
<!-- ./ Latest Streams -->
<section class="section">
<h2 class="title is-2">Latest VTubers</h2>
<table class="table striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Image</th>
<!-- Latest Streams --> </tr>
<section id="tables"> </thead>
<h2>Latest Streams</h2> <tbody>
{{#each vtubers}}
<tr>
<td><a href="/vt/{{this.id}}">{{this.id}}</a></td>
<td>
{{this.displayName}}
</td>
<td><img class="avatar" src="{{getCdnUrl this.image}}"></td>
</tr>
{{/each}}
</tbody>
</table>
</section>
{{!--
<section>
<h2 class="title is-2">Latest Streams</h2>
<div class="overflow-auto"> <div class="overflow-auto">
<table class="striped"> <table class="title striped">
<thead> <thead>
<tr> <tr>
<th scope="col">🚧 ID</th> <th scope="col">🚧 ID</th>
@ -121,13 +113,11 @@
</div> </div>
</section> </section>
<!-- ./ Latest Streams -->
<!-- Latest Tags --> <section>
<section id="tables"> <h2 class="title is-2">Latest Tags</h2>
<h2>Latest Tags</h2>
<div class="overflow-auto"> <div class="overflow-auto">
<table class="striped"> <table class="title striped">
<thead> <thead>
<tr> <tr>
<th scope="col">🚧 ID</th> <th scope="col">🚧 ID</th>
@ -166,8 +156,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</section> </section> --}}
<!-- ./ Latest Tags -->
{{>footer}} {{>footer}}
</main> </main>

View File

@ -11,8 +11,9 @@
<meta name="twitter:creator" content="@cj_clippy" /> <meta name="twitter:creator" content="@cj_clippy" />
<meta name="twitter:title" content="Futureporn.net" /> <meta name="twitter:title" content="Futureporn.net" />
<meta name="twitter:description" content="{{site.description}}" /> <meta name="twitter:description" content="{{site.description}}" />
<link rel="stylesheet" href="/css/pico.conditional.pink.min.css"> {{!-- <link rel="stylesheet" href="/css/pico.conditional.pink.min.css"> --}}
<style> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
{{!-- <style>
.logo { .logo {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -33,14 +34,14 @@
img.avatar { img.avatar {
height: 24px; height: 24px;
} }
</style> </style> --}}
</head> </head>
<body> <body>
{{{body}}} {{{body}}}
<script src="/js/htmx.min.js"></script> <script src="/js/htmx.min.js"></script>
<script> <script>
@ -56,6 +57,31 @@
</script> </script>
<script src="/js/alpine/cdn.min.js"></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> </body>

View File

@ -1,45 +1,55 @@
<div class="pico"> <nav class="navbar" role="navigation" aria-label="main navigation">
<nav> <div class="navbar-brand">
<ul> <a class="navbar-item" href="/">
<li> <div class="logo">
<a href="/"> <img class="logo" src="/favicon.ico" alt="Logo">
<div class="logo"> </div>
<img class="logo" src="/favicon.ico"> </a>
</div>
</a> <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
</li> <span aria-hidden="true"></span>
<li><a href="/vods">VODs</a></li> <span aria-hidden="true"></span>
<li><a href="/streams"><s>🚧 Streams</s></a></li> <span aria-hidden="true"></span>
<li><a href="/vtubers">VTubers</a></li> </a>
<li><a href="/perks">Perks</a></li> </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)}} {{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
<li><a href="/uploads">Uploads</a></li> <a class="navbar-item" href="/uploads">Uploads</a>
{{/if}} {{/if}}
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}} {{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
<li> <a class="navbar-item" href="/upload">
<a href="/upload"> <span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" <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" /> 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> </svg>
Upload </span>
</a> Upload
</li> </a>
<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"> <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" /> <path fill="currentColor" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" />
</svg> </svg>
Add VTuber </span>
</a> Add VTuber
</li> </a>
{{/if}}
{{#if user.id}}
<li><a href="/profile">Profile</a></li>
{{else}}
<li><a href="/auth/patreon">Log in via Patreon</a></li>
{{/if}} {{/if}}
</ul> {{#if user.id}}
</nav> <a class="navbar-item" href="/profile">Profile</a>
</div> {{else}}
<a class="navbar-item" href="/auth/patreon">Log in via Patreon</a>
{{/if}}
</div>
</div>
</nav>

View File

@ -1,18 +1,19 @@
{{#> main}} {{#> main}}
<header class="container"> <header>
{{> navbar}} {{> navbar}}
</header> </header>
<main class="container pico"> <main class="container">
<section id="perks"> <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> to supporters.</p>
<table> <table class="table striped">
<thead> <thead>
<tr> <tr>
<th>Feature</th> <th>Feature</th>
@ -24,78 +25,90 @@
<tbody> <tbody>
<tr> <tr>
<td>View</td> <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>
<tr> <tr>
<td>Torrent Downloads</td> <td>Torrent Downloads</td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>CDN Downloads</td> <td>CDN Downloads</td>
<td></td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>Ad-Free</td> <td>Ad-Free</td>
<td></td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>Upload</td> <td>Upload</td>
<td></td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td><abbr title="Sex toy playback syncronization">Funscripts</abbr></td> <td><abbr title="Sex toy playback syncronization">Funscripts</abbr></td>
<td></td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>Closed Captions</td> <td>Closed Captions</td>
<td></td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
{{!-- {{!--
@todo add these things @todo add these things
<tr> <tr>
<td><abbr title="Closed Captions">CC</abbr> Search</td> <td><abbr title="Closed Captions">CC</abbr> Search</td>
<td></td> <td></td>
<td>✔️</td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>CSV Export</td> <td>CSV Export</td>
<td></td> <td></td>
<td></td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>SQL Export</td> <td>SQL Export</td>
<td></td> <td></td>
<td></td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
<tr> <tr>
<td>vibeui PyTorch Model</td> <td>vibeui PyTorch Model</td>
<td></td> <td></td>
<td></td> <td></td>
<td>✔️</td> <td></td>
</tr> </tr>
--}} --}}
</tbody> </tbody>
</table> </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> <p>Become a patron at <a target="_blank" href="https://patreon.com/CJ_Clippy">patreon.com/CJ_Clippy</a></p>
</article> </article>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,44 +1,47 @@
{{#> main}} {{#> main}}
<header class="container">
<link rel="alternate" type="application/rss+xml" href="/vods?format=rss" />
<header>
{{> navbar}} {{> navbar}}
</header> </header>
<main class="container pico"> <main class="container pico">
<section id="tables"> <section>
<h2>VODs</h2> <h2 class="title is-1">VODs
<div class="overflow-auto"> <a href="/vods?format=rss" alt="RSS feed for all VODs">
<table class="striped"> {{icon "rss" 32}}</a>
<thead> </h2>
<tr> <table class="table striped">
<th>Upload Date</th> <thead>
<th>VOD ID</th> <tr>
<th>VTuber</th> <th>Upload Date</th>
<th>Stream Date</th> <th>VOD ID</th>
<th>Uploader</th> <th>VTuber</th>
<th>Notes</th> <th>Stream Date</th>
<th>Status</th> <th>Uploader</th>
</tr> <th>Notes</th>
</thead> <th>Status</th>
<tbody> </tr>
{{#each vods}} </thead>
<tr> <tbody>
<td>{{formatDate this.createdAt}}</td> {{#each vods}}
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td> <tr>
<td> <td>{{formatDate this.createdAt}}</td>
{{#each this.vtubers}} <td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
{{this.displayName}} <td>
{{/each}} {{#each this.vtubers}}
</td> {{this.displayName}}
<td>{{formatDate this.stream.date}}</td> {{/each}}
<td>{{{identicon this.upload.user.id 24}}}</td> </td>
<td>{{#if this.notes }}yes{{else}}no{{/if}}</td> <td>{{formatDate this.stream.date}}</td>
<td>{{this.status}}</td> <td>{{{identicon this.upload.user.id 24}}}</td>
</tr> <td>{{#if this.notes }}yes{{else}}no{{/if}}</td>
{{/each}} <td>{{this.status}}</td>
</tbody> </tr>
</table> {{/each}}
</div> </tbody>
</table>
</section> </section>

View File

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

View File

@ -8,7 +8,7 @@
</header> </header>
<main class="container"> <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}} {{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
{{#each info}} {{#each info}}
@ -27,74 +27,172 @@
{{/each}} {{/each}}
<h3></h3> <form id="details" method="POST" action="/vt/create">
<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>
<label for="themeColor">Theme Color <span style="color: red;">*</span></label> {{!-- VTuber Name --}}
<input type="color" id="themeColor" name="themeColor" required> <div class="field">
<label class="label" for="displayName">VTuber Name <span class="has-text-danger">*</span></label>
<label for="chaturbate">Chaturbate URL</label> <div class="control">
<input type="url" id="chaturbate" name="chaturbate"> <input class="input" type="text" id="displayName" name="displayName" required>
</div>
<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">
</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> {{!-- Social / Platform URLs --}}
<div class="pico"> {{!-- Chaturbate --}}
<button type="submit">Submit</button> <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> </div>
</form> </form>
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}} {{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
{{>footer}} {{>footer}}

File diff suppressed because one or more lines are too long