diff --git a/services/backend/generated/types/index.ts b/services/backend/generated/types/index.ts new file mode 100644 index 0000000..d6564ad --- /dev/null +++ b/services/backend/generated/types/index.ts @@ -0,0 +1,64 @@ +// Code generated by tygo. DO NOT EDIT. + +////////// +// source: job.go + +export interface DownloadJob { + ID: string; + URL: string; + TIMESTAMP: string /* RFC3339 */; +} +export interface JobData { + ID: string; + JobID: string; + URL: string; + IsPlaylist: boolean; + STATUS: string; + PROGRESS: number /* int */; + CreatedAt: string /* RFC3339 */; + UpdatedAt: string /* RFC3339 */; +} + +////////// +// source: metadata.go + +export interface VideoMetadata { + id: string; + title: string; + uploader: string; + filesize: number /* int64 */; + duration: number /* float64 */; + format: string; + thumbnail: string; +} +export interface PlaylistMetadata { + id: string; + title: string; + description: string; +} + +////////// +// source: video.go + +/** + * Video represents metadata for a single video. + */ +export interface Video { + id: number /* int */; // Primary key in database + job_id: string; // ID associated with the download job + title: string; // Title of the video + uploader: string; // Uploader or channel name + file_path: string; // Path where the video file is stored + last_downloaded_at: string /* RFC3339 */; // Timestamp of when the video was last downloaded + length: number /* float64 */; // Duration of the video in seconds + size: number /* int64 */; // File size in bytes + quality: string; // Video quality +} +/** + * Playlist represents metadata for a playlist or channel. + */ +export interface Playlist { + id: string; // Playlist or channel ID + title: string; // Title of the playlist or channel + description: string; // Description of the playlist or channel +} diff --git a/web/app/page.tsx b/web/app/page.tsx index e6449b0..35c47de 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -2,16 +2,17 @@ import {UrlInput} from "@/components/url-input"; import JobProgress from "@/components/job-progress"; +import Recent from "@/components/recent"; export default function Home() { - - return ( -
-
- - -
-
- ); +
+
+ + + +
+
+ ); } diff --git a/web/components/recent.tsx b/web/components/recent.tsx new file mode 100644 index 0000000..f3b5800 --- /dev/null +++ b/web/components/recent.tsx @@ -0,0 +1,52 @@ +import React, {useEffect, useState} from 'react'; +import {Card, CardHeader, CardContent, CardTitle} from "@/components/ui/card"; +import {Progress} from "@/components/ui/progress"; +import {JobData} from "@/types"; +import useAppState from "@/store/appState"; + +const Recent: React.FC = () => { + const [jobs, setJobs] = useState(); + const [message, setMessage] = useState(""); + + useEffect(() => { + fetch(process.env.NEXT_PUBLIC_SERVER_URL + "/recent").then((res) => { + if (!res.ok) { + setMessage("No recent jobs found."); + return; + } + return res.json() + }).then((data) => { + setJobs(data.message); + }); + + const unsubscribe = useAppState.subscribe( + (state) => { + if (state.isDownloading) { + setMessage('') + unsubscribe(); + } + } + ); + }, []); + + return ( +
+ {jobs && !message && jobs.map((job) => ( + + + {job.URL} + + +

+ Progress: ({job.PROGRESS}%) +

+ +
+
+ ))} + {message &&

{message}

} +
+ ); +}; + +export default Recent; diff --git a/web/components/url-input.tsx b/web/components/url-input.tsx index 66c11d1..97d8091 100644 --- a/web/components/url-input.tsx +++ b/web/components/url-input.tsx @@ -7,12 +7,15 @@ import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" import {AlertDestructive} from "@/components/alert-destructive"; import {toast} from "sonner"; +import useAppState from "@/store/appState"; export function UrlInput() { const [url, setUrl] = useState("") const [loading, setLoading] = useState(false) const [error, setError] = useState("") + const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL + const setIsDownloading = useAppState((state) => state.setIsDownloading); const isValidYoutubeUrl = (url: string) => { const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/ @@ -27,6 +30,7 @@ export function UrlInput() { } setLoading(true) + setIsDownloading(true) try { const response = await fetch(`${SERVER_URL}/download`, { method: "POST", diff --git a/web/package.json b/web/package.json index b641d41..d3602a8 100644 --- a/web/package.json +++ b/web/package.json @@ -24,7 +24,8 @@ "react-dom": "19.0.0-rc-69d4b800-20241021", "sonner": "^1.7.0", "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.2" }, "devDependencies": { "@types/node": "^22", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 01a8399..aacb3b1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.16) + zustand: + specifier: ^5.0.2 + version: 5.0.2(@types/react@18.3.12)(react@19.0.0-rc-69d4b800-20241021) devDependencies: '@types/node': specifier: ^22 @@ -2107,6 +2110,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.2: + resolution: {integrity: sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3154,7 +3175,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 9.14.0(jiti@1.21.6) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@1.21.6)))(eslint@9.14.0(jiti@1.21.6)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@1.21.6)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -3167,7 +3188,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@1.21.6)))(eslint@9.14.0(jiti@1.21.6)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: @@ -3189,7 +3210,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.14.0(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.14.0(jiti@1.21.6)))(eslint@9.14.0(jiti@1.21.6)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.14.0(jiti@1.21.6)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -4320,3 +4341,8 @@ snapshots: yaml@2.6.0: {} yocto-queue@0.1.0: {} + + zustand@5.0.2(@types/react@18.3.12)(react@19.0.0-rc-69d4b800-20241021): + optionalDependencies: + '@types/react': 18.3.12 + react: 19.0.0-rc-69d4b800-20241021 diff --git a/web/store/appState.ts b/web/store/appState.ts new file mode 100644 index 0000000..09da902 --- /dev/null +++ b/web/store/appState.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +interface AppState { + isDownloading: boolean; + setIsDownloading: (value: boolean) => void; +} + +const useAppState = create((set) => ({ + isDownloading: false, // Initial state + setIsDownloading: (value) => set(() => ({ isDownloading: value })), // Mutator +})); + +export default useAppState; diff --git a/web/types/index.ts b/web/types/index.ts new file mode 100644 index 0000000..d6564ad --- /dev/null +++ b/web/types/index.ts @@ -0,0 +1,64 @@ +// Code generated by tygo. DO NOT EDIT. + +////////// +// source: job.go + +export interface DownloadJob { + ID: string; + URL: string; + TIMESTAMP: string /* RFC3339 */; +} +export interface JobData { + ID: string; + JobID: string; + URL: string; + IsPlaylist: boolean; + STATUS: string; + PROGRESS: number /* int */; + CreatedAt: string /* RFC3339 */; + UpdatedAt: string /* RFC3339 */; +} + +////////// +// source: metadata.go + +export interface VideoMetadata { + id: string; + title: string; + uploader: string; + filesize: number /* int64 */; + duration: number /* float64 */; + format: string; + thumbnail: string; +} +export interface PlaylistMetadata { + id: string; + title: string; + description: string; +} + +////////// +// source: video.go + +/** + * Video represents metadata for a single video. + */ +export interface Video { + id: number /* int */; // Primary key in database + job_id: string; // ID associated with the download job + title: string; // Title of the video + uploader: string; // Uploader or channel name + file_path: string; // Path where the video file is stored + last_downloaded_at: string /* RFC3339 */; // Timestamp of when the video was last downloaded + length: number /* float64 */; // Duration of the video in seconds + size: number /* int64 */; // File size in bytes + quality: string; // Video quality +} +/** + * Playlist represents metadata for a playlist or channel. + */ +export interface Playlist { + id: string; // Playlist or channel ID + title: string; // Title of the playlist or channel + description: string; // Description of the playlist or channel +}