From 7b5c256e07450026757cf814fa37d4158f4c337b Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 4 Feb 2026 00:06:16 -0500 Subject: [PATCH] start --- .eslintrc.cjs | 11 +++++ .gitignore | 33 ++++++++++++++ README.md | 15 +++++++ build.ts | 12 +++++ bun.lockb | Bin 37576 -> 0 bytes bunfig.toml | 1 + lint.ts | 18 ++++++++ package.json | 34 +++++++++++++- src/App.tsx | 46 +++++++++++++++++++ src/components/BoxLayout.tsx | 42 ++++++++++++++++++ src/components/Column.tsx | 35 +++++++++++++++ src/components/ExportDialog.tsx | 36 +++++++++++++++ src/components/FileInfo.tsx | 17 +++++++ src/components/FilePicker.tsx | 22 +++++++++ src/components/ImportDialog.tsx | 21 +++++++++ src/components/KeyboardHandler.tsx | 21 +++++++++ src/components/Layout.tsx | 17 +++++++ src/components/Navigation.tsx | 24 ++++++++++ src/components/ResponsiveContainer.tsx | 19 ++++++++ src/components/Row.tsx | 35 +++++++++++++++ src/components/ShortcutHelp.tsx | 26 +++++++++++ src/components/SyncError.tsx | 15 +++++++ src/components/SyncPanel.tsx | 30 +++++++++++++ src/components/SyncProgress.tsx | 25 +++++++++++ src/components/SyncStatus.tsx | 50 +++++++++++++++++++++ src/components/Tab.tsx | 36 +++++++++++++++ src/components/TabNavigation.tsx | 36 +++++++++++++++ src/config/shortcuts.ts | 6 +++ src/constants/sync-formats.ts | 12 +++++ src/hooks/useKeyboardShortcuts.ts | 37 ++++++++++++++++ src/index.tsx | 4 ++ src/opentui-jsx.d.ts | 27 +++++++++++ src/types/sync-json.ts | 24 ++++++++++ src/types/sync-xml.ts | 28 ++++++++++++ src/utils/file-detector.ts | 10 +++++ src/utils/sync-validation.ts | 25 +++++++++++ src/utils/sync.ts | 59 +++++++++++++++++++++++++ tsconfig.json | 25 +++++++++++ 38 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.cjs create mode 100644 README.md create mode 100644 build.ts delete mode 100755 bun.lockb create mode 100644 bunfig.toml create mode 100644 lint.ts create mode 100644 src/App.tsx create mode 100644 src/components/BoxLayout.tsx create mode 100644 src/components/Column.tsx create mode 100644 src/components/ExportDialog.tsx create mode 100644 src/components/FileInfo.tsx create mode 100644 src/components/FilePicker.tsx create mode 100644 src/components/ImportDialog.tsx create mode 100644 src/components/KeyboardHandler.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/Navigation.tsx create mode 100644 src/components/ResponsiveContainer.tsx create mode 100644 src/components/Row.tsx create mode 100644 src/components/ShortcutHelp.tsx create mode 100644 src/components/SyncError.tsx create mode 100644 src/components/SyncPanel.tsx create mode 100644 src/components/SyncProgress.tsx create mode 100644 src/components/SyncStatus.tsx create mode 100644 src/components/Tab.tsx create mode 100644 src/components/TabNavigation.tsx create mode 100644 src/config/shortcuts.ts create mode 100644 src/constants/sync-formats.ts create mode 100644 src/hooks/useKeyboardShortcuts.ts create mode 100644 src/index.tsx create mode 100644 src/opentui-jsx.d.ts create mode 100644 src/types/sync-json.ts create mode 100644 src/types/sync-xml.ts create mode 100644 src/utils/file-detector.ts create mode 100644 src/utils/sync-validation.ts create mode 100644 src/utils/sync.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..edfd56a --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + env: { + es2022: true, + node: true, + }, + ignorePatterns: ["dist", "node_modules"], +} diff --git a/.gitignore b/.gitignore index 28d236f..d2b6655 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,35 @@ .opencode +# dependencies (bun install) node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +*.log +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..984eba3 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# solid + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun dev +``` + +This project was created using `bun create tui`. [create-tui](https://git.new/create-tui) is the easiest way to get started with OpenTUI. diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..b45bd8d --- /dev/null +++ b/build.ts @@ -0,0 +1,12 @@ +import solidPlugin from "@opentui/solid/bun-plugin" + +await Bun.build({ + entrypoints: ["./src/index.tsx"], + outdir: "./dist", + target: "bun", + minify: true, + sourcemap: "external", + plugins: [solidPlugin], +}) + +console.log("Build complete") diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 174e94e7f54f2107d28780e05b64196ef5c2c1af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37576 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: T): [() => T, (next: T) => void] => { + let current = value + return [() => current, (next) => { + current = next + }] +} +import { Layout } from "./components/Layout" +import { Navigation } from "./components/Navigation" +import { TabNavigation } from "./components/TabNavigation" +import { KeyboardHandler } from "./components/KeyboardHandler" +import { SyncPanel } from "./components/SyncPanel" +import type { TabId } from "./components/Tab" + +export function App() { + const activeTab = createSignal("discover") + + return ( + + + } + footer={ + + } + > + + {activeTab[0]() === "settings" ? ( + + ) : ( + + + {`${activeTab[0]()}`} +
+ Content placeholder +
+
+ )} +
+
+
+ ) +} diff --git a/src/components/BoxLayout.tsx b/src/components/BoxLayout.tsx new file mode 100644 index 0000000..3ca3dfc --- /dev/null +++ b/src/components/BoxLayout.tsx @@ -0,0 +1,42 @@ +import type { JSX } from "solid-js" + +type BoxLayoutProps = { + children?: JSX.Element + flexDirection?: "row" | "column" | "row-reverse" | "column-reverse" + justifyContent?: + | "flex-start" + | "flex-end" + | "center" + | "space-between" + | "space-around" + | "space-evenly" + alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline" + gap?: number + width?: number | "auto" | `${number}%` + height?: number | "auto" | `${number}%` + padding?: number + margin?: number + border?: boolean + title?: string +} + +export function BoxLayout(props: BoxLayoutProps) { + return ( + + {props.children} + + ) +} diff --git a/src/components/Column.tsx b/src/components/Column.tsx new file mode 100644 index 0000000..8685287 --- /dev/null +++ b/src/components/Column.tsx @@ -0,0 +1,35 @@ +import type { JSX } from "solid-js" + +type ColumnProps = { + children?: JSX.Element + gap?: number + alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline" + justifyContent?: + | "flex-start" + | "flex-end" + | "center" + | "space-between" + | "space-around" + | "space-evenly" + width?: number | "auto" | `${number}%` + height?: number | "auto" | `${number}%` + padding?: number +} + +export function Column(props: ColumnProps) { + return ( + + {props.children} + + ) +} diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx new file mode 100644 index 0000000..e358f39 --- /dev/null +++ b/src/components/ExportDialog.tsx @@ -0,0 +1,36 @@ +const createSignal = (value: T): [() => T, (next: T) => void] => { + let current = value + return [() => current, (next) => { + current = next + }] +} + +import { SyncStatus } from "./SyncStatus" + +export function ExportDialog() { + const filename = createSignal("podcast-sync.json") + const format = createSignal<"json" | "xml">("json") + + return ( + + + File: + + + + Format: + format[1](index === 0 ? "json" : "xml")} + /> + + + Export {format[0]()} to {filename[0]()} + + + + ) +} diff --git a/src/components/FileInfo.tsx b/src/components/FileInfo.tsx new file mode 100644 index 0000000..8bb8fab --- /dev/null +++ b/src/components/FileInfo.tsx @@ -0,0 +1,17 @@ +type FileInfoProps = { + path: string + format: string + size: string + modifiedAt: string +} + +export function FileInfo(props: FileInfoProps) { + return ( + + Path: {props.path} + Format: {props.format} + Size: {props.size} + Modified: {props.modifiedAt} + + ) +} diff --git a/src/components/FilePicker.tsx b/src/components/FilePicker.tsx new file mode 100644 index 0000000..998ace9 --- /dev/null +++ b/src/components/FilePicker.tsx @@ -0,0 +1,22 @@ +import { detectFormat } from "../utils/file-detector" + +type FilePickerProps = { + value: string + onChange: (value: string) => void +} + +export function FilePicker(props: FilePickerProps) { + const format = detectFormat(props.value) + + return ( + + + Format: {format} + + ) +} diff --git a/src/components/ImportDialog.tsx b/src/components/ImportDialog.tsx new file mode 100644 index 0000000..ef6f8d4 --- /dev/null +++ b/src/components/ImportDialog.tsx @@ -0,0 +1,21 @@ +const createSignal = (value: T): [() => T, (next: T) => void] => { + let current = value + return [() => current, (next) => { + current = next + }] +} + +import { FilePicker } from "./FilePicker" + +export function ImportDialog() { + const filePath = createSignal("") + + return ( + + + + Import selected file + + + ) +} diff --git a/src/components/KeyboardHandler.tsx b/src/components/KeyboardHandler.tsx new file mode 100644 index 0000000..66d3a53 --- /dev/null +++ b/src/components/KeyboardHandler.tsx @@ -0,0 +1,21 @@ +import type { JSX } from "solid-js" +import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts" +import type { TabId } from "./Tab" + +type KeyboardHandlerProps = { + children?: JSX.Element + onTabSelect: (tab: TabId) => void +} + +export function KeyboardHandler(props: KeyboardHandlerProps) { + useKeyboardShortcuts({ + onTabNext: () => { + props.onTabSelect("discover") + }, + onTabPrev: () => { + props.onTabSelect("settings") + }, + }) + + return <>{props.children} +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..238d853 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,17 @@ +import type { JSX } from "solid-js" + +type LayoutProps = { + header?: JSX.Element + footer?: JSX.Element + children?: JSX.Element +} + +export function Layout(props: LayoutProps) { + return ( + + {props.header ? {props.header} : } + {props.children} + {props.footer ? {props.footer} : } + + ) +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx new file mode 100644 index 0000000..59f18ce --- /dev/null +++ b/src/components/Navigation.tsx @@ -0,0 +1,24 @@ +import type { TabId } from "./Tab" + +type NavigationProps = { + activeTab: TabId + onTabSelect: (tab: TabId) => void +} + +export function Navigation(props: NavigationProps) { + return ( + + + {props.activeTab === "discover" ? "[" : " "}Discover{props.activeTab === "discover" ? "]" : " "} + + {props.activeTab === "feeds" ? "[" : " "}My Feeds{props.activeTab === "feeds" ? "]" : " "} + + {props.activeTab === "search" ? "[" : " "}Search{props.activeTab === "search" ? "]" : " "} + + {props.activeTab === "player" ? "[" : " "}Player{props.activeTab === "player" ? "]" : " "} + + {props.activeTab === "settings" ? "[" : " "}Settings{props.activeTab === "settings" ? "]" : " "} + + + ) +} diff --git a/src/components/ResponsiveContainer.tsx b/src/components/ResponsiveContainer.tsx new file mode 100644 index 0000000..f07767e --- /dev/null +++ b/src/components/ResponsiveContainer.tsx @@ -0,0 +1,19 @@ +import { createMemo, type JSX } from "solid-js" +import { useTerminalDimensions } from "@opentui/solid" + +type ResponsiveContainerProps = { + children?: (size: "small" | "medium" | "large") => JSX.Element +} + +export function ResponsiveContainer(props: ResponsiveContainerProps) { + const dimensions = useTerminalDimensions() + + const size = createMemo<"small" | "medium" | "large">(() => { + const width = dimensions().width + if (width < 60) return "small" + if (width < 100) return "medium" + return "large" + }) + + return <>{props.children?.(size())} +} diff --git a/src/components/Row.tsx b/src/components/Row.tsx new file mode 100644 index 0000000..01b7bc2 --- /dev/null +++ b/src/components/Row.tsx @@ -0,0 +1,35 @@ +import type { JSX } from "solid-js" + +type RowProps = { + children?: JSX.Element + gap?: number + alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline" + justifyContent?: + | "flex-start" + | "flex-end" + | "center" + | "space-between" + | "space-around" + | "space-evenly" + width?: number | "auto" | `${number}%` + height?: number | "auto" | `${number}%` + padding?: number +} + +export function Row(props: RowProps) { + return ( + + {props.children} + + ) +} diff --git a/src/components/ShortcutHelp.tsx b/src/components/ShortcutHelp.tsx new file mode 100644 index 0000000..550a4b5 --- /dev/null +++ b/src/components/ShortcutHelp.tsx @@ -0,0 +1,26 @@ +import { shortcuts } from "../config/shortcuts" + +export function ShortcutHelp() { + return ( + + + + {shortcuts[0]?.keys ?? ""} + {shortcuts[0]?.action ?? ""} + + + {shortcuts[1]?.keys ?? ""} + {shortcuts[1]?.action ?? ""} + + + {shortcuts[2]?.keys ?? ""} + {shortcuts[2]?.action ?? ""} + + + {shortcuts[3]?.keys ?? ""} + {shortcuts[3]?.action ?? ""} + + + + ) +} diff --git a/src/components/SyncError.tsx b/src/components/SyncError.tsx new file mode 100644 index 0000000..4d9a79f --- /dev/null +++ b/src/components/SyncError.tsx @@ -0,0 +1,15 @@ +type SyncErrorProps = { + message: string + onRetry: () => void +} + +export function SyncError(props: SyncErrorProps) { + return ( + + {props.message} + + Retry + + + ) +} diff --git a/src/components/SyncPanel.tsx b/src/components/SyncPanel.tsx new file mode 100644 index 0000000..27a4529 --- /dev/null +++ b/src/components/SyncPanel.tsx @@ -0,0 +1,30 @@ +const createSignal = (value: T): [() => T, (next: T) => void] => { + let current = value + return [() => current, (next) => { + current = next + }] +} + +import { ImportDialog } from "./ImportDialog" +import { ExportDialog } from "./ExportDialog" +import { SyncStatus } from "./SyncStatus" + +export function SyncPanel() { + const mode = createSignal<"import" | "export" | null>(null) + + return ( + + + mode[1]("import")}> + Import + + mode[1]("export")}> + Export + + + + {mode[0]() === "import" ? : null} + {mode[0]() === "export" ? : null} + + ) +} diff --git a/src/components/SyncProgress.tsx b/src/components/SyncProgress.tsx new file mode 100644 index 0000000..b76cd46 --- /dev/null +++ b/src/components/SyncProgress.tsx @@ -0,0 +1,25 @@ +type SyncProgressProps = { + value: number +} + +export function SyncProgress(props: SyncProgressProps) { + const width = 30 + let filled = (props.value / 100) * width + filled = filled >= 0 ? filled : 0 + filled = filled <= width ? filled : width + filled = filled | 0 + if (filled < 0) filled = 0 + if (filled > width) filled = width + + let bar = "" + for (let i = 0; i < width; i += 1) { + bar += i < filled ? "#" : "-" + } + + return ( + + {bar} + {props.value}% + + ) +} diff --git a/src/components/SyncStatus.tsx b/src/components/SyncStatus.tsx new file mode 100644 index 0000000..8e81be3 --- /dev/null +++ b/src/components/SyncStatus.tsx @@ -0,0 +1,50 @@ +const createSignal = (value: T): [() => T, (next: T) => void] => { + let current = value + return [() => current, (next) => { + current = next + }] +} + +import { SyncProgress } from "./SyncProgress" +import { SyncError } from "./SyncError" + +type SyncState = "idle" | "syncing" | "complete" | "error" + +export function SyncStatus() { + const state = createSignal("idle") + const message = createSignal("Idle") + const progress = createSignal(0) + + const toggle = () => { + if (state[0]() === "idle") { + state[1]("syncing") + message[1]("Syncing...") + progress[1](40) + } else if (state[0]() === "syncing") { + state[1]("complete") + message[1]("Sync complete") + progress[1](100) + } else if (state[0]() === "complete") { + state[1]("error") + message[1]("Sync failed") + } else { + state[1]("idle") + message[1]("Idle") + progress[1](0) + } + } + + return ( + + + Status: + {message[0]()} + + + {state[0]() === "error" ? toggle()} /> : null} + + Cycle Status + + + ) +} diff --git a/src/components/Tab.tsx b/src/components/Tab.tsx new file mode 100644 index 0000000..a6b9bed --- /dev/null +++ b/src/components/Tab.tsx @@ -0,0 +1,36 @@ +export type TabId = "discover" | "feeds" | "search" | "player" | "settings" + +export type TabDefinition = { + id: TabId + label: string +} + +export const tabs: TabDefinition[] = [ + { id: "discover", label: "Discover" }, + { id: "feeds", label: "My Feeds" }, + { id: "search", label: "Search" }, + { id: "player", label: "Player" }, + { id: "settings", label: "Settings" }, +] + +type TabProps = { + tab: TabDefinition + active: boolean + onSelect: (tab: TabId) => void +} + +export function Tab(props: TabProps) { + return ( + props.onSelect(props.tab.id)} + style={{ padding: 1, backgroundColor: props.active ? "#333333" : "transparent" }} + > + + {props.active ? "[" : " "} + {props.tab.label} + {props.active ? "]" : " "} + + + ) +} diff --git a/src/components/TabNavigation.tsx b/src/components/TabNavigation.tsx new file mode 100644 index 0000000..791a0a6 --- /dev/null +++ b/src/components/TabNavigation.tsx @@ -0,0 +1,36 @@ +import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts" +import { Tab, type TabId } from "./Tab" + +type TabNavigationProps = { + activeTab: TabId + onTabSelect: (tab: TabId) => void +} + +export function TabNavigation(props: TabNavigationProps) { + useKeyboardShortcuts({ + onTabNext: () => { + if (props.activeTab === "discover") props.onTabSelect("feeds") + else if (props.activeTab === "feeds") props.onTabSelect("search") + else if (props.activeTab === "search") props.onTabSelect("player") + else if (props.activeTab === "player") props.onTabSelect("settings") + else props.onTabSelect("discover") + }, + onTabPrev: () => { + if (props.activeTab === "discover") props.onTabSelect("settings") + else if (props.activeTab === "settings") props.onTabSelect("player") + else if (props.activeTab === "player") props.onTabSelect("search") + else if (props.activeTab === "search") props.onTabSelect("feeds") + else props.onTabSelect("discover") + }, + }) + + return ( + + + + + + + + ) +} diff --git a/src/config/shortcuts.ts b/src/config/shortcuts.ts new file mode 100644 index 0000000..2da2785 --- /dev/null +++ b/src/config/shortcuts.ts @@ -0,0 +1,6 @@ +export const shortcuts = [ + { keys: "Ctrl+Q", action: "Quit" }, + { keys: "Ctrl+S", action: "Save" }, + { keys: "Left/Right", action: "Switch tabs" }, + { keys: "Esc", action: "Close modal" }, +] as const diff --git a/src/constants/sync-formats.ts b/src/constants/sync-formats.ts new file mode 100644 index 0000000..26c714e --- /dev/null +++ b/src/constants/sync-formats.ts @@ -0,0 +1,12 @@ +export const syncFormats = { + json: { + version: "1.0", + extension: ".json", + }, + xml: { + version: "1.0", + extension: ".xml", + }, +} + +export const supportedSyncVersions = [syncFormats.json.version, syncFormats.xml.version] diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..558abce --- /dev/null +++ b/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,37 @@ +import { useKeyboard, useRenderer } from "@opentui/solid" + +type ShortcutOptions = { + onSave?: () => void + onQuit?: () => void + onTabNext?: () => void + onTabPrev?: () => void +} + +export function useKeyboardShortcuts(options: ShortcutOptions) { + const renderer = useRenderer() + + useKeyboard((key) => { + if (key.ctrl && key.name === "q") { + if (options.onQuit) { + options.onQuit() + } else { + renderer.destroy() + } + return + } + + if (key.ctrl && key.name === "s") { + options.onSave?.() + return + } + + if (key.name === "right") { + options.onTabNext?.() + return + } + + if (key.name === "left") { + options.onTabPrev?.() + } + }) +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..ba39920 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,4 @@ +import { render } from "@opentui/solid" +import { App } from "./App" + +render(() => ) diff --git a/src/opentui-jsx.d.ts b/src/opentui-jsx.d.ts new file mode 100644 index 0000000..bd87e52 --- /dev/null +++ b/src/opentui-jsx.d.ts @@ -0,0 +1,27 @@ +declare namespace JSX { + type Element = any + interface IntrinsicElements { + box: any + text: any + span: any + input: any + select: any + textarea: any + scrollbox: any + tab_select: any + ascii_font: any + code: any + diff: any + line_number: any + markdown: any + b: any + strong: any + i: any + em: any + u: any + br: any + a: any + } +} + +export {} diff --git a/src/types/sync-json.ts b/src/types/sync-json.ts new file mode 100644 index 0000000..c409303 --- /dev/null +++ b/src/types/sync-json.ts @@ -0,0 +1,24 @@ +export type SyncData = { + version: string + lastSyncedAt: string + feeds: { + id: string + title: string + url: string + isPrivate: boolean + }[] + sources: { + id: string + name: string + url: string + }[] + settings: { + theme: string + playbackSpeed: number + downloadPath: string + } + preferences: { + showExplicit: boolean + autoDownload: boolean + } +} diff --git a/src/types/sync-xml.ts b/src/types/sync-xml.ts new file mode 100644 index 0000000..403966a --- /dev/null +++ b/src/types/sync-xml.ts @@ -0,0 +1,28 @@ +export type SyncDataXML = { + version: string + lastSyncedAt: string + feeds: { + feed: { + id: string + title: string + url: string + isPrivate: boolean + }[] + } + sources: { + source: { + id: string + name: string + url: string + }[] + } + settings: { + theme: string + playbackSpeed: number + downloadPath: string + } + preferences: { + showExplicit: boolean + autoDownload: boolean + } +} diff --git a/src/utils/file-detector.ts b/src/utils/file-detector.ts new file mode 100644 index 0000000..8ca3ccd --- /dev/null +++ b/src/utils/file-detector.ts @@ -0,0 +1,10 @@ +export type DetectedFormat = "json" | "xml" | "unknown" + +export function detectFormat(filePath: string): DetectedFormat { + const length = filePath.length + const jsonSuffix = length >= 5 ? filePath.substr(length - 5) : "" + const xmlSuffix = length >= 4 ? filePath.substr(length - 4) : "" + if (jsonSuffix === ".json" || jsonSuffix === ".JSON") return "json" + if (xmlSuffix === ".xml" || xmlSuffix === ".XML") return "xml" + return "unknown" +} diff --git a/src/utils/sync-validation.ts b/src/utils/sync-validation.ts new file mode 100644 index 0000000..ae60e14 --- /dev/null +++ b/src/utils/sync-validation.ts @@ -0,0 +1,25 @@ +import type { SyncData } from "../types/sync-json" +import type { SyncDataXML } from "../types/sync-xml" +import { syncFormats } from "../constants/sync-formats" + +const isObject = (value: unknown): value is { [key: string]: unknown } => + typeof value === "object" && value !== null + +const hasVersion = (value: unknown): value is { version: string } => + isObject(value) && typeof value.version === "string" + +export function validateJSONSync(data: unknown): SyncData { + if (!hasVersion(data) || data.version !== syncFormats.json.version) { + throw { message: "Unsupported sync format" } + } + + return data as SyncData +} + +export function validateXMLSync(data: unknown): SyncDataXML { + if (!hasVersion(data) || data.version !== syncFormats.xml.version) { + throw { message: "Unsupported sync format" } + } + + return data as SyncDataXML +} diff --git a/src/utils/sync.ts b/src/utils/sync.ts new file mode 100644 index 0000000..398283d --- /dev/null +++ b/src/utils/sync.ts @@ -0,0 +1,59 @@ +import type { SyncData } from "../types/sync-json" +import type { SyncDataXML } from "../types/sync-xml" +import { validateJSONSync, validateXMLSync } from "./sync-validation" +import { syncFormats } from "../constants/sync-formats" + +export function exportToJSON(data: SyncData): string { + return `{\n "version": "${data.version}",\n "lastSyncedAt": "${data.lastSyncedAt}",\n "feeds": [],\n "sources": [],\n "settings": {\n "theme": "${data.settings.theme}",\n "playbackSpeed": ${data.settings.playbackSpeed},\n "downloadPath": "${data.settings.downloadPath}"\n },\n "preferences": {\n "showExplicit": ${data.preferences.showExplicit},\n "autoDownload": ${data.preferences.autoDownload}\n }\n}` +} + +export function importFromJSON(json: string): SyncData { + const data = json + return validateJSONSync(data as unknown) +} + +export function exportToXML(data: SyncDataXML): string { + const feedItems = "" + const sourceItems = "" + + return `\n` + + `\n` + + ` ${data.lastSyncedAt}\n` + + ` \n` + + feedItems + + ` \n` + + ` \n` + + sourceItems + + ` \n` + + ` \n` + + ` ${data.settings.theme}\n` + + ` ${data.settings.playbackSpeed}\n` + + ` ${data.settings.downloadPath}\n` + + ` \n` + + ` \n` + + ` ${data.preferences.showExplicit}\n` + + ` ${data.preferences.autoDownload}\n` + + ` \n` + + `` +} + +export function importFromXML(xml: string): SyncDataXML { + const version = syncFormats.xml.version + const data = { + version, + lastSyncedAt: "", + feeds: { feed: [] }, + sources: { source: [] }, + settings: { + theme: "system", + playbackSpeed: 1, + downloadPath: "", + }, + preferences: { + showExplicit: false, + autoDownload: false, + }, + } as SyncDataXML + + return validateXMLSync(data) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..173f81f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable", "ES2015", "ES2015.Promise"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["bun-types"], + "baseUrl": ".", + "paths": { + "@/components/*": ["src/components/*"], + "@/stores/*": ["src/stores/*"], + "@/types/*": ["src/types/*"], + "@/utils/*": ["src/utils/*"], + "@/hooks/*": ["src/hooks/*"], + "@/api/*": ["src/api/*"], + "@/data/*": ["src/data/*"] + } + }, + "include": ["src/**/*", "tests/**/*"] +}