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 174e94e..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..7693482 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["@opentui/solid/preload"] diff --git a/lint.ts b/lint.ts new file mode 100644 index 0000000..d88728a --- /dev/null +++ b/lint.ts @@ -0,0 +1,18 @@ +const proc = Bun.spawn({ + cmd: [ + "bunx", + "eslint", + "src/**/*.ts", + "src/**/*.tsx", + "tests/**/*.ts", + "tests/**/*.tsx", + ], + stdio: ["inherit", "inherit", "inherit"], +}) + +const exitCode = await proc.exited +if (exitCode !== 0) { + process.exit(exitCode) +} + +export {} diff --git a/package.json b/package.json index 348eb49..9ce412d 100644 --- a/package.json +++ b/package.json @@ -1 +1,33 @@ -{ "dependencies": { "@opentui/core": "^0.1.77" } } \ No newline at end of file +{ + "name": "podcast-tui-app", + "module": "src/index.tsx", + "type": "module", + "private": true, + "scripts": { + "start": "bun run src/index.tsx", + "dev": "bun run --watch src/index.tsx", + "build": "bun run build.ts", + "test": "bun test", + "lint": "bun run lint.ts" + }, + "devDependencies": { + "@opentui/react": "^0.1.77", + "@types/bun": "latest", + "@types/uuid": "^11.0.0", + "@typescript-eslint/eslint-plugin": "^8.54.0", + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^9.39.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@opentui/core": "^0.1.77", + "@opentui/solid": "^0.1.77", + "babel-preset-solid": "1.9.9", + "date-fns": "^4.1.0", + "solid-js": "^1.9.11", + "uuid": "^13.0.0", + "zustand": "^5.0.11" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..3dfd31a --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,46 @@ +const createSignal = (value: 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/**/*"] +}