diff --git a/app/api/media_files/GetMediaByUser.ts b/app/api/media_files/GetMediaByUser.ts index 4ea5c95f6d..fa267b3f86 100644 --- a/app/api/media_files/GetMediaByUser.ts +++ b/app/api/media_files/GetMediaByUser.ts @@ -1,6 +1,5 @@ import MakeRequest from "../MakeRequest"; -import { MediaFile } from "./GetMedia"; -import { Pagination } from "../_common/SharedFetchTypes"; +import { MediaFile, Pagination } from "~/pages/PageEnigma/models"; export interface GetMediaRequest {} @@ -12,6 +11,7 @@ export interface GetMediaListResponse { export interface GetMediaParams { page_index: number; + filter_media_type: string; } export const GetMediaByUser = MakeRequest< @@ -23,4 +23,4 @@ export const GetMediaByUser = MakeRequest< method: "GET", routingFunction: (userToken: string) => `/v1/media_files/list/user/${userToken}`, -}); \ No newline at end of file +}); diff --git a/app/modules/TopBar/MyMoviesButton.tsx b/app/modules/TopBar/MyMoviesButton.tsx index 62edec8b81..04763172c3 100644 --- a/app/modules/TopBar/MyMoviesButton.tsx +++ b/app/modules/TopBar/MyMoviesButton.tsx @@ -1,14 +1,23 @@ import { faFilm } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button } from "~/components"; -import { activeJobs, viewMyMovies } from "~/pages/PageEnigma/store"; +import { + activeJobs, + generateMovieId, + viewMyMovies, +} from "~/pages/PageEnigma/store"; export const MyMoviesButton = () => { const activeCount = activeJobs.value.jobs.length; return (
- diff --git a/app/pages/PageEnigma/comps/GenerateModals/GenerateModals.tsx b/app/pages/PageEnigma/comps/GenerateModals/GenerateModals.tsx new file mode 100644 index 0000000000..abea8b57bd --- /dev/null +++ b/app/pages/PageEnigma/comps/GenerateModals/GenerateModals.tsx @@ -0,0 +1,35 @@ +import { TransitionDialogue } from "~/components"; +import { generateMovieId, viewMyMovies } from "~/pages/PageEnigma/store"; +import { faFilm } from "@fortawesome/pro-solid-svg-icons"; +import { useContext, useEffect, useState } from "react"; +import { MyMovies } from "~/pages/PageEnigma/comps/GenerateModals/MyMovies"; +import { Sharing } from "~/pages/PageEnigma/comps/GenerateModals/Sharing"; +import { useSignals } from "@preact/signals-react/runtime"; +import dayjs from "dayjs"; +import { MediaFile } from "~/pages/PageEnigma/models"; +import { GetMediaFileByToken } from "~/pages/PageEnigma/comps/SidePanelTabs/AudioTab/utilities"; +import { ToasterContext, ToastTypes } from "~/contexts/ToasterContext"; + +export function GenerateModals() { + useSignals(); + const [movieId, setMovieId] = useState(generateMovieId.value); + const [mediaFile, setMediaFile] = useState(null); + const { addToast } = useContext(ToasterContext); + + useEffect(() => { + if (movieId) { + GetMediaFileByToken(movieId).then((res) => { + if (!res.success) { + addToast(ToastTypes.ERROR, "Unable to read media file"); + return; + } + setMediaFile(res.media_file ?? null); + }); + } + }, [movieId, addToast]); + + if (!mediaFile) { + return ; + } + return ; +} diff --git a/app/pages/PageEnigma/comps/MyMovies/InProgressCard.tsx b/app/pages/PageEnigma/comps/GenerateModals/InProgressCard.tsx similarity index 100% rename from app/pages/PageEnigma/comps/MyMovies/InProgressCard.tsx rename to app/pages/PageEnigma/comps/GenerateModals/InProgressCard.tsx diff --git a/app/pages/PageEnigma/comps/MyMovies/MyMovies.tsx b/app/pages/PageEnigma/comps/GenerateModals/MyMovies.tsx similarity index 61% rename from app/pages/PageEnigma/comps/MyMovies/MyMovies.tsx rename to app/pages/PageEnigma/comps/GenerateModals/MyMovies.tsx index 8df6101e4d..7a0d1ee3e0 100644 --- a/app/pages/PageEnigma/comps/MyMovies/MyMovies.tsx +++ b/app/pages/PageEnigma/comps/GenerateModals/MyMovies.tsx @@ -1,11 +1,20 @@ -import { TransitionDialogue } from "~/components"; -import { activeJobs, movies, viewMyMovies } from "~/pages/PageEnigma/store"; -import { CompletedCard } from "~/pages/PageEnigma/comps/MyMovies/CompletedCard"; -import { InProgressCard } from "~/pages/PageEnigma/comps/MyMovies/InProgressCard"; +import { + activeJobs, + generateMovieId, + movies, + viewMyMovies, +} from "~/pages/PageEnigma/store"; +import { CompletedCard } from "~/pages/PageEnigma/comps/GenerateModals/CompletedCard"; +import { InProgressCard } from "~/pages/PageEnigma/comps/GenerateModals/InProgressCard"; import { useSignals } from "@preact/signals-react/runtime"; import { faFilm } from "@fortawesome/pro-solid-svg-icons"; +import { TransitionDialogue } from "~/components"; -export function MyMovies() { +interface Props { + setMovieId: (page: string) => void; +} + +export function MyMovies({ setMovieId }: Props) { useSignals(); return ( @@ -15,7 +24,10 @@ export function MyMovies() { className="max-w-4xl" childPadding={false} isOpen={viewMyMovies.value} - onClose={() => (viewMyMovies.value = false)}> + onClose={() => { + setMovieId(""); + viewMyMovies.value = false; + }}>
{activeJobs.value.jobs.length > 0 && (
@@ -29,7 +41,11 @@ export function MyMovies() {
Completed
{movies.value.movies.map((movie) => ( - + ))}
diff --git a/app/pages/PageEnigma/comps/GenerateModals/Sharing.tsx b/app/pages/PageEnigma/comps/GenerateModals/Sharing.tsx new file mode 100644 index 0000000000..81b52bf847 --- /dev/null +++ b/app/pages/PageEnigma/comps/GenerateModals/Sharing.tsx @@ -0,0 +1,133 @@ +import { useSignals } from "@preact/signals-react/runtime"; +import { useLayoutEffect, useState } from "react"; +import { MediaFile } from "~/pages/PageEnigma/models"; +import { Button, Input, TransitionDialogue } from "~/components"; +import { + faArrowDownToLine, + faFilm, + faLink, +} from "@fortawesome/pro-solid-svg-icons"; +import SocialButton from "./SocialButton"; +import { generateMovieId, viewMyMovies } from "~/pages/PageEnigma/store"; +import dayjs from "dayjs"; +import { environmentVariables } from "~/store"; + +interface Props { + mediaFile: MediaFile; + setMediaFile: (file: MediaFile | null) => void; +} + +export function Sharing({ mediaFile, setMediaFile }: Props) { + useSignals(); + const shareUrl = `https://storyteller.ai/media/${mediaFile?.token || ""}`; + const shareText = "Check out this media on StoryTeller.ai"; + const [buttonLabel, setButtonLabel] = useState("Copy"); + const downloadLink = `${environmentVariables.value.GOOGLE_API}/vocodes-public${mediaFile?.public_bucket_path}`; + const openUrl = `/media/${mediaFile.token}`; + + const handleCopyLink = () => { + if (navigator.clipboard) { + navigator.clipboard.writeText(shareUrl); + } + setButtonLabel("Copied!"); + setTimeout(() => setButtonLabel("Copy"), 2000); + }; + + const generateTitle = () => { + return ( +
+ + {mediaFile?.maybe_title ?? mediaFile?.token} + + + {dayjs(mediaFile?.updated_at).format("MMM DD, YYYY HH:mm:SS")} + +
+ ); + }; + + return ( + { + if (generateMovieId.value) { + viewMyMovies.value = false; + return; + } + setMediaFile(null); + }}> +
+
+ +
+
+
Share movie to
+
+ + + + + +
+
+
+ +
+ + +
+ + +
+
+
+ ); +} diff --git a/app/pages/PageEnigma/comps/GenerateModals/SocialButton.tsx b/app/pages/PageEnigma/comps/GenerateModals/SocialButton.tsx new file mode 100644 index 0000000000..69747d5c92 --- /dev/null +++ b/app/pages/PageEnigma/comps/GenerateModals/SocialButton.tsx @@ -0,0 +1,81 @@ +import { + faFacebookF, + faRedditAlien, + faWhatsapp, + faTwitter, +} from "@fortawesome/free-brands-svg-icons"; +import { faXmark, faEnvelope } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface SocialButtonProps { + social: "x" | "whatsapp" | "facebook" | "reddit" | "email"; + shareUrl: string; + shareText: string; +} + +export default function SocialButton({ + social, + shareUrl, + shareText, +}: SocialButtonProps) { + const socialIcons = { + x: faTwitter, + whatsapp: faWhatsapp, + facebook: faFacebookF, + reddit: faRedditAlien, + email: faEnvelope, + }; + + const socialIcon = socialIcons[social] || faXmark; + + const getShareLink = (social: string, url: string, text: string) => { + switch (social) { + case "x": + return `https://twitter.com/intent/tweet?url=${encodeURIComponent( + url, + )}&text=${encodeURIComponent(text)}`; + case "facebook": + return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent( + url, + )}`; + case "whatsapp": + return `https://wa.me/?text=${encodeURIComponent( + text, + )} ${encodeURIComponent(url)}`; + case "reddit": + return `https://www.reddit.com/submit?url=${encodeURIComponent( + url, + )}&title=${encodeURIComponent(text)}`; + case "email": + return `mailto:?subject=${encodeURIComponent( + text, + )}&body=${encodeURIComponent(url)}`; + default: + return "#"; + } + }; + + const handleClick = () => { + const shareLink = getShareLink(social, shareUrl, shareText); + window.open(shareLink, "_blank"); + }; + + return ( + + ); +} diff --git a/app/pages/PageEnigma/comps/MyMovies/listMoviesByUser.tsx b/app/pages/PageEnigma/comps/GenerateModals/listMoviesByUser.tsx similarity index 100% rename from app/pages/PageEnigma/comps/MyMovies/listMoviesByUser.tsx rename to app/pages/PageEnigma/comps/GenerateModals/listMoviesByUser.tsx diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTab/utilities.ts b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTab/utilities.ts index bf97ec778b..fa3f7c23c2 100644 --- a/app/pages/PageEnigma/comps/SidePanelTabs/AudioTab/utilities.ts +++ b/app/pages/PageEnigma/comps/SidePanelTabs/AudioTab/utilities.ts @@ -1,4 +1,11 @@ -import { listTts, listV2V, inferTts, listMediaByUser, getMediaFileByToken, inferV2V } from '~/api'; +import { + listTts, + listV2V, + inferTts, + listMediaByUser, + getMediaFileByToken, + inferV2V, +} from "~/api"; import { MediaFile, GetMediaFileResponse, @@ -6,7 +13,7 @@ import { VoiceConversionModelListResponse, EnqueueVoiceConversionRequest, EnqueueVoiceConversionResponse, -} from './typesImported'; +} from "./typesImported"; import { TtsModelListItem, TtsModelListResponsePayload, @@ -14,51 +21,59 @@ import { StatusLike, GenerateTtsAudioRequest, GenerateTtsAudioResponse, -} from '~/pages/PageEnigma/models/tts'; +} from "~/pages/PageEnigma/models/tts"; +import { STORAGE_KEYS } from "~/contexts/Authentication/types"; - - -export const ListAudioByUser = async(username:string, sessionToken: string) => { - return await fetch(listMediaByUser(username),{ - method: 'GET', +export const ListAudioByUser = async ( + username: string, + sessionToken: string, +) => { + return await fetch(listMediaByUser(username), { + method: "GET", headers: { - "Accept": "application/json", - 'session': sessionToken, + Accept: "application/json", + session: sessionToken, }, }) - .then(res => res.json()) - .then(res => { - if(res.success && res.results){ - return res.results.filter((item:MediaFile)=>item['media_type']==='audio'); - }else{ - Promise.reject(); - } - }) - .catch(e => ({ success : false })); -} + .then((res) => res.json()) + .then((res) => { + if (res.success && res.results) { + return res.results.filter( + (item: MediaFile) => item["media_type"] === "audio", + ); + } else { + Promise.reject(); + } + }) + .catch(() => ({ success: false })); +}; -export async function ListTtsModels(sessionToken:string) : Promise| undefined> { +export async function ListTtsModels( + sessionToken: string, +): Promise | undefined> { return await fetch(listTts, { - method: 'GET', + method: "GET", headers: { - 'Accept': 'application/json', - 'session': sessionToken, + Accept: "application/json", + session: sessionToken, }, }) - .then(res => res.json()) - .then(res => { - const response : TtsModelListResponsePayload = res; - if (!response.success) { - return; - } - return response?.models; - }) - .catch(e => { - return undefined; - }); + .then((res) => res.json()) + .then((res) => { + const response: TtsModelListResponsePayload = res; + if (!response.success) { + return; + } + return response?.models; + }) + .catch(() => { + return undefined; + }); } -export function maybeMapError(statuslike: StatusLike) : GenerateTtsAudioErrorType | undefined { +export function maybeMapError( + statuslike: StatusLike, +): GenerateTtsAudioErrorType | undefined { switch (statuslike.status) { case 400: return GenerateTtsAudioErrorType.BadRequest; @@ -71,121 +86,124 @@ export function maybeMapError(statuslike: StatusLike) : GenerateTtsAudioErrorTyp } } -export async function GenerateTtsAudio(request: GenerateTtsAudioRequest, sessionToken:string) : Promise -{ +export async function GenerateTtsAudio( + request: GenerateTtsAudioRequest, + sessionToken: string, +): Promise { return await fetch(inferTts, { - method: 'POST', + method: "POST", headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'session': sessionToken, + Accept: "application/json", + "Content-Type": "application/json", + session: sessionToken, }, body: JSON.stringify(request), }) - .then(res => res.json()) - .then(res => { - if (!('inference_job_token' in res)) { + .then((res) => res.json()) + .then((res) => { + if (!("inference_job_token" in res)) { + return { error: GenerateTtsAudioErrorType.UnknownError }; + } + const ret: GenerateTtsAudioResponse = { + success: true, + inference_job_token: res.inference_job_token, + inference_job_token_type: res.inference_job_token_type, + }; + return ret; + }) + .catch((e) => { + const maybeError = maybeMapError(e); + if (maybeError !== undefined) { + return { error: maybeError }; + } return { error: GenerateTtsAudioErrorType.UnknownError }; - } - const ret:GenerateTtsAudioResponse = { - success: true, - inference_job_token: res.inference_job_token, - inference_job_token_type: res.inference_job_token_type, - } - return ret; - }) - .catch(e => { - let maybeError = maybeMapError(e); - if (maybeError !== undefined) { - return { error: maybeError }; - } - return { error: GenerateTtsAudioErrorType.UnknownError }; - }); + }); +} -}; - -export async function GetMediaFileByToken (fileToken: string, sessionToken: string) : Promise -{ +export async function GetMediaFileByToken( + fileToken: string, +): Promise { + const sessionToken = localStorage.getItem(STORAGE_KEYS.SESSION_TOKEN) ?? ""; return await fetch(getMediaFileByToken(fileToken), { method: "GET", headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'session': sessionToken, + Accept: "application/json", + "Content-Type": "application/json", + session: sessionToken, }, }) - .then(res => res.json()) - .then(res => { - const response: GetMediaFileResponse = res; + .then((res) => res.json()) + .then((res) => { + const response: GetMediaFileResponse = res; if (response && response.success && response.media_file) { // NB: Timestamps aren't converted to Date objects on their own! response.media_file.created_at = new Date( - response.media_file.created_at + response.media_file.created_at, ); response.media_file.updated_at = new Date( - response.media_file.updated_at + response.media_file.updated_at, ); return response; } else { return { success: false }; } - }) - .catch(e => { - return { success: false }; - }); - ; + }) + .catch(() => { + return { success: false }; + }); } -export async function ListVoiceConversionModels(sessionToken: string) : Promise| undefined> { +export async function ListVoiceConversionModels( + sessionToken: string, +): Promise | undefined> { return await fetch(listV2V, { - method: 'GET', + method: "GET", headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'session': sessionToken, + Accept: "application/json", + "Content-Type": "application/json", + session: sessionToken, }, }) - .then(res => res.json()) - .then(res => { - const response : VoiceConversionModelListResponse = res; - if (!response.success) { - return; - } - return response?.models; - }) - .catch(e => { - return undefined; - }); + .then((res) => res.json()) + .then((res) => { + const response: VoiceConversionModelListResponse = res; + if (!response.success) { + return; + } + return response?.models; + }) + .catch(() => { + return undefined; + }); } export async function GenerateVoiceConversion( request: EnqueueVoiceConversionRequest, sessionToken: string, -) : Promise -{ +): Promise { return await fetch(inferV2V, { - method: 'POST', + method: "POST", headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'session': sessionToken, + Accept: "application/json", + "Content-Type": "application/json", + session: sessionToken, }, body: JSON.stringify(request), }) - .then(res => res.json()) - .then(res => { - if (!res) { - return { success : false }; - } + .then((res) => res.json()) + .then((res) => { + if (!res) { + return { success: false }; + } - if (res && 'success' in res) { - return res; - } else { - return { success : false }; - } - }) - .catch(e => { - return { success : false }; - }); -} \ No newline at end of file + if (res && "success" in res) { + return res; + } else { + return { success: false }; + } + }) + .catch(() => { + return { success: false }; + }); +} diff --git a/app/pages/PageEnigma/models/index.ts b/app/pages/PageEnigma/models/index.ts index c1883d2c3a..9d79656614 100644 --- a/app/pages/PageEnigma/models/index.ts +++ b/app/pages/PageEnigma/models/index.ts @@ -2,6 +2,7 @@ export * from "./assets"; export * from "./inferenceJobs"; export * from "./message"; export * from "./movies"; +export * from "./pagination"; export * from "./sidePanel"; export * from "./track"; export * from "./types"; diff --git a/app/pages/PageEnigma/models/pagination.ts b/app/pages/PageEnigma/models/pagination.ts new file mode 100644 index 0000000000..3bb9fccc2d --- /dev/null +++ b/app/pages/PageEnigma/models/pagination.ts @@ -0,0 +1,4 @@ +export interface Pagination { + current: number; + total_page_count: number; +} diff --git a/app/pages/PageEnigma/store/myMovies.ts b/app/pages/PageEnigma/store/myMovies.ts index 50e600a06a..97851861cc 100644 --- a/app/pages/PageEnigma/store/myMovies.ts +++ b/app/pages/PageEnigma/store/myMovies.ts @@ -3,6 +3,7 @@ import { ActiveJob } from "~/pages/PageEnigma/models"; import { MediaInfo } from "~/pages/PageEnigma/models/movies"; export const viewMyMovies = signal(false); +export const generateMovieId = signal(""); export const activeJobs = signal<{ jobs: ActiveJob[] }>({ jobs: [] }); export const movies = signal<{ movies: MediaInfo[] }>({ movies: [] }); diff --git a/package-lock.json b/package-lock.json index 9f6638f6da..9aae8332fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@ffmpeg/ffmpeg": "0.11.6", "@ffmpeg/util": "0.12.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/pro-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.18", @@ -1334,6 +1335,27 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.5.2", + "resolved": "https://npm.fontawesome.com/@fortawesome/free-brands-svg-icons/-/6.5.2/free-brands-svg-icons-6.5.2.tgz", + "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.5.2/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/pro-solid-svg-icons": { "version": "6.5.1", "resolved": "https://npm.fontawesome.com/@fortawesome/pro-solid-svg-icons/-/6.5.1/pro-solid-svg-icons-6.5.1.tgz", diff --git a/package.json b/package.json index 55acfa3f38..66dcfebcf0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@ffmpeg/ffmpeg": "0.11.6", "@ffmpeg/util": "0.12.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/pro-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.18", diff --git a/tailwind.config.ts b/tailwind.config.ts index 6e03f73bd7..ff630e1140 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -147,7 +147,7 @@ export default { selectedTab: "#55556B", }, - ui:{ + ui: { background: "#1a1a27", panel: "#242433", "panel-border": "#393948", @@ -156,20 +156,20 @@ export default { divider: "#515168", }, - media:{ + media: { "audio-tts": "#6AAA88", "audio-v2v": "#6691B9", "audio-upload": "#9760C2", - "audio-demo" : "#B96675", + "audio-demo": "#B96675", "is-new": "#FFDC26", }, - inference:{ + inference: { pending: "#6b728b", generating: "#46527C", error: "#924C4B", }, - + "axis-x": "#D33242", "axis-y": "#308752", "axis-z": "#2E70FF", @@ -177,6 +177,12 @@ export default { green: "#308752", blue: "#2E70FF", + facebook: "#4267b2", + reddit: "#ff5700", + whatsapp: "#25d366", + x: "#1da1f2", + email: "#858585", + white: colors.white, gray: colors.gray, black: colors.black, @@ -196,5 +202,10 @@ export default { "bg-object-selected", "bg-object-unselected", "bg-object-clip", + "bg-facebook", + "bg-reddit", + "bg-whatsapp", + "bg-x", + "bg-email", ], } satisfies Config;