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
+
+
- {value
- ? voices.find((voice) => voice.value === value)?.label
- : "Select voice..."}
+ className="w-[200px] justify-between">
+ {value ? voices.find(v => Number.parseInt(v.value) == value)?.label : "Select 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