diff --git a/frontend/app/components/Header.tsx b/frontend/app/components/Header.tsx index 36edf45..2df77a4 100644 --- a/frontend/app/components/Header.tsx +++ b/frontend/app/components/Header.tsx @@ -3,10 +3,11 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useState, useRef, useEffect } from 'react'; -import { IoSearchOutline, IoMoonOutline, IoSunnyOutline, IoArrowBack, IoMenuOutline } from 'react-icons/io5'; +import { IoMoonOutline, IoSunnyOutline, IoArrowBack, IoMenuOutline } from 'react-icons/io5'; import RegionSelector from './RegionSelector'; import { useTheme } from '../context/ThemeContext'; import { useSidebar } from '../context/SidebarContext'; +import { Logo, SearchIcon } from '../icons'; export default function Header() { const [searchQuery, setSearchQuery] = useState(''); @@ -45,8 +46,8 @@ export default function Header() { }} title="Menu"> - - KV-Tube + + @@ -54,7 +55,7 @@ export default function Header() {
- + )}
@@ -87,7 +88,7 @@ export default function Header() { {/* Right - Region and Theme */}
- + , label: 'Home', path: '/' }, - // { icon: , label: 'Shorts', path: '/shorts' }, - { icon: , label: 'Sub', path: '/feed/subscriptions' }, - { icon: , label: 'You', path: '/feed/library' }, + { icon: , label: 'Home', path: '/' }, + { icon: , label: 'Sub', path: '/feed/subscriptions' }, + { icon: , label: 'You', path: '/feed/library' }, ]; return ( diff --git a/frontend/app/components/Sidebar.tsx b/frontend/app/components/Sidebar.tsx index 1b8b61d..bbfca38 100644 --- a/frontend/app/components/Sidebar.tsx +++ b/frontend/app/components/Sidebar.tsx @@ -2,19 +2,17 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { MdHomeFilled, MdOutlineSubscriptions, MdOutlineVideoLibrary } from 'react-icons/md'; -import { SiYoutubeshorts } from 'react-icons/si'; import { useSidebar } from '../context/SidebarContext'; +import { HomeIcon, SubscriptionsIcon, LibraryIcon } from '../icons'; export default function Sidebar() { const pathname = usePathname(); const { isSidebarOpen } = useSidebar(); const navItems = [ - { icon: , label: 'Home', path: '/' }, - // { icon: , label: 'Shorts', path: '/shorts' }, - { icon: , label: 'Sub', path: '/feed/subscriptions' }, - { icon: , label: 'You', path: '/feed/library' }, + { icon: , label: 'Home', path: '/' }, + { icon: , label: 'Sub', path: '/feed/subscriptions' }, + { icon: , label: 'You', path: '/feed/library' }, ]; return ( diff --git a/frontend/app/icons/HomeIcon.tsx b/frontend/app/icons/HomeIcon.tsx new file mode 100644 index 0000000..ca68832 --- /dev/null +++ b/frontend/app/icons/HomeIcon.tsx @@ -0,0 +1,26 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function HomeIcon({ size = 24, className, style }: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/LibraryIcon.tsx b/frontend/app/icons/LibraryIcon.tsx new file mode 100644 index 0000000..92a1c39 --- /dev/null +++ b/frontend/app/icons/LibraryIcon.tsx @@ -0,0 +1,26 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function LibraryIcon({ size = 24, className, style }: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/Logo.tsx b/frontend/app/icons/Logo.tsx new file mode 100644 index 0000000..cb2ba9c --- /dev/null +++ b/frontend/app/icons/Logo.tsx @@ -0,0 +1,46 @@ +'use client'; + +interface LogoProps { + size?: number; + showText?: boolean; + className?: string; +} + +export default function Logo({ size = 24, showText = true, className }: LogoProps) { + return ( +
+ {/* Play Button Icon */} + + + + + + {/* Text */} + {showText && ( + + KV-Tube + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/app/icons/PlayIcon.tsx b/frontend/app/icons/PlayIcon.tsx new file mode 100644 index 0000000..32f02b5 --- /dev/null +++ b/frontend/app/icons/PlayIcon.tsx @@ -0,0 +1,24 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function PlayIcon({ size = 24, className, style }: IconProps) { + return ( + + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/SearchIcon.tsx b/frontend/app/icons/SearchIcon.tsx new file mode 100644 index 0000000..63be3d3 --- /dev/null +++ b/frontend/app/icons/SearchIcon.tsx @@ -0,0 +1,26 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function SearchIcon({ size = 24, className, style }: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/ShortsIcon.tsx b/frontend/app/icons/ShortsIcon.tsx new file mode 100644 index 0000000..25d9611 --- /dev/null +++ b/frontend/app/icons/ShortsIcon.tsx @@ -0,0 +1,24 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function ShortsIcon({ size = 24, className, style }: IconProps) { + return ( + + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/SubscriptionsIcon.tsx b/frontend/app/icons/SubscriptionsIcon.tsx new file mode 100644 index 0000000..f653753 --- /dev/null +++ b/frontend/app/icons/SubscriptionsIcon.tsx @@ -0,0 +1,26 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function SubscriptionsIcon({ size = 24, className, style }: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/TrendingIcon.tsx b/frontend/app/icons/TrendingIcon.tsx new file mode 100644 index 0000000..a8e2809 --- /dev/null +++ b/frontend/app/icons/TrendingIcon.tsx @@ -0,0 +1,26 @@ +'use client'; + +interface IconProps { + size?: number; + className?: string; + style?: React.CSSProperties; +} + +export default function TrendingIcon({ size = 24, className, style }: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/frontend/app/icons/index.ts b/frontend/app/icons/index.ts new file mode 100644 index 0000000..8a76ef3 --- /dev/null +++ b/frontend/app/icons/index.ts @@ -0,0 +1,8 @@ +export { default as Logo } from './Logo'; +export { default as HomeIcon } from './HomeIcon'; +export { default as TrendingIcon } from './TrendingIcon'; +export { default as SubscriptionsIcon } from './SubscriptionsIcon'; +export { default as LibraryIcon } from './LibraryIcon'; +export { default as ShortsIcon } from './ShortsIcon'; +export { default as PlayIcon } from './PlayIcon'; +export { default as SearchIcon } from './SearchIcon'; \ No newline at end of file diff --git a/frontend/app/watch/ClientWatchPage.tsx b/frontend/app/watch/ClientWatchPage.tsx index a1efeec..fcdf087 100644 --- a/frontend/app/watch/ClientWatchPage.tsx +++ b/frontend/app/watch/ClientWatchPage.tsx @@ -612,6 +612,8 @@ export default function ClientWatchPage() { const [currentIndex, setCurrentIndex] = useState(-1); const [activeTab, setActiveTab] = useState<'upnext' | 'mix'>('upnext'); const [apiError, setApiError] = useState(null); + const [wideMode, setWideMode] = useState(false); + const [loopMode, setLoopMode] = useState(false); // Scroll to top when video changes or page loads useEffect(() => { @@ -763,11 +765,11 @@ export default function ClientWatchPage() { minHeight: '100vh', }}>
{/* Main Content */} @@ -779,6 +781,7 @@ export default function ClientWatchPage() { title={videoInfo?.title} autoplay={true} onVideoEnd={handleVideoEnd} + loop={loopMode} />
@@ -790,52 +793,106 @@ export default function ClientWatchPage() { padding: '8px 0', gap: '8px', }}> - +
+ + + +
- +
+ {/* Loop Toggle */} + + + {/* Wide Mode Toggle */} + +
{/* Video Info */} @@ -852,7 +909,7 @@ export default function ClientWatchPage() { height: 'fit-content', maxHeight: 'calc(100vh - 80px)', overflowY: 'auto', - display: 'flex', + display: wideMode ? 'none' : 'flex', flexDirection: 'column', gap: '12px', }}> diff --git a/frontend/app/watch/YouTubePlayer.tsx b/frontend/app/watch/YouTubePlayer.tsx index 7592b6d..2983737 100644 --- a/frontend/app/watch/YouTubePlayer.tsx +++ b/frontend/app/watch/YouTubePlayer.tsx @@ -17,6 +17,7 @@ interface YouTubePlayerProps { autoplay?: boolean; onVideoEnd?: () => void; onVideoReady?: () => void; + loop?: boolean; } function PlayerSkeleton() { @@ -40,15 +41,31 @@ export default function YouTubePlayer({ title, autoplay = true, onVideoEnd, - onVideoReady + onVideoReady, + loop = false }: YouTubePlayerProps) { const playerRef = useRef(null); + const playerContainerRef = useRef(null); const playerInstanceRef = useRef(null); + const loopRef = useRef(loop); const [isApiReady, setIsApiReady] = useState(false); const [isPlayerReady, setIsPlayerReady] = useState(false); const [error, setError] = useState(null); + const [isFullscreen, setIsFullscreen] = useState(false); const router = useRouter(); + // Keep loop ref in sync + loopRef.current = loop; + + // Fullscreen change listener + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => document.removeEventListener('fullscreenchange', handleFullscreenChange); + }, []); + // Load YouTube IFrame API useEffect(() => { if (window.YT && window.YT.Player) { @@ -134,7 +151,11 @@ export default function YouTubePlayer({ onStateChange: (event: any) => { // Video ended if (event.data === window.YT.PlayerState.ENDED) { - if (onVideoEnd) { + if (loopRef.current) { + // Loop mode: restart video + event.target.seekTo(0); + event.target.playVideo(); + } else if (onVideoEnd) { onVideoEnd(); } } @@ -208,7 +229,17 @@ export default function YouTubePlayer({ } return ( -
+
{!isPlayerReady && !error && }
+ {/* Fullscreen button */} +
); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c770bfc..97953a3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,18 +8,12 @@ "name": "frontend", "version": "0.1.0", "dependencies": { - "@clappr/core": "^0.13.2", - "@clappr/player": "^0.11.16", "@fontsource/roboto": "^5.2.9", - "@vidstack/react": "^1.12.13", - "artplayer": "^5.3.0", - "clappr": "^0.3.13", "hls.js": "^1.6.15", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3", - "react-icons": "^5.5.0", - "vidstack": "^1.12.13" + "react-icons": "^5.5.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -285,18 +279,6 @@ "node": ">=6.9.0" } }, - "node_modules/@clappr/core": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@clappr/core/-/core-0.13.2.tgz", - "integrity": "sha512-QW2wx5BHFfnoQY6biGLyVYBHCrx4amMScHCVpXZhSWgwqP6l8YNcz8fNkQuJWjLT5ISgFym3F4cxO8Imhmk2Kg==", - "license": "BSD-3-Clause" - }, - "node_modules/@clappr/player": { - "version": "0.11.16", - "resolved": "https://registry.npmjs.org/@clappr/player/-/player-0.11.16.tgz", - "integrity": "sha512-A6rVmOqJ93rBJ4KDc3bFe3HHdtazC3LKGcnsw+0w52wfa8IpsiU2XpeSSLY44tybEJ8gpPKtm7vJrMf4uFVSzA==", - "license": "BSD-3-Clause" - }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -474,31 +456,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, "node_modules/@fontsource/roboto": { "version": "5.2.9", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.9.tgz", @@ -1614,6 +1571,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1629,12 +1587,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -2186,27 +2138,11 @@ "win32" ] }, - "node_modules/@vidstack/react": { - "version": "1.12.13", - "resolved": "https://registry.npmjs.org/@vidstack/react/-/react-1.12.13.tgz", - "integrity": "sha512-zyNydy1+HtoK6cJ8EmqFNkPPGHIFMrr2KH+ef3654EqXx4IcJ8A5LCNMXBuALQE8IMxtk040JMoR9OKyeXjBOQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.6.10", - "media-captions": "^1.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2435,15 +2371,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/artplayer": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/artplayer/-/artplayer-5.3.0.tgz", - "integrity": "sha512-yExO39MpEg4P+bxgChxx1eJfiUPE4q1QQRLCmqGhlsj+ANuaoEkR8hF93LdI5ZyrAcIbJkuEndxEiUoKobifDw==", - "license": "MIT", - "dependencies": { - "option-validator": "^2.0.6" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2671,13 +2598,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/clappr": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/clappr/-/clappr-0.3.13.tgz", - "integrity": "sha512-cAtGhtSAYIavKqVQb/wX5pB9twb/W7gFUGBaGHy6dbpUqCtCmtH0Eu05rNSwtbaREWgSTUJyEMTgaNSZ/62KlQ==", - "deprecated": "This version is no longer supported. Please use the @clappr/player versions instead.", - "license": "BSD-3-Clause" - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2737,6 +2657,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4611,15 +4532,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -4915,15 +4827,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4990,15 +4893,6 @@ "node": ">= 0.4" } }, - "node_modules/media-captions": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.4.tgz", - "integrity": "sha512-cyDNmuZvvO4H27rcBq2Eudxo9IZRDCOX/I7VEyqbxsEiD2Ei7UYUhG/Sc5fvMZjmathgz3fEK7iAKqvpY+Ux1w==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5324,15 +5218,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/option-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/option-validator/-/option-validator-2.0.6.tgz", - "integrity": "sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==", - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.3" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6483,19 +6368,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unplugin": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", - "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -6572,27 +6444,6 @@ "punycode": "^2.1.0" } }, - "node_modules/vidstack": { - "version": "1.12.13", - "resolved": "https://registry.npmjs.org/vidstack/-/vidstack-1.12.13.tgz", - "integrity": "sha512-vuNeyRmWoH/7EoFVDYjp9nkgcqtCMmal518LDeb78dYKgWb+p6+vtY0AzDhrkBv5q1UiCn+xwmjMmwvSlPLuhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.6.10", - "lit-html": "^2.8.0", - "media-captions": "^1.0.4", - "unplugin": "^1.12.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..9600a51 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/public/icons/pwa-icon.svg b/frontend/public/icons/pwa-icon.svg new file mode 100644 index 0000000..b18e16c --- /dev/null +++ b/frontend/public/icons/pwa-icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2e5c241 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "kv-tube", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}