From ca9d84a25ac4a42b2f13be725f33f40994410053 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 4 Jan 2024 07:14:11 +0000 Subject: [PATCH] Added username filter, word replacement filter and voice selection --- app/api/settings/tts/default/route.ts | 55 +++++++ app/api/settings/tts/filter/users/route.ts | 10 +- app/api/settings/tts/filter/words/route.ts | 111 +++++++++++++ app/api/settings/tts/route.ts | 66 ++++++++ app/api/tokens/route.ts | 2 - app/page.tsx | 7 +- app/settings/tts/filters/page.tsx | 178 ++++++++++++++++----- app/settings/tts/voices/page.tsx | 117 ++++++++------ components/ui/button.tsx | 1 + data/tts.ts | 126 +++++++++++++++ prisma/schema.prisma | 7 +- 11 files changed, 584 insertions(+), 96 deletions(-) create mode 100644 app/api/settings/tts/default/route.ts create mode 100644 app/api/settings/tts/filter/words/route.ts create mode 100644 app/api/settings/tts/route.ts create mode 100644 data/tts.ts diff --git a/app/api/settings/tts/default/route.ts b/app/api/settings/tts/default/route.ts new file mode 100644 index 0000000..53b85f9 --- /dev/null +++ b/app/api/settings/tts/default/route.ts @@ -0,0 +1,55 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserUsingAPI from "@/lib/validate-api"; +import voices from "@/data/tts"; + +export async function GET(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const u = await db.user.findFirst({ + where: { + id: user.id + } + }); + + const voice = voices.find(v => v.value == new String(u?.ttsDefaultVoice)) + return NextResponse.json(voice); + } catch (error) { + console.log("[TTS/FILTER/USER]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function POST(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { voice } = await req.json(); + console.log(voice) + + const v = voices.find(v => v.label.toLowerCase() == voice.toLowerCase()) + if (v == null) + return new NextResponse("Bad Request", { status: 400 }); + + await db.user.update({ + where: { + id: user.id + }, + data: { + ttsDefaultVoice: Number.parseInt(v.value) + } + }); + + return new NextResponse("", { status: 200 }); + } catch (error) { + console.log("[TTS/FILTER/USER]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/settings/tts/filter/users/route.ts b/app/api/settings/tts/filter/users/route.ts index 81cdf11..ef17317 100644 --- a/app/api/settings/tts/filter/users/route.ts +++ b/app/api/settings/tts/filter/users/route.ts @@ -17,7 +17,7 @@ export async function GET(req: Request) { return NextResponse.json(filters); } catch (error) { - console.log("[TTS/FILTER/USER]", error); + console.log("[TTS/FILTER/USERS]", error); return new NextResponse("Internal Error", { status: 500 }); } } @@ -35,7 +35,7 @@ export async function POST(req: Request) { where: { userId_username: { userId: user.id as string, - username + username: username.toLowerCase() } }, update: { @@ -43,14 +43,14 @@ export async function POST(req: Request) { }, create: { userId: user.id as string, - username, + username: username.toLowerCase(), tag } }); return NextResponse.json(filter); } catch (error) { - console.log("[TTS/FILTER/USER]", error); + console.log("[TTS/FILTER/USERS]", error); return new NextResponse("Internal Error", { status: 500 }); } } @@ -76,7 +76,7 @@ export async function DELETE(req: Request) { return NextResponse.json(filter) } catch (error) { - console.log("[TTS/FILTER/USER]", error); + console.log("[TTS/FILTER/USERS]", error); return new NextResponse("Internal Error", { status: 500 }); } } \ No newline at end of file diff --git a/app/api/settings/tts/filter/words/route.ts b/app/api/settings/tts/filter/words/route.ts new file mode 100644 index 0000000..88c8029 --- /dev/null +++ b/app/api/settings/tts/filter/words/route.ts @@ -0,0 +1,111 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserUsingAPI from "@/lib/validate-api"; + +export async function GET(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const filters = await db.ttsWordFilter.findMany({ + where: { + userId: user.id + } + }); + + return NextResponse.json(filters); + } catch (error) { + console.log("[TTS/FILTER/WORDS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function POST(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { search, replace } = await req.json(); + + const filter = await db.ttsWordFilter.create({ + data: { + search, + replace, + userId: user.id as string + } + }); + + return NextResponse.json(filter); + } catch (error) { + console.log("[TTS/FILTER/WORDS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function PUT(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { id, search, replace } = await req.json(); + + const filter = await db.ttsWordFilter.update({ + where: { + id + }, + data: { + search, + replace + } + }); + + return NextResponse.json(filter); + } catch (error) { + console.log("[TTS/FILTER/WORDS]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function DELETE(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { searchParams } = new URL(req.url) + const id = searchParams.get('id') as string + const search = searchParams.get('search') as string + + if (search) { + const filter = await db.ttsWordFilter.delete({ + where: { + userId_search: { + userId: user.id as string, + search + } + } + }); + + return NextResponse.json(filter) + } else if (id) { + const filter = await db.ttsWordFilter.delete({ + where: { + id: id + } + }); + + return NextResponse.json(filter) + } + return new NextResponse("Bad Request", { status: 400 }); + } catch (error) { + console.log("[TTS/FILTER/WORDS]", error); + return new NextResponse("Internal Error" + error, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/settings/tts/route.ts b/app/api/settings/tts/route.ts new file mode 100644 index 0000000..58ce379 --- /dev/null +++ b/app/api/settings/tts/route.ts @@ -0,0 +1,66 @@ +import { db } from "@/lib/db" +import { NextResponse } from "next/server"; +import fetchUserUsingAPI from "@/lib/validate-api"; +import voices from "@/data/tts"; + +export async function GET(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const u = await db.user.findFirst({ + where: { + id: user.id + } + }); + + let list : { + value: string; + label: string; + gender: string; + language: string; + }[] = [] + const enabled = u?.ttsEnabledVoice ?? 0 + for (let i = 0; i < voices.length; i++) { + var v = voices[i] + var n = Number.parseInt(v.value) - 1 + if ((enabled & (1 << n)) > 0) { + list.push(v) + } + } + + return NextResponse.json(list); + } catch (error) { + console.log("[TTS/FILTER/USER]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} + +export async function POST(req: Request) { + try { + const user = await fetchUserUsingAPI(req) + if (!user) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + let { voice } = await req.json(); + console.log(voice) + voice = voice & ((1 << voices.length) - 1) + + await db.user.update({ + where: { + id: user.id + }, + data: { + ttsEnabledVoice: voice + } + }); + + return new NextResponse("", { status: 200 }); + } catch (error) { + console.log("[TTS/FILTER/USER]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/tokens/route.ts b/app/api/tokens/route.ts index 6a3ed46..9199d81 100644 --- a/app/api/tokens/route.ts +++ b/app/api/tokens/route.ts @@ -14,8 +14,6 @@ export async function GET(req: Request) { } } - console.log("TOKEN KEY:", userId) - const tokens = await db.apiKey.findMany({ where: { userId: userId as string diff --git a/app/page.tsx b/app/page.tsx index e8b49ff..9934004 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import { redirect } from "next/navigation"; import Link from "next/link"; -import { useSession, signIn, signOut } from "next-auth/react"; +import { signOut, useSession } from "next-auth/react"; import { useEffect, useState } from "react"; import axios from "axios"; @@ -43,16 +43,13 @@ export default function Home() { }} >
- -

NextAuth.js

- {session && ( signOut()} className="btn-signin"> Sign out )} {!session && ( - signIn()} className="btn-signin"> + Sign in )} diff --git a/app/settings/tts/filters/page.tsx b/app/settings/tts/filters/page.tsx index 2a11534..4f5667d 100644 --- a/app/settings/tts/filters/page.tsx +++ b/app/settings/tts/filters/page.tsx @@ -2,19 +2,12 @@ import axios from "axios"; import * as React from 'react'; -import { Calendar, Check, ChevronsUpDown, MoreHorizontal, Plus, Tags, Trash, User } from "lucide-react" -import { ApiKey, TtsUsernameFilter, TwitchConnection } from "@prisma/client"; +import { InfoIcon, MoreHorizontal, Plus, Save, Tags, Trash } from "lucide-react" import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; -import Link from "next/link"; -import { cn } from "@/lib/utils"; -import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; @@ -23,14 +16,13 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; import { Label } from "@/components/ui/label"; -import { db } from "@/lib/db"; + const TTSFiltersPage = () => { const { data: session, status } = useSession(); - const [loading, setLoading] = useState(true) - const [moreOpen, setMoreOpen] = useState(0) + const [moreOpen, setMoreOpen] = useState(0) const [tag, setTag] = useState("blacklisted") - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(false) const [userTags, setUserTag] = useState<{ username: string, tag: string }[]>([]) const router = useRouter(); @@ -42,11 +34,10 @@ const TTSFiltersPage = () => { // Username blacklist const usernameFilteredFormSchema = z.object({ //userId: z.string().trim().min(1), - username: z.string().trim().min(4).max(25), //.regex(new RegExp("[a-zA-Z0-9][a-zA-Z0-9_]{3, 24}"), "Must be a valid twitch username.") + username: z.string().trim().min(4).max(25).regex(new RegExp("[a-zA-Z0-9][a-zA-Z0-9\_]{3,24}"), "Must be a valid twitch username."), tag: z.string().trim() }); - const usernameFilteredForm = useForm({ resolver: zodResolver(usernameFilteredFormSchema), defaultValues: { @@ -59,11 +50,19 @@ const TTSFiltersPage = () => { useEffect(() => { const fetchData = async () => { try { - const userFiltersData = await axios.get("/api/settings/tts/filter/users"); + const userFiltersData = await axios.get("/api/settings/tts/filter/users") setUserTag(userFiltersData.data ?? []) } catch (error) { console.log("ERROR", error) } + + try { + const replacementData = await axios.get("/api/settings/tts/filter/words") + console.log(replacementData.data) + setReplacements(replacementData.data ?? []) + } catch (e) { + console.log("ERROR", e) + } }; fetchData().catch(console.error); @@ -77,14 +76,22 @@ const TTSFiltersPage = () => { }).catch((e) => console.error(e)) } - const isLoading = usernameFilteredForm.formState.isSubmitting; + const isSubmitting = usernameFilteredForm.formState.isSubmitting; - const onAdd = (values: z.infer) => { + const onAddExtended = (values: z.infer, test: boolean = true) => { try { - values.tag = tag + const original = userTags.find(u => u.username.toLowerCase() == values.username.toLowerCase()) + + if (test) + values.tag = tag + axios.post("/api/settings/tts/filter/users", values) .then((d) => { - userTags.push({ username: values.username, tag: tag }) + if (original == null) { + userTags.push({ username: values.username.toLowerCase(), tag: values.tag }) + } else { + original.tag = values.tag + } setUserTag(userTags) usernameFilteredForm.reset(); @@ -92,30 +99,89 @@ const TTSFiltersPage = () => { }) } catch (error) { console.log("[TTS/FILTERS/USER]", error); - return; } } + const onAdd = (values: z.infer) => { + onAddExtended(values, true) + } + + // Word replacement + const [replacements, setReplacements] = useState<{ id: string, search: string, replace: string, userId: string }[]>([]) + + const onReplaceAdd = async () => { + await axios.post("/api/settings/tts/filter/words", { search, replace }) + .then(d => { + replacements.push(d.data) + setReplacements(replacements) + }).catch(e => { + // TODO: handle already exist. + console.log("LOGGED:", e) + return null + }) + } + + const onReplaceUpdate = async (data: { id: string, search: string, replace: string, userId: string }) => { + await axios.put("/api/settings/tts/filter/words", data) + .then(d => { + //setReplacements(replacements.filter(r => r.id != id)) + }).catch(e => { + // TODO: handle does not exist. + console.log("LOGGED:", e) + return null + }) + } + + const onReplaceDelete = async (id: string) => { + await axios.delete("/api/settings/tts/filter/words?id=" + id) + .then(d => { + setReplacements(replacements.filter(r => r.id != id)) + }).catch(e => { + // TODO: handle does not exist. + console.log("LOGGED:", e) + return null + }) + } + + // const replaceFormSchema = z.object({ + // //userId: z.string().trim().min(1), + // search: z.string().trim().min(1), + // replace: z.string().trim().min(0), + // }); + + // const replaceForm = useForm({ + // resolver: zodResolver(replaceFormSchema), + // defaultValues: { + // //userId: session?.user?.id ?? "", + // search: "", + // replace: "" + // } + // }); + + let [search, setSearch] = useState("") + let [replace, setReplace] = useState("") + let [searchInfo, setSearchInfo] = useState("") + return (
TTS Filters
-
-
+
+
{userTags.map((user, index) => ( -
+

{user.tag} {user.username}

- 0} onOpenChange={() => setMoreOpen((v) => v ^ (1 << index))}> + 0} onOpenChange={() => setMoreOpen(v => v ^ (1 << index))}> - - + Actions @@ -137,8 +203,7 @@ const TTSFiltersPage = () => { key={tag} value={tag} onSelect={(value) => { - userTags[index].tag = value - setUserTag(userTags) + onAddExtended({ username: userTags[index].username, tag: value}, false) setMoreOpen(0) }} > @@ -162,8 +227,8 @@ const TTSFiltersPage = () => { ))}
-
-
); diff --git a/app/settings/tts/voices/page.tsx b/app/settings/tts/voices/page.tsx index 332eb1f..b406218 100644 --- a/app/settings/tts/voices/page.tsx +++ b/app/settings/tts/voices/page.tsx @@ -3,95 +3,103 @@ import axios from "axios"; import * as React from 'react'; import { Check, ChevronsUpDown } from "lucide-react" -import { ApiKey, TwitchConnection, User } from "@prisma/client"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; -import Link from "next/link"; import { cn } from "@/lib/utils"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import voices from "@/data/tts"; const TTSFiltersPage = () => { const { data: session, status } = useSession(); const [loading, setLoading] = useState(true) const [open, setOpen] = useState(false) - const [value, setValue] = useState("") + const [value, setValue] = useState(0) + const [enabled, setEnabled] = useState(0) - const voices = [ - { - value: "brian", - label: "Brian", - gender: "Male", - language: "en" - }, - { - value: "sveltekit", - label: "SvelteKit", - gender: "Male" - }, - { - value: "nuxt.js", - label: "Nuxt.js", - gender: "Male", - language: "en" - }, - { - value: "remix", - label: "Remix", - gender: "Male", - language: "en" - }, - { - value: "astro", - label: "Astro", - gender: "Male", - language: "en" - }, - ] + useEffect(() => { + axios.get("/api/settings/tts/default") + .then((voice) => { + setValue(Number.parseInt(voice.data.value)) + }) + + axios.get("/api/settings/tts") + .then((d) => { + const total = d.data.reduce((acc: number, item: {value: number, label: string, gender: string, language: string}) => acc |= 1 << (item.value - 1), 0) + setEnabled(total) + }) + }, []) + + const onDefaultChange = (voice: string) => { + try { + axios.post("/api/settings/tts/default", { voice }) + .then(d => { + console.log(d) + }) + .catch(e => console.error(e)) + } catch (error) { + console.log("[TTS/DEFAULT]", error); + return; + } + } + + const onEnabledChanged = (val: number) => { + try { + axios.post("/api/settings/tts", { voice: val }) + .then(d => { + console.log(d) + }) + .catch(e => console.error(e)) + } catch (error) { + console.log("[TTS]", error); + return; + } + } return (
TTS Voices
-
-
-
Default Voice
- +
+
+
+
Default Voice
+ +
- No framework found. + No voices found. {voices.map((voice) => ( { - setValue(currentValue === value ? "" : currentValue) + setValue(Number.parseInt(currentValue)) + onDefaultChange(voice.label) setOpen(false) }} > {voice.label} @@ -102,6 +110,23 @@ const TTSFiltersPage = () => {
+ +
+

Voices Enabled

+
+ {voices.map((v, i) => ( +
+ { + const newVal = enabled ^ (1 << (Number.parseInt(v.value) - 1)) + setEnabled(newVal) + onEnabledChanged(newVal) + }} + checked={(enabled & (1 << (Number.parseInt(v.value) - 1))) > 0} /> +
{v.label}
+
+ ))} +
+
); diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 0ba4277..2252cf4 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -21,6 +21,7 @@ const buttonVariants = cva( }, size: { default: "h-10 px-4 py-2", + xs: "h-7 rounded-md px-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", diff --git a/data/tts.ts b/data/tts.ts new file mode 100644 index 0000000..09d54be --- /dev/null +++ b/data/tts.ts @@ -0,0 +1,126 @@ +let voices_data = [ + { + value: "1", + label: "Brian", + gender: "Male", + language: "en" + }, + { + value: "2", + label: "Amy", + gender: "Female", + language: "en" + }, + { + value: "3", + label: "Emma", + gender: "Female", + language: "en" + }, + { + value: "4", + label: "Geraint", + gender: "Male", + language: "en" + }, + { + value: "5", + label: "Russel", + gender: "Male", + language: "en" + }, + { + value: "6", + label: "Nicole", + gender: "Female", + language: "en" + }, + { + value: "7", + label: "Joey", + gender: "Male", + language: "en" + }, + { + value: "8", + label: "Justin", + gender: "Male", + language: "en" + }, + { + value: "9", + label: "Matthew", + gender: "Male", + language: "en" + }, + { + value: "10", + label: "Ivy", + gender: "Female", + language: "en" + }, + { + value: "11", + label: "Joanna", + gender: "Female", + language: "en" + }, + { + value: "12", + label: "Kendra", + gender: "Female", + language: "en" + }, + { + value: "13", + label: "Kimberly", + gender: "Female", + language: "en" + }, + { + value: "14", + label: "Salli", + gender: "Female", + language: "en" + }, + { + value: "15", + label: "Raveena", + gender: "Female", + language: "en" + }, + { + value: "16", + label: "Carter", + gender: "Male", + language: "en" + }, + { + value: "17", + label: "Paul", + gender: "Male", + language: "en" + }, + { + value: "18", + label: "Evelyn", + gender: "Female", + language: "en" + }, + { + value: "19", + label: "Liam", + gender: "Male", + language: "en" + }, + { + value: "20", + label: "Jasmine", + gender: "Female", + language: "en" + }, +] + +const voices = voices_data.sort((a, b) => a.label < b.label ? -1 : a.label > b.label ? 1 : 0) + +export default voices \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f46b87b..281516c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,6 +14,8 @@ model User { email String? @unique emailVerified DateTime? image String? + ttsDefaultVoice Int @default(1) + ttsEnabledVoice Int @default(1048575) apiKeys ApiKey[] accounts Account[] @@ -77,12 +79,13 @@ model TtsUsernameFilter { } model TtsWordFilter { + id String @id @default(cuid()) search String replace String userId String - profile User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) - @@id([userId, search]) + @@unique([userId, search]) } \ No newline at end of file