feat: migrate frontend from Next.js to Vite+TanStack Router

This commit is contained in:
codeforreal1 2025-01-26 22:41:03 +05:45
parent 54fbac3a68
commit fd5107f518
43 changed files with 3637 additions and 2477 deletions

View file

@ -5,13 +5,7 @@
"es2021": true
},
"plugins": ["@typescript-eslint", "prettier", "unused-imports"],
"extends": [
"airbnb",
"airbnb-typescript",
"airbnb/hooks",
"prettier",
"next/core-web-vitals"
],
"extends": ["airbnb", "airbnb-typescript", "airbnb/hooks", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
@ -72,6 +66,23 @@
"ignoreCase": false
}
],
"no-nested-ternary": "off"
"no-nested-ternary": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": ["**/vite.config.*", "**/*.test.*", "**/*.spec.*"]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"": "never",
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
]
}
}

8
.gitignore vendored
View file

@ -9,13 +9,10 @@
# testing
/coverage
# next.js
/.next/
/out/
/dist/
# production
/build
/out/
/dist/
# misc
.DS_Store
@ -34,7 +31,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/**/.env

View file

@ -2,5 +2,7 @@
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"semi": false
"semi": false,
"plugins": ["prettier-plugin-organize-imports"],
"organizeImportsSkipDestructiveCodeActions": true
}

34
index.html Normal file
View file

@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="HandheldFriendly" content="true" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#000000" />
<title>CompressO</title>
<style>
body {
background-color: #000000;
}
</style>
</head>
<body>
<div id="app"></div>
<div id="portal"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/scripts/accessibility-only-when-focused.js"></script>
<script src="/scripts/disable-context-menu.js"></script>
<script src="/scripts/disable-zoom.js"></script>
<script src="/scripts/disable-reload.js"></script>
</body>
</html>

View file

@ -1,39 +0,0 @@
/** @type {import('next').NextConfig} */
import analyzeBundle from '@next/bundle-analyzer'
const packageJSON = await import('./package.json', {
assert: { type: 'json' },
})
const withBundleAnalyzer = analyzeBundle({
enabled: process.env.ANALYZE === 'true',
})
const nextConfig = withBundleAnalyzer({
output: 'export',
distDir: './dist',
cleanDistDir: true,
reactStrictMode: false,
webpack(config, { webpack }) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
})
if (process.env.NODE_ENV === 'production') {
config.plugins.push(
new webpack.DefinePlugin({
'globalThis.__DEV__': false,
}),
)
}
return config
},
env: {
version: packageJSON?.default?.version,
},
})
export default nextConfig

View file

@ -7,10 +7,12 @@
"url": "https://www.threads.net/@codeforreal"
},
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"next:dev": "next dev",
"next:build": "next build",
"next:start": "next start",
"vite:dev": "vite --port=3001",
"vite:build": "vite build",
"vite:preview": "vite preview",
"vite:start": "vite",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:build:debug": "tauri build --debug",
@ -35,16 +37,17 @@
"@heroui/tabs": "2.2.8",
"@heroui/theme": "2.4.6",
"@heroui/tooltip": "2.2.8",
"@heroui/use-theme": "^2.1.2",
"@tanstack/react-router": "^1.97.21",
"@tanstack/router-devtools": "^1.97.21",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-dialog": "2.0.0-beta.3",
"@tauri-apps/plugin-fs": "2.0.0-beta.3",
"@tauri-apps/plugin-os": "2.0.0-beta.5",
"@tauri-apps/plugin-shell": "2.0.0-beta.3",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
"framer-motion": "11.18.2",
"lodash": "^4.17.21",
"next": "14.0.2",
"next-themes": "^0.2.1",
"pretty-bytes": "^6.1.1",
"react": "^18",
"react-dom": "^18",
@ -54,9 +57,8 @@
"vaul": "^0.9.1"
},
"devDependencies": {
"@next/bundle-analyzer": "^14.0.3",
"@next/eslint-plugin-next": "^12.1.0",
"@svgr/webpack": "^8.1.0",
"@tanstack/router-plugin": "^1.97.22",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/lodash": "^4.17.4",
"@types/node": "^20",
@ -64,11 +66,11 @@
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.16",
"eslint": "8.9.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "^14.1.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
@ -76,14 +78,17 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"husky": "^8.0.1",
"lint-staged": "^13.2.3",
"postcss": "^8.4.31",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.0",
"typescript": "^5",
"husky": "^8.0.1"
"vite": "^6.0.3",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-svgr": "^4.3.0",
"prettier-plugin-organize-imports": "^4.1.0"
},
"husky": {
"hooks": {

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
postcss.config.ts Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -4,9 +4,9 @@
"version": "1.2.0",
"identifier": "com.compresso.app",
"build": {
"beforeBuildCommand": "pnpm next:build",
"beforeBuildCommand": "pnpm vite:build",
"frontendDist": "../dist",
"devUrl": "http://localhost:3000"
"devUrl": "http://localhost:3001"
},
"bundle": {
"longDescription": "Compress any video file to a tiny size.",

View file

@ -1,36 +0,0 @@
import React from "react";
function Head() {
return (
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="HandheldFriendly" content="true" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#000000" />
</head>
);
}
export default Head;

View file

@ -1,50 +0,0 @@
'use client'
import React from 'react'
import Script from 'next/script'
import './globals.css'
import { Toaster } from '@/components/Toast'
import { combinedFonts } from '@/assets/fonts'
import UIProvider from '../providers/UIProvider'
import ThemeProvider from '../providers/ThemeProvider'
import Head from './head'
const version = process.env.version
const env = process.env.NODE_ENV
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<Head />
<body className={`antialiased ${combinedFonts}`}>
<ThemeProvider>
<UIProvider>{children}</UIProvider>
<Toaster />
</ThemeProvider>
<Script
src={`/scripts/accessibility-only-when-focused.js?nonce=${version}`}
data-env={env}
/>
<Script
src={`/scripts/disable-context-menu.js?nonce=${version}`}
data-env={env}
/>
<Script
src={`/scripts/disable-zoom.js?nonce=${version}`}
data-env={env}
/>
<Script
src={`/scripts/disable-reload.js?nonce=${version}`}
data-env={env}
/>
<div id="portal" />
</body>
</html>
)
}

View file

@ -1,24 +0,0 @@
import localFont from "next/font/local";
const kayakRegular = localFont({
preload: true,
variable: "--font-poppins",
src: [
{
path: "./poppins/Poppins-Regular.ttf",
weight: "400",
style: "normal",
},
{
path: "./poppins/Poppins-Italic.ttf",
weight: "400",
style: "italic",
},
],
});
const fonts = [kayakRegular];
export const combinedFonts = fonts
.map((font) => `${font.className} ${font.variable}`)
.join(" ");

View file

@ -1,28 +1,28 @@
import React from 'react'
import Logo from '@/assets/icons/logo.svg'
import Moon from '@/assets/icons/moon.svg'
import Sun from '@/assets/icons/sun.svg'
import VideoFile from '@/assets/icons/video-file.svg'
import Star from '@/assets/icons/star.svg'
import Cross from '@/assets/icons/cross.svg'
import CurvedArrow from '@/assets/icons/curved-arrow.svg'
import Save from '@/assets/icons/save.svg'
import Tick from '@/assets/icons/tick.svg'
import FileExplorer from '@/assets/icons/file-explorer.svg'
import Play from '@/assets/icons/play.svg'
import Info from '@/assets/icons/info.svg'
import LowResHeart from '@/assets/icons/low-res-heart.svg'
import Github from '@/assets/icons/github.svg'
import Question from '@/assets/icons/question.svg'
import Setting from '@/assets/icons/setting.svg'
import Trash from '@/assets/icons/trash.svg'
import DragAndDrop from '@/assets/icons/drag-and-drop.svg'
import Warning from '@/assets/icons/warning.svg'
import Error from '@/assets/icons/error.svg'
import Redo from '@/assets/icons/redo.svg'
import Cross from '@/assets/icons/cross.svg?react'
import CurvedArrow from '@/assets/icons/curved-arrow.svg?react'
import DragAndDrop from '@/assets/icons/drag-and-drop.svg?react'
import Error from '@/assets/icons/error.svg?react'
import FileExplorer from '@/assets/icons/file-explorer.svg?react'
import Github from '@/assets/icons/github.svg?react'
import Info from '@/assets/icons/info.svg?react'
import Logo from '@/assets/icons/logo.svg?react'
import LowResHeart from '@/assets/icons/low-res-heart.svg?react'
import Moon from '@/assets/icons/moon.svg?react'
import Play from '@/assets/icons/play.svg?react'
import Question from '@/assets/icons/question.svg?react'
import Redo from '@/assets/icons/redo.svg?react'
import Save from '@/assets/icons/save.svg?react'
import Setting from '@/assets/icons/setting.svg?react'
import Star from '@/assets/icons/star.svg?react'
import Sun from '@/assets/icons/sun.svg?react'
import Tick from '@/assets/icons/tick.svg?react'
import Trash from '@/assets/icons/trash.svg?react'
import VideoFile from '@/assets/icons/video-file.svg?react'
import Warning from '@/assets/icons/warning.svg?react'
type SVGAsComponent = React.FC<React.SVGProps<SVGElement>>
type SVGAsComponent = React.FunctionComponent<React.SVGProps<SVGSVGElement>>
function asRegistry<T extends string>(
arg: Record<T, SVGAsComponent>,

View file

@ -3,10 +3,10 @@
import React, { ComponentProps } from 'react'
import { cn } from '@/utils/tailwind'
import Header from './Header'
import Footer from './Footer'
import { LayoutContext } from './context'
import Image from '../Image'
import Footer from './Footer'
import Header from './Header'
import { LayoutContext } from './context'
interface LayoutProps {
children: React.ReactNode
@ -49,7 +49,7 @@ const Layout = (props: LayoutProps) => {
<section
{...(containerProps ?? {})}
className={cn([
'w-full h-full flex flex-col overflow-y-auto',
'w-full h-full flex flex-col overflow-y-auto dark:bg-black1 bg-white1',
containerProps?.className ?? '',
])}
>

View file

@ -1,15 +1,11 @@
import React from 'react'
import { useTheme } from 'next-themes'
import { ThemeProxy, useTheme } from '@/hooks/useTheme'
import Button from '../Button'
import Icon from '../Icon'
import Tooltip from '../Tooltip'
interface ThemeSwitcherChildrenProps {
theme: string | undefined
setTheme(theme: string | undefined): void
}
type ThemeSwitcherChildrenProps = ThemeProxy
interface ThemeSwitcherProps {
children?(props: ThemeSwitcherChildrenProps): React.ReactNode
@ -18,23 +14,14 @@ interface ThemeSwitcherProps {
function ThemeSwitcher(props: ThemeSwitcherProps) {
const { children } = props
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
const { theme, setTheme, toggleTheme } = useTheme()
return children == null ? (
<Button
isIconOnly
size="sm"
onClick={() => {
setTheme(theme === 'light' ? 'dark' : 'light')
onPress={() => {
toggleTheme()
}}
>
<Tooltip
@ -46,7 +33,7 @@ function ThemeSwitcher(props: ThemeSwitcherProps) {
</Tooltip>
</Button>
) : (
children({ theme, setTheme })
children({ theme, setTheme, toggleTheme })
)
}

View file

@ -1,6 +1,6 @@
import { useTheme } from '@/hooks/useTheme'
import React from 'react'
import { Toaster as NativeToaster, toast } from 'sonner'
import { useTheme } from 'next-themes'
export function Toaster() {
const { theme } = useTheme()
@ -8,13 +8,15 @@ export function Toaster() {
<NativeToaster
position="bottom-center"
richColors
theme={theme === 'dark' ? 'dark' : 'light'}
theme={theme}
toastOptions={{
classNames: {
default: 'rounded-[3rem] px-4 py-2 w-fit',
default:
'w-fit rounded-[3rem] px-4 py-2 flex justify-center align-center',
},
duration: 3000,
duration: 2500,
}}
className="flex justify-center items-center"
/>
)
}

1
src/constants/index.ts Normal file
View file

@ -0,0 +1 @@
export const theme = 'theme'

View file

@ -2,8 +2,23 @@
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Poppins';
font-weight: 100 200 300 400 500 600 700 800 900;
src: url('./assets/fonts/poppins/Poppins-Regular.ttf');
}
@font-face {
font-family: 'Poppins';
font-weight: 100 200 300 400 500 600 700 800 900;
src: url('./assets/fonts/poppins/Poppins-Italic.ttf');
font-style: italic;
}
:root {
box-sizing: border-box;
--font-poppins: 'Poppins', system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
}
*,
@ -15,13 +30,14 @@
user-select: none;
-webkit-user-drag: none;
-webkit-app-region: no-drag;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
font-family: var(--font-poppins);
}
*:focus {
outline-color: theme('colors.primary') !important;
font-family: var(--font-poppins);
}
:root {
@ -30,7 +46,8 @@
touch-action: pan-x pan-y;
}
body {
body #app {
font-family: var(--font-poppins);
margin: 0;
padding: 0;
background-color: theme('colors.black1');
@ -38,11 +55,7 @@ body {
width: 100vw;
height: 100vh;
overflow-x: auto;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
}
[class='light'] body {

53
src/hooks/useTheme.ts Normal file
View file

@ -0,0 +1,53 @@
import * as constants from '@/constants'
import { useEffect } from 'react'
import { proxy, subscribe, useSnapshot } from 'valtio'
export type ThemeVariant = 'light' | 'dark'
export type ThemeProxy = {
theme: ThemeVariant
setTheme: (newTheme: ThemeVariant) => void
toggleTheme: () => void
}
let persistedTheme: ThemeVariant | null = localStorage.getItem(
constants.theme,
) as ThemeVariant
if (persistedTheme && !['dark', 'light'].includes(persistedTheme)) {
localStorage.removeItem(constants.theme)
persistedTheme = null
}
const themeProxy: ThemeProxy = proxy({
theme: persistedTheme ?? 'dark',
setTheme(newTheme) {
themeProxy.theme = newTheme
},
toggleTheme() {
themeProxy.theme = themeProxy.theme === 'dark' ? 'light' : 'dark'
},
})
export function useTheme() {
const snapshot = useSnapshot(themeProxy)
useEffect(() => {
if (snapshot.theme === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, [snapshot.theme])
useEffect(() => {
const unsubscribe = subscribe(themeProxy, () => {
localStorage.setItem(constants.theme, themeProxy.theme)
})
return () => {
unsubscribe()
}
}, [])
return snapshot
}

23
src/main.tsx Normal file
View file

@ -0,0 +1,23 @@
import { RouterProvider, createRouter } from '@tanstack/react-router'
import React from 'react'
import ReactDOM from 'react-dom/client'
import './global.css'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
defaultPreload: 'intent',
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
const rootElement = document.getElementById('app')!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(<RouterProvider router={router} />)
}

View file

@ -1,16 +0,0 @@
import React from 'react'
import { ThemeProvider as NextThemeProvider } from 'next-themes'
function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
>
{children}
</NextThemeProvider>
)
}
export default ThemeProvider

View file

@ -1,5 +1,5 @@
import React from 'react'
import { HeroUIProvider } from '@heroui/react'
import React from 'react'
function UIProvider({ children }: { children: React.ReactNode }) {
return (

88
src/routeTree.gen.ts Normal file
View file

@ -0,0 +1,88 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootIndexImport } from './routes/(root)/index'
import { Route as rootRoute } from './routes/__root'
// Create/Update Routes
const rootIndexRoute = rootIndexImport.update({
id: '/(root)/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/(root)/': {
id: '/(root)/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof rootIndexImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof rootIndexRoute
}
export interface FileRoutesByTo {
'/': typeof rootIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/(root)/': typeof rootIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/(root)/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
rootIndexRoute: typeof rootIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
rootIndexRoute: rootIndexRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/(root)/"
]
},
"/(root)/": {
"filePath": "(root)/index.tsx"
}
}
}
ROUTE_MANIFEST_END */

View file

@ -1,7 +1,7 @@
import { proxy } from 'valtio'
import cloneDeep from 'lodash/cloneDeep'
import { proxy } from 'valtio'
import { Video, VideoConfig } from './types'
import { Video, VideoConfig } from './-types'
const videoConfigInitialState: VideoConfig = {
convertToExtension: 'mp4',

View file

@ -1,4 +1,4 @@
import { extensions, compressionPresets } from '@/types/compression'
import { compressionPresets, extensions } from '@/types/compression'
export type VideoConfig = {
convertToExtension: keyof typeof extensions.video

View file

@ -1,28 +1,30 @@
'use client'
import React from 'react'
import dynamic from 'next/dynamic'
import { createFileRoute } from '@tanstack/react-router'
import { core } from '@tauri-apps/api'
import { motion } from 'framer-motion'
import React from 'react'
import { useSnapshot } from 'valtio'
import VideoPicker from '@/tauri/components/VideoPicker'
import Icon from '@/components/Icon'
import Layout from '@/components/Layout'
import { toast } from '@/components/Toast'
import { formatBytes } from '@/utils/fs'
import {
generateVideoThumbnail,
getVideoDuration,
} from '@/tauri/commands/ffmpeg'
import { getFileMetadata } from '@/tauri/commands/fs'
import VideoPicker from '@/tauri/components/VideoPicker'
import { extensions } from '@/types/compression'
import { formatBytes } from '@/utils/fs'
import { convertDurationToMilliseconds } from '@/utils/string'
import Layout from '@/components/Layout'
import Setting from './ui/Setting'
import { videoProxy } from './-state'
import DragAndDrop from './ui/DragAndDrop'
import { videoProxy } from './state'
import Setting from './ui/Setting'
import VideoConfig from './ui/VideoConfig'
export const Route = createFileRoute('/(root)/')({
component: Root,
})
function Root() {
const { state, resetProxy } = useSnapshot(videoProxy)
@ -134,4 +136,4 @@ function Root() {
)
}
export default dynamic(() => Promise.resolve(Root), { ssr: false })
export default Root

View file

@ -1,10 +1,10 @@
import React from 'react'
import { open } from '@tauri-apps/plugin-shell'
import React from 'react'
import Image from '@/components/Image'
import Icon from '@/components/Icon'
import TauriLink from '@/tauri/components/Link'
import Image from '@/components/Image'
import Title from '@/components/Title'
import TauriLink from '@/tauri/components/Link'
function About() {
return (
@ -17,7 +17,13 @@ function About() {
<h2 className="text-3xl mr-2 font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
compressO
</h2>
<Image src="/logo.png" alt="logo" width={50} height={50} />
<Image
disableAnimation
src="/logo.png"
alt="logo"
width={50}
height={50}
/>
</div>
<p className="text-center italic text-gray-600 dark:text-gray-400 text-sm my-1">
Compress any video to a tiny size.
@ -63,7 +69,7 @@ function About() {
</p>
</section>
<p className="self-end text-gray-400 ml-2 text-lg font-bold text-center">
v{process.env.version}
v{window.__appVersion ?? ''}
</p>
</section>
)

View file

@ -1,16 +1,16 @@
'use client'
import React from 'react'
import { event } from '@tauri-apps/api'
import { AnimatePresence, motion } from 'framer-motion'
import { snapshot, useSnapshot } from 'valtio'
import { emitTo } from '@tauri-apps/api/event'
import { AnimatePresence, motion } from 'framer-motion'
import React from 'react'
import { snapshot, useSnapshot } from 'valtio'
import { CustomEvents, VideoCompressionProgress } from '@/types/compression'
import { convertDurationToMilliseconds } from '@/utils/string'
import Button from '@/components/Button'
import { toast } from '@/components/Toast'
import { videoProxy } from '../state'
import { CustomEvents, VideoCompressionProgress } from '@/types/compression'
import { convertDurationToMilliseconds } from '@/utils/string'
import { videoProxy } from '../-state'
function CancelCompression() {
const {
@ -79,7 +79,7 @@ function CancelCompression() {
color="danger"
size="lg"
variant={confirmCancellation ? 'solid' : 'flat'}
onClick={() => {
onPress={() => {
if (!confirmCancellation) {
setConfirmCancellation(true)
} else {

View file

@ -1,12 +1,12 @@
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import React from 'react'
import { useSnapshot } from 'valtio'
import Progress from '@/components/Progress'
import Image from '@/components/Image'
import { videoProxy } from '../state'
import Progress from '@/components/Progress'
import { videoProxy } from '../-state'
function Compressing() {
const {

View file

@ -1,9 +1,9 @@
import React from 'react'
import { snapshot, useSnapshot } from 'valtio'
import Slider from '@/components/Slider/Slider'
import Checkbox from '@/components/Checkbox'
import { videoProxy } from '../state'
import Slider from '@/components/Slider/Slider'
import { videoProxy } from '../-state'
function CompressionQuality() {
const {

View file

@ -1,11 +1,11 @@
import React from 'react'
import { event } from '@tauri-apps/api'
import { AnimatePresence, motion } from 'framer-motion'
import React from 'react'
import ReactDOM from 'react-dom'
import { motion, AnimatePresence } from 'framer-motion'
import Icon from '@/components/Icon'
import { toast } from '@/components/Toast'
import { extensions } from '@/types/compression'
import Icon from '@/components/Icon'
import { zoomInTransition } from '@/utils/animation'
const videoExtensions = Object.keys(extensions?.video)

View file

@ -1,16 +1,16 @@
'use client'
import React from 'react'
import { useDisclosure, UseDisclosureProps } from '@heroui/modal'
import React from 'react'
import { snapshot, useSnapshot } from 'valtio'
import Button from '@/components/Button'
import Code from '@/components/Code'
import Icon from '@/components/Icon'
import AlertDialog, { AlertDialogButton } from '@/ui/Dialogs/AlertDialog'
import Tooltip from '@/components/Tooltip'
import { deleteFile } from '@/tauri/commands/fs'
import { videoProxy } from '../state'
import AlertDialog, { AlertDialogButton } from '@/ui/Dialogs/AlertDialog'
import { videoProxy } from '../-state'
function FileName() {
const {
@ -77,7 +77,7 @@ function FileName() {
<Button
isIconOnly
size="sm"
onClick={handleReconfigure}
onPress={handleReconfigure}
className="bg-transparent"
>
<Tooltip content="Reconfigure" aria-label="Reconfigure">
@ -89,7 +89,7 @@ function FileName() {
<Button
isIconOnly
size="sm"
onClick={handleCancelCompression}
onPress={handleCancelCompression}
className="bg-transparent"
>
<Icon name="cross" size={22} />
@ -102,10 +102,10 @@ function FileName() {
description="Your compressed video is not yet saved. Are you sure you want to discard it?"
renderFooter={({ closeModal }) => (
<>
<AlertDialogButton onClick={closeModal}>Go Back</AlertDialogButton>
<AlertDialogButton onPress={closeModal}>Go Back</AlertDialogButton>
<AlertDialogButton
color="danger"
onClick={() => handleDiscard({ closeModal })}
onPress={() => handleDiscard({ closeModal })}
>
Yes
</AlertDialogButton>

View file

@ -1,16 +1,16 @@
'use client'
import React from 'react'
import { save } from '@tauri-apps/plugin-dialog'
import React from 'react'
import { snapshot, useSnapshot } from 'valtio'
import Button from '@/components/Button'
import Icon from '@/components/Icon'
import { toast } from '@/components/Toast'
import { moveFile, showItemInFileManager } from '@/tauri/commands/fs'
import Tooltip from '@/components/Tooltip'
import { videoProxy } from '../state'
import { moveFile, showItemInFileManager } from '@/tauri/commands/fs'
import { videoProxy } from '../-state'
function Success() {
const {
@ -65,7 +65,7 @@ function Success() {
<Button
className="flex justify-center items-center"
color="success"
onClick={handleCompressedVideoSave}
onPress={handleCompressedVideoSave}
isLoading={compressedVideo?.isSaving}
isDisabled={compressedVideo?.isSaving || compressedVideo?.isSaved}
fullWidth
@ -85,7 +85,7 @@ function Success() {
<Button
isIconOnly
className="ml-2 text-green-500"
onClick={openInFileManager}
onPress={openInFileManager}
>
<Icon name="fileExplorer" />
</Button>

View file

@ -1,19 +1,19 @@
import React from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { DropdownItem } from '@heroui/dropdown'
import { useDisclosure } from '@heroui/modal'
import { AnimatePresence, motion } from 'framer-motion'
import React from 'react'
import ThemeSwitcher from '@/components/ThemeSwitcher'
import Divider from '@/components/Divider'
import Button from '@/components/Button'
import Icon from '@/components/Icon'
import Tooltip from '@/components/Tooltip'
import { deleteCache as invokeDeleteCache } from '@/tauri/commands/fs'
import { toast } from '@/components/Toast'
import Title from '@/components/Title'
import Modal, { ModalContent } from '@/components/Modal'
import Divider from '@/components/Divider'
import Dropdown, { DropdownMenu, DropdownTrigger } from '@/components/Dropdown'
import Icon from '@/components/Icon'
import Modal, { ModalContent } from '@/components/Modal'
import ThemeSwitcher from '@/components/ThemeSwitcher'
import Title from '@/components/Title'
import { toast } from '@/components/Toast'
import Tooltip from '@/components/Tooltip'
import { usePlatform } from '@/hooks/usePlatform'
import { deleteCache as invokeDeleteCache } from '@/tauri/commands/fs'
import About from './About'
type DropdownKey = 'settings' | 'about'
@ -111,7 +111,7 @@ function AppSetting() {
size="sm"
color="danger"
variant={confirmClearCache ? 'solid' : 'flat'}
onClick={() => {
onPress={() => {
if (!confirmClearCache) {
setConfirmClearCache(true)
} else {

View file

@ -4,7 +4,7 @@ import React from 'react'
import { useSnapshot } from 'valtio'
import Icon from '@/components/Icon'
import { videoProxy } from '../state'
import { videoProxy } from '../-state'
function Success() {
const {

View file

@ -1,34 +1,34 @@
'use client'
import React from 'react'
import { core } from '@tauri-apps/api'
import { SelectItem } from '@heroui/select'
import { core } from '@tauri-apps/api'
import { AnimatePresence, motion } from 'framer-motion'
import { useSnapshot, snapshot } from 'valtio'
import React from 'react'
import { snapshot, useSnapshot } from 'valtio'
import Button from '@/components/Button'
import Checkbox from '@/components/Checkbox'
import Divider from '@/components/Divider'
import Icon from '@/components/Icon'
import Image from '@/components/Image'
import Layout from '@/components/Layout'
import Select from '@/components/Select'
import Spinner from '@/components/Spinner'
import Divider from '@/components/Divider'
import Image from '@/components/Image'
import Icon from '@/components/Icon'
import { toast } from '@/components/Toast'
import { formatBytes } from '@/utils/fs'
import Tooltip from '@/components/Tooltip'
import { compressVideo } from '@/tauri/commands/ffmpeg'
import { getFileMetadata } from '@/tauri/commands/fs'
import { compressionPresets, extensions } from '@/types/compression'
import Tooltip from '@/components/Tooltip'
import Checkbox from '@/components/Checkbox'
import { cn } from '@/utils/tailwind'
import { zoomInTransition } from '@/utils/animation'
import Layout from '@/components/Layout'
import { videoProxy } from '../state'
import Compressing from './Compressing'
import FileName from './FileName'
import Success from './Success'
import { formatBytes } from '@/utils/fs'
import { cn } from '@/utils/tailwind'
import { videoProxy } from '../-state'
import CancelCompression from './CancelCompression'
import SaveVideo from './SaveVideo'
import Compressing from './Compressing'
import CompressionQuality from './CompressionQuality'
import FileName from './FileName'
import SaveVideo from './SaveVideo'
import Success from './Success'
import styles from './styles.module.css'
const videoExtensions = Object.keys(extensions?.video)
@ -213,24 +213,26 @@ function VideoConfig() {
<span className="text-gray-600 dark:text-gray-400 block mr-2 text-sm">
Disable Compression
</span>
<Tooltip
delay={0}
content={
<div className="max-w-[10rem] p-2">
<p>
You can disable the compression if you just want to
change the extension of the video.
</p>
</div>
}
aria-label="You can disable the compression if you just
want to change the extension of the video"
className="flex justify-center items-center"
>
<Icon name="question" className="block" />
</Tooltip>
</div>
</Checkbox>
<div className="z-10">
<Tooltip
delay={0}
content={
<div className="max-w-[10rem] p-2">
<p>
You can disable the compression if you just want to
change the extension of the video.
</p>
</div>
}
aria-label="You can disable the compression if you just
want to change the extension of the video"
className="flex justify-center items-center"
>
<Icon name="question" className="block" />
</Tooltip>
</div>
</div>
<Divider className="my-3" />
<Select
@ -311,9 +313,10 @@ function VideoConfig() {
<Button
as={motion.button}
color="primary"
onClick={handleCompression}
onPress={handleCompression}
fullWidth
size="lg"
className="text-primary"
>
Compress <Icon name="logo" size={25} />
</Button>

22
src/routes/__root.tsx Normal file
View file

@ -0,0 +1,22 @@
import { Outlet, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import * as React from 'react'
import { Toaster } from '@/components/Toast'
import UIProvider from '../providers/UIProvider'
export const Route = createRootRoute({
component: RootComponent,
})
function RootComponent() {
return (
<>
<UIProvider>
<Outlet />
</UIProvider>
<Toaster />
<TanStackRouterDevtools position="bottom-right" />
</>
)
}

10
src/vite.env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
declare global {
interface Window {
__appVersion: string
}
}
export {}

View file

@ -1,5 +1,5 @@
import type { Config } from 'tailwindcss'
import { heroui } from "@heroui/react"
import { heroui } from '@heroui/react'
const WIDTHS = Object.freeze({
xs: '320px',
@ -33,7 +33,7 @@ const config: Config = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
// NextUI Components
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {

View file

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -23,19 +19,9 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"./dist/types/**/*.ts",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["**/*.ts", "**/*.tsx", "./dist/types/**/*.ts"],
"exclude": ["node_modules"]
}

26
vite.config.ts Normal file
View file

@ -0,0 +1,26 @@
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
import { defineConfig } from 'vite'
import eslint from 'vite-plugin-eslint'
import svgr from 'vite-plugin-svgr'
import packageJSON from './package.json'
export default defineConfig({
plugins: [
TanStackRouterVite({
routeFileIgnorePattern: '(^[A-Z].*)',
}),
react(),
svgr(),
eslint(),
],
resolve: {
alias: {
'@': resolve('./src'),
},
},
define: {
__appVersion: JSON.stringify(packageJSON.version),
},
})