add upload form
finally
This commit is contained in:
parent
49f21d869c
commit
e8f3e99f88
9 changed files with 449 additions and 15 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/.env.local
|
||||
|
|
@ -4,4 +4,12 @@ esbuild.build({
|
|||
in: ["./client/viewer.tsx"],
|
||||
outDir: "./web/dist",
|
||||
watch: Deno.args.includes("--watch"),
|
||||
plugins: [esbuild.envPlugin([".env", ".env.local"])],
|
||||
});
|
||||
|
||||
esbuild.build({
|
||||
in: ["./client/upload.tsx"],
|
||||
outDir: "./web/dist",
|
||||
watch: Deno.args.includes("--watch"),
|
||||
plugins: [esbuild.envPlugin([".env", ".env.local"])],
|
||||
});
|
||||
|
|
|
|||
8
client/_env.ts
Normal file
8
client/_env.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// dw kitten, esbuild handles this
|
||||
import env from "build-system-env";
|
||||
Object.assign(import.meta, { env });
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
readonly env: { [key: string]: string | undefined };
|
||||
}
|
||||
}
|
||||
266
client/upload.tsx
Normal file
266
client/upload.tsx
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import "./_env.ts";
|
||||
|
||||
import * as oauth from "@atcute/oauth-browser-client";
|
||||
|
||||
import {
|
||||
CompositeDidDocumentResolver,
|
||||
CompositeHandleResolver,
|
||||
DohJsonHandleResolver,
|
||||
LocalActorResolver,
|
||||
PlcDidDocumentResolver,
|
||||
WebDidDocumentResolver,
|
||||
WellKnownHandleResolver,
|
||||
} from "@atcute/identity-resolver";
|
||||
|
||||
// import type { Infer as InferLexicon } from "@char/lexicon.ts";
|
||||
// import type { ATProtoUniverse } from "@char/lexicon.ts/atproto";
|
||||
// type UploadBlob = InferLexicon<ATProtoUniverse, "com.atproto.repo.uploadBlob">;
|
||||
type UploadBlob = any;
|
||||
|
||||
const didDocumentResolver = new CompositeDidDocumentResolver({
|
||||
methods: {
|
||||
plc: new PlcDidDocumentResolver(),
|
||||
web: new WebDidDocumentResolver(),
|
||||
},
|
||||
});
|
||||
|
||||
oauth.configureOAuth({
|
||||
metadata: {
|
||||
client_id:
|
||||
import.meta.env.OAUTH_CLIENT_ID ??
|
||||
"https://video.cerulea.blue/upload/client-metadata.json",
|
||||
redirect_uri: import.meta.env.OAUTH_REDIRECT_URI ?? "https://video.cerulea.blue/upload/",
|
||||
},
|
||||
identityResolver: new LocalActorResolver({
|
||||
handleResolver: new CompositeHandleResolver({
|
||||
methods: {
|
||||
dns: new DohJsonHandleResolver({
|
||||
dohUrl: "https://mozilla.cloudflare-dns.com/dns-query",
|
||||
}),
|
||||
http: new WellKnownHandleResolver(),
|
||||
},
|
||||
}),
|
||||
didDocumentResolver,
|
||||
}),
|
||||
});
|
||||
|
||||
type Did = `did:${"web" | "plc"}:${string}`;
|
||||
type ActorIdentifier = Did | `${string}.${string}`;
|
||||
const login = async (identifier: ActorIdentifier) => {
|
||||
const authUrl = await oauth.createAuthorizationUrl({
|
||||
target: { type: "account", identifier: identifier },
|
||||
scope: "atproto transition:generic",
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
globalThis.location.assign(authUrl);
|
||||
};
|
||||
|
||||
const loginForm = (): HTMLFormElement => {
|
||||
const handleField = (
|
||||
<input
|
||||
type="text"
|
||||
name="handle"
|
||||
title="Enter a valid handle or DID"
|
||||
placeholder="my-handle.example.com"
|
||||
pattern="(did:.*)|(.*\..*)"
|
||||
required
|
||||
/>
|
||||
) as HTMLInputElement;
|
||||
|
||||
const form = (
|
||||
<form>
|
||||
{handleField}
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
) as HTMLFormElement;
|
||||
|
||||
form.addEventListener("submit", e => {
|
||||
e.preventDefault();
|
||||
if (handleField.value) {
|
||||
login(handleField.value as ActorIdentifier);
|
||||
}
|
||||
});
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
const auth = async (): Promise<oauth.OAuthUserAgent | undefined> => {
|
||||
for (const sessionId of oauth.listStoredSessions()) {
|
||||
try {
|
||||
const session = await oauth.getSession(sessionId);
|
||||
const agent = new oauth.OAuthUserAgent(session);
|
||||
return agent;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(globalThis.location.hash.slice(1));
|
||||
if (params.has("state") && params.has("code")) {
|
||||
history.replaceState(null, "", location.pathname + location.search);
|
||||
const { session } = await oauth.finalizeAuthorization(params);
|
||||
const agent = new oauth.OAuthUserAgent(session);
|
||||
return agent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const uploadForm = (agent: oauth.OAuthUserAgent): HTMLFormElement => {
|
||||
const uploadField = (<input type="file" accept="video/*" required />) as HTMLInputElement;
|
||||
const preview = (<video controls />) as HTMLVideoElement;
|
||||
preview.volume = 0.66;
|
||||
const uploadButton = (<button type="submit">upload</button>) as HTMLButtonElement;
|
||||
|
||||
const slugField = (
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
placeholder="slug (optional)"
|
||||
title="Optional: Custom identifier for your video"
|
||||
/>
|
||||
) as HTMLInputElement;
|
||||
|
||||
const titleField = (
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
placeholder="title (optional)"
|
||||
title="Optional: Title for your video"
|
||||
/>
|
||||
) as HTMLInputElement;
|
||||
|
||||
const descriptionField = (
|
||||
<textarea
|
||||
name="description"
|
||||
placeholder="description (optional)"
|
||||
rows={4}
|
||||
title="Optional: Description of your video"
|
||||
/>
|
||||
) as HTMLTextAreaElement;
|
||||
|
||||
const uploadStatus = <span style={{ display: "none" }}>Uploading…</span>;
|
||||
|
||||
uploadField.addEventListener("change", e => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.files && target.files[0]) {
|
||||
const file = target.files[0];
|
||||
preview.src = URL.createObjectURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
const uploadWidget = (
|
||||
<div>
|
||||
{uploadButton}
|
||||
{uploadStatus}
|
||||
</div>
|
||||
);
|
||||
|
||||
const form = (
|
||||
<form>
|
||||
{uploadField}
|
||||
{slugField}
|
||||
{titleField}
|
||||
{descriptionField}
|
||||
<div id="player">{preview}</div>
|
||||
{uploadWidget}
|
||||
</form>
|
||||
) as HTMLFormElement;
|
||||
|
||||
form.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const file = uploadField.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
uploadButton.style.display = "none";
|
||||
uploadStatus.style.display = "inline";
|
||||
|
||||
const uploadBlobRes: UploadBlob["output"] = await agent
|
||||
.handle("/xrpc/com.atproto.repo.uploadBlob", {
|
||||
method: "POST",
|
||||
headers: { "content-type": file.type },
|
||||
body: file,
|
||||
})
|
||||
.then(r => r.json());
|
||||
const blobRef = uploadBlobRes.blob;
|
||||
const cid = "$type" in blobRef ? blobRef.ref.$link : blobRef.cid;
|
||||
console.log({ cid });
|
||||
|
||||
uploadStatus.textContent = "Writing record…";
|
||||
|
||||
const putRecordRes = await agent
|
||||
.handle("/xrpc/com.atproto.repo.createRecord", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
repo: agent.sub,
|
||||
collection: "blue.cerulea.video.video",
|
||||
rkey: slugField.value || undefined,
|
||||
record: {
|
||||
$type: "blue.cerulea.video.video",
|
||||
title: titleField.value || undefined,
|
||||
description: descriptionField.value || undefined,
|
||||
video: blobRef,
|
||||
},
|
||||
validate: false,
|
||||
}),
|
||||
})
|
||||
.then(r => r.json());
|
||||
|
||||
const [repo, _collection, rkey] = putRecordRes.uri.substring("at://".length).split("/");
|
||||
globalThis.location.replace(`/${repo}/video/${rkey}`);
|
||||
});
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
const resolveHandle = async (did: string) => {
|
||||
try {
|
||||
const doc = await didDocumentResolver.resolve(did as Did);
|
||||
return (
|
||||
doc.alsoKnownAs?.find(it => it.startsWith("at://"))?.substring("at://".length) ?? did
|
||||
);
|
||||
} catch {
|
||||
return did;
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const mainElem = document.querySelector("main")!;
|
||||
|
||||
const agent = await auth();
|
||||
if (!agent) {
|
||||
mainElem.appendChild(loginForm());
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = await resolveHandle(agent.session.info.sub);
|
||||
mainElem.appendChild(
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1em",
|
||||
alignItems: "baseline",
|
||||
marginBottom: "1rem",
|
||||
justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
signed in as <strong>{handle}</strong>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
_onclick={() => {
|
||||
oauth.listStoredSessions().forEach(oauth.deleteStoredSession);
|
||||
globalThis.location.reload();
|
||||
}}
|
||||
>
|
||||
sign out
|
||||
</button>
|
||||
</div>,
|
||||
);
|
||||
mainElem.appendChild(uploadForm(agent));
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
@ -5,8 +5,10 @@
|
|||
"client:watch": "deno run -A ./_client_build.ts --watch"
|
||||
},
|
||||
"imports": {
|
||||
"@atcute/client": "npm:@atcute/client@^4.2.1",
|
||||
"@atcute/identity": "npm:@atcute/identity@^1.0.2",
|
||||
"@atcute/identity-resolver": "npm:@atcute/identity-resolver@^1.1.3",
|
||||
"@atcute/oauth-browser-client": "npm:@atcute/oauth-browser-client@^2.0.3",
|
||||
"@char/aftercare": "jsr:@char/aftercare@^0.4.2",
|
||||
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
||||
"@std/fs": "jsr:@std/fs@^1.0.17",
|
||||
|
|
|
|||
77
deno.lock
77
deno.lock
|
|
@ -32,8 +32,10 @@
|
|||
"jsr:@std/path@^1.0.9": "1.1.0",
|
||||
"jsr:@std/path@^1.1.0": "1.1.0",
|
||||
"jsr:@std/streams@^1.0.9": "1.0.9",
|
||||
"npm:@atcute/identity-resolver@^1.1.3": "1.1.3_@atcute+identity@1.0.2",
|
||||
"npm:@atcute/identity@^1.0.2": "1.0.2",
|
||||
"npm:@atcute/client@^4.2.1": "4.2.1",
|
||||
"npm:@atcute/identity-resolver@^1.1.3": "1.2.2_@atcute+identity@1.1.3",
|
||||
"npm:@atcute/identity@^1.0.2": "1.1.3",
|
||||
"npm:@atcute/oauth-browser-client@^2.0.3": "2.0.3_@atcute+identity@1.1.3",
|
||||
"npm:@zod/mini@^4.0.0-beta.20250505T195954": "4.0.0-beta.20250505T195954",
|
||||
"npm:esbuild@0.24": "0.24.2",
|
||||
"npm:urlpattern-polyfill@10": "10.0.0"
|
||||
|
|
@ -164,8 +166,15 @@
|
|||
}
|
||||
},
|
||||
"npm": {
|
||||
"@atcute/identity-resolver@1.1.3_@atcute+identity@1.0.2": {
|
||||
"integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==",
|
||||
"@atcute/client@4.2.1": {
|
||||
"integrity": "sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==",
|
||||
"dependencies": [
|
||||
"@atcute/identity",
|
||||
"@atcute/lexicons"
|
||||
]
|
||||
},
|
||||
"@atcute/identity-resolver@1.2.2_@atcute+identity@1.1.3": {
|
||||
"integrity": "sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==",
|
||||
"dependencies": [
|
||||
"@atcute/identity",
|
||||
"@atcute/lexicons",
|
||||
|
|
@ -173,27 +182,56 @@
|
|||
"@badrap/valita"
|
||||
]
|
||||
},
|
||||
"@atcute/identity@1.0.2": {
|
||||
"integrity": "sha512-SrDPHuEarEHj9bx7NfYn7DYG6kIgJIMRU581iOCIaVaiZ1WhE9D8QxTxeYG/rbGNSa85E891ECp1sQcKiBN0kg==",
|
||||
"@atcute/identity@1.1.3": {
|
||||
"integrity": "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==",
|
||||
"dependencies": [
|
||||
"@atcute/lexicons",
|
||||
"@badrap/valita"
|
||||
]
|
||||
},
|
||||
"@atcute/lexicons@1.0.4": {
|
||||
"integrity": "sha512-VyGJuGKAIeE+71UT9aSMJJdvfxfXsdsGMG9acv9rnGT7enVy4TD5XoYQy7TCHZ4YpxXzuHkqjyAqBz95c4WkRg==",
|
||||
"@atcute/lexicons@1.2.6": {
|
||||
"integrity": "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==",
|
||||
"dependencies": [
|
||||
"@atcute/uint8array",
|
||||
"@atcute/util-text",
|
||||
"@standard-schema/spec",
|
||||
"esm-env"
|
||||
]
|
||||
},
|
||||
"@atcute/util-fetch@1.0.1": {
|
||||
"integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==",
|
||||
"@atcute/multibase@1.1.6": {
|
||||
"integrity": "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==",
|
||||
"dependencies": [
|
||||
"@atcute/uint8array"
|
||||
]
|
||||
},
|
||||
"@atcute/oauth-browser-client@2.0.3_@atcute+identity@1.1.3": {
|
||||
"integrity": "sha512-rzUjwhjE4LRRKdQnCFQag/zXRZMEAB1hhBoLfnoQuHwWbmDUCL7fzwC3jRhDPp3om8XaYNDj8a/iqRip0wRqoQ==",
|
||||
"dependencies": [
|
||||
"@atcute/client",
|
||||
"@atcute/identity-resolver",
|
||||
"@atcute/lexicons",
|
||||
"@atcute/multibase",
|
||||
"@atcute/uint8array",
|
||||
"nanoid"
|
||||
]
|
||||
},
|
||||
"@atcute/uint8array@1.0.6": {
|
||||
"integrity": "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="
|
||||
},
|
||||
"@atcute/util-fetch@1.0.5": {
|
||||
"integrity": "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==",
|
||||
"dependencies": [
|
||||
"@badrap/valita"
|
||||
]
|
||||
},
|
||||
"@badrap/valita@0.4.5": {
|
||||
"integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ=="
|
||||
"@atcute/util-text@0.0.1": {
|
||||
"integrity": "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==",
|
||||
"dependencies": [
|
||||
"unicode-segmenter"
|
||||
]
|
||||
},
|
||||
"@badrap/valita@0.4.6": {
|
||||
"integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="
|
||||
},
|
||||
"@esbuild/aix-ppc64@0.24.2": {
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
|
|
@ -320,6 +358,9 @@
|
|||
"os": ["win32"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@standard-schema/spec@1.1.0": {
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
|
||||
},
|
||||
"@zod/core@0.11.6": {
|
||||
"integrity": "sha512-03Bv82fFSfjDAvMfdHHdGSS6SOJs0iCcJlWJv1kJHRtoTT02hZpyip/2Lk6oo4l4FtjuwTrsEQTwg/LD8I7dJA=="
|
||||
},
|
||||
|
|
@ -327,7 +368,8 @@
|
|||
"integrity": "sha512-ioybPtU4w4TqwHvJv0gkAiYNaBkZ/BaGHBpK7viCIRSE8BiiZucVZ8vS0YE04Qy1R120nAnFy1d+tD9ByMO0yw==",
|
||||
"dependencies": [
|
||||
"@zod/core"
|
||||
]
|
||||
],
|
||||
"deprecated": true
|
||||
},
|
||||
"esbuild@0.24.2": {
|
||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||
|
|
@ -364,6 +406,13 @@
|
|||
"esm-env@1.2.2": {
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
|
||||
},
|
||||
"nanoid@5.1.6": {
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"bin": true
|
||||
},
|
||||
"unicode-segmenter@0.14.5": {
|
||||
"integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="
|
||||
},
|
||||
"urlpattern-polyfill@10.0.0": {
|
||||
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
|
||||
}
|
||||
|
|
@ -375,8 +424,10 @@
|
|||
"jsr:@std/fs@^1.0.17",
|
||||
"jsr:@std/http@^1.0.17",
|
||||
"jsr:@std/path@^1.1.0",
|
||||
"npm:@atcute/client@^4.2.1",
|
||||
"npm:@atcute/identity-resolver@^1.1.3",
|
||||
"npm:@atcute/identity@^1.0.2",
|
||||
"npm:@atcute/oauth-browser-client@^2.0.3",
|
||||
"npm:@zod/mini@^4.0.0-beta.20250505T195954"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
--color-text: var(--color-fg);
|
||||
--alpha-text: 0.9;
|
||||
|
||||
--font-sans:
|
||||
Inter, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
|
||||
--font-sans: Inter, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
|
||||
|
||||
background-color: rgb(var(--color-bg) / 1);
|
||||
color: rgb(var(--color-fg) / var(--alpha-text));
|
||||
|
|
@ -42,9 +41,87 @@ main {
|
|||
#player {
|
||||
video {
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.description {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
:is(input, button, textarea) {
|
||||
appearance: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
padding: 0.75em;
|
||||
border: 1px solid rgb(var(--color-fg) / 0.2);
|
||||
border-radius: 8px;
|
||||
transition-property: color, border-color, background-color;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
input:is([type="text"], [type="url"], [type="search"], [type="file"]),
|
||||
textarea {
|
||||
background-color: rgb(var(--color-bg) / 0.5);
|
||||
color: rgb(var(--color-fg) / var(--alpha-text));
|
||||
|
||||
&::placeholder {
|
||||
color: rgb(var(--color-fg) / 0.4);
|
||||
}
|
||||
&:focus {
|
||||
border-color: rgb(var(--color-accent) / 1);
|
||||
color: rgb(var(--color-fg) / 0.95);
|
||||
}
|
||||
&:focus::placeholder {
|
||||
color: rgb(var(--color-fg) / 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="file"] {
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
color: rgb(var(--color-accent) / var(--alpha-text));
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: rgb(var(--color-accent) / 0.5);
|
||||
background-color: rgb(var(--color-accent) / 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
color: unset;
|
||||
|
||||
&::file-selector-button {
|
||||
appearance: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin-right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: rgb(var(--color-accent) / var(--alpha-text));
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
&::file-selector-button:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
web/upload/client-metadata.json
Normal file
12
web/upload/client-metadata.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"client_id": "https://video.cerulea.blue/upload/client-metadata.json",
|
||||
"client_name": "Cerulea Video",
|
||||
"client_uri": "https://video.cerulea.blue",
|
||||
"redirect_uris": ["https://video.cerulea.blue/upload/"],
|
||||
"scope": "atproto transition:generic",
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"response_types": ["code"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
"application_type": "web",
|
||||
"dpop_bound_access_tokens": true
|
||||
}
|
||||
9
web/upload/index.html
Normal file
9
web/upload/index.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>upload a video | video.cerulea.blue</title>
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
|
||||
<main></main>
|
||||
|
||||
<script src="/dist/upload.js" type="module"></script>
|
||||
Loading…
Reference in a new issue