) => {
+ PopupData.value = {
+ dismissible: true,
+ buttonText: "OK"
+ }
+ PopupData.value.contents = (
+ <>
+ Help
+ Truly universal online file converter.
+ Many online file conversion tools are boring and insecure . They only allow conversion between two formats in the same medium (images to images, videos to videos, etc.), and they require that you upload your files to some server .
+ This is not just terrible for privacy, it's also incredibly lame. What if you really need to convert an AVI video to a PDF document? Try to find an online tool for that, I dare you.
+ Convert.to.it aims to be a tool that "just works". You're almost guaranteed to get an output - perhaps not always the one you expected, but it'll try its best to not leave you hanging.
+ Usage
+
+ Upload your file using the file browser, or drag and drop your file.
+ Select an output format.
+ Click Convert!
+ Hopefully, after a bit (or a lot) of thinking, the program will spit out the file you wanted.
+
+ Advanced mode
+ Advanced mode exposes additional conversion methods for some file types. If you do not intend on using a specific conversion method, it's better to leave it in Simple mode.
+ >
+ )
+ popupOpen.value = true;
+ }
+
+ return (
+ Help
+ )
+}
diff --git a/src/ui/components/Icon.css b/src/ui/components/Icon.css
new file mode 100644
index 00000000..f6381ec3
--- /dev/null
+++ b/src/ui/components/Icon.css
@@ -0,0 +1,13 @@
+.icon {
+ display: inline-block;
+ vertical-align: middle;
+ flex-shrink: 0;
+
+ /* Static properties moved from JS */
+ mask-repeat: no-repeat;
+ -webkit-mask-repeat: no-repeat;
+ mask-size: contain;
+ -webkit-mask-size: contain;
+ mask-position: center;
+ -webkit-mask-position: center;
+}
\ No newline at end of file
diff --git a/src/ui/components/Icon.tsx b/src/ui/components/Icon.tsx
new file mode 100644
index 00000000..dac041f1
--- /dev/null
+++ b/src/ui/components/Icon.tsx
@@ -0,0 +1,34 @@
+import type { CSSProperties } from "preact";
+import './Icon.css'
+
+type IconProps = {
+ src: string
+ size?: number | string
+ color?: string
+ className?: string
+ style?: CSSProperties
+};
+
+export function Icon({
+ src,
+ size = 24,
+ color,
+ className = "",
+ style = {},
+}: IconProps) {
+
+ const computedSize = typeof size === "number" ? `${size}px` : size;
+
+ const maskStyles: CSSProperties = {
+ ...(color ? { backgroundColor: color } : {}),
+
+ maskImage: `url("${src}")`,
+ WebkitMaskImage: `url("${src}")`,
+
+ width: computedSize,
+ height: computedSize,
+ ...style,
+ };
+
+ return
;
+}
diff --git a/src/ui/components/Popup.css b/src/ui/components/Popup.css
new file mode 100644
index 00000000..a82d02a0
--- /dev/null
+++ b/src/ui/components/Popup.css
@@ -0,0 +1,45 @@
+#popup {
+ max-width: 50rem;
+ padding: 1.5em 1.75em;
+
+ background-color: var(--bg-card-alt);
+ color: var(--text-primary);
+
+ border: 0;
+ border-radius: 8px;
+
+ &[open] {
+ display: flex;
+ }
+
+ flex-direction: column;
+ gap: 1rem;
+
+ /* position: absolute has unbelievable quirks */
+ margin: auto;
+
+ button {
+ appearance: none;
+ border: .1rem solid var(--btn-border);
+ border-radius: 8px;
+ padding: .5em;
+ margin-top: .5em;
+ align-self: center;
+ min-width: 25%;
+ color: var(--text-on-primary);
+ background: var(--btn-bg-primary);
+ transition: color 150ms, background 150ms;
+ font-weight: bold;
+
+ &:hover,
+ &:focus-visible {
+ color: var(--text-primary);
+ background: var(--btn-bg-secondary-hover);
+ }
+ }
+
+ &::backdrop {
+ backdrop-filter: brightness(0.7) blur(4px);
+ background: rgba(0 0 0 / 0.4);
+ }
+}
diff --git a/src/ui/components/Popup.tsx b/src/ui/components/Popup.tsx
new file mode 100644
index 00000000..097afce9
--- /dev/null
+++ b/src/ui/components/Popup.tsx
@@ -0,0 +1,81 @@
+import type { TargetedMouseEvent } from "preact";
+import { useSignalEffect } from "@preact/signals";
+import { useEffect, useRef } from "preact/hooks";
+import { popupOpen } from "../PopupStore";
+
+import "./Popup.css";
+import { PopupData } from "..";
+
+export default function Popup() {
+ const ref = useRef(null);
+
+ // Use vanilla JS APIs to control popup state
+ useSignalEffect(() => {
+ const elem = ref.current!;
+
+ if (popupOpen.value) {
+ if (!elem.open) elem.showModal();
+ } else {
+ if (elem.open) elem.close();
+ }
+ });
+
+ // Listen to soft-dismiss events
+ useEffect(() => {
+ window.addEventListener("keydown", (ev: KeyboardEvent) => {
+ if (ev.key === "Escape") ev.preventDefault();
+ if (
+ ev.key === "Escape"
+ && (typeof PopupData.value.dismissible === "undefined" || PopupData.value.dismissible)
+ ) popupOpen.value = false;
+ })
+ }, []);
+
+ /** Handle clicks to the backdrop/outisde of the dialog */
+ const clickHandler = (ev: TargetedMouseEvent) => {
+ const elem = ref.current!;
+ const rect = elem.getBoundingClientRect();
+
+ const isInside =
+ rect.top <= ev.clientY
+ && ev.clientY <= rect.top + rect.height
+ && rect.left <= ev.clientX
+ && ev.clientX <= rect.left + rect.width;
+
+ if (
+ !isInside
+ && PopupData.value.dismissible
+ ) popupOpen.value = false;
+ };
+
+ const getPopupContents = () => {
+ return (PopupData.value.contents) ? PopupData.value.contents : (
+ <>
+ { PopupData.value.title }
+ { PopupData.value.text }
+ >
+ )
+ }
+
+ return (
+
+ );
+}
diff --git a/src/ui/components/StyledButton.css b/src/ui/components/StyledButton.css
new file mode 100644
index 00000000..584ce9d0
--- /dev/null
+++ b/src/ui/components/StyledButton.css
@@ -0,0 +1,73 @@
+.styled-button {
+ appearance: none;
+ border: .1rem solid var(--btn-border);
+ border-radius: 8px;
+ padding: .75em;
+ color: var(--btn-text-secondary);
+ background: var(--btn-bg-secondary);
+ transition: color 150ms, background 150ms;
+ cursor: pointer;
+ user-select: none;
+
+ &:hover,
+ &:focus-visible {
+ color: var(--text-primary);
+ background: var(--btn-bg-secondary-hover);
+ }
+
+ /* Compact variant for smaller buttons */
+ &.compact {
+ padding: 0;
+ border-radius: 1rem;
+ width: 41px;
+ height: 41px;
+ font-weight: bold;
+ font-size: 1.5rem;
+ }
+
+ /* Primary button variant */
+ &.primary {
+ background: var(--btn-bg-primary);
+ color: var(--btn-text-primary);
+ border-color: transparent;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+
+ &:hover,
+ &:focus-visible {
+ background: var(--btn-bg-primary-hover);
+ }
+ }
+
+ /* Icon button variant */
+ &.icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 41px;
+ height: 41px;
+ padding: 0;
+ border-radius: 1rem;
+ background: var(--btn-bg-secondary);
+ border-color: var(--border-primary);
+
+ img {
+ width: 24px;
+ height: 24px;
+ filter: contrast(0);
+ }
+
+ &:hover,
+ &:focus-visible {
+ background: var(--btn-bg-secondary-hover);
+ border-color: var(--border-secondary);
+ box-shadow: 0 6px 6px rgba(0, 0, 0, 0.07);
+ }
+ }
+
+ /* Disabled state */
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+}
diff --git a/src/ui/components/StyledButton.tsx b/src/ui/components/StyledButton.tsx
new file mode 100644
index 00000000..04aeca08
--- /dev/null
+++ b/src/ui/components/StyledButton.tsx
@@ -0,0 +1,43 @@
+import { h } from "preact";
+
+type ButtonVariant = 'default' | 'primary' | 'compact' | 'icon';
+
+interface StyledButtonProps {
+ className?: string;
+ variant?: ButtonVariant;
+ onClick?: (ev: preact.TargetedMouseEvent) => void;
+ title?: string;
+ tabIndex?: number;
+ disabled?: boolean;
+ children: preact.ComponentChildren;
+}
+
+import "./StyledButton.css";
+
+export default function StyledButton({
+ className,
+ variant = 'default',
+ onClick,
+ title,
+ tabIndex,
+ disabled = false,
+ children
+}: StyledButtonProps) {
+ // Combine base class with variant and any additional classes
+ const variantClass = variant === 'default' ? '' : variant;
+ const combinedClassName = className
+ ? `styled-button ${variantClass} ${className}`
+ : `styled-button ${variantClass}`.trim();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/ui/components/Upload/UploadField.css b/src/ui/components/Upload/UploadField.css
new file mode 100644
index 00000000..b221710c
--- /dev/null
+++ b/src/ui/components/Upload/UploadField.css
@@ -0,0 +1,118 @@
+.upload-field {
+ height: 100%;
+ display: flex;
+ flex-grow: 1;
+
+ .upload-card {
+ width: clamp(400px, 75vw, 800px);
+ height: max(450px, 50vh);
+ border-radius: 20px;
+ align-self: center;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ /* padding: 2em; */
+
+ background-color: var(--bg-card);
+ box-shadow: 0 15px 15px rgba(0 0 0 / .1);
+ border: .1rem solid rgba(0 0 0 / .1);
+
+ .upload-card-header {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ border-block-end: .1rem solid rgba(0 0 0 / .1);
+ padding: 2em;
+
+ h1 {
+ display: flex;
+ font-weight: 900;
+ align-items: center;
+ gap: .5em;
+ font-size: 2rem;
+ color: var(--text-primary);
+
+
+ .upload-card-logo {
+ display: inline-block;
+ width: 60px;
+ height: 60px;
+ background-color: var(--primary);
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-size: contain;
+ -webkit-mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-position: center;
+ }
+ }
+ }
+
+ .upload-card-theme-toggle {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ }
+
+ .upload-card-dropzone-hint {
+ flex-grow: 1;
+ margin: 1.5em;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+
+ background-color: var(--bg-card-alt);
+ border: 2px dashed var(--bg-overlay);
+ border-radius: 12px;
+
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+ user-select: none;
+
+ &:hover,
+ &.active-drag {
+ background-color: var(--bg-card-alt-hover);
+ border-color: var(--btn-bg-primary);
+ }
+
+ input[type="file"] {
+ display: none;
+ }
+
+ .upload-card-dropzone-icon-container {
+ background-color: var(--bg-card);
+ padding: 1em;
+ border-radius: 50%;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ margin-bottom: 1em;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &:hover .upload-card-dropzone-icon-container,
+ &.active-drag .upload-card-dropzone-icon-container {
+ transform: scale(1.1);
+ }
+
+
+ .upload-card-dropzone-subtext {
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ }
+ }
+
+ .upload-card-buttons {
+ margin-block: auto 0;
+ display: flex;
+ justify-content: center;
+ gap: 1em;
+ margin-block-end: 1em;
+ }
+ }
+}
diff --git a/src/ui/components/Upload/UploadField.tsx b/src/ui/components/Upload/UploadField.tsx
new file mode 100644
index 00000000..a8d30e74
--- /dev/null
+++ b/src/ui/components/Upload/UploadField.tsx
@@ -0,0 +1,126 @@
+import { useRef, useState } from 'preact/hooks';
+
+import { CurrentPage, Pages } from '../../index';
+import { SelectedFiles } from 'src/main.new';
+
+import uploadImage from '../../img/fa-upload-solid-full.svg';
+import logoImage from '../../img/logo.svg';
+
+import DarkModeToggle from '../DarkModeToggle';
+import { Icon } from "../Icon";
+import StyledButton from "../StyledButton";
+
+import './UploadField.css'
+
+import AdvancedModeToggle from '../AdvancedModeToggle';
+import HelpButton from '../HelpButton';
+
+interface UploadFieldComponentProps {
+ disabled?: boolean
+}
+
+export default function UploadField({ disabled = false }: UploadFieldComponentProps) {
+ const [isDisabled, setIsDisabled] = useState(disabled);
+ const [isDragging, setIsDragging] = useState(false);
+
+ const dragCounter = useRef(0);
+ const fileRef = useRef(null);
+
+ const handleClick = (ev: MouseEvent) => {
+ ev.preventDefault();
+ fileRef.current?.click();
+ }
+
+ const handleDrop = (ev: DragEvent) => {
+ ev.preventDefault();
+ console.debug(ev.dataTransfer?.files);
+ }
+
+ const handleDragEnter = (ev: DragEvent) => {
+ ev.preventDefault();
+ dragCounter.current++;
+ if (ev.dataTransfer?.types.includes('Files')) setIsDragging(true);
+ }
+
+ const handleDragLeave = (ev: DragEvent) => {
+ ev.preventDefault();
+ dragCounter.current--;
+ if (dragCounter.current == 0) setIsDragging(false);
+ }
+
+ const handleDragOver = (ev: DragEvent) => {
+ ev.preventDefault()
+
+ }
+
+ const handleChange = (_ev: preact.TargetedEvent) => {
+ setIsDisabled(true);
+ const files = fileRef.current?.files;
+
+ // Check if files uploaded were empty
+ if (
+ !files
+ || files.length === 0
+ ) return
+
+ // Map array item to object format
+ for (const file of files) {
+ SelectedFiles.value[`${file.name}-${file.lastModified}`] = file
+ }
+
+ CurrentPage.value = Pages.Conversion;
+ }
+
+ return (
+
+
+
+
+
+
+ Convert to it!
+
+
+
+
+
+
+
+
ev.stopPropagation() }
+ tabIndex={ 0 }
+ multiple
+ />
+
+
+
+
Click to add your file
+
or drag and drop here
+
+
+
+
+
+
+ )
+}
diff --git a/src/ui/img/fa-box-archive-solid-full.svg b/src/ui/img/fa-box-archive-solid-full.svg
new file mode 100644
index 00000000..22b87a18
--- /dev/null
+++ b/src/ui/img/fa-box-archive-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-chevron-down-solid-full.svg b/src/ui/img/fa-chevron-down-solid-full.svg
new file mode 100644
index 00000000..2cb73192
--- /dev/null
+++ b/src/ui/img/fa-chevron-down-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-discord-brands-solid-full.svg b/src/ui/img/fa-discord-brands-solid-full.svg
new file mode 100644
index 00000000..7857ba95
--- /dev/null
+++ b/src/ui/img/fa-discord-brands-solid-full.svg
@@ -0,0 +1 @@
+
diff --git a/src/ui/img/fa-file-lines-regular-full.svg b/src/ui/img/fa-file-lines-regular-full.svg
new file mode 100644
index 00000000..847455d4
--- /dev/null
+++ b/src/ui/img/fa-file-lines-regular-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-github-brands-solid-full.svg b/src/ui/img/fa-github-brands-solid-full.svg
new file mode 100644
index 00000000..47882fc8
--- /dev/null
+++ b/src/ui/img/fa-github-brands-solid-full.svg
@@ -0,0 +1 @@
+
diff --git a/src/ui/img/fa-image-regular-full.svg b/src/ui/img/fa-image-regular-full.svg
new file mode 100644
index 00000000..69de0dbc
--- /dev/null
+++ b/src/ui/img/fa-image-regular-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-image-solid-full.svg b/src/ui/img/fa-image-solid-full.svg
new file mode 100644
index 00000000..956d330a
--- /dev/null
+++ b/src/ui/img/fa-image-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-link-solid-full.svg b/src/ui/img/fa-link-solid-full.svg
new file mode 100644
index 00000000..b779bf42
--- /dev/null
+++ b/src/ui/img/fa-link-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-magnifying-glass-solid-full.svg b/src/ui/img/fa-magnifying-glass-solid-full.svg
new file mode 100644
index 00000000..957dc21f
--- /dev/null
+++ b/src/ui/img/fa-magnifying-glass-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-moon-solid-full.svg b/src/ui/img/fa-moon-solid-full.svg
new file mode 100644
index 00000000..d910a7ce
--- /dev/null
+++ b/src/ui/img/fa-moon-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-music-solid-full.svg b/src/ui/img/fa-music-solid-full.svg
new file mode 100644
index 00000000..ec3583ec
--- /dev/null
+++ b/src/ui/img/fa-music-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-sliders-solid-full.svg b/src/ui/img/fa-sliders-solid-full.svg
new file mode 100644
index 00000000..7aa4ecdd
--- /dev/null
+++ b/src/ui/img/fa-sliders-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-sun-solid-full.svg b/src/ui/img/fa-sun-solid-full.svg
new file mode 100644
index 00000000..c0deb6cc
--- /dev/null
+++ b/src/ui/img/fa-sun-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-upload-solid-full.svg b/src/ui/img/fa-upload-solid-full.svg
new file mode 100644
index 00000000..d9a21dc4
--- /dev/null
+++ b/src/ui/img/fa-upload-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-video-solid-full.svg b/src/ui/img/fa-video-solid-full.svg
new file mode 100644
index 00000000..45f7c972
--- /dev/null
+++ b/src/ui/img/fa-video-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-wrench-solid-full.svg b/src/ui/img/fa-wrench-solid-full.svg
new file mode 100644
index 00000000..d2334022
--- /dev/null
+++ b/src/ui/img/fa-wrench-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/fa-x-solid-full.svg b/src/ui/img/fa-x-solid-full.svg
new file mode 100644
index 00000000..fb3886ca
--- /dev/null
+++ b/src/ui/img/fa-x-solid-full.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/img/logo.svg b/src/ui/img/logo.svg
new file mode 100644
index 00000000..59bf5533
--- /dev/null
+++ b/src/ui/img/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ui/index.tsx b/src/ui/index.tsx
new file mode 100644
index 00000000..1e594968
--- /dev/null
+++ b/src/ui/index.tsx
@@ -0,0 +1,49 @@
+import { render } from "preact";
+import { signal } from "@preact/signals";
+
+import UploadPage from "./pages/Upload";
+import ConversionPage from "./pages/Conversion";
+import { initTheme } from "./ThemeStore";
+import { openPopup, type PopupDataContainer } from "./PopupStore";
+import Popup from "./components/Popup";
+import { initMode } from "./ModeStore";
+
+console.log("Rendering UI");
+
+export const enum Pages {
+ Upload = "uploadPage",
+ Conversion = "conversionPage"
+}
+
+export const CurrentPage = signal(Pages.Upload);
+export let PopupData = signal({
+ title: "Loading tools...",
+ text: "Please wait while the app loads conversion tools.",
+ dismissible: false,
+ buttonText: 'Ignore'
+})
+
+function App() {
+ return (
+ <>
+ { CurrentPage.value === Pages.Conversion && }
+ { CurrentPage.value === Pages.Upload && }
+
+ >
+ )
+}
+
+/**
+ * Debug function to change pages without user workflow
+*/
+// @ts-expect-error
+window.changePage = (page: Pages) => {
+ CurrentPage.value = page
+}
+
+render( , document.body);
+
+openPopup();
+
+initTheme();
+initMode();
diff --git a/src/ui/pages/Conversion.css b/src/ui/pages/Conversion.css
new file mode 100644
index 00000000..d113130b
--- /dev/null
+++ b/src/ui/pages/Conversion.css
@@ -0,0 +1,86 @@
+.conversion-body {
+ background-color: var(--bg-page);
+ color: var(--text-secondary);
+ min-height: 100vh;
+ display: flex;
+
+ min-width: 100%;
+ flex-direction: column;
+ overflow: hidden;
+
+ @media (max-width: 1024px) {
+ /*height: auto;*/
+ overflow-x: hidden;
+ /*overflow-y: auto;*/
+ }
+}
+
+.scroller {
+ overflow-y: auto;
+}
+
+.conversion-main {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ }
+}
+
+.settings-sidebar {
+ width: 320px;
+ background: var(--bg-card);
+ border-inline-start: 1px solid var(--border-primary);
+ display: flex;
+ flex-direction: column;
+
+ position: sticky;
+ bottom: 0;
+
+ @media (max-width: 1024px) {
+ width: 100%;
+ }
+
+ .spacer {
+ flex: 1 1 auto;
+ }
+}
+
+.action-footer {
+ padding: 1.25rem;
+ background: var(--bg-page);
+ border-top: 1px solid var(--border-secondary);
+
+ .btn-convert {
+ width: 100%;
+ background: var(--btn-bg-primary);
+ color: var(--text-on-primary);
+ border: none;
+ padding: 1rem;
+ border-radius: 12px;
+ font-weight: 800;
+ font-size: 1.1rem;
+ cursor: pointer;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--primary) 30%, transparent);
+ transition: all 0.2s;
+
+ &:hover {
+ background: var(--btn-bg-primary-hover);
+ transform: translateY(-1px);
+ }
+ }
+}
+
+.desktop-only {
+ @media (max-width: 1024px) {
+ display: none !important;
+ }
+}
+
+.mobile-only {
+ @media (min-width: 1025px) {
+ display: none !important;
+ }
+}
diff --git a/src/ui/pages/Conversion.tsx b/src/ui/pages/Conversion.tsx
new file mode 100644
index 00000000..c4858511
--- /dev/null
+++ b/src/ui/pages/Conversion.tsx
@@ -0,0 +1,62 @@
+import faImageRegular from '../img/fa-image-regular-full.svg';
+import faBoxArchiveSolid from '../img/fa-box-archive-solid-full.svg';
+import faFileLinesRegular from '../img/fa-file-lines-regular-full.svg';
+import faVideoSolid from '../img/fa-video-solid-full.svg';
+import faMusicSolid from '../img/fa-music-solid-full.svg';
+
+import './Conversion.css'
+
+import { type FormatCategory } from "../components/Conversion/SideNav";
+import Footer from "../components/Footer";
+import ConversionSidebar from "../components/Conversion/ConversionSidebar";
+import SelectedFileInfo from "../components/Conversion/SelectedFileInfo";
+import ConversionHeader from "../components/Conversion/ConversionHeader";
+import { ConversionOptions, type ConversionOption, type ConversionOptionsMap } from 'src/main.new';
+
+import FormatExplorer from "../components/Conversion/FormatExplorer.tsx";
+import { useState } from "preact/hooks";
+
+interface ConversionPageProps {
+
+}
+
+const sidebarItems: FormatCategory[] = [ // Placeholder categories
+ { id: "arc", category: "Archive", icon: faBoxArchiveSolid },
+ { id: "img", category: "Image", icon: faImageRegular },
+ { id: "doc", category: "Document", icon: faFileLinesRegular },
+ { id: "vid", category: "Video", icon: faVideoSolid },
+ { id: "aud", category: "Audio", icon: faMusicSolid },
+ { id: "ebk", category: "E-Book", icon: faFileLinesRegular },
+];
+
+/**
+ * Flimsy getter to check to see if the conversion backend
+ * borked and didn't return any conversion options
+ */
+function getConversionOptions() {
+ if (ConversionOptions.size) {
+ return ConversionOptions
+ } else throw new Error("Can't build format list! Failed to get global format list");
+}
+
+export default function Conversion({ }: ConversionPageProps) {
+ const AvailableConversionOptions: ConversionOptionsMap = getConversionOptions();
+ const [selectedOption, setSelectedOption] = useState(null);
+
+ return (
+
+
+
+ {/* Mobile File Info */ }
+
+
+
+
+
+ {/* Right Settings Sidebar / Bottom Settings Accordion */ }
+
+
+
+
+ );
+}
diff --git a/src/ui/pages/Upload.css b/src/ui/pages/Upload.css
new file mode 100644
index 00000000..eb63b08e
--- /dev/null
+++ b/src/ui/pages/Upload.css
@@ -0,0 +1,12 @@
+body {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+
+ justify-content: center;
+ align-items: center;
+}
+
+button {
+ cursor: pointer;
+}
diff --git a/src/ui/pages/Upload.tsx b/src/ui/pages/Upload.tsx
new file mode 100644
index 00000000..f4388085
--- /dev/null
+++ b/src/ui/pages/Upload.tsx
@@ -0,0 +1,17 @@
+import Footer from "../components/Footer"
+import UploadField from "../components/Upload/UploadField"
+
+import './Upload.css'
+
+interface UploadPageProps {
+
+}
+
+export default function Upload(props: UploadPageProps | undefined) {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/src/ui/scripts/theme-detection.ts b/src/ui/scripts/theme-detection.ts
new file mode 100644
index 00000000..af96a268
--- /dev/null
+++ b/src/ui/scripts/theme-detection.ts
@@ -0,0 +1,5 @@
+const stored = localStorage.getItem("theme");
+const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+const theme = stored || (systemDark ? "dark" : "light");
+
+document.documentElement.dataset.theme = theme;
diff --git a/style.css b/style.css
index 8b47ce29..1a3ea997 100644
--- a/style.css
+++ b/style.css
@@ -1,210 +1,93 @@
-body {
- margin: 0;
- font-family: sans-serif;
- --highlight-color: #1C77FF;
-}
-
-#file-input {
- display: none;
-}
-
-#file-area {
- display: flex;
- flex-direction: column;
- justify-content: center;
- width: 100%;
- height: 30vh;
- background-color: var(--highlight-color);
- color: white;
- text-align: center;
- cursor: pointer;
-}
-
-#file-area h2,
-#file-area p {
- margin: 5px;
-}
-
-#side-panel {
- position: absolute;
- top: 0;
- right: 0;
- width: 20em;
- height: 30vh;
- padding: 2em;
- box-sizing: border-box;
- pointer-events: none;
-}
-
-#side-panel button {
- font-size: 1rem;
- width: 100%;
- padding: 10px 20px;
- border: 0;
- border-radius: 10px;
- background-color: white;
- color: var(--highlight-color);
- font-weight: bold;
- cursor: pointer;
- box-shadow: 0 5px 0 0 rgba(169, 169, 169, 0.4);
- pointer-events: all;
-}
-#side-panel button:active {
- transform: translateY(5px);
- box-shadow: 0 0 0 0;
-}
-
-#commit-id {
- color: #fff9;
- text-align: center;
- display: block;
- font-size: x-small;
- position: absolute;
- margin: 4px;
- text-decoration: none;
-}
-#commit-id:hover {
- text-decoration: underline;
-}
-
-#format-containers {
- display: flex;
-}
-
-.format-container {
- display: flex;
- flex-flow: column nowrap;
- align-items: center;
- width: 50vw;
- min-height: 70vh;
- padding: 0.8em 7em;
-}
-.format-container h2 {
- text-align: center;
- margin-bottom: 20px;
-}
-
-#from-container {
- background-color: lightgray;
-}
-#to-container {
- background-color: rgb(184, 184, 184);
-}
-
-.search {
- text-align: center;
- margin-bottom: 30px;
- padding: 10px 20px;
- border: 0;
- border-radius: 5px;
- outline: none;
- box-shadow: 0 5px 0 0 darkgrey;
-}
-
-.format-list {
- display: flex;
- flex-flow: column nowrap;
- width: 100%;
- align-items: center;
- background-color: white;
- border-radius: 10px;
- padding: 30px;
- margin-bottom: 5vw;
-}
-
-.format-list button {
- width: 80%;
- margin: 5px 0;
- padding: 10px;
- color: black;
- background-color: lightgray;
- border: 0;
- border-radius: 10px;
- font-family: monospace;
- word-break: break-word;
- cursor: pointer;
-}
-
-button.selected {
- background-color: var(--highlight-color);
- color: white;
- font-weight: bold;
-}
-
-#convert-button {
- position: fixed;
- left: 50%;
- bottom: 5%;
- transform: translateX(-50%);
- font-size: 1.5rem;
- padding: 10px 20px;
- border: 0;
- border-radius: 10px;
- background-color: var(--highlight-color);
- color: white;
- cursor: pointer;
-}
-
-.disabled {
- filter: grayscale(1);
- pointer-events: none;
-}
-
-#popup-bg {
- position: fixed;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
-}
-#popup {
- position: fixed;
- left: 50%;
- top: 50%;
- width: 20vw;
- transform: translate(-50%, -50%);
- background-color: white;
- padding: 15px;
- border-radius: 10px;
- text-align: center;
-}
-
-#popup button {
- font-size: 1rem;
- padding: 7px 20px;
- border: 0;
- border-radius: 10px;
- background-color: lightgray;
- color: black;
- cursor: pointer;
-}
-
-@media only screen and (max-width: 800px) {
- #drop-hint-text {
- display: none;
- }
- .format-container {
- width: 100%;
- box-sizing: border-box;
- padding: 0.8em 4em;
- }
- .format-list button {
- width: 100%;
- }
- #format-containers {
- flex-flow: column nowrap;
- }
- #popup {
- width: 80vw;
- }
- #side-panel {
- width: 50%;
- padding: 0.8em;
- text-align: right;
- }
- #side-panel button {
- font-size: 0.8rem;
- padding: 10px;
- }
+:root {
+ /* Globals */
+ --primary: #1C77FF;
+ --selection-color: hsl(from var(--primary) h s calc(l - 5.5) / 0.2);
+
+ /* Background */
+ --bg-page: #F9FAFB;
+ --bg-card: #FFFFFF;
+ --bg-card-alt: hsl(from var(--primary) calc(h - 2.2) s calc(l + 41.4));
+ --bg-card-alt-hover: hsl(from var(--primary) calc(h - 1.7) calc(s - 5.4) calc(l + 37.3));
+ --bg-section: #F3F4F6;
+ --bg-input: #FFFFFF;
+ --bg-input-placeholder: #F3F4F6;
+ --bg-light: #D1D5DB;
+ --bg-overlay: hsl(from var(--primary) calc(h + 5) calc(s - 17) calc(l - 2.2) / 0.702);
+
+ /* Text */
+ --text-on-primary: #FFFFFF;
+ --text-primary: #1E293B;
+ --text-secondary: #475569;
+ --text-tertiary: #64748B;
+ --text-muted: #9CA3AF;
+ --text-inverse: #FFFFFF;
+ --text-black: #000000;
+
+ /* Border */
+ --border-light: #E5E7EB;
+ --border-primary: #D1D5DB;
+ --border-secondary: #475569;
+ --border-transparent: #00000000;
+
+ /* Button */
+ --btn-border: #E5E7EB;
+
+ --btn-text-primary: var(--text-on-primary);
+ --btn-bg-primary: var(--primary);
+ --btn-bg-primary-hover: hsl(from var(--primary) h calc(s - 24) calc(l - 7.5));
+
+ --btn-text-secondary: var(--text-muted);
+ --btn-bg-secondary: #FFFFFF;
+ --btn-bg-secondary-hover: #F9FAFB;
+
+ scrollbar-color: var(--primary) var(--bg-page);
+}
+
+:root[data-theme="dark"] {
+ /* Background */
+ --bg-page: color-mix(in lch increasing hue, var(--primary), black 80%);
+ /* --bg-page: #0F172A; */
+ --bg-section: hsl(from var(--primary) h calc(s - 60) calc(l - 44.5));
+ --bg-card: color-mix(in hsl, var(--primary), black 70%);
+ /* --bg-card: #1E293B; */
+ --bg-card-alt: hsl(from var(--primary) calc(h - 1.9) calc(s - 66) calc(l - 34.1));
+ --bg-card-alt-hover: hsl(from var(--primary) calc(h - 0.4) calc(s - 65.7) calc(l - 27.5));
+ --bg-input: --bg-card;
+ --bg-input-placeholder: #334155;
+ --bg-light: #334155;
+ --bg-overlay: hsl(from var(--primary) calc(h + 5) calc(s - 17) calc(l - 2.2) / 0.702);
+
+ /* Text */
+ --text-on-primary: #FFFFFF;
+ --text-primary: #F1F5F9;
+ --text-secondary: #CBD5E1;
+ --text-tertiary: #94A3B8;
+ --text-muted: hsl(from var(--primary) h calc(s - 84) calc(l - 8.5));
+ --text-inverse: #000000;
+ --text-black: #000000;
+
+ /* Border */
+ --border-primary: hsl(from var(--primary) h calc(s - 75) calc(l - 28.5));
+ --border-secondary: hsl(from var(--primary) h calc(s - 80) calc(l - 21));
+ --border-transparent: #00000000;
+
+ /* Button */
+ --btn-border: hsl(from var(--primary) h calc(s - 81) calc(l - 28.5));
+
+ --btn-text-primary: #FFFFFF;
+ --btn-bg-primary: var(--primary);
+ --btn-bg-primary-hover: hsl(from var(--primary) h calc(s - 40) calc(l - 10));
+
+ --btn-text-secondary: var(--text-tertiary);
+ --btn-bg-secondary: hsl(from var(--primary) h calc(s - 67) calc(l - 38));
+ --btn-bg-secondary-hover: hsl(from var(--primary) h calc(s - 64) calc(l - 34.5));
+}
+
+html {
+ background-color: var(--bg-page);
+ color: var(--text-primary);
+}
+
+::selection {
+ background-color: var(--selection-color);
}
diff --git a/tsconfig.json b/tsconfig.json
index 8e40c70e..19c16655 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -37,7 +37,12 @@
"paths": {
"qoi-fu": ["./src/handlers/qoi-fu/transpiled/QOI.js"],
"qoa-fu": ["./src/handlers/qoa-fu/transpiled/QOA.js"]
- }
+ },
+ /**
+ * JSX setup
+ */
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
},
"include": [
"src"
diff --git a/vite.config.js b/vite.config.js
index 3812369b..1cecbdca 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
import tsconfigPaths from "vite-tsconfig-paths";
+import preact from "@preact/preset-vite"
export default defineConfig({
optimizeDeps: {
@@ -59,6 +60,10 @@ export default defineConfig({
}
]
}),
- tsconfigPaths()
+ tsconfigPaths(),
+ preact({
+ // prefreshEnabled: true,
+ reactAliasesEnabled: true
+ })
]
});