diff --git a/.gitignore b/.gitignore index 43c6e99..211650e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ coverage # Transpiled files build/ +dist/ main/ out/ .next/ diff --git a/package-lock.json b/package-lock.json index ca06099..0862723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@studiorack/core": "^2.0.15", + "@studiorack/core": "^2.0.16", "electron-is-dev": "^2.0.0", "electron-next": "^3.1.5", "fix-path": "^3.0.0", @@ -18,7 +18,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "remark": "^15.0.1", - "remark-html": "^16.0.1" + "remark-html": "^16.0.1", + "slugify": "^1.6.6" }, "devDependencies": { "@eslint/js": "^9.2.0", @@ -1856,9 +1857,9 @@ } }, "node_modules/@studiorack/core": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@studiorack/core/-/core-2.0.15.tgz", - "integrity": "sha512-D04T35wfzb46tISS0cJih2lIbJEFbGAW29XoOcQRKg9NJf+6YwHQIj8tUWwSWGCgiDoqo/t9dGLRkUpapQAshQ==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@studiorack/core/-/core-2.0.16.tgz", + "integrity": "sha512-WJX/FP+ucrtGj0thy2ikn3ArrFzkH8EiPcnsr26jkGimEHzZ/sdLIMzppV4uqBJolNJWL4TOjIvv35dm5wCk4A==", "license": "MIT", "dependencies": { "@vscode/sudo-prompt": "^9.3.1", diff --git a/package.json b/package.json index 7b31470..ee05228 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "vitest": "^1.6.0" }, "dependencies": { - "@studiorack/core": "^2.0.15", + "@studiorack/core": "^2.0.16", "electron-is-dev": "^2.0.0", "electron-next": "^3.1.5", "fix-path": "^3.0.0", @@ -99,7 +99,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "remark": "^15.0.1", - "remark-html": "^16.0.1" + "remark-html": "^16.0.1", + "slugify": "^1.6.6" }, "overrides": { "path-to-regexp": "^8.1.0" diff --git a/renderer/components/audio.tsx b/renderer/components/audio.tsx new file mode 100644 index 0000000..8bc118f --- /dev/null +++ b/renderer/components/audio.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { getBasePath } from '../lib/path'; +import styles from '../styles/components/audio.module.css'; +import { PluginFile } from '@studiorack/core'; + +type AudioProps = { + file: PluginFile; +}; + +const Audio = ({ file }: AudioProps) => { + const [isPlaying, setIsPlaying] = useState(false); + + const play = () => { + const el = document.getElementById('audio') as HTMLAudioElement; + if (el.paused) { + el.removeEventListener('ended', ended); + el.addEventListener('ended', ended); + el.currentTime = 0; + el.play(); + setIsPlaying(true); + } + }; + + const pause = () => { + const el = document.getElementById('audio') as HTMLAudioElement; + if (!el.paused) { + el.pause(); + setIsPlaying(false); + } + }; + + const ended = () => { + setIsPlaying(false); + }; + + return ( +
+ {isPlaying ? ( + Pause + ) : ( + Play + )} + +
+ ); +}; + +export default Audio; diff --git a/renderer/components/card.tsx b/renderer/components/card.tsx new file mode 100644 index 0000000..6a173c1 --- /dev/null +++ b/renderer/components/card.tsx @@ -0,0 +1,55 @@ +import styles from '../styles/components/card.module.css'; +import Link from 'next/link'; +import { getBasePath } from '../lib/path'; +import { imageError } from '../lib/image'; +import { pluginFileUrl } from '@studiorack/core'; + +type CardProps = { + section: string; + plugin: any; + pluginIndex: number; +}; + +const Card = ({ section, plugin, pluginIndex }: CardProps) => ( + +
+
+
+

+ {plugin.name} v{plugin.version} +

+ + Download + +
+ +
+ {plugin.files.image ? ( + {plugin.name} + ) : ( + '' + )} +
+ +); + +export default Card; diff --git a/renderer/components/code.tsx b/renderer/components/code.tsx new file mode 100644 index 0000000..b0d5848 --- /dev/null +++ b/renderer/components/code.tsx @@ -0,0 +1,32 @@ +import styles from '../styles/components/code.module.css'; +import { PluginVersion } from '@studiorack/core'; + +type CodeProps = { + plugin: PluginVersion; +}; + +const Code = ({ plugin }: CodeProps) => ( +
+

+ Install via{' '} + + StudioRack CLI + +

+ {plugin.tags.includes('sfz') ? ( + +
studiorack plugin install sfztools/sfizz
+
studiorack plugin install {plugin.id}
+
+ ) : plugin.tags.includes('sf2') ? ( + +
studiorack plugin install studiorack/juicysf
+
studiorack plugin install {plugin.id}
+
+ ) : ( +
studiorack plugin install {plugin.id}
+ )} +
+); + +export default Code; diff --git a/renderer/components/dependency.tsx b/renderer/components/dependency.tsx index 7bd8bd6..41756e2 100644 --- a/renderer/components/dependency.tsx +++ b/renderer/components/dependency.tsx @@ -1,43 +1,33 @@ -import styles from '../styles/plugin.module.css'; import { getBasePath } from '../lib/path'; import { PluginVersion } from '@studiorack/core'; type DependencyProps = { plugin: PluginVersion; - message?: boolean; }; -const Dependency = ({ plugin, message = false }: DependencyProps) => { +const Dependency = ({ plugin }: DependencyProps) => { if (plugin.tags.includes('sfz')) { - if (message) { - return ( - - {' '} - (This instrument needs to be loaded into a{' '} - - SFZ player - - ) - - ); - } else { - return
studiorack plugin install studiorack/sfizz
; - } + return ( + + {' '} + (This instrument needs to be loaded into a{' '} + + SFZ player + + ) + + ); } else if (plugin.tags.includes('sf2')) { - if (message) { - return ( - - {' '} - (This instrument needs to be loaded into a{' '} - - SoundFont 2 player - - ) - - ); - } else { - return
studiorack plugin install studiorack/juicysf
; - } + return ( + + {' '} + (This instrument needs to be loaded into a{' '} + + SoundFont 2 player + + ) + + ); } else { return ; } diff --git a/renderer/components/details.tsx b/renderer/components/details.tsx new file mode 100644 index 0000000..01e78c4 --- /dev/null +++ b/renderer/components/details.tsx @@ -0,0 +1,106 @@ +import styles from '../styles/components/details.module.css'; +import { getBasePath } from '../lib/path'; +import Crumb from './crumb'; +import { pluginGetOrgId, pluginGetPluginId, timeSince } from '../lib/utils'; +import { pluginFileUrl, PluginVersion, PluginVersionLocal } from '@studiorack/core'; +import Audio from './audio'; +import Player from './player'; +import Dependency from './dependency'; +import Downloads from './download'; +import Code from './code'; +import { pluginLicense } from '../lib/plugin'; + +type DetailsProps = { + plugin: PluginVersion | PluginVersionLocal; + type: string; +}; + +const Details = ({ plugin, type }: DetailsProps) => ( +
+
+
+ +
+
+
+
+ {plugin.files.audio ?
+
+
+

+ {plugin.name || ''} v{plugin.version} +

+

+ By{' '} + + {plugin.author} + +

+

+ {plugin.description} + +

+
+ {/*
Filesize {formatBytes(plugin.files.linux.size)}
*/} +
+ Date updated{' '} + {timeSince(plugin.date)} ago +
+
+ License{' '} + {plugin.license ? ( + + {pluginLicense(plugin.license).name} + + ) : ( + 'none' + )} +
+
+ Tags +
    + {plugin.tags.map((tag: string, tagIndex: number) => ( +
  • + {tag} + {tagIndex !== plugin.tags.length - 1 ? ',' : ''} +
  • + ))} +
+
+ +
+
+
+
+
+
+
+ + +
+
+
+); + +export default Details; diff --git a/renderer/components/download.tsx b/renderer/components/download.tsx index 333faaa..5c6538f 100644 --- a/renderer/components/download.tsx +++ b/renderer/components/download.tsx @@ -1,51 +1,43 @@ -import styles from '../styles/plugin.module.css'; -import { PluginVersion } from '@studiorack/core'; -import { pluginFileUrl, pathGetExt, pathGetWithoutExt } from '../../node_modules/@studiorack/core/build/utils'; +import styles from '../styles/components/download.module.css'; +import { pluginFileUrl, PluginVersion } from '@studiorack/core'; import { getBasePath } from '../lib/path'; +// import { pluginFileUrlCompressed } from '../lib/plugin'; type DownloadsProps = { plugin: PluginVersion; }; -function pluginFileUrlCompressed(plugin: any, platform: any) { - const fileUrl: string = pluginFileUrl(plugin, platform); - const fileWithoutExt: string = pathGetWithoutExt(fileUrl); - const fileExt: string = pathGetExt(fileUrl); - return `${fileWithoutExt}-compact.${fileExt}`; -} - -const Downloads = ({ plugin }: DownloadsProps) => { - if (plugin.tags.includes('sfz') || plugin.tags.includes('sf2')) { - return ( +const Downloads = ({ plugin }: DownloadsProps) => ( +
+

Download and install manually:

+ {plugin.tags.includes('sfz') || plugin.tags.includes('sf2') ? ( - + High-quality Download - + {/* Compressed Download - + */} - ); - } else { - return ( + ) : ( {plugin.files.linux ? ( - + Linux Download { '' )} {plugin.files.mac ? ( - + MacOS Download { '' )} {plugin.files.win ? ( - + Windows Download { '' )} - ); - } -}; + )} +
+); export default Downloads; diff --git a/renderer/components/filters.tsx b/renderer/components/filters.tsx new file mode 100644 index 0000000..cc7eb59 --- /dev/null +++ b/renderer/components/filters.tsx @@ -0,0 +1,43 @@ +import { useRouter } from 'next/router'; +import { getCategoriesLabels, getLicensesLabels, getPlatformsLabels } from '../lib/api-browser'; +import styles from '../styles/components/filters.module.css'; +import MultiSelect from './multi-select'; +import { ChangeEvent } from 'react'; + +type FiltersProps = { + section: string; +}; + +const Filters = ({ section }: FiltersProps) => { + const router = useRouter(); + const search: string = router.query['search'] as string; + + const onSearch = (event: ChangeEvent) => { + const el: HTMLInputElement = event.target as HTMLInputElement; + router.query['search'] = el.value ? el.value.toLowerCase() : ''; + router.push({ + pathname: router.pathname, + query: router.query, + }); + }; + + return ( +
+ Filter by: + + + + +
+ ); +}; + +export default Filters; diff --git a/renderer/components/header.tsx b/renderer/components/header.tsx new file mode 100644 index 0000000..13a17f8 --- /dev/null +++ b/renderer/components/header.tsx @@ -0,0 +1,16 @@ +import styles from '../styles/components/header.module.css'; + +type HeaderProps = { + count?: number; + title: string; +}; + +const Header = ({ title, count }: HeaderProps) => ( +
+

+ {title} {count ? ({count}) : ''} +

+
+); + +export default Header; diff --git a/renderer/components/layout.tsx b/renderer/components/layout.tsx index df48ae5..c46aaf8 100644 --- a/renderer/components/layout.tsx +++ b/renderer/components/layout.tsx @@ -2,6 +2,7 @@ import Head from 'next/head'; import Navigation from './navigation'; import styles from '../styles/components/layout.module.css'; import { getBasePath } from '../lib/path'; +import { pageTitle } from '../lib/utils'; export const siteTitle = 'StudioRack'; export const siteDesc = 'Automate your plugin publishing workflow, easy plugin installation and management'; @@ -11,9 +12,9 @@ type LayoutProps = { }; const Layout = ({ children }: LayoutProps) => ( -
+
- {siteTitle} + {pageTitle(['An open-source audio plugin ecosystem'])} @@ -27,16 +28,11 @@ const Layout = ({ children }: LayoutProps) => ( -
- - {siteTitle} - - StudioRack +
+ + {siteTitle} + + StudioRack diff --git a/renderer/components/list.tsx b/renderer/components/list.tsx new file mode 100644 index 0000000..e9453d1 --- /dev/null +++ b/renderer/components/list.tsx @@ -0,0 +1,30 @@ +import styles from '../styles/components/list.module.css'; +import { PluginVersion, PluginVersionLocal, ProjectVersion, ProjectVersionLocal } from '@studiorack/core'; +import Header from './header'; +import Card from './card'; +import Filters from './filters'; +import Crumb from './crumb'; + +type ListProps = { + filters?: boolean; + plugins: PluginVersion[] | PluginVersionLocal[] | ProjectVersion[] | ProjectVersionLocal[]; + type: string; + title: string; +}; + +const List = ({ filters = true, plugins, type, title }: ListProps) => ( +
+ +
+ {filters ? : ''} +
+ {plugins.map( + (plugin: PluginVersion | PluginVersionLocal | ProjectVersion | ProjectVersionLocal, pluginIndex: number) => ( + + ), + )} +
+
+); + +export default List; diff --git a/renderer/components/multi-select.tsx b/renderer/components/multi-select.tsx new file mode 100644 index 0000000..b439903 --- /dev/null +++ b/renderer/components/multi-select.tsx @@ -0,0 +1,74 @@ +import { useRouter } from 'next/router'; +import { includesValue, toSlug } from '../lib/utils'; +import styles from '../styles/components/multi-select.module.css'; + +type MultiSelectItem = { + label: string; + value: string; +}; + +type MultiSelectProps = { + label: string; + items: MultiSelectItem[]; +}; + +const MultiSelect = ({ label, items }: MultiSelectProps) => { + const router = useRouter(); + const slug: string = toSlug(label); + + const showCheckboxes = (e: any) => { + e.preventDefault(); + e.target.blur(); + window.focus(); + var checkboxes = document.getElementById(label); + if (checkboxes?.style.display === 'block') { + if (checkboxes) checkboxes.style.display = 'none'; + } else { + if (checkboxes) checkboxes.style.display = 'block'; + } + }; + + const isChecked = (value: string) => { + if (!router.query[slug]) return false; + return includesValue(router.query[slug], value); + }; + + const updateUrl = () => { + const form: HTMLFormElement = document.getElementById(slug) as HTMLFormElement; + router.query[slug] = Array.from(new FormData(form).keys()); + router.push({ + pathname: router.pathname, + query: router.query, + }); + }; + + return ( +
+ +
+ {items.map((item: MultiSelectItem, index: number) => ( + + ))} +
+
+ ); +}; + +export default MultiSelect; diff --git a/renderer/components/navigation.tsx b/renderer/components/navigation.tsx index 117c1c4..ae5ad42 100644 --- a/renderer/components/navigation.tsx +++ b/renderer/components/navigation.tsx @@ -1,18 +1,28 @@ import styles from '../styles/components/navigation.module.css'; import { getBasePath, isSelected } from '../lib/path'; +import { ELECTRON_APP } from '../lib/utils'; const Navigation = () => ( -
- -