diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..02c4f0b2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@opendaw/eslint-config/index.js"], +}; diff --git a/.gitignore b/.gitignore index 85e7c1df..73b0db25 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,20 @@ -/.idea/ +.DS_Store +.turbo +.idea +.env +.cache +*.log +*.local +node_modules +dist +dist-ssr +server/dist +public/dist +public/build-info.json +lerna-debug.log +/packages/lib/box-forge/test/gen/** +/packages/studio/boxes/src/** +/packages/studio/adapters/src/index.ts +/packages/app/studio/public/build-info.json +/packages/app/localhost.pem +/packages/app/localhost-key.pem diff --git a/README.md b/README.md index 3a61f0fa..294cb7d3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,21 @@ We are committed to transparency and community-driven development. The source code for openDAW is available under GPL3 (links below). +### Built on Trust and Transparency + +**openDAW stands for radical simplicity and respect.** + +- **No SignUp** +- **No Tracking** +- **No Cookie Banners** +- **No User Profiling** +- **No Terms & Conditions** +- **No Ads** +- **No Paywalls** +- **No Data Mining** + +**Just a free, open-source DAW in your browser.** + ## Huge Shoutout To The Incredible openDAW Community! To everyone who has contributed feedback, reported bugs, suggested improvements, or helped spread the word — thank you! @@ -35,10 +50,8 @@ Huge thanks to our [ambassadors](https://opendaw.org/ambassadors), whose dedicat ## And big hugs to all our supporters! -### openDAW Top Supporter — $100.00 -- Polarity - ### openDAW Visionary — $25.00 +- Polarity - kanaris - Stephen Tai - Thad Guidry @@ -71,15 +84,46 @@ Huge thanks to our [ambassadors](https://opendaw.org/ambassadors), whose dedicat ### openDAW Custom Pledge - lokomotywa ($2.47) -**Let’s keep building together!** - --- ### Repositories * [openDAW](https://github.com/andremichelle/opendaw) -* [openDAW · studio](https://github.com/andremichelle/opendaw-studio) -* [openDAW · library (submodule)](https://github.com/andremichelle/opendaw-lib) + +### Prepare, Clone, Installation, and Run + +openDAW tries to avoid external libraries and frameworks. Following is a list of the internal core libraries and their +dependencies. This is a list of the external libraries we currently use in the web studio: + +* [jszip](https://www.npmjs.com/package/jszip) (for openDAW project bundle file) +* [markdown-it](https://www.npmjs.com/package/markdown-it) + [markdown-it-table](https://www.npmjs.com/package/markdown-it-table) (for help pages) + +Before starting, ensure you have the following installed on your system: + +- [Git](https://git-scm.com/) is required for cloning the repository and managing submodules. +- [mkcert](https://github.com/FiloSottile/mkcert#installation) is required to create a certificate for developing with + https protocol. +- [Node.js](nodejs.org) version **>= 23**. This is necessary for running the development server and installing + dependencies. +- [Sass](https://sass-lang.com/) While Sass is handled internally during the development process, you will need to + ensure you have the + binaries available in your environment if used outside the build system. +- [TypeScript](https://www.typescriptlang.org/) +- [OpenSSL](https://chocolatey.org/) For generating local development certificates (), OpenSSL needs to be installed on + your system. Most Linux/macOS systems have OpenSSL pre-installed. + +### Clone + +`git clone https://github.com/andremichelle/opendaw.git && cd opendaw` + +### Installation + +* `npm run cert` (only for the very first time) +* `npm run clean` (to revert to clean slate, removes all `node_modules` and `dist` folders) +* `npm install` (for the first time and after `npm run clean`) +* `npm run build` (for the first time and after `npm run clean`) +* `npm run dev:studio` | `npm run dev:headless` (start dev server) +* Navigate to https://localhost:8080 (port is important > cors sample api) --- @@ -92,6 +136,19 @@ Watch Polarity's Video *"there's a new FREE DAW in town"* We welcome contributions from developers, musicians, educators, and enthusiasts. To learn more about how you can participate, visit our [Contribute](https://opendaw.org/contribute) page. +### What We Are Looking For: + +1. **Offline desktop build (e.g., via Tauri) or a standalone installable PWA** — offer offline capability. +2. **Cloud-agnostic project storage** — a facade layer that lets users plug in different cloud services (e.g., Drive, + S3, Dropbox) for projects and sample libraries. +3. **Live remote collaboration** — real-time session sharing and sync so multiple users can edit the same project + concurrently. +4. **AI manual assistant** — an embedded agent that answers context-aware questions and guides users through features as + they work. +5. **AI-powered stem splitting** — integrated source-separation to extract vocals, drums, and other stems directly + inside the DAW. +6. **Import and Export** - Contribute every possible file format IO + ## Links * [opendaw.studio (prototype)](https://opendaw.studio) diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..26ee9f6e --- /dev/null +++ b/lerna.json @@ -0,0 +1,12 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "version": "independent", + "npmClient": "npm", + "command": { + "publish": { + "conventionalCommits": true, + "registry": "http://localhost:4873/" + } + }, + "packages": ["packages/**"] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..bcfbbca1 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "opendaw", + "private": true, + "license": "LGPL-3.0-or-later", + "scripts": { + "cert": "bash ./scripts/cert.sh", + "clean": "bash ./scripts/clean.sh", + "build": "turbo build", + "dev:studio": "turbo run dev --filter=@opendaw/app-studio", + "dev:headless": "turbo run dev --filter=@opendaw/app-headless", + "dev": "turbo run dev", + "test": "turbo run test", + "lint": "turbo run lint", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "publish-sdk": "lerna publish" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "lerna": "^8.2.3", + "prettier": "^3.6.2", + "turbo": "^2.5.4", + "typescript": "^5.8.3", + "vitest": "^3.1.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/andremichelle/opendaw" + }, + "packageManager": "npm@11.4.2", + "workspaces": [ + "packages/**/*" + ], + "dependencies": { + "@opendaw/studio-core": "^0.0.0" + } +} diff --git a/packages/app/headless/.eslintrc.cjs b/packages/app/headless/.eslintrc.cjs new file mode 100644 index 00000000..02c4f0b2 --- /dev/null +++ b/packages/app/headless/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@opendaw/eslint-config/index.js"], +}; diff --git a/packages/app/headless/CHANGELOG.md b/packages/app/headless/CHANGELOG.md new file mode 100644 index 00000000..e10c00f3 --- /dev/null +++ b/packages/app/headless/CHANGELOG.md @@ -0,0 +1,32 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.7](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-headless@0.0.6...@opendaw/app-headless@0.0.7) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless + +## [0.0.6](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-headless@0.0.5...@opendaw/app-headless@0.0.6) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless + +## [0.0.5](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-headless@0.0.4...@opendaw/app-headless@0.0.5) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless + +## [0.0.4](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-headless@0.0.3...@opendaw/app-headless@0.0.4) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless + +## [0.0.3](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-headless@0.0.2...@opendaw/app-headless@0.0.3) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless + +## [0.0.2](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-headless@0.0.1...@opendaw/app-headless@0.0.2) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless + +## 0.0.1 (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-headless diff --git a/packages/app/headless/index.html b/packages/app/headless/index.html new file mode 100644 index 00000000..0b136736 --- /dev/null +++ b/packages/app/headless/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + TS + + +
+ + + diff --git a/packages/app/headless/package.json b/packages/app/headless/package.json new file mode 100644 index 00000000..238650b2 --- /dev/null +++ b/packages/app/headless/package.json @@ -0,0 +1,35 @@ +{ + "name": "@opendaw/app-headless", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --clearScreen false", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint \"src/**/*.ts\"" + }, + "dependencies": { + "@opendaw/lib-box": "^0.0.0", + "@opendaw/lib-dom": "^0.0.0", + "@opendaw/lib-dsp": "^0.0.0", + "@opendaw/lib-jsx": "^0.0.0", + "@opendaw/lib-runtime": "^0.0.0", + "@opendaw/lib-std": "^0.0.0", + "@opendaw/studio-adapters": "^0.0.0", + "@opendaw/studio-boxes": "^0.0.0", + "@opendaw/studio-core": "^0.0.0", + "@opendaw/studio-enums": "^0.0.0", + "@types/markdown-it": "^14.1.2", + "markdown-it": "^14.1.0" + }, + "devDependencies": { + "@opendaw/eslint-config": "^0.0.0", + "@opendaw/typescript-config": "^0.0.0", + "@types/node": "^22.15.18", + "eslint": "^8.57.0", + "sass": "^1.88.0", + "vite": "^7.0.2", + "vite-plugin-cross-origin-isolation": "^0.1.6" + } +} diff --git a/packages/app/headless/public/favicon.svg b/packages/app/headless/public/favicon.svg new file mode 100644 index 00000000..c5a39337 --- /dev/null +++ b/packages/app/headless/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/app/headless/public/subset.od b/packages/app/headless/public/subset.od new file mode 100644 index 00000000..e34bfa17 Binary files /dev/null and b/packages/app/headless/public/subset.od differ diff --git a/packages/app/headless/src/MainThreadAudioLoader.ts b/packages/app/headless/src/MainThreadAudioLoader.ts new file mode 100644 index 00000000..68633aa7 --- /dev/null +++ b/packages/app/headless/src/MainThreadAudioLoader.ts @@ -0,0 +1,57 @@ +import {int, Notifier, Observer, Option, Subscription, Terminable, UUID} from "@opendaw/lib-std" +import {Peaks} from "@opendaw/lib-fusion" +import {AudioData, AudioLoader, AudioLoaderState} from "@opendaw/studio-adapters" +import {SampleApi} from "./SampleApi" + +export class MainThreadAudioLoader implements AudioLoader { + readonly #context: AudioContext + readonly #uuid: UUID.Format + readonly #notifier: Notifier + + #data: Option = Option.None + #state: AudioLoaderState = {type: "progress", progress: 0.0} + #version: int = 0 + + constructor(context: AudioContext, uuid: UUID.Format) { + this.#context = context + this.#uuid = uuid + + this.#notifier = new Notifier() + this.#get() + } + + invalidate(): void { + this.#state = {type: "progress", progress: 0.0} + this.#data = Option.None + this.#version++ + this.#get() + } + + subscribe(observer: Observer): Subscription { + if (this.#state.type === "loaded") { + observer(this.#state) + return Terminable.Empty + } + return this.#notifier.subscribe(observer) + } + + get uuid(): UUID.Format {return this.#uuid} + get data(): Option {return this.#data} + get peaks(): Option {return Option.None} + get state(): AudioLoaderState {return this.#state} + + #setState(value: AudioLoaderState): void { + this.#state = value + this.#notifier.notify(this.#state) + } + + #get(): void { + console.debug("GET", UUID.toString(this.#uuid)) + SampleApi.load(this.#context, this.#uuid, progress => this.#setState({type: "progress", progress})) + .then(([data]) => { + console.debug("LOADED", UUID.toString(this.#uuid)) + this.#data = Option.wrap(data) + this.#setState({type: "loaded"}) + }, reason => this.#setState({type: "error", reason})) + } +} \ No newline at end of file diff --git a/packages/app/headless/src/MainThreadAudioLoaderManager.ts b/packages/app/headless/src/MainThreadAudioLoaderManager.ts new file mode 100644 index 00000000..3f503d05 --- /dev/null +++ b/packages/app/headless/src/MainThreadAudioLoaderManager.ts @@ -0,0 +1,19 @@ +import {SortedSet, UUID} from "@opendaw/lib-std" +import {AudioLoader, AudioLoaderManager} from "@opendaw/studio-adapters" +import {MainThreadAudioLoader} from "./MainThreadAudioLoader" + +export class MainThreadAudioLoaderManager implements AudioLoaderManager { + readonly #context: AudioContext + readonly #loaders: SortedSet + + constructor(context: AudioContext) { + this.#context = context + this.#loaders = UUID.newSet(loader => loader.uuid) + } + + getOrCreate(uuid: UUID.Format): AudioLoader { + return this.#loaders.getOrCreate(uuid, uuid => new MainThreadAudioLoader(this.#context, uuid)) + } + + invalidate(_uuid: UUID.Format): void {} +} \ No newline at end of file diff --git a/packages/app/headless/src/SampleApi.ts b/packages/app/headless/src/SampleApi.ts new file mode 100644 index 00000000..2c5a0719 --- /dev/null +++ b/packages/app/headless/src/SampleApi.ts @@ -0,0 +1,70 @@ +import {Arrays, asDefined, panic, Procedure, unitValue, UUID} from "@opendaw/lib-std" +import {AudioData, AudioMetaData, AudioSample} from "@opendaw/studio-adapters" +import {network, Promises} from "@opendaw/lib-runtime" + +const username = "openDAW" +const password = "prototype" +const base64Credentials = btoa(`${username}:${password}`) +const headers: RequestInit = { + method: "GET", + headers: {"Authorization": `Basic ${base64Credentials}`}, + credentials: "include" +} + +export namespace SampleApi { + export const ApiRoot = "https://api.opendaw.studio/samples" + export const FileRoot = "https://assets.opendaw.studio/samples" + + export const all = async (): Promise> => { + return await Promises.retry(() => fetch(`${ApiRoot}/list.php`, headers).then(x => x.json(), () => [])) + } + + export const get = async (uuid: UUID.Format): Promise => { + const url = `${ApiRoot}/get.php?uuid=${UUID.toString(uuid)}` + const sample: AudioSample = await Promises.retry(() => network.limitFetch(url, headers) + .then(x => x.json())) + .then(x => {if ("error" in x) {return panic(x.error)} else {return x}}) + return Object.freeze({...sample, cloud: true}) + } + + export const load = async (context: AudioContext, + uuid: UUID.Format, + progress: Procedure): Promise<[AudioData, AudioMetaData]> => { + console.debug(`fetch ${UUID.toString(uuid)}`) + return get(uuid) + .then(({uuid, name, bpm}) => Promises.retry(() => network.limitFetch(`${FileRoot}/${uuid}`, headers)) + .then(response => { + const total = parseInt(response.headers.get("Content-Length") ?? "0") + let loaded = 0 + return new Promise((resolve, reject) => { + const reader = asDefined(response.body, "No body in response").getReader() + const chunks: Array = [] + const nextChunk = ({done, value}: ReadableStreamReadResult) => { + if (done) { + resolve(new Blob(chunks).arrayBuffer()) + } else { + chunks.push(value) + loaded += value.length + progress(loaded / total) + reader.read().then(nextChunk, reject) + } + } + reader.read().then(nextChunk, reject) + }) + }) + .then(arrayBuffer => context.decodeAudioData(arrayBuffer)) + .then(audioBuffer => ([fromAudioBuffer(audioBuffer), { + bpm, + name, + duration: audioBuffer.duration, + sample_rate: audioBuffer.sampleRate + }]))) + } + + const fromAudioBuffer = (buffer: AudioBuffer): AudioData => ({ + frames: Arrays.create(channel => buffer.getChannelData(channel), buffer.numberOfChannels), + sampleRate: buffer.sampleRate, + numberOfFrames: buffer.length, + numberOfChannels: buffer.numberOfChannels + }) +} \ No newline at end of file diff --git a/packages/app/headless/src/Test.tsx b/packages/app/headless/src/Test.tsx new file mode 100644 index 00000000..4f371041 --- /dev/null +++ b/packages/app/headless/src/Test.tsx @@ -0,0 +1,9 @@ +import {createElement} from "@opendaw/lib-jsx" + +type Construct = {} + +export const Test = ({}: Construct) => { + return ( +
Hello Jsx
+ ) +} \ No newline at end of file diff --git a/packages/app/headless/src/features.ts b/packages/app/headless/src/features.ts new file mode 100644 index 00000000..95c06dea --- /dev/null +++ b/packages/app/headless/src/features.ts @@ -0,0 +1,12 @@ +import {requireProperty} from "@opendaw/lib-std" + +export const testFeatures = async (): Promise => { + requireProperty(Promise, "withResolvers") + requireProperty(window, "indexedDB") + requireProperty(window, "AudioWorkletNode") + requireProperty(navigator, "storage") + requireProperty(navigator.storage, "getDirectory") + requireProperty(crypto, "randomUUID") + requireProperty(crypto, "subtle") + requireProperty(crypto.subtle, "digest") +} \ No newline at end of file diff --git a/packages/app/headless/src/main.ts b/packages/app/headless/src/main.ts new file mode 100644 index 00000000..54c55f12 --- /dev/null +++ b/packages/app/headless/src/main.ts @@ -0,0 +1,62 @@ +import "./style.css" +import {assert} from "@opendaw/lib-std" +import {PPQN} from "@opendaw/lib-dsp" +import {AnimationFrame, Browser} from "@opendaw/lib-dom" +import {Promises} from "@opendaw/lib-runtime" +import {Project, Worklets} from "@opendaw/studio-core" +import MeterProcessorUrl from "@opendaw/studio-core/meter-processor.js?url" +import EngineProcessorUrl from "@opendaw/studio-core/engine-processor.js?url" +import RecordingProcessorUrl from "@opendaw/studio-core/recording-processor.js?url" +import {testFeatures} from "./features" +import {MainThreadAudioLoaderManager} from "./MainThreadAudioLoaderManager" + +(async () => { + console.debug("openDAW -> headless") + console.debug("Agent", Browser.userAgent) + console.debug("isLocalHost", Browser.isLocalHost()) + assert(crossOriginIsolated, "window must be crossOriginIsolated") + console.debug("booting...") + document.body.textContent = "booting..." + { + const {status, error} = await Promises.tryCatch(testFeatures()) + if (status === "rejected") { + document.querySelector("#preloader")?.remove() + alert(`Could not test features (${error})`) + return + } + } + const context = new AudioContext({latencyHint: 0}) + console.debug(`AudioContext state: ${context.state}, sampleRate: ${context.sampleRate}`) + { + const {status, error} = await Promises.tryCatch(Worklets.install(context, { + meter: MeterProcessorUrl, + engine: EngineProcessorUrl, + recording: RecordingProcessorUrl + })) + if (status === "rejected") { + alert(`Could not install Worklets (${error})`) + return + } + } + { + const audioManager = new MainThreadAudioLoaderManager(context) + const project = Project.load({audioManager}, await fetch("subset.od").then(x => x.arrayBuffer())) + const worklet = Worklets.get(context).createEngine(project) + await worklet.isReady() + while (!await worklet.queryLoadingComplete()) {} + worklet.connect(context.destination) + worklet.isPlaying().setValue(true) + AnimationFrame.add(() => { + const ppqn = worklet.position().getValue() + const {bars, beats} = PPQN.toParts(ppqn) + document.body.textContent = `${bars + 1}:${beats + 1}` + }) + } + if (context.state === "suspended") { + window.addEventListener("click", + async () => await context.resume().then(() => + console.debug(`AudioContext resumed (${context.state})`)), {capture: true, once: true}) + } + AnimationFrame.start() + document.querySelector("#preloader")?.remove() +})() \ No newline at end of file diff --git a/packages/app/headless/src/style.css b/packages/app/headless/src/style.css new file mode 100644 index 00000000..12320801 --- /dev/null +++ b/packages/app/headless/src/style.css @@ -0,0 +1,97 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/packages/app/headless/src/vite-env.d.ts b/packages/app/headless/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/packages/app/headless/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/app/headless/tsconfig.json b/packages/app/headless/tsconfig.json new file mode 100644 index 00000000..13ed5c02 --- /dev/null +++ b/packages/app/headless/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@opendaw/typescript-config/vite.json", + "include": [ + "src" + ] +} diff --git a/packages/app/headless/vite.config.ts b/packages/app/headless/vite.config.ts new file mode 100644 index 00000000..bd93d763 --- /dev/null +++ b/packages/app/headless/vite.config.ts @@ -0,0 +1,31 @@ +import {defineConfig} from "vite" +import crossOriginIsolation from "vite-plugin-cross-origin-isolation" +import {readFileSync} from "fs" +import {resolve} from "path" + +export default defineConfig({ + resolve: { + alias: { + "@": resolve(__dirname, "./src") + } + }, + server: { + port: 8080, + host: "localhost", + https: { + key: readFileSync("../localhost-key.pem"), + cert: readFileSync("../localhost.pem") + }, + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" + }, + fs: { + // Allow serving files from the entire workspace + allow: [".."] + } + }, + plugins: [ + crossOriginIsolation() + ] +}) \ No newline at end of file diff --git a/packages/app/studio/.eslintrc.cjs b/packages/app/studio/.eslintrc.cjs new file mode 100644 index 00000000..02c4f0b2 --- /dev/null +++ b/packages/app/studio/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@opendaw/eslint-config/index.js"], +}; diff --git a/packages/app/studio/CHANGELOG.md b/packages/app/studio/CHANGELOG.md new file mode 100644 index 00000000..cde60b1b --- /dev/null +++ b/packages/app/studio/CHANGELOG.md @@ -0,0 +1,32 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.7](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-studio@0.0.6...@opendaw/app-studio@0.0.7) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio + +## [0.0.6](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-studio@0.0.5...@opendaw/app-studio@0.0.6) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio + +## [0.0.5](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-studio@0.0.4...@opendaw/app-studio@0.0.5) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio + +## [0.0.4](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-studio@0.0.3...@opendaw/app-studio@0.0.4) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio + +## [0.0.3](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-studio@0.0.2...@opendaw/app-studio@0.0.3) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio + +## [0.0.2](https://github.com/andremichelle/opendaw-turbo/compare/@opendaw/app-studio@0.0.1...@opendaw/app-studio@0.0.2) (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio + +## 0.0.1 (2025-07-11) + +**Note:** Version bump only for package @opendaw/app-studio diff --git a/packages/app/studio/index.html b/packages/app/studio/index.html new file mode 100644 index 00000000..c5070ae9 --- /dev/null +++ b/packages/app/studio/index.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 🎧openDAW.studio + + + + + + + \ No newline at end of file diff --git a/packages/app/studio/package.json b/packages/app/studio/package.json new file mode 100644 index 00000000..2f3c867d --- /dev/null +++ b/packages/app/studio/package.json @@ -0,0 +1,37 @@ +{ + "name": "@opendaw/app-studio", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --clearScreen false", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint \"src/**/*.ts\"" + }, + "dependencies": { + "@opendaw/lib-box": "^0.0.0", + "@opendaw/lib-dom": "^0.0.0", + "@opendaw/lib-dsp": "^0.0.0", + "@opendaw/lib-jsx": "^0.0.0", + "@opendaw/lib-runtime": "^0.0.0", + "@opendaw/lib-std": "^0.0.0", + "@opendaw/studio-adapters": "^0.0.0", + "@opendaw/studio-boxes": "^0.0.0", + "@opendaw/studio-core": "^0.0.0", + "@opendaw/studio-enums": "^0.0.0", + "jszip": "^3.10.1", + "markdown-it": "^14.1.0", + "markdown-it-table": "^4.1.1" + }, + "devDependencies": { + "@opendaw/eslint-config": "^0.0.0", + "@opendaw/typescript-config": "^0.0.0", + "@types/markdown-it": "^14.1.2", + "@types/node": "^22.15.18", + "eslint": "^8.57.0", + "sass": "^1.88.0", + "vite": "^7.0.2", + "vite-plugin-cross-origin-isolation": "^0.1.6" + } +} diff --git a/packages/app/studio/public/.htaccess b/packages/app/studio/public/.htaccess new file mode 100644 index 00000000..066d397d --- /dev/null +++ b/packages/app/studio/public/.htaccess @@ -0,0 +1,32 @@ +# openDAW +# prototype + +# CORS Headers +Header set Access-Control-Allow-Origin "https://localhost:8080" +Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" +Header set Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" +Header set Access-Control-Allow-Credentials "true" +Header set Cross-Origin-Opener-Policy "same-origin" +Header set Cross-Origin-Embedder-Policy "require-corp" + +# Enable Brotli Compression for supported files, only if they exist +RewriteEngine On + +# Serve .br files if the client supports Brotli +RewriteCond %{HTTP:Accept-Encoding} br +RewriteCond %{REQUEST_FILENAME}.br -f +RewriteRule ^(.*)$ $1.br [QSA,L] + +# Set Content-Encoding header only for actual Brotli-compressed files + + Header set Content-Encoding br + + +# URL rewriting (if needed) +RewriteBase / +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ index.html [QSA,L] + +# Redirect /manuals/ to root index.html for SPA handling +RewriteRule ^manuals/$ /index.html [L] \ No newline at end of file diff --git a/packages/app/studio/public/become_a_patron_button.png b/packages/app/studio/public/become_a_patron_button.png new file mode 100644 index 00000000..291b7bcf Binary files /dev/null and b/packages/app/studio/public/become_a_patron_button.png differ diff --git a/packages/app/studio/public/cover.png b/packages/app/studio/public/cover.png new file mode 100644 index 00000000..5965e823 Binary files /dev/null and b/packages/app/studio/public/cover.png differ diff --git a/packages/app/studio/public/favicon.svg b/packages/app/studio/public/favicon.svg new file mode 100644 index 00000000..c5a39337 --- /dev/null +++ b/packages/app/studio/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/app/studio/public/fonts/OpenSans-Regular.ttf b/packages/app/studio/public/fonts/OpenSans-Regular.ttf new file mode 100644 index 00000000..67803bb6 Binary files /dev/null and b/packages/app/studio/public/fonts/OpenSans-Regular.ttf differ diff --git a/packages/app/studio/public/fonts/rubik.woff2 b/packages/app/studio/public/fonts/rubik.woff2 new file mode 100644 index 00000000..9237ec3b Binary files /dev/null and b/packages/app/studio/public/fonts/rubik.woff2 differ diff --git a/packages/app/studio/public/grid16.svg b/packages/app/studio/public/grid16.svg new file mode 100644 index 00000000..28192e69 --- /dev/null +++ b/packages/app/studio/public/grid16.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/app/studio/public/images/meta.jpg b/packages/app/studio/public/images/meta.jpg new file mode 100644 index 00000000..11dc61cb Binary files /dev/null and b/packages/app/studio/public/images/meta.jpg differ diff --git a/packages/app/studio/public/logo.png b/packages/app/studio/public/logo.png new file mode 100644 index 00000000..ba6cb5bd Binary files /dev/null and b/packages/app/studio/public/logo.png differ diff --git a/packages/app/studio/public/manuals/browser-support.md b/packages/app/studio/public/manuals/browser-support.md new file mode 100644 index 00000000..91a60269 --- /dev/null +++ b/packages/app/studio/public/manuals/browser-support.md @@ -0,0 +1,17 @@ +# Browser Support Manuals + +openDAW is tested on the latest version of all modern browsers, such as Chrome, Firefox, and Safari. + +However, there are differences in which web APIs are supported by each browser. + +| Feature | Chrome | Firefox | Safari | +|---------------------|--------|------------------------------|--------| +| Select Audio-Output | Yes | No | No | +| Midi Devices | Yes | [Yes](/manuals/firefox-midi) | No | +| File Api | Yes | ⚠️ | ⚠️ | + +## Legend + +### ⚠️ Workaround +This symbol indicates a temporary solution or alternative approach to bypass a limitation, bug, or +missing feature. It is not a permanent fix but allows functionality until an official resolution is available. \ No newline at end of file diff --git a/packages/app/studio/public/manuals/dev-log.md b/packages/app/studio/public/manuals/dev-log.md new file mode 100644 index 00000000..296a1a29 --- /dev/null +++ b/packages/app/studio/public/manuals/dev-log.md @@ -0,0 +1,243 @@ +# Development + +## TODO + +* PianoModePanel + * Show timeline navigation? + * Different note labels for different countries (Global Switch) + * ~~dialog? to disable note tracks~~ + * ~~Control to show and edit signature~~ + * ~~Scroll Y should change engine position (FW, RW)~~ + * ~~Rename to PianoModePanel~~ + * ~~Transpose~~ + * ~~Go back to the timeline view~~ + * ~~Open MidiFall view (or Piano Tutorial Mode?)~~ + * ~~labels on falling notes (~~Hide when note is too short~~ clip)~~ + * ~~control to adjust visible time range~~ + * ~~active piano colors~~ + * ~~different keyboard layouts https://familypiano.com/blog/piano-keys-faq/~~ + * ~~time signature / octave (C, F) grid~~ +* ~~Playfield bug: Samples appear louder when polyphone~~ +* Attach a CurveBox to ValueEventBox (enables having different curve shapes in future) +* Make effect-bypass parameter automatable +* Add SoundFont device +* Polish audio playback -> will have unwanted sounds when start/stop audio segments +* AcceptingType for audio-unit should be a set? +* Admin sample management (rename, edit, delete) +* Do not use an unknown track for busses. This must be handled by the timeline view. +* https://bungee.parabolaresearch.com/ +* Audio/Midi Recording + * Solid state for recording in main-thread (none, running, cancel, abort) + * Listen to all incoming midi-signals and create track, region, notes and automation (revertible process) + * Record audio to memory and create track and region (revertible process) + * Preview / update peaks + * ~~Count in~~ + * ~~Global shuffle~~ + * ~~Midi generator effects should continue to run when transport is stopped~~ + * ~~Time-Manipulation Midi-Effects~~ + * ~~clamp pitch 0-127~~ + * ~~release notes when changing midi effect chain~~ + * ~~release notes when pausing~~ + * ~~Sending notes to broadcaster~~ + * ~~Revive regions/clips~~ + * ~~groove unipolar (percentage)~~ + * ~~Replace event-targets with NoteEventSource~~ + * ~~NoteEvent~~ + * ~~Remove chance, playCount, playCurve (not needed in processing)~~ +* Playfield + * ~~Panics when replacing synth with Playfield and undo~~ + * ~~Remember~~ + * ~~sample in edit mode~~ + * ~~octave index~~ + * ~~cache editor~~ + * Fix envelope (not in prototype) + * Fix loop discontinuations with fades (not in prototype) + * Update start & end while playing (not in prototype) + * ~~Reset Playfield and delete samples~~ + * ~~Automation~~ + * ~~Exclusion group~~ + * ~~Gate-mode selector (Off, On, Loop)~~ + * ~~Monophone / Polyphone switcher~~ +* Stereo Tool + * Stereo Widthing is not really working (not in prototype < no easy fix) + * ~~Interpolate matrix~~ + * ~~swap channels~~ +* PointerLock Api +* Midi Pitch-bend +* List all midi connections and edit (range, remove, etc..) +* Name constraints (min, max) +* Find a modifier key to always select +* Offline Tauri version +* Offline PWA version +* studio header > peak-meter +* peak-meter > hold-value | rms < PeakBroadcaster +* Implement presets for devices and complete device-chain +* Preview midi notes (https://discord.com/channels/1241019312328675399/1337837099302391849) +* Different schedule switch-times for clips +* Double-click the input name in the track-header to rename +* [kurp] Making region bounds in content-editor fully operational +* Timeline navigation in other workspace views (mixer) +* Spotlight +* Flatten ValueRegion +* Flatten AudioRegion +* Absolute time display +* Pre- & Post-gain FX / Gain-stage + +### Optimisation + +* Only produce and stream values in the audio-engine if there is a visible consumer in the user-interface + +### Misc Bugs + +* Setting a pointer to two different targets in one modification breaks undo + +### Panel Management + +* Resizers are only working on the adjacent elements. +* Resizers do not open/close panels while dragging to increase/decrease space +* Resizers on min-size and max-size should remember their size in pixels, not flex (example: Browser). +* No indicator that a panel reached min- or max-size +* Switching screen will rebuild ALL panels (should be reused if the screen contains the same panel-states) + +## Done + +* ~~Learn Midi Device~~ + * ~~Keys~~ + * ~~Control~~ + * ~~Store midi connections and reload when loading project~~ + * ~~Copy on 'Save As'~~ +* ~~DEL to delete track~~ +* ~~Having extra space to drop new content below tracks~~ +* ~~Long labels can blow layouts~~ +* ~~Table of contents for manual~~ +* ~~Reset Parameter Option~~ +* ~~Piano Roll: Velocity represented by Opacity~~ +* ~~Auto-scroll on text-element~~ +* ~~List local projects in dashboard~~ +* ~~Bake colours and remove filter~~ +* ~~Audio Output Device Selector~~ +* ~~Choose region color hue~~ +* ~~[kurp] Creating notes and value-nodes should appear on the second pointer-down not release~~ +* ~~Change song length~~ +* ~~Changing hash in url throws error~~ +* ~~checkboxes do not show automated state~~ +* ~~Note Editor Properties are not editible anymore~~ +* ~~[Polarity] Simple Sampler~~ +* ~~[kurp] arrow keys do not do anything~~ +* ~~Add description, tags and cover to project's metadata~~ +* ~~Add new effect to chain (right to the instrument or last device)~~ +* ~~Move audio-units within bounds~~ +* ~~Drag'n'drop devices in chain~~ +* ~~Add message to error handler to deactivate extensions~~ +* ~~Recovery Mode~~ +* ~~Drag & Drop inside app to create and sort~~ +* ~~[Polarity] Add + button to create a tape and add a sample in one go~~ +* ~~[razcore-rad] Revamp EQ-enable control has no automation menu~~ +* ~~Update Manual with shortcuts (like undo/redo & scissor)~~ +* ~~[razcore-rad] Dropdown triangle (note editor > property selector)~~ +* ~~Value-Clips~~ +* ~~When deleting samples check existing projects, if they will break~~ +* ~~Waiting state not working on clips when starting column~~ +* ~~Follow playback cursor~~ +* ~~List selection~~ +* ~~Multiple samples dragging into the timeline (changed to context-menu)~~ +* ~~Show Playback-Timestamp indicator~~ +* ~~Converting clip to region~~ +* ~~Midi Transposing (https://discord.com/channels/1241019312328675399/1337836390628462614)~~ +* ~~Sample Preview~~ +* ~~Show insert marker when adding sample~~ +* ~~Ask for replacement for deleted or incomplete local samples~~ +* ~~Drag mulitple files~~ +* ~~Change name & bpm for samples~~ +* ~~Drag & Drop Files directly onto the timeline~~ +* ~~Import custom samples~~ +* ~~Parameter-Knob has too many control state indicators~~ +* ~~Show clip-area when new clip is added (e.g. convert to clip)~~ +* ~~Import / Export zip-package~~ +* ~~[Coral] Killing all buffers (on stop) can still reintroduce feedback out of the blue when delay has 100% fb~~ +* ~~[Jetdarc] Missing automation state for channelstrips controls (track & timeline)~~ +* ~~[2L&L] Possible to create a region with negative position~~ +* ~~[Polarity] Seen that an automation on Revamp (freq) were not updating the curve~~ +* ~~[Jetdarc] Apparently you can set the bpm to 9999999~~ +* ~~[raii] Selected notes with higher pitch are not captured with higher priority~~ +* ~~[Polarity] search sample names within the string not just the beginning~~ +* ~~[Polarity] Sample Browser Search~~ +* ~~Implement Clips DSP~~ + * ~~Better visual state for playing clips (hard to distinguish from non-playing clips)~~ + * ~~Drag sample on clip-area~~ + * ~~One-shot playback~~ + * ~~Do not allow dropping clips on mismatching track~~ + * ~~Double stop must stop all playing clips~~ +* ~~[kurp] 0% in gain mapping is not silent~~ +* ~~[kurp] overlapping region when creating in too smaller gaps~~ +* ~~ADD Donation Link!~~ +* ~~Show Update Banner~~ +* ~~Wav-Preview in sample-browser (better than nothing)~~ +* ~~[kurp] Always play from the last *set* position~~ +* ~~swap devices in chain~~ +* ~~_Cannot have panel open in multiple location_ when adding a hash to url~~ +* ~~[Truls] Playing notes from the piano-roll~~ +* ~~[Truls] Restart audio-engine if errored out~~ +* ~~Export must open FileDialog with a new user-interaction (security issue)~~ +* ~~[Truls] When the play head is moved during playback, playing notes should be released~~ +* ~~[Truls] note region preview is a little unpredictable for short notes~~ +* ~~Solution for output device chain automation in timeline~~ +* ~~Option to auto open content editor when the region is already in edit-mode~~ +* ~~[Truls] Channelstrip at the end of the device-chain~~ +* ~~[Truls] ValueRegionEditor does not allow dragging loop-duration~~ +* ~~ValueRegion must hold value when ended~~ +* ~~Channelstrip text overflow fix~~ +* ~~Select new region after flatten~~ +* ~~Indices of AudioUnits can be broken after deletion~~ +* ~~Error handling like Sentry~~ +* ~~Still something wrong with the track indices~~ +* ~~ArpeggioDeviceProcessor & PitchDeviceProcessor UpdateClock~~ +* ~~Kill all buffers and reset all devices on double-stop~~ +* ~~Delete Bus (2 channels without output) > Create new output > ERROR (dub-techno)~~ +* ~~Create a test all pointer targets entry in app-menu for debugging~~ +* ~~Proper solution for ordering instrument and bus tracks~~ +* ~~Allow F12 & Reload with modal dialogs~~ +* ~~Minimized device version (collapse)~~ +* ~~Check audio-unit "bus" behaviour in timeline (creates an empty track)~~ +* ~~The content-editor does not show its scroller (no much content in header)~~ +* ~~Label has "unassigned" in ValueHeader and missing in track header~~ +* ~~Automation track are not being deleted when deleting the device~~ +* ~~DelayDeviceProcessor cross missing~~ +* ~~Show control state on revamp ui~~ +* ~~Introduce mapping in ValueTrackHeader~~ +* ~~Show format value in ValueTrackHeader~~ +* ~~Make the ProjectInfo page nicer too~~ +* ~~Make Revamp parameters automatable~~ +* ~~No modular views available should be nicer~~ +* ~~'Finish' Revamp DSP (BiquadStack order)~~ +* ~~Disable track~~ +* ~~Keyboard Shortcut for Play/Pause at least~~ +* ~~'Finish' Delay DSP > Filter~~ +* ~~'Finish' Vaporisateur DSP > Bandlimited Osc?~~ +* ~~Make this part of the running studio: https://localhost:8080/issues~~ +* ~~Channelstrip Solo~~ +* ~~[BUG] Index on delete audio-unit seems to be wrong (optic glitch)~~ +* ~~Load Midi~~ +* ~~'Finish' sample management (cache)~~ +* ~~Audio region drag-preview in timeline~~ +* ~~Create Tape if sample is dropped below tracks~~ +* ~~Do we need TrackAssignmentBox for anything other than value-automation?~~ +* ~~Audio region drag > clip~~ +* ~~Connect all(?) AudioFileBoxes to RootBox~~ +* ~~Either make Clips working fully or hide them~~ +* ~~Default color region type~~ +* ~~Do not allow dropping regions on different track types~~ +* ~~Pimp dashboard~~ +* ~~Pimp publish page~~ +* ~~Progress dialog~~ +* ~~Implement 'enabled' for effects (bypass)~~ +* ~~Device icon colers~~ +* ~~No cut-indicator when outside region~~ +* ~~Play many audio-tracks~~ +* ~~Fix audio region split~~ +* ~~Create new track for audio-unit (note or audio, depending on type)~~ +* ~~Temp remove tape parameter controls~~ +* ~~Simple dialog layering~~ +* ~~Revamp DSP~~ +* ~~ValueTrackHeader~~ +* ~~Export2wav~~ \ No newline at end of file diff --git a/packages/app/studio/public/manuals/firefox-midi.md b/packages/app/studio/public/manuals/firefox-midi.md new file mode 100644 index 00000000..6d78a56e --- /dev/null +++ b/packages/app/studio/public/manuals/firefox-midi.md @@ -0,0 +1,32 @@ +# Request Midi Access On Firefox Does Not Work + +This is a known issue with Firefox's MIDI implementation. Unlike Chrome, which allows requesting MIDI access even if no +devices are currently connected, Firefox denies access outright if **no MIDI devices are detected** at the time of the +request. + +### Solutions + +1. **Restart Firefox** + - If you connected the MIDI device after starting Firefox, restart the browser and try again. + +2. **Check Firefox Permissions** + - Open `about:config` in a new tab. + - Search for `dom.webmidi.enabled`. + - Ensure it is set to `true`. If not, double-click to enable it. + - Search for `privacy.resistFingerprinting` (if enabled, it can block MIDI access). + - Set `privacy.resistFingerprinting` to `false` if you trust the site. + +3. **Verify Device Detection** + - Check if your OS detects the MIDI device. + - On macOS: Open `Audio MIDI Setup` > `MIDI Studio` and ensure your device is listed. + - On Windows: Check `Device Manager` under "Sound, video and game controllers." + +4. **Force Firefox to Recognize the Device** + - Connect your MIDI device before starting Firefox. + - Restart the browser with the device already connected. + +5. **Use Chrome or Edge** + - If Firefox's handling of MIDI is problematic, you might consider using a Chromium-based browser for MIDI-related + tasks. + +(ChatGPT Feb. 2025) \ No newline at end of file diff --git a/packages/app/studio/public/manuals/index.md b/packages/app/studio/public/manuals/index.md new file mode 100644 index 00000000..109e4cbc --- /dev/null +++ b/packages/app/studio/public/manuals/index.md @@ -0,0 +1,18 @@ +# openDAW Manuals + +Please join our [discord community](https://discord.com/invite/ZRm8du7vn4) for updates and news! + +## Known Issues + +**openDAW is in an early prototype stage and NOT ready for production use!** + +### General + +* Some features in openDAW may be unpolished or not functional yet. +* No backward compatibility guaranteed. New studio versions may invalidate old project files. + +### Audio Engine + +* Not optimised. +* Clicks & Pops can occur, since there are no fadings to avoid discontinuities. +* Fast automation or value changes can potentially crash the dsp, since there is no interpolation. \ No newline at end of file diff --git a/packages/app/studio/public/manuals/keyboard-shortcuts.md b/packages/app/studio/public/manuals/keyboard-shortcuts.md new file mode 100644 index 00000000..c6db0bb3 --- /dev/null +++ b/packages/app/studio/public/manuals/keyboard-shortcuts.md @@ -0,0 +1,29 @@ +# Keyboard Shortcuts + +## Misc + +* SPACE: Toggle Playback +* ⌘Z Undo +* ⌘⇧Z Redo + +## Views + +* ⇧0: Show Dashboard +* ⇧1: Show Classic View +* ⇧2: Show Mixer +* ⇧3: Show Modular Systems +* ⇧4: Show Project Info + +## Panels + +* B: Toggle **B**rowser +* E: Toggle **E**ditor +* D: Toggle **D**evices +* M: Toggle **M**ixer + +## Timeline + +* Hold ⌘ and click to cut a region +* Hold ⌥ and drag to copy a region / clip +* Hold ⌥⇧ and drag to copy a region / clip as mirrored +* Click track header and hit ⌫ to delete track \ No newline at end of file diff --git a/packages/app/studio/public/manuals/permissions.md b/packages/app/studio/public/manuals/permissions.md new file mode 100644 index 00000000..ce873dca --- /dev/null +++ b/packages/app/studio/public/manuals/permissions.md @@ -0,0 +1,3 @@ +# Browser Permissions + +openDAW requires grand access to your microphone. tbc... \ No newline at end of file diff --git a/packages/app/studio/public/manuals/private-file-system.md b/packages/app/studio/public/manuals/private-file-system.md new file mode 100644 index 00000000..853ce770 --- /dev/null +++ b/packages/app/studio/public/manuals/private-file-system.md @@ -0,0 +1,23 @@ +# openDAW File System + +openDAW stores your projects and samples in the +[Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) ( +OPFS). + +## Why Samples? + +openDAW uses the OPFS to store user-imported samples because modern browsers do not retain +information about the file origins of imported local files due to security restrictions. This means that once a user +imports a file, the browser has no way to automatically reload it the next time openDAW starts. To ensure persistent +access to user samples, openDAW copies every imported sample into OPFS, allowing the application to manage and retrieve +these files securely without relying on external file paths. + +## Granting OPFS Storage Permission in Firefox +In Firefox, storing data in OPFS requires explicit user approval due to its privacy settings. Without permission, openDAW may not be able to retain imported samples across sessions. To allow persistent storage for openDAW in Firefox, follow these steps: + +* Open Firefox Settings +* Navigate to Privacy & Security +* Scroll down to Permissions and find Storage Permissions +* Click Exceptions and enter the openDAW studio URL `https://opendaw.studio` +* Select Allow and confirm the changes +* Restart Firefox for the settings to take effect \ No newline at end of file diff --git a/packages/app/studio/public/manuals/tech-stack.md b/packages/app/studio/public/manuals/tech-stack.md new file mode 100644 index 00000000..093801bf --- /dev/null +++ b/packages/app/studio/public/manuals/tech-stack.md @@ -0,0 +1,44 @@ +# Tech-Stack + +## Toolchain + +* [Vite](https://vite.dev) +* [Typescript](https://www.typescriptlang.org) +* [Sass](https://sass-lang.com) + +## Libraries + +openDAW uses minimal external dependencies, avoiding hidden behaviors from bulky UI frameworks. + +Each in-house library has a clear, focused purpose. [github repository](https://github.com/andremichelle/opendaw-lib) + +### Dependency Table + +| Library | Dependencies | +|---------------|-------------------------------------| +| **std** | none | +| **dsp** | std | +| **dom** | std | +| **jsx** | std, dom | +| **runtime** | std | +| **box** | std, dom, runtime | +| **box-forge** | std, dom, box | +| **fusion** | std, dom, box, runtime (all peered) | + +### In-House Runtime + +* std (Core) +* dsp (DSP & Sequencing) +* dom (DOM Integration) +* jsx ([JSX](https://en.wikipedia.org/wiki/JSX_(JavaScript)) Integration) +* runtime (Runtime and Scheduling) + +### In-House Data Management + +* box (Runtime Immutable Data Graph) +* box-forge (Box Code Generator) + +### External + +* [jszip](https://www.npmjs.com/package/jszip) (Pack & Unpack Zip-Files) +* [markdown-it](https://www.npmjs.com/package/markdown-it) (Markdown parser) \ No newline at end of file diff --git a/packages/app/studio/public/templates/BMX_LiquidDrums.od b/packages/app/studio/public/templates/BMX_LiquidDrums.od new file mode 100644 index 00000000..3b8be3b2 Binary files /dev/null and b/packages/app/studio/public/templates/BMX_LiquidDrums.od differ diff --git a/packages/app/studio/public/templates/BMX_Playfield.od b/packages/app/studio/public/templates/BMX_Playfield.od new file mode 100644 index 00000000..bd1f85c2 Binary files /dev/null and b/packages/app/studio/public/templates/BMX_Playfield.od differ diff --git a/packages/app/studio/public/templates/BMX_Skyence_buryme_Remix.od b/packages/app/studio/public/templates/BMX_Skyence_buryme_Remix.od new file mode 100644 index 00000000..8e3352b0 Binary files /dev/null and b/packages/app/studio/public/templates/BMX_Skyence_buryme_Remix.od differ diff --git a/packages/app/studio/public/templates/Ben.od b/packages/app/studio/public/templates/Ben.od new file mode 100644 index 00000000..70ffcf7c Binary files /dev/null and b/packages/app/studio/public/templates/Ben.od differ diff --git a/packages/app/studio/public/templates/Breeze.od b/packages/app/studio/public/templates/Breeze.od new file mode 100644 index 00000000..f18d9a24 Binary files /dev/null and b/packages/app/studio/public/templates/Breeze.od differ diff --git a/packages/app/studio/public/templates/BuryMe.od b/packages/app/studio/public/templates/BuryMe.od new file mode 100644 index 00000000..291fecde Binary files /dev/null and b/packages/app/studio/public/templates/BuryMe.od differ diff --git a/packages/app/studio/public/templates/Dub-Techno.od b/packages/app/studio/public/templates/Dub-Techno.od new file mode 100644 index 00000000..f1e7b46c Binary files /dev/null and b/packages/app/studio/public/templates/Dub-Techno.od differ diff --git a/packages/app/studio/public/templates/Fatso.od b/packages/app/studio/public/templates/Fatso.od new file mode 100644 index 00000000..b1066a32 Binary files /dev/null and b/packages/app/studio/public/templates/Fatso.od differ diff --git a/packages/app/studio/public/templates/Release.od b/packages/app/studio/public/templates/Release.od new file mode 100644 index 00000000..c0004845 Binary files /dev/null and b/packages/app/studio/public/templates/Release.od differ diff --git a/packages/app/studio/public/templates/SeekDeeper.od b/packages/app/studio/public/templates/SeekDeeper.od new file mode 100644 index 00000000..b65e60fc Binary files /dev/null and b/packages/app/studio/public/templates/SeekDeeper.od differ diff --git a/packages/app/studio/public/templates/Shafted.od b/packages/app/studio/public/templates/Shafted.od new file mode 100644 index 00000000..8b081474 Binary files /dev/null and b/packages/app/studio/public/templates/Shafted.od differ diff --git a/packages/app/studio/public/templates/Sunset.od b/packages/app/studio/public/templates/Sunset.od new file mode 100644 index 00000000..cc6ab174 Binary files /dev/null and b/packages/app/studio/public/templates/Sunset.od differ diff --git a/packages/app/studio/public/update.html b/packages/app/studio/public/update.html new file mode 100644 index 00000000..aa928e11 --- /dev/null +++ b/packages/app/studio/public/update.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + 🎧openDAW.studio + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + updating +
+ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-000.svg b/packages/app/studio/public/viscious-speed/abstract-000.svg new file mode 100644 index 00000000..d7af1902 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-000.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/studio/public/viscious-speed/abstract-001.svg b/packages/app/studio/public/viscious-speed/abstract-001.svg new file mode 100644 index 00000000..00c73fdf --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-002.svg b/packages/app/studio/public/viscious-speed/abstract-002.svg new file mode 100644 index 00000000..f70fb952 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-002.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-003.svg b/packages/app/studio/public/viscious-speed/abstract-003.svg new file mode 100644 index 00000000..339973c3 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-003.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-004.svg b/packages/app/studio/public/viscious-speed/abstract-004.svg new file mode 100644 index 00000000..8bd91766 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-004.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-005.svg b/packages/app/studio/public/viscious-speed/abstract-005.svg new file mode 100644 index 00000000..58249328 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-005.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-006.svg b/packages/app/studio/public/viscious-speed/abstract-006.svg new file mode 100644 index 00000000..4163dfd8 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-006.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-007.svg b/packages/app/studio/public/viscious-speed/abstract-007.svg new file mode 100644 index 00000000..9de373c7 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-007.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-008.svg b/packages/app/studio/public/viscious-speed/abstract-008.svg new file mode 100644 index 00000000..5c7eef2f --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-008.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-009.svg b/packages/app/studio/public/viscious-speed/abstract-009.svg new file mode 100644 index 00000000..36660395 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-009.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-010.svg b/packages/app/studio/public/viscious-speed/abstract-010.svg new file mode 100644 index 00000000..c5d4164f --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-010.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-011.svg b/packages/app/studio/public/viscious-speed/abstract-011.svg new file mode 100644 index 00000000..ae61e361 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-011.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-012.svg b/packages/app/studio/public/viscious-speed/abstract-012.svg new file mode 100644 index 00000000..564093ba --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-012.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-013.svg b/packages/app/studio/public/viscious-speed/abstract-013.svg new file mode 100644 index 00000000..bc5abb42 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-013.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-014.svg b/packages/app/studio/public/viscious-speed/abstract-014.svg new file mode 100644 index 00000000..a11465ef --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-014.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-015.svg b/packages/app/studio/public/viscious-speed/abstract-015.svg new file mode 100644 index 00000000..08070265 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-015.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-016.svg b/packages/app/studio/public/viscious-speed/abstract-016.svg new file mode 100644 index 00000000..e1ac9390 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-016.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-017.svg b/packages/app/studio/public/viscious-speed/abstract-017.svg new file mode 100644 index 00000000..18a11285 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-017.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-018.svg b/packages/app/studio/public/viscious-speed/abstract-018.svg new file mode 100644 index 00000000..a5965d26 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-018.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-019.svg b/packages/app/studio/public/viscious-speed/abstract-019.svg new file mode 100644 index 00000000..e8fe525a --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-019.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-020.svg b/packages/app/studio/public/viscious-speed/abstract-020.svg new file mode 100644 index 00000000..ef1c69e5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-020.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-021.svg b/packages/app/studio/public/viscious-speed/abstract-021.svg new file mode 100644 index 00000000..72d0e2b5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-021.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-022.svg b/packages/app/studio/public/viscious-speed/abstract-022.svg new file mode 100644 index 00000000..bcbc20bf --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-022.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-023.svg b/packages/app/studio/public/viscious-speed/abstract-023.svg new file mode 100644 index 00000000..0bf4ce79 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-023.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-024.svg b/packages/app/studio/public/viscious-speed/abstract-024.svg new file mode 100644 index 00000000..e751071e --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-024.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-025.svg b/packages/app/studio/public/viscious-speed/abstract-025.svg new file mode 100644 index 00000000..4b757aa5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-025.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-026.svg b/packages/app/studio/public/viscious-speed/abstract-026.svg new file mode 100644 index 00000000..9f12e0ce --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-026.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-027.svg b/packages/app/studio/public/viscious-speed/abstract-027.svg new file mode 100644 index 00000000..28dfcb28 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-027.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-028.svg b/packages/app/studio/public/viscious-speed/abstract-028.svg new file mode 100644 index 00000000..8e9d2d18 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-028.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-029.svg b/packages/app/studio/public/viscious-speed/abstract-029.svg new file mode 100644 index 00000000..873b57cf --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-029.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-030.svg b/packages/app/studio/public/viscious-speed/abstract-030.svg new file mode 100644 index 00000000..ad248f9c --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-030.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-031.svg b/packages/app/studio/public/viscious-speed/abstract-031.svg new file mode 100644 index 00000000..13451876 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-031.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-032.svg b/packages/app/studio/public/viscious-speed/abstract-032.svg new file mode 100644 index 00000000..b87548ea --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-032.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-033.svg b/packages/app/studio/public/viscious-speed/abstract-033.svg new file mode 100644 index 00000000..cd0bd604 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-033.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-034.svg b/packages/app/studio/public/viscious-speed/abstract-034.svg new file mode 100644 index 00000000..c436a37b --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-034.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-035.svg b/packages/app/studio/public/viscious-speed/abstract-035.svg new file mode 100644 index 00000000..ef90f0bd --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-035.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-036.svg b/packages/app/studio/public/viscious-speed/abstract-036.svg new file mode 100644 index 00000000..43eaf5b0 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-036.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-037.svg b/packages/app/studio/public/viscious-speed/abstract-037.svg new file mode 100644 index 00000000..8682ef64 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-037.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-038.svg b/packages/app/studio/public/viscious-speed/abstract-038.svg new file mode 100644 index 00000000..ea75e189 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-038.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-039.svg b/packages/app/studio/public/viscious-speed/abstract-039.svg new file mode 100644 index 00000000..82cde8b1 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-039.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-040.svg b/packages/app/studio/public/viscious-speed/abstract-040.svg new file mode 100644 index 00000000..52cc601e --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-040.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-041.svg b/packages/app/studio/public/viscious-speed/abstract-041.svg new file mode 100644 index 00000000..4453d2f5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-041.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-042.svg b/packages/app/studio/public/viscious-speed/abstract-042.svg new file mode 100644 index 00000000..e1175dc9 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-042.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-043.svg b/packages/app/studio/public/viscious-speed/abstract-043.svg new file mode 100644 index 00000000..5424a731 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-043.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-044.svg b/packages/app/studio/public/viscious-speed/abstract-044.svg new file mode 100644 index 00000000..827044e5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-044.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-045.svg b/packages/app/studio/public/viscious-speed/abstract-045.svg new file mode 100644 index 00000000..c9ca8a47 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-045.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-046.svg b/packages/app/studio/public/viscious-speed/abstract-046.svg new file mode 100644 index 00000000..a045d208 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-046.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-047.svg b/packages/app/studio/public/viscious-speed/abstract-047.svg new file mode 100644 index 00000000..2ed49640 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-047.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-048.svg b/packages/app/studio/public/viscious-speed/abstract-048.svg new file mode 100644 index 00000000..efc430ef --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-048.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-049.svg b/packages/app/studio/public/viscious-speed/abstract-049.svg new file mode 100644 index 00000000..b83dec48 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-049.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-050.svg b/packages/app/studio/public/viscious-speed/abstract-050.svg new file mode 100644 index 00000000..76673d90 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-050.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-051.svg b/packages/app/studio/public/viscious-speed/abstract-051.svg new file mode 100644 index 00000000..1620752c --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-051.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-052.svg b/packages/app/studio/public/viscious-speed/abstract-052.svg new file mode 100644 index 00000000..25bb67b7 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-052.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-053.svg b/packages/app/studio/public/viscious-speed/abstract-053.svg new file mode 100644 index 00000000..85a93e4b --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-053.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-054.svg b/packages/app/studio/public/viscious-speed/abstract-054.svg new file mode 100644 index 00000000..e6767bf6 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-054.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-055.svg b/packages/app/studio/public/viscious-speed/abstract-055.svg new file mode 100644 index 00000000..80ca5ff2 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-055.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-056.svg b/packages/app/studio/public/viscious-speed/abstract-056.svg new file mode 100644 index 00000000..da035a76 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-056.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-057.svg b/packages/app/studio/public/viscious-speed/abstract-057.svg new file mode 100644 index 00000000..b22a5d56 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-057.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-058.svg b/packages/app/studio/public/viscious-speed/abstract-058.svg new file mode 100644 index 00000000..58d4cb7e --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-058.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-059.svg b/packages/app/studio/public/viscious-speed/abstract-059.svg new file mode 100644 index 00000000..3fc4d3d9 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-059.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-060.svg b/packages/app/studio/public/viscious-speed/abstract-060.svg new file mode 100644 index 00000000..f803b8af --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-060.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-061.svg b/packages/app/studio/public/viscious-speed/abstract-061.svg new file mode 100644 index 00000000..3526e80d --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-061.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-062.svg b/packages/app/studio/public/viscious-speed/abstract-062.svg new file mode 100644 index 00000000..53cc106b --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-062.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-063.svg b/packages/app/studio/public/viscious-speed/abstract-063.svg new file mode 100644 index 00000000..3c9dae87 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-063.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-064.svg b/packages/app/studio/public/viscious-speed/abstract-064.svg new file mode 100644 index 00000000..b33f312f --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-064.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-065.svg b/packages/app/studio/public/viscious-speed/abstract-065.svg new file mode 100644 index 00000000..f3f26bf5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-065.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-066.svg b/packages/app/studio/public/viscious-speed/abstract-066.svg new file mode 100644 index 00000000..2dc64643 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-066.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-067.svg b/packages/app/studio/public/viscious-speed/abstract-067.svg new file mode 100644 index 00000000..669264b5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-067.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-068.svg b/packages/app/studio/public/viscious-speed/abstract-068.svg new file mode 100644 index 00000000..0148e2f3 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-068.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-069.svg b/packages/app/studio/public/viscious-speed/abstract-069.svg new file mode 100644 index 00000000..50419b09 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-069.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-070.svg b/packages/app/studio/public/viscious-speed/abstract-070.svg new file mode 100644 index 00000000..9ffacde8 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-070.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-071.svg b/packages/app/studio/public/viscious-speed/abstract-071.svg new file mode 100644 index 00000000..a71d03a2 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-071.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-072.svg b/packages/app/studio/public/viscious-speed/abstract-072.svg new file mode 100644 index 00000000..3d533100 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-072.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-073.svg b/packages/app/studio/public/viscious-speed/abstract-073.svg new file mode 100644 index 00000000..49f319d2 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-073.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-074.svg b/packages/app/studio/public/viscious-speed/abstract-074.svg new file mode 100644 index 00000000..9eff42c5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-074.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-075.svg b/packages/app/studio/public/viscious-speed/abstract-075.svg new file mode 100644 index 00000000..e1e61de4 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-075.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-076.svg b/packages/app/studio/public/viscious-speed/abstract-076.svg new file mode 100644 index 00000000..d1f9d536 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-076.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-077.svg b/packages/app/studio/public/viscious-speed/abstract-077.svg new file mode 100644 index 00000000..80d464af --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-077.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-078.svg b/packages/app/studio/public/viscious-speed/abstract-078.svg new file mode 100644 index 00000000..1f9a4615 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-078.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-079.svg b/packages/app/studio/public/viscious-speed/abstract-079.svg new file mode 100644 index 00000000..b717721d --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-079.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-080.svg b/packages/app/studio/public/viscious-speed/abstract-080.svg new file mode 100644 index 00000000..c7960257 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-080.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-081.svg b/packages/app/studio/public/viscious-speed/abstract-081.svg new file mode 100644 index 00000000..779bcb05 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-081.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-082.svg b/packages/app/studio/public/viscious-speed/abstract-082.svg new file mode 100644 index 00000000..075573c5 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-082.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-083.svg b/packages/app/studio/public/viscious-speed/abstract-083.svg new file mode 100644 index 00000000..81aaec5a --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-083.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-084.svg b/packages/app/studio/public/viscious-speed/abstract-084.svg new file mode 100644 index 00000000..f1c146b1 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-084.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-085.svg b/packages/app/studio/public/viscious-speed/abstract-085.svg new file mode 100644 index 00000000..36da4594 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-085.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-086.svg b/packages/app/studio/public/viscious-speed/abstract-086.svg new file mode 100644 index 00000000..f8b9e635 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-086.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-087.svg b/packages/app/studio/public/viscious-speed/abstract-087.svg new file mode 100644 index 00000000..754634fd --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-087.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-088.svg b/packages/app/studio/public/viscious-speed/abstract-088.svg new file mode 100644 index 00000000..cc0e07b6 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-088.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-089.svg b/packages/app/studio/public/viscious-speed/abstract-089.svg new file mode 100644 index 00000000..96346a1d --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-089.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-090.svg b/packages/app/studio/public/viscious-speed/abstract-090.svg new file mode 100644 index 00000000..36dbec29 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-090.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-091.svg b/packages/app/studio/public/viscious-speed/abstract-091.svg new file mode 100644 index 00000000..b81c6a37 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-091.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-092.svg b/packages/app/studio/public/viscious-speed/abstract-092.svg new file mode 100644 index 00000000..e2b79437 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-092.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-093.svg b/packages/app/studio/public/viscious-speed/abstract-093.svg new file mode 100644 index 00000000..7faf1544 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-093.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-094.svg b/packages/app/studio/public/viscious-speed/abstract-094.svg new file mode 100644 index 00000000..936de988 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-094.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-095.svg b/packages/app/studio/public/viscious-speed/abstract-095.svg new file mode 100644 index 00000000..387d2ef3 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-095.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-096.svg b/packages/app/studio/public/viscious-speed/abstract-096.svg new file mode 100644 index 00000000..3a96ebfb --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-096.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-097.svg b/packages/app/studio/public/viscious-speed/abstract-097.svg new file mode 100644 index 00000000..3aff8d7c --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-097.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-098.svg b/packages/app/studio/public/viscious-speed/abstract-098.svg new file mode 100644 index 00000000..9de0cf88 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-098.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-099.svg b/packages/app/studio/public/viscious-speed/abstract-099.svg new file mode 100644 index 00000000..67094137 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-099.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-100.svg b/packages/app/studio/public/viscious-speed/abstract-100.svg new file mode 100644 index 00000000..61e75b2a --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-100.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-101.svg b/packages/app/studio/public/viscious-speed/abstract-101.svg new file mode 100644 index 00000000..0ae9ddc7 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-101.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-102.svg b/packages/app/studio/public/viscious-speed/abstract-102.svg new file mode 100644 index 00000000..6bc11dd4 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-102.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-103.svg b/packages/app/studio/public/viscious-speed/abstract-103.svg new file mode 100644 index 00000000..f23cfdb9 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-103.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-104.svg b/packages/app/studio/public/viscious-speed/abstract-104.svg new file mode 100644 index 00000000..e454c168 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-104.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-105.svg b/packages/app/studio/public/viscious-speed/abstract-105.svg new file mode 100644 index 00000000..85f25e15 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-105.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-106.svg b/packages/app/studio/public/viscious-speed/abstract-106.svg new file mode 100644 index 00000000..8d313ab3 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-106.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-107.svg b/packages/app/studio/public/viscious-speed/abstract-107.svg new file mode 100644 index 00000000..c9fdaa39 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-107.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-108.svg b/packages/app/studio/public/viscious-speed/abstract-108.svg new file mode 100644 index 00000000..1cf5b14f --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-108.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-109.svg b/packages/app/studio/public/viscious-speed/abstract-109.svg new file mode 100644 index 00000000..6a8c59a1 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-109.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-110.svg b/packages/app/studio/public/viscious-speed/abstract-110.svg new file mode 100644 index 00000000..28556a15 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-110.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-111.svg b/packages/app/studio/public/viscious-speed/abstract-111.svg new file mode 100644 index 00000000..4f8b59a0 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-111.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-112.svg b/packages/app/studio/public/viscious-speed/abstract-112.svg new file mode 100644 index 00000000..9648636f --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-112.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-113.svg b/packages/app/studio/public/viscious-speed/abstract-113.svg new file mode 100644 index 00000000..702bdee3 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-113.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-114.svg b/packages/app/studio/public/viscious-speed/abstract-114.svg new file mode 100644 index 00000000..41a9fb33 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-114.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-115.svg b/packages/app/studio/public/viscious-speed/abstract-115.svg new file mode 100644 index 00000000..54b4c1e4 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-115.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-116.svg b/packages/app/studio/public/viscious-speed/abstract-116.svg new file mode 100644 index 00000000..fc2da334 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-116.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-117.svg b/packages/app/studio/public/viscious-speed/abstract-117.svg new file mode 100644 index 00000000..db3e5d8f --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-117.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-118.svg b/packages/app/studio/public/viscious-speed/abstract-118.svg new file mode 100644 index 00000000..503b3728 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-118.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-119.svg b/packages/app/studio/public/viscious-speed/abstract-119.svg new file mode 100644 index 00000000..8a5adee9 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-119.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-120.svg b/packages/app/studio/public/viscious-speed/abstract-120.svg new file mode 100644 index 00000000..b1d72cf9 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-120.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/public/viscious-speed/abstract-121.svg b/packages/app/studio/public/viscious-speed/abstract-121.svg new file mode 100644 index 00000000..3293bbb1 --- /dev/null +++ b/packages/app/studio/public/viscious-speed/abstract-121.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/studio/src/AppDialogs.tsx b/packages/app/studio/src/AppDialogs.tsx new file mode 100644 index 00000000..f9a4ca3a --- /dev/null +++ b/packages/app/studio/src/AppDialogs.tsx @@ -0,0 +1,37 @@ +import {Dialog} from "@/ui/components/Dialog" +import { IconSymbol } from "@opendaw/studio-adapters" +import {Surface} from "@/ui/surface/Surface" +import {Promises} from "@opendaw/lib-runtime" +import {createElement} from "@opendaw/lib-jsx" +import {Colors} from "@/ui/Colors" + +export const showStoragePersistDialog = (): Promise => { + const {resolve, promise} = Promise.withResolvers() + const dialog: HTMLDialogElement = ( + Promises.tryCatch(navigator.storage.persist()).then(({status, value}) => { + if (status === "resolved" && value) { + console.debug("Firefox now persists storage.") + handler.close() + resolve() + } + }) + }]}> +
+

Data loss is probable if you do not take action.

+

To make this a permanent friendship, please go to:

+

Preferences - Privacy & Security - Cookies & Site Data - Manage + Exceptions...

+

and add opendaw.studio to the list. You will never be bothered again.

+
+
+ ) + Surface.get().body.appendChild(dialog) + dialog.showModal() + return promise +} \ No newline at end of file diff --git a/packages/app/studio/src/BuildInfo.ts b/packages/app/studio/src/BuildInfo.ts new file mode 100644 index 00000000..419f7a7c --- /dev/null +++ b/packages/app/studio/src/BuildInfo.ts @@ -0,0 +1,6 @@ +// This JSON gets created right before building (check ../vite.config.ts) and stored in the public folder. +export type BuildInfo = { + date: number + uuid: string + env: "production" | "development" +} \ No newline at end of file diff --git a/packages/app/studio/src/ErrorMail.txt b/packages/app/studio/src/ErrorMail.txt new file mode 100644 index 00000000..0b9a472c --- /dev/null +++ b/packages/app/studio/src/ErrorMail.txt @@ -0,0 +1,17 @@ +Hello openDAW Support, + +I'd like to report a bug encountered while using openDAW. Below are the details: + +What I did: +[Briefly describe the steps you performed before the issue occurred.] + +What I expected to happen: +[Explain what you expected to happen instead.] + +How to reproduce: +[List clear steps that can help support reproducing this issue.] + +Please let me know if you need any additional information. + +Thank you, +[Your Name] \ No newline at end of file diff --git a/packages/app/studio/src/Recovery.ts b/packages/app/studio/src/Recovery.ts new file mode 100644 index 00000000..6aa475e6 --- /dev/null +++ b/packages/app/studio/src/Recovery.ts @@ -0,0 +1,50 @@ +import {Option, Provider, UUID} from "@opendaw/lib-std" +import {Promises} from "@opendaw/lib-runtime" +import {Project} from "@opendaw/studio-core" +import {StudioService} from "@/service/StudioService.ts" +import {ProjectSession} from "./project/ProjectSession" +import {OpfsAgent} from "@/service/agents.ts" +import {ProjectMeta} from "@/project/ProjectMeta.ts" + +export class Recovery { + static readonly #RESTORE_FILE_PATH = ".backup" + + readonly #service: StudioService + + constructor(service: StudioService) {this.#service = service} + + async restoreSession(): Promise> { + const backupResult = await Promises.tryCatch(OpfsAgent.list(Recovery.#RESTORE_FILE_PATH)) + if (backupResult.status === "rejected" || backupResult.value.length === 0) {return Option.None} + const readResult = await Promises.tryCatch(Promise.all([ + OpfsAgent.read(`${Recovery.#RESTORE_FILE_PATH}/uuid`) + .then(x => UUID.validate(x)), + OpfsAgent.read(`${Recovery.#RESTORE_FILE_PATH}/project.od`) + .then(x => Project.load(this.#service, x.buffer as ArrayBuffer)), + OpfsAgent.read(`${Recovery.#RESTORE_FILE_PATH}/meta.json`) + .then(x => JSON.parse(new TextDecoder().decode(x.buffer as ArrayBuffer)) as ProjectMeta), + OpfsAgent.read(`${Recovery.#RESTORE_FILE_PATH}/saved`) + .then(x => x.at(0) === 1) + ])) + const deleteResult = await Promises.tryCatch(OpfsAgent.delete(Recovery.#RESTORE_FILE_PATH)) + console.debug(`delete backup: "${deleteResult.status}"`) + if (readResult.status === "rejected") {return Option.None} + const [uuid, project, meta, saved] = readResult.value + const session = new ProjectSession(this.#service, uuid, project, meta, Option.None, saved) + console.debug(`restore ${session}, saved: ${saved}`) + return Option.wrap(session) + } + + createBackupCommand(): Option>> { + return this.#service.sessionService.getValue().map((session: ProjectSession) => async () => { + console.debug("temp storing project") + const {project, meta, uuid} = session + return Promises.tryCatch(Promise.all([ + OpfsAgent.write(`${Recovery.#RESTORE_FILE_PATH}/uuid`, uuid), + OpfsAgent.write(`${Recovery.#RESTORE_FILE_PATH}/project.od`, new Uint8Array(project.toArrayBuffer())), + OpfsAgent.write(`${Recovery.#RESTORE_FILE_PATH}/meta.json`, new TextEncoder().encode(JSON.stringify(meta))), + OpfsAgent.write(`${Recovery.#RESTORE_FILE_PATH}/saved`, new Uint8Array([session.saved() ? 1 : 0])) + ])).then(result => console.debug(`backup result: ${result.status}`)) + }) + } +} \ No newline at end of file diff --git a/packages/app/studio/src/audio/AudioDevices.ts b/packages/app/studio/src/audio/AudioDevices.ts new file mode 100644 index 00000000..0bc7949d --- /dev/null +++ b/packages/app/studio/src/audio/AudioDevices.ts @@ -0,0 +1,102 @@ +import {Func, int, Option, panic} from "@opendaw/lib-std" +import {Promises} from "@opendaw/lib-runtime" +import {showInfoDialog} from "@/ui/components/dialogs" +import {Button} from "@/ui/components/Dialog" + +export class AudioDevices { + static #granted: Option> = Option.None + + static async query(query: Func): Promise { + if (this.#granted.isEmpty()) { + this.#granted = Option.wrap(this.#request().catch(reason => { + console.warn("Could not get permission to use microphone / audio devices.") + this.#granted = Option.None + throw reason + })) + } + return this.#granted.unwrap().then(() => query(navigator.mediaDevices)) + } + + static async queryListInputDevices(): Promise> { + return this.query(this.#queryEnumerateDevices("audioinput")) + } + + static async queryListOutputDevices(): Promise> { + return this.query(this.#queryEnumerateDevices("audiooutput")) + } + + static async queryAudioInputDeviceStream(sampleRate: number, deviceId?: string, channelCount: int = 2): Promise { + console.debug(`requestAudioInputDevice deviceId: ${deviceId ?? "default"}, channelCount: ${channelCount}`) + return this.query(mediaDevices => mediaDevices.getUserMedia({ + audio: { + ...AudioDevices.#DefaultMediaTrackConstraints(sampleRate), + channelCount, + deviceId + } satisfies MediaTrackConstraints + })) + } + + static async #request(): Promise { + console.debug("Requesting permission to use microphone / audio devices...") + if (!await this.#queryPermissions()) {return Promise.reject("Permission denied")} + if (!await this.#getUserMedia()) {return Promise.reject("Permission to getUserMedia denied")} + console.debug("Granted permission to use microphone / audio devices.") + } + + static #DefaultMediaTrackConstraints(sampleRate: number = 48_000): MediaTrackConstraints { + return Object.freeze({ + sampleRate, + sampleSize: 32, + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false + }) + } + + static async #queryPermissions(): Promise { + const headline = "Permission Api" + const {status, value: permissionState, error} = + await Promises.tryCatch(navigator.permissions.query({name: "microphone"})) + if (status === "rejected") {return await showInfoDialog({headline, message: String(error)}) ?? false} + console.debug(`Permission state(${permissionState.name}): ${permissionState.state}`) + if (permissionState.state === "granted") {return true} + const buttons: ReadonlyArray + + )} success={(result) => { + const update = () => { + entriesLifeSpan.terminate() + replaceChildren(entries, result + .filter(({name}) => name.toLowerCase().includes(filter.getValue().toLowerCase())) + .toSorted((a, b) => StringComparator(a.name.toLowerCase(), b.name.toLowerCase())) + .map(sample => ( + reload.get().update()} + /> + ))) + } + lifecycle.own(filter.catchupAndSubscribe(update)) + lifecycle.own(service.subscribeSignal(() => { + Runtime.debounce(() => { + location.setValue(SampleLocation.Local) + reload.get().update() + }, 500) + }, "import-sample")) + return ( + +
+ Name + bpm + sec +
+ {entries} +
+ ) + }}/> + ) + }}> + + +
+ {slider} +
+ + ) + lifecycle.ownAll( + Events.subscribe(slider, "input", + () => linearVolume.setValue(clamp(slider.valueAsNumber, 0.0, 1.0))), + linearVolume.catchupAndSubscribe(owner => slider.valueAsNumber = owner.getValue()), + Events.subscribe(element, "keydown", async event => { + if (Keyboard.GlobalShortcut.isDelete(event) && location.getValue() === SampleLocation.Local) { + await sampleService.deleteSelected() + reload.get().update() + } + }) + ) + return element +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/browse/SampleDialogs.tsx b/packages/app/studio/src/ui/browse/SampleDialogs.tsx new file mode 100644 index 00000000..559c65c7 --- /dev/null +++ b/packages/app/studio/src/ui/browse/SampleDialogs.tsx @@ -0,0 +1,138 @@ +import {AudioSample} from "@/audio/AudioSample" +import {Dialog} from "@/ui/components/Dialog" +import {IconSymbol} from "@opendaw/studio-adapters" +import {Surface} from "@/ui/surface/Surface" +import {createElement} from "@opendaw/lib-jsx" +import {showInfoDialog} from "@/ui/components/dialogs" +import {isDefined, UUID} from "@opendaw/lib-std" +import {Promises} from "@opendaw/lib-runtime" +import {Errors, Files} from "@opendaw/lib-dom" +import {SampleImporter} from "@/project/SampleImporter" + +const SampleFileTypes: Array = [ + {description: "openDAW audio-files", accept: {"application/octet-stream": [".wav"]}} +] + +export namespace SampleDialogs { + export const nativeFileBrowser = async (multiple: boolean = true) => + Promises.tryCatch(Files.open({types: SampleFileTypes, multiple})) + + export const missingSampleDialog = async (importer: SampleImporter, uuid: UUID.Format, name: string): Promise => { + const {resolve, reject, promise} = Promise.withResolvers() + const dialog: HTMLDialogElement = ( + { + reject(Errors.AbortError) + handler.close() + } + }, { + text: "Browse", + primary: true, + onClick: async handler => { + const {error, status, value: files} = await SampleDialogs.nativeFileBrowser(false) + if (status === "rejected") { + if (!Errors.isAbort(error)) { + throw error + } + return + } + const file = files?.at(0) + if (isDefined(file)) { + const { + status, + value: sample + } = await Promises.tryCatch( + importer.importSample({ + uuid, + name: file.name, + arrayBuffer: await file.arrayBuffer() + })) + if (status === "resolved") { + handler.close() + resolve(sample) + } + } + } + }]}> +
{name}
+
+ ) + dialog.oncancel = () => reject(Errors.AbortError) + Surface.get().flyout.appendChild(dialog) + dialog.showModal() + return promise + } + + export const showEditSampleDialog = async (sample: AudioSample): Promise => { + if (sample.cloud) { + return Promise.reject("Cannot change sample from the cloud") + } + const {resolve, reject, promise} = Promise.withResolvers() + const inputName: HTMLInputElement = + inputName.select() + inputName.focus() + const inputBpm: HTMLInputElement = + const approve = () => { + const name = inputName.value + if (name.trim().length < 3) { + showInfoDialog({headline: "Invalid Name", message: "Must be at least 3 letters long."}) + return false + } + const bpm = parseFloat(inputBpm.value) + if (isNaN(bpm)) { + showInfoDialog({headline: "Invalid Bpm", message: "Must be a number."}) + return false + } + sample.name = name + sample.bpm = bpm + resolve(sample) + return true + } + const dialog: HTMLDialogElement = ( + { + if (approve()) { + handler.close() + } + } + }]}> +
+
Name:
+ {inputName} +
Bpm:
+ {inputBpm} +
+
+ ) + dialog.oncancel = () => reject(Errors.AbortError) + dialog.onkeydown = event => { + if (event.code === "Enter") { + if (approve()) { + dialog.close() + } + } + } + Surface.get().flyout.appendChild(dialog) + dialog.showModal() + return promise + } +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/browse/SampleLocation.tsx b/packages/app/studio/src/ui/browse/SampleLocation.tsx new file mode 100644 index 00000000..3f08d680 --- /dev/null +++ b/packages/app/studio/src/ui/browse/SampleLocation.tsx @@ -0,0 +1 @@ +export const enum SampleLocation {Cloud, Local} \ No newline at end of file diff --git a/packages/app/studio/src/ui/browse/SampleService.ts b/packages/app/studio/src/ui/browse/SampleService.ts new file mode 100644 index 00000000..1451064f --- /dev/null +++ b/packages/app/studio/src/ui/browse/SampleService.ts @@ -0,0 +1,90 @@ +import {AudioSample} from "@/audio/AudioSample" +import {HTMLSelection} from "@/ui/HTMLSelection" +import {asDefined, DefaultObservableValue, UUID} from "@opendaw/lib-std" +import {StudioService} from "@/service/StudioService" +import {AudioFileBox, AudioRegionBox} from "@opendaw/studio-boxes" +import {Modifier} from "../Modifier" +import {AudioUnitType} from "@opendaw/studio-enums" +import {Instruments} from "@/service/Instruments" +import {PPQN} from "@opendaw/lib-dsp" +import {ColorCodes} from "../mixer/ColorCodes" +import {IconSymbol} from "@opendaw/studio-adapters" +import {showApproveDialog, showInfoDialog, showProcessDialog} from "../components/dialogs" +import {AudioStorage} from "@/audio/AudioStorage" +import {Projects} from "@/project/Projects" +import {SampleApi} from "@/service/SampleApi" +import {Promises} from "@opendaw/lib-runtime" +import {AudioUnitBoxAdapter} from "@opendaw/studio-adapters" + +export class SampleService { + readonly #service: StudioService + readonly #selection: HTMLSelection + + constructor(service: StudioService, selection: HTMLSelection) { + this.#service = service + this.#selection = selection + } + + requestTapes(): void { + if (!this.#service.hasProjectSession) {return} + const project = this.#service.project + const {editing, boxGraph, boxAdapters, rootBoxAdapter} = project + editing.modify(() => { + const samples = this.#samples() + const startIndex = Modifier.pushAudioUnitsIndices(rootBoxAdapter, AudioUnitType.Instrument, samples.length) + samples.forEach(({uuid: uuidAsString, name, bpm, duration: durationInSeconds}, index) => { + const uuid = UUID.parse(uuidAsString) + const audioUnitBox = Modifier.createAudioUnit(project, AudioUnitType.Instrument, startIndex + index) + const audioUnitBoxAdapter = boxAdapters.adapterFor(audioUnitBox, AudioUnitBoxAdapter) + const audioFileBox = boxGraph.findBox(uuid) + .unwrapOrElse(() => AudioFileBox.create(boxGraph, uuid, box => { + box.fileName.setValue(name) + box.startInSeconds.setValue(0) + box.endInSeconds.setValue(durationInSeconds) + })) + const tape = Instruments.Tape + tape.createDevice(boxGraph, audioUnitBoxAdapter, name, IconSymbol.Tape) + const trackBox = tape.createTrack(boxGraph, audioUnitBoxAdapter) + const duration = Math.round(PPQN.secondsToPulses(durationInSeconds, bpm)) + AudioRegionBox.create(boxGraph, UUID.generate(), box => { + box.position.setValue(0) + box.duration.setValue(duration) + box.loopDuration.setValue(duration) + box.regions.refer(trackBox.regions) + box.hue.setValue(ColorCodes.forTrackType(trackBox.type.getValue())) + box.label.setValue(name) + box.file.refer(audioFileBox) + }) + }) + }) + } + + async deleteSelected() {return this.deleteSamples(...this.#samples())} + + async deleteSamples(...samples: ReadonlyArray) { + const processDialog = showProcessDialog("Checking Sample Usages", new DefaultObservableValue(0.5)) + const used = await Projects.listUsedSamples() + const online = new Set((await SampleApi.all()).map(({uuid}) => uuid)) + processDialog.close() + const {status} = await Promises.tryCatch(showApproveDialog({ + headline: "Remove Sample(s)?", + message: "This cannot be undone!", + approveText: "Remove" + })) + if (status === "rejected") {return} + for (const {uuid, name} of samples) { + const isUsed = used.has(uuid) + const isOnline = online.has(uuid) + if (isUsed && !isOnline) { + await showInfoDialog({headline: "Cannot Delete Sample", message: `${name} is used by a project.`}) + } else { + await AudioStorage.remove(UUID.parse(uuid)) + } + } + } + + #samples(): ReadonlyArray { + const selected = this.#selection.getSelected() + return selected.map(element => JSON.parse(asDefined(element.getAttribute("data-selection"))) as AudioSample) + } +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/browse/SampleView.sass b/packages/app/studio/src/ui/browse/SampleView.sass new file mode 100644 index 00000000..8037d280 --- /dev/null +++ b/packages/app/studio/src/ui/browse/SampleView.sass @@ -0,0 +1,46 @@ +component + display: grid + grid-template-columns: subgrid + grid-column: 1 / -1 + color: var(--color-dark) + align-items: center + + &.selected + color: var(--color-gray) + + > div.edit + display: flex + + > div.meta + display: grid + grid-template-columns: subgrid + grid-column: span 3 + padding-left: 3px + border-left: 3px transparent solid + + &:hover + border-left: 3px var(--color-blue) solid + cursor: grab + + &:active + cursor: grabbing + + &.buffering + color: var(--color-black) + background-color: var(--color-shadow) + + &.playing + color: var(--color-black) + background-color: var(--color-blue) + + &.error + color: var(--color-black) + background-color: var(--color-red) + + > span + overflow: hidden + white-space: nowrap + text-overflow: ellipsis + + &.right + text-align: right \ No newline at end of file diff --git a/packages/app/studio/src/ui/browse/SampleView.tsx b/packages/app/studio/src/ui/browse/SampleView.tsx new file mode 100644 index 00000000..6fead37a --- /dev/null +++ b/packages/app/studio/src/ui/browse/SampleView.tsx @@ -0,0 +1,105 @@ +import css from "./SampleView.sass?inline" +import {createElement} from "@opendaw/lib-jsx" +import {Exec, Lifecycle, Objects, UUID} from "@opendaw/lib-std" +import {SamplePlayback} from "@/service/SamplePlayback" +import {AudioSample} from "@/audio/AudioSample" +import {Icon} from "../components/Icon" +import {IconSymbol} from "@opendaw/studio-adapters" +import {SampleLocation} from "@/ui/browse/SampleLocation" +import {Button} from "../components/Button" +import {SampleDialogs} from "@/ui/browse/SampleDialogs" +import {AudioStorage} from "@/audio/AudioStorage" +import {ContextMenu} from "@/ui/ContextMenu" +import {MenuItem} from "@/ui/model/menu-item" +import {SampleService} from "@/ui/browse/SampleService" +import {Html} from "@opendaw/lib-dom" +import {Promises} from "@opendaw/lib-runtime" +import {DragAndDrop} from "@/ui/DragAndDrop" + +const className = Html.adoptStyleSheet(css, "Sample") + +type Construct = { + lifecycle: Lifecycle + sampleService: SampleService + sample: AudioSample + playback: SamplePlayback + location: SampleLocation + refresh: Exec +} + +export const SampleView = ({lifecycle, sampleService, sample, playback, location, refresh}: Construct) => { + const {name, duration, bpm} = sample + const labelName: HTMLElement = {name} + const labelBpm: HTMLElement = {bpm.toFixed(1)} + const editButton: Element = ( + + ) + const deleteButton: Element = ( + + ) + const metaElement: HTMLElement = ( +
playback.toggle(sample.uuid)}> + {labelName} + {labelBpm} + {duration.toFixed(1)} +
+ ) + const element: HTMLElement = ( +
playback.eject()} + draggable> + {metaElement} + {location === SampleLocation.Local && ( +
+ {editButton} + {deleteButton} +
+ )} +
+ ) + lifecycle.ownAll( + DragAndDrop.installSource(element, () => ({type: "sample", sample})), + ContextMenu.subscribe(element, collector => collector.addItems( + MenuItem.default({label: "Create Audio Track(s)"}) + .setTriggerProcedure(() => sampleService.requestTapes()), + MenuItem.default({label: "Delete Sample(s)", selectable: location === SampleLocation.Local}) + .setTriggerProcedure(async () => { + await sampleService.deleteSelected() + refresh() + })) + ), + playback.subscribe(sample.uuid, event => { + metaElement.classList.remove("buffering", "playing", "error") + metaElement.classList.add(event.type) + switch (event.type) { + case "idle": + break + case "buffering": + break + case "playing": + break + case "error": + break + } + }) + ) + return element +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/canvas/capturing.ts b/packages/app/studio/src/ui/canvas/capturing.ts new file mode 100644 index 00000000..2c08498d --- /dev/null +++ b/packages/app/studio/src/ui/canvas/capturing.ts @@ -0,0 +1,27 @@ +import {Nullable} from "@opendaw/lib-std" + +export interface Capturing {capture(localX: number, localY: number): Nullable} + +export class ElementCapturing { + readonly #element: Element + readonly #capturing: Capturing + + constructor(element: Element, capturing: Capturing) { + this.#element = element + this.#capturing = capturing + } + + get element(): Element {return this.#element} + get capturing(): Capturing {return this.#capturing} + + captureEvent(event: { clientX: number, clientY: number }): Nullable { + return this.capturePoint(event.clientX, event.clientY) + } + + capturePoint(clientX: number, clientY: number): Nullable { + const {left, top} = this.#element.getBoundingClientRect() + return this.captureLocalPoint(clientX - left, clientY - top) + } + + captureLocalPoint(x: number, y: number): Nullable {return this.#capturing.capture(x, y)} +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/canvas/painter.ts b/packages/app/studio/src/ui/canvas/painter.ts new file mode 100644 index 00000000..13dbccf5 --- /dev/null +++ b/packages/app/studio/src/ui/canvas/painter.ts @@ -0,0 +1,101 @@ +import {asDefined, Exec, Procedure, Terminable, Terminator} from "@opendaw/lib-std" +import {Scale} from "@/ui/canvas/scale" +import {AnimationFrame, Html} from "@opendaw/lib-dom" + +export class CanvasPainter implements Terminable { + readonly #rendering: CanvasRenderer + + constructor(canvas: HTMLCanvasElement, render: Procedure) { + this.#rendering = new CanvasRenderer(canvas, () => render(this)) + } + + readonly requestUpdate = (): void => {this.#rendering.requestUpdate()} + + get isResized(): boolean {return this.#rendering.isResized} + get devicePixelRatio(): number {return this.#rendering.devicePixelRatio} + get width(): number {return this.#rendering.width} + get height(): number {return this.#rendering.height} + get actualWidth(): number {return this.#rendering.actualWidth} + get actualHeight(): number {return this.#rendering.actualHeight} + get context(): CanvasRenderingContext2D {return this.#rendering.context} + terminate(): void {this.#rendering.terminate()} +} + +export class CanvasUnitPainter implements Terminable { + readonly #rendering: CanvasRenderer + + readonly #xAxis: Scale + readonly #yAxis: Scale + + constructor(canvas: HTMLCanvasElement, + xAxis: Scale, + yAxis: Scale, + render: Procedure) { + this.#rendering = new CanvasRenderer(canvas, () => render(this)) + this.#xAxis = xAxis + this.#yAxis = yAxis + } + + readonly requestUpdate = (): void => {this.#rendering.requestUpdate()} + + xToUnit(x: number): number {return this.#xAxis.normToUnit(x / this.#rendering.actualWidth)} + unitToX(value: number): number {return this.#xAxis.unitToNorm(value) * this.#rendering.actualWidth} + yToUnit(y: number): number {return this.#yAxis.normToUnit(1.0 - y / this.#rendering.actualHeight)} + unitToY(value: number): number {return (1.0 - this.#yAxis.unitToNorm(value)) * this.#rendering.actualHeight} + get context(): CanvasRenderingContext2D {return this.#rendering.context} + get isResized(): boolean {return this.#rendering.isResized} + get width(): number {return this.#rendering.width} + get height(): number {return this.#rendering.height} + get actualWidth(): number {return this.#rendering.actualWidth} + get actualHeight(): number {return this.#rendering.actualHeight} + terminate(): void {this.#rendering.terminate()} +} + +class CanvasRenderer implements Terminable { + readonly #lifecycle = new Terminator() + + readonly #context: CanvasRenderingContext2D + readonly #update: Exec + + #width: number = 0 + #height: number = 0 + #devicePixelRatio: number = 1 + #isResized: boolean = true + #needsUpdate: boolean = true + + constructor(canvas: HTMLCanvasElement, update: Exec) { + this.#context = asDefined(canvas.getContext("2d")) + this.#update = update + + this.#lifecycle.ownAll( + Html.watchResize(canvas, () => { + this.#isResized = true + this.#needsUpdate = true + }), + this.#lifecycle.own(AnimationFrame.add(() => { + const width = canvas.clientWidth + const height = canvas.clientHeight + if (!this.#needsUpdate || width === 0 || height === 0) {return} + this.#isResized = width !== this.#width || height !== this.#height || devicePixelRatio !== this.#devicePixelRatio + this.#width = width + this.#height = height + this.#devicePixelRatio = devicePixelRatio + canvas.width = width * devicePixelRatio + canvas.height = height * devicePixelRatio + this.#update() + this.#isResized = false + this.#needsUpdate = false + })) + ) + } + + get isResized(): boolean {return this.#isResized} + get devicePixelRatio(): number {return this.#devicePixelRatio} + get width(): number {return this.#width} + get height(): number {return this.#height} + get actualWidth(): number {return this.#width * this.#devicePixelRatio} + get actualHeight(): number {return this.#height * this.#devicePixelRatio} + get context(): CanvasRenderingContext2D {return this.#context} + requestUpdate(): void {this.#needsUpdate = true} + terminate(): void {this.#lifecycle.terminate()} +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/canvas/scale.ts b/packages/app/studio/src/ui/canvas/scale.ts new file mode 100644 index 00000000..598475c2 --- /dev/null +++ b/packages/app/studio/src/ui/canvas/scale.ts @@ -0,0 +1,48 @@ +import {unitValue} from "@opendaw/lib-std" + +export interface Scale { + unitToNorm(unit: number): unitValue + normToUnit(norm: unitValue): number +} + +export class LinearScale implements Scale { + readonly #min: number + readonly #max: number + readonly #range: number + readonly #rangeInv: number + + constructor(min: number, max: number) { + this.#min = min + this.#max = max + this.#range = max - min + this.#rangeInv = 1.0 / this.#range + } + + get min(): number {return this.#min} + get max(): number {return this.#max} + + normToUnit(norm: number): number {return this.#min + norm * this.#range} + unitToNorm(unit: number): number {return (unit - this.#min) * this.#rangeInv} +} + +export class LogScale implements Scale { + readonly #min: number + readonly #max: number + readonly #range: number + readonly #logMin: number + readonly #logRangeInv: number + + constructor(min: number, max: number) { + this.#min = min + this.#max = max + this.#range = Math.log(max / min) + this.#logMin = Math.log(min) + this.#logRangeInv = 1.0 / (Math.log(max) - this.#logMin) + } + + get min(): number {return this.#min} + get max(): number {return this.#max} + + normToUnit(norm: unitValue): number {return this.#min * Math.exp(norm * this.#range)} + unitToNorm(unit: number): unitValue {return (Math.log(unit) - this.#logMin) * this.#logRangeInv} +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/BoxDebugView.sass b/packages/app/studio/src/ui/components/BoxDebugView.sass new file mode 100644 index 00000000..d638382d --- /dev/null +++ b/packages/app/studio/src/ui/components/BoxDebugView.sass @@ -0,0 +1,54 @@ +component + font-size: 0.75rem + display: flex + flex-direction: column + row-gap: 0.5rem + + > h2 + white-space: nowrap + font-family: monospace + font-size: 0.75rem + text-transform: uppercase + opacity: 0.3 + border-bottom: 1px white solid + + div.fields + display: grid + grid-template-columns: auto auto 1fr + column-gap: 0.75rem + white-space: nowrap + + > div.type + color: var(--color-green) + grid-column: span 3 + + > div.field + margin-left: 0.25rem + display: grid + grid-template-columns: subgrid + grid-column: span 3 + + > span.key + text-align: right + opacity: 0.5 + + > div.value > div.target + font-family: monospace + font-size: 0.75rem + margin-top: 0.0625rem + + > div.value + color: var(--color-green) + + > div.target + color: var(--color-blue) + + &.clickable + cursor: pointer + + > div.object + display: flex + flex-direction: column + + span.pointer + color: var(--color-shadow) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/BoxDebugView.tsx b/packages/app/studio/src/ui/components/BoxDebugView.tsx new file mode 100644 index 00000000..ac4f046a --- /dev/null +++ b/packages/app/studio/src/ui/components/BoxDebugView.tsx @@ -0,0 +1,74 @@ +import css from "./BoxDebugView.sass?inline" +import {UUID} from "@opendaw/lib-std" +import {ArrayField, Box, Field, ObjectField, PointerField, PrimitiveField, Vertex} from "@opendaw/lib-box" +import {createElement, JsxValue, replaceChildren} from "@opendaw/lib-jsx" +import {Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "BoxDebugView") + +export type Construct = { + box: Box +} + +export const BoxDebugView = ({box}: Construct) => { + const content =
+ const uuidElement: HTMLElement =

{UUID.toString(box.address.uuid)}

+ const incoming = (vertex: Vertex) => `← ${(vertex.pointerHub.incoming().length)}` + const render = (vertex: Vertex): JsxValue => ( +
+
+ + {vertex.isBox() ? vertex.name : vertex.isField() ? vertex.fieldName : ""} + {incoming(vertex)} +
+ {Array.from(vertex.fields()).map(field => { + return ( +
+ {field.fieldKey} + {field.fieldName} + { + field.accept({ + visitPrimitiveField: (field: PrimitiveField): JsxValue => ( +
+ {`${field.getValue()}`} {incoming(field)} +
+ ), + visitPointerField: (field: PointerField): JsxValue => ( + field.targetVertex.match({ + none: () =>
→ unset
, + some: vertex => ( +
{ + uuidElement.textContent = UUID.toString(vertex.box.address.uuid) + replaceChildren(content, render(vertex.box)) + }}> + {`→ ${vertex.box.name}/${vertex.address.fieldKeys}`} +
+ ) + }) + ), + visitObjectField: (field: ObjectField): JsxValue => ( +
+ {render(field)} +
+ ), + visitArrayField: (field: ArrayField): JsxValue => ( + N:{field.size()}), + visitField: (field: Field): JsxValue => ( +
{incoming(field)}
) + }) + } +
+ ) + })} +
+ ) + replaceChildren(content, render(box)) + return ( +
+ {uuidElement} + {content} +
+ ) +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/BoxesDebugView.sass b/packages/app/studio/src/ui/components/BoxesDebugView.sass new file mode 100644 index 00000000..9caf7bc6 --- /dev/null +++ b/packages/app/studio/src/ui/components/BoxesDebugView.sass @@ -0,0 +1,47 @@ +component + font-size: 0.75rem + display: flex + flex-direction: column + row-gap: 0.5rem + overflow: hidden + + > div.boxes + display: flex + flex-direction: column + display: grid + grid-template-columns: 1fr auto auto auto + column-gap: 0.5rem + + > div.title + display: grid + grid-template-columns: subgrid + grid-column: span 4 + color: var(--color-green) + margin-bottom: 0.125rem + + > div.scrollable + display: grid + grid-template-columns: subgrid + grid-column: span 4 + overflow-y: scroll + align-content: start + height: 14rem + + > div.box + display: grid + grid-template-columns: subgrid + grid-column: span 4 + color: var(--color-dark) + cursor: pointer + + &:hover + color: white + + > span.dependencies + justify-self: center + + > span.uuid + white-space: nowrap + font-size: 0.625rem + line-height: 0.875rem + font-family: monospace \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/BoxesDebugView.tsx b/packages/app/studio/src/ui/components/BoxesDebugView.tsx new file mode 100644 index 00000000..9c93ccec --- /dev/null +++ b/packages/app/studio/src/ui/components/BoxesDebugView.tsx @@ -0,0 +1,38 @@ +import css from "./BoxesDebugView.sass?inline" +import {NumberComparator, UUID} from "@opendaw/lib-std" +import {createElement} from "@opendaw/lib-jsx" +import {BoxGraph} from "@opendaw/lib-box" +import {showDebugBoxDialog} from "@/ui/components/dialogs.tsx" +import {Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "BoxesDebugView") + +type Construct = { + boxGraph: BoxGraph +} + +export const BoxesDebugView = ({boxGraph}: Construct) => { + return ( +
+

Boxes

+
+
+ Name + In + Out + ID +
+
+ {boxGraph.boxes().toSorted((a, b) => NumberComparator(a.creationIndex, b.creationIndex)).map(box => ( +
showDebugBoxDialog(box)}> + {box.name} + {box.incomingEdges().length} + {box.outgoingEdges().length} + {UUID.toString(box.address.uuid)} +
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Button.tsx b/packages/app/studio/src/ui/components/Button.tsx new file mode 100644 index 00000000..8553642f --- /dev/null +++ b/packages/app/studio/src/ui/components/Button.tsx @@ -0,0 +1,22 @@ +import {Lifecycle, Procedure} from "@opendaw/lib-std" +import {createElement, JsxValue} from "@opendaw/lib-jsx" +import {Appearance, ButtonCheckboxRadio} from "@/ui/components/ButtonCheckboxRadio" +import {Html} from "@opendaw/lib-dom" + +export type ButtonParameters = { + lifecycle: Lifecycle + onClick: Procedure + style?: Partial + appearance?: Appearance +} + +export const Button = ({lifecycle, onClick, style, appearance}: ButtonParameters, children: JsxValue) => { + const id = Html.nextID() + const input: HTMLInputElement = + return ( + + {input} + + + ) +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/ButtonCheckboxRadio.sass b/packages/app/studio/src/ui/components/ButtonCheckboxRadio.sass new file mode 100644 index 00000000..a5406312 --- /dev/null +++ b/packages/app/studio/src/ui/components/ButtonCheckboxRadio.sass @@ -0,0 +1,81 @@ +@use "@/mixins" + +component + --color: var(--color-shadow) + --color-active: var(--color-blue) + + appearance: none + margin: 0 + min-width: 1em + min-height: 1em + flex: 0 0 auto + display: flex + place-items: center + outline: none + border: none + background: none + color: var(--color) + font: inherit + + @include mixins.controllable + + &.active + color: var(--color-active) + + > label + padding: 0 0.25em + min-width: 1.5em + height: 1.5em + display: flex + place-items: center + place-content: center + pointer-events: all + position: relative + + * + pointer-events: none + + > input + display: none + + &:checked + label, &[type="button"]:active + label + color: var(--color-active) + + &.framed + > input:not([type="radio"]) + label + border-radius: 0.25em + border: 0.5px solid var(--color) + box-shadow: 0 0 3px -1px var(--color) + + > span + padding: 0 0.5em + + &:after + content: "" + position: absolute + width: 100% + height: 100% + border-radius: inherit + background-color: var(--color) + opacity: 0.04 + pointer-events: none + + &.landscape + > label + width: 1.625em + + > input:checked + label, > input[type="button"]:active + label + color: var(--color-active) + border-radius: 0.25em + border: 0.5px solid var(--color-active) + box-shadow: 0 0 3px -1px var(--color-active) + + &:after + content: "" + position: absolute + width: 100% + height: 100% + border-radius: inherit + background-color: var(--color-active) + opacity: 0.08 + pointer-events: none \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/ButtonCheckboxRadio.tsx b/packages/app/studio/src/ui/components/ButtonCheckboxRadio.tsx new file mode 100644 index 00000000..5b370b45 --- /dev/null +++ b/packages/app/studio/src/ui/components/ButtonCheckboxRadio.tsx @@ -0,0 +1,70 @@ +import css from "./ButtonCheckboxRadio.sass?inline" +import {isDefined, Lifecycle} from "@opendaw/lib-std" +import {createElement, JsxValue} from "@opendaw/lib-jsx" +import {TextTooltip} from "@/ui/surface/TextTooltip.tsx" +import {CssUtils, Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "ButtonCheckboxRadio") + +export type Appearance = { + color?: string + activeColor?: string + framed?: boolean + landscape?: boolean + tooltip?: string + cursor?: CssUtils.Cursor +} + +type Construct = { + lifecycle: Lifecycle + dataClass: string + style?: Partial + className?: string + appearance?: Appearance +} + +export const ButtonCheckboxRadio = ({ + lifecycle, + dataClass, + style, + className: externalClassName, + appearance + }: Construct, children: JsxValue) => { + const wrapper: HTMLElement = ( +
{ + self.getSelection()?.removeAllRanges() + event.preventDefault() + event.stopPropagation() + }}> + {children} +
+ ) + + if (appearance?.tooltip) { + lifecycle.own(TextTooltip.simple(wrapper, () => { + const clientRect = wrapper.getBoundingClientRect() + return { + clientX: clientRect.left, + clientY: clientRect.bottom + 8, + text: appearance.tooltip ?? "" + } + })) + } + + if (isDefined(appearance?.color)) { + wrapper.style.setProperty("--color", appearance.color) + } + if (isDefined(appearance?.activeColor)) { + wrapper.style.setProperty("--color-active", appearance.activeColor) + } + if (isDefined(style)) { + Object.assign(wrapper.style, style) + } + return wrapper +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Checkbox.tsx b/packages/app/studio/src/ui/components/Checkbox.tsx new file mode 100644 index 00000000..9f2601ff --- /dev/null +++ b/packages/app/studio/src/ui/components/Checkbox.tsx @@ -0,0 +1,38 @@ +import {Lifecycle, MutableObservableValue} from "@opendaw/lib-std" +import {createElement, JsxValue} from "@opendaw/lib-jsx" +import {Appearance, ButtonCheckboxRadio} from "@/ui/components/ButtonCheckboxRadio.tsx" +import {Html} from "@opendaw/lib-dom" + +type Construct = { + lifecycle: Lifecycle + model: MutableObservableValue + style?: Partial + className?: string + appearance?: Appearance +} + +export const Checkbox = ({lifecycle, model, style, className, appearance}: Construct, children: JsxValue) => { + const id = Html.nextID() + const input: HTMLInputElement = ( + { + model.setValue(input.checked) + input.checked = model.getValue() + }} + checked={model.getValue()}/> + ) + lifecycle.own(model.subscribe(model => { + input.checked = model.getValue() + })) + return ( + + {input} + + + ) +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/ControlIndicator.tsx b/packages/app/studio/src/ui/components/ControlIndicator.tsx new file mode 100644 index 00000000..548094e8 --- /dev/null +++ b/packages/app/studio/src/ui/components/ControlIndicator.tsx @@ -0,0 +1,17 @@ +import {Lifecycle} from "@opendaw/lib-std" +import {AutomatableParameterFieldAdapter} from "@opendaw/studio-adapters" +import {createElement, Group, JsxValue} from "@opendaw/lib-jsx" + +type Construct = { + lifecycle: Lifecycle + parameter: AutomatableParameterFieldAdapter +} + +export const ControlIndicator = ({lifecycle, parameter}: Construct, children: JsxValue) => { + const element: HTMLElement = {children} + lifecycle.own(parameter.catchupAndSubscribeControlSources({ + onControlSourceAdd: () => element.classList.add("automated"), + onControlSourceRemove: () => element.classList.remove("automated") + })) + return element +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Dialog.sass b/packages/app/studio/src/ui/components/Dialog.sass new file mode 100644 index 00000000..3c394062 --- /dev/null +++ b/packages/app/studio/src/ui/components/Dialog.sass @@ -0,0 +1,34 @@ +@use "@/mixins" + +component + @include mixins.floating(false) + flex-direction: column + min-width: 25em + max-width: 50% + padding: 2em + + &.error::backdrop + background: rgba(255, 0, 0, 0.04) + + > h1 + display: flex + flex-direction: row + align-items: center + column-gap: 0.25em + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + + > footer + padding-top: 1em + margin-top: auto + display: flex + flex-direction: row + justify-content: end + column-gap: 1em + + &:empty + display: none + + > [data-class='button'] label + min-width: 4em \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Dialog.tsx b/packages/app/studio/src/ui/components/Dialog.tsx new file mode 100644 index 00000000..4d4e233e --- /dev/null +++ b/packages/app/studio/src/ui/components/Dialog.tsx @@ -0,0 +1,72 @@ +import css from "./Dialog.sass?inline" +import {Exec, Procedure, safeExecute, Terminator} from "@opendaw/lib-std" +import {createElement, JsxValue} from "@opendaw/lib-jsx" +import {Button} from "@/ui/components/Button.tsx" +import {Icon} from "@/ui/components/Icon.tsx" +import {Colors} from "@/ui/Colors.ts" +import {IconSymbol} from "@opendaw/studio-adapters" +import {Events, Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "Dialog") + +export interface DialogHandler { + close(): void +} + +export type Button = { + text: string + onClick: Procedure + primary?: boolean +} + +type Construct = { + headline: string + icon: IconSymbol + onCancel?: Exec + cancelable?: boolean + buttons?: ReadonlyArray + ))} + + + ) + if (cancelable === false) { + dialog.oncancel = (event) => event.preventDefault() + } + dialog.onkeydown = (event) => { + if (!(cancelable === true && event.key === "Escape") && !Events.isTextInput(event.target)) { + if (event.code !== "F12") { + event.preventDefault() + } + event.stopPropagation() + } + } + dialog.onclose = () => { + lifecycle.terminate() + if (dialog.returnValue === "") { + safeExecute(onCancel) + } + dialog.remove() + } + return dialog +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/FlexSpacer.tsx b/packages/app/studio/src/ui/components/FlexSpacer.tsx new file mode 100644 index 00000000..56939483 --- /dev/null +++ b/packages/app/studio/src/ui/components/FlexSpacer.tsx @@ -0,0 +1,5 @@ +import {createElement} from "@opendaw/lib-jsx" + +export const FlexSpacer = ({pixels}: { pixels?: number }) => ( +
+) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/FloatingTextInput.sass b/packages/app/studio/src/ui/components/FloatingTextInput.sass new file mode 100644 index 00000000..f987af28 --- /dev/null +++ b/packages/app/studio/src/ui/components/FloatingTextInput.sass @@ -0,0 +1,31 @@ +@use "@/mixins" + +component + @include mixins.floating + top: -1rem + left: -0.25rem + height: 2rem + align-items: center + + &:after + content: attr(unit) + padding: 0 0.5em 0 0 + opacity: 0.25 + + > input + color: inherit + font-size: 0.75rem + font-weight: inherit + font-family: inherit + padding: 0 0.5em + appearance: none + border: none + background: none + outline: none + width: 7em + max-width: 11em + + &[unit] > input + text-align: right + width: 6em + max-width: 8em \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/FloatingTextInput.tsx b/packages/app/studio/src/ui/components/FloatingTextInput.tsx new file mode 100644 index 00000000..8d270015 --- /dev/null +++ b/packages/app/studio/src/ui/components/FloatingTextInput.tsx @@ -0,0 +1,50 @@ +import css from "./FloatingTextInput.sass?inline" +import {isDefined, Point} from "@opendaw/lib-std" +import {createElement} from "@opendaw/lib-jsx" +import {Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "TextInput") + +type Construct = { + resolvers?: PromiseWithResolvers + position?: Point + value?: boolean | number | string + unit?: string +} + +export const FloatingTextInput = ({resolvers, position, value, unit}: Construct) => { + const inputField: HTMLInputElement = () + requestAnimationFrame(() => { + inputField.select() + inputField.focus() + }) + if (isDefined(resolvers)) { + const {reject, resolve} = resolvers + const remove = () => { + inputField.onblur = null + inputField.onkeydown = null + element.remove() + } + inputField.onblur = () => { + remove() + reject("cancel") + } + inputField.onkeydown = (event: KeyboardEvent) => { + if (event.key.toLowerCase() === "enter") { + const value = inputField.value + remove() + resolve(value) + } + } + } + const element: HTMLElement = ( +
+ {inputField} +
+ ) + return element +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Icon.sass b/packages/app/studio/src/ui/components/Icon.sass new file mode 100644 index 00000000..f60d04e7 --- /dev/null +++ b/packages/app/studio/src/ui/components/Icon.sass @@ -0,0 +1,5 @@ +component + width: 1em + height: 1em + min-width: 1em + min-height: 1em \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Icon.tsx b/packages/app/studio/src/ui/components/Icon.tsx new file mode 100644 index 00000000..119c07ce --- /dev/null +++ b/packages/app/studio/src/ui/components/Icon.tsx @@ -0,0 +1,30 @@ +import css from "./Icon.sass?inline" +import {Lifecycle, ObservableValue} from "@opendaw/lib-std" +import {createElement} from "@opendaw/lib-jsx" +import {IconSymbol} from "@opendaw/studio-adapters" +import {Html} from "@opendaw/lib-dom" + +const defaultClassName = Html.adoptStyleSheet(css, "Icon") + +export const Icon = ({symbol, className, style}: { + symbol: IconSymbol, + className?: string, + style?: Partial +}) => ( + + + +) + +export const IconCartridge = ({lifecycle, symbol, className, style}: { + lifecycle: Lifecycle, + symbol: ObservableValue, + className?: string, + style?: Partial +}) => { + const use: SVGUseElement = + const updater = () => use.href.baseVal = `#${IconSymbol.toName(symbol.getValue())}` + updater() + lifecycle.own(symbol.subscribe(updater)) + return ({use}) +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/InsertMarker.sass b/packages/app/studio/src/ui/components/InsertMarker.sass new file mode 100644 index 00000000..a30dfc39 --- /dev/null +++ b/packages/app/studio/src/ui/components/InsertMarker.sass @@ -0,0 +1,10 @@ +component + flex: 0 0 20px + height: 100% + display: flex + justify-content: center + color: var(--color-red) + filter: drop-shadow(0px 4px 1px rgba(black, 0.5)) + top: -0.25em + position: relative + pointer-events: none \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/InsertMarker.tsx b/packages/app/studio/src/ui/components/InsertMarker.tsx new file mode 100644 index 00000000..9dc820f9 --- /dev/null +++ b/packages/app/studio/src/ui/components/InsertMarker.tsx @@ -0,0 +1,15 @@ +import css from "./InsertMarker.sass?inline" +import {Html} from "@opendaw/lib-dom" +import {IconSymbol} from "@opendaw/studio-adapters" +import {Icon} from "@/ui/components/Icon" +import {createElement} from "@opendaw/lib-jsx" + +const className = Html.adoptStyleSheet(css, "InsertMarker") + +export const InsertMarker = () => { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Knob.sass b/packages/app/studio/src/ui/components/Knob.sass new file mode 100644 index 00000000..361a033a --- /dev/null +++ b/packages/app/studio/src/ui/components/Knob.sass @@ -0,0 +1,16 @@ +@use "@/mixins" + +component + min-width: 1.75em + max-width: 1.75em + min-height: 1.75em + max-height: 1.75em + place-self: center + align-self: center + justify-content: center + + @include mixins.controllable + + .light + opacity: 0.25 + filter: blur(3px) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Knob.tsx b/packages/app/studio/src/ui/components/Knob.tsx new file mode 100644 index 00000000..81491e3d --- /dev/null +++ b/packages/app/studio/src/ui/components/Knob.tsx @@ -0,0 +1,92 @@ +import {isDefined, Lifecycle, Parameter, PI_HALF, TAU, unitValue} from "@opendaw/lib-std" +import {createElement} from "@opendaw/lib-jsx" +import css from "./Knob.sass?inline" +import {Html, Svg} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "knob") + +export const DefaultDesign: Readonly = Object.freeze({ + radius: 20, + trackWidth: 1.5, + angleOffset: Math.PI / 5.0, + indicator: [0.3, 0.6], + indicatorWidth: 2.5 +} satisfies Design) + +export const TinyDesign: Readonly = Object.freeze({ + radius: 20, + trackWidth: 1.5, + angleOffset: Math.PI / 5.0, + indicator: [0.2, 0.66], + indicatorWidth: 2.5 +} satisfies Design) + +type Design = { + readonly radius: number // defines the size + readonly trackWidth: number // thickness of the arc + readonly angleOffset: number // positive & smaller than PI/2 + readonly indicator: [unitValue, unitValue] // allows floating indicator + readonly indicatorWidth: number +} + +type Construct = { + lifecycle: Lifecycle + value: Parameter + anchor: unitValue + color?: string + design?: Design +} + +export const Knob = ({lifecycle, value, anchor, color, design}: Construct) => { + const {radius, trackWidth, angleOffset, indicator: [min, max], indicatorWidth} = design ?? DefaultDesign + + const trackRadius = Math.floor(radius - trackWidth * 0.5) + const angleMin = PI_HALF + angleOffset + const angleMax = PI_HALF - angleOffset + const angleRange = (TAU - angleOffset * 2.0) + const angleAnc = angleMin + anchor * angleRange + const width = radius * 2.0 + const height = radius + Math.ceil(Math.cos(angleOffset) * radius) + const paths = [ + , + + ] + const update = (unitValue: unitValue) => { + const angleVal = angleMin + unitValue * angleRange + const aMinValAnc = Math.min(angleVal, angleAnc) + const aMaxValAnc = Math.max(angleVal, angleAnc) + const [value, line] = paths + value.setAttribute("d", Svg.pathBuilder() + .circleSegment(0, 0, trackRadius, aMinValAnc - 1.0 / trackRadius, aMaxValAnc + 1.0 / trackRadius) + .get()) + const cos = Math.cos(angleVal) * trackRadius + const sin = Math.sin(angleVal) * trackRadius + line.setAttribute("d", Svg.pathBuilder() + .moveTo(cos * min, sin * min) + .lineTo(cos * max, sin * max) + .get()) + } + const svg: SVGSVGElement = ( + + + + + + {paths} + + + ) + if (isDefined(color)) { + svg.style.color = color + } + lifecycle.own(value.subscribe(model => update(model.getControlledUnitValue()))) + update(value.getControlledUnitValue()) + return svg +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Menu.sass b/packages/app/studio/src/ui/components/Menu.sass new file mode 100644 index 00000000..cc050396 --- /dev/null +++ b/packages/app/studio/src/ui/components/Menu.sass @@ -0,0 +1,125 @@ +@use "@/mixins" +@use "@/colors" + +$ACTIVE: hsl(210, 90%, 30%) +$PARENT: hsla(210, 90%, 30%, 0.5) + +component + @include mixins.floating + position: fixed + pointer-events: all + flex-direction: column + border-collapse: collapse + color: var(--color-bright) + min-width: 12em + + &:after + content: "" + top: -4px + left: -4px + width: calc(100% + 8px) + height: calc(100% + 8px) + position: absolute + background-color: transparent + z-index: -1 + + &:focus > div.container > div.item.selected > div.default + color: white + background-color: $ACTIVE + + &.overflowing + max-height: calc(100vh - 1em) + + > div.scroll + display: flex + + > div.container + display: flex + flex-direction: column + overflow: hidden + pointer-events: all + + > div.item + flex: 1 0 2.25em + display: flex + padding: 0 0.5em + pointer-events: inherit + + &.selected > div.default + color: white + background-color: $PARENT + + &:hover > div.default + color: white + background-color: $ACTIVE + + > div.shortcut + opacity: inherit + + &.has-children > div.default > svg.children-icon + display: flex + + &:not(.selectable) + color: var(--color-dark) + pointer-events: none + + > div.default + width: 100% + display: flex + align-items: center + border-radius: 0.25em + padding: 0 0.5em 0 0.25em + white-space: nowrap + + &.checked > svg.check-icon + visibility: visible + + > svg.check-icon, svg.children-icon + pointer-events: none + fill: none + stroke: currentColor + stroke-width: 1.5 + stroke-linecap: round + width: 1em + height: 1em + + > svg.check-icon + visibility: hidden + + > svg.children-icon + display: none + + > div.label + pointer-events: none + flex: 1 + margin: 0 3em 0 0.25em + + > div.shortcut + display: flex + opacity: 0.3 + pointer-events: none + margin: 0 + justify-content: flex-end + + > span + flex: 0 0 1em + text-align: center + + > hr + margin: 0.375em 0.75em + width: calc(100% - 1.5em) + height: 0 + border: 0 + border-top: 1px solid rgba(white, 0.07) + pointer-events: none + + > div.scroll + display: none + align-items: center + justify-content: center + flex: 1 0 1.5em + width: 100% + color: var(--color-gray) + + &:hover + color: var(--color-bright) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/Menu.tsx b/packages/app/studio/src/ui/components/Menu.tsx new file mode 100644 index 00000000..f823692e --- /dev/null +++ b/packages/app/studio/src/ui/components/Menu.tsx @@ -0,0 +1,271 @@ +import css from "./Menu.sass?inline" +import {DefaultMenuData, MenuItem} from "@/ui/model/menu-item.ts" +import {createElement, Frag} from "@opendaw/lib-jsx" +import {int, isDefined, Lifecycle, Nullable, Option, panic, Terminable, Terminator} from "@opendaw/lib-std" +import {Icon} from "@/ui/components/Icon.tsx" +import {Surface} from "@/ui/surface/Surface.tsx" +import {IconSymbol} from "@opendaw/studio-adapters" +import {AnimationFrame, Events, Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "menu") + +export const DefaultMenuDataElement = ({data}: { data: DefaultMenuData }) => ( +
+ + + + {data.icon && } +
{data.label}
+
{[...data.shortcut ?? []].map(s => {s})}
+ + + +
+) + +type MenuHtmlStructure = { + element: HTMLElement + scrollUp: HTMLElement + container: HTMLElement + scrollDown: HTMLElement +} + +export class Menu implements Terminable, Lifecycle { + static create(item: MenuItem, groupId?: string): Menu {return new Menu(Option.None, item, groupId ?? "")} + + static Padding = 4 // this is the invisible increase of the hitarea to have seamless connection to the source + static MIN_TIME_MS = 250 // if the menu is placed under the pointer, we avoid an accidental click + + readonly #terminator: Terminator + readonly #parent: Option + readonly #item: MenuItem + readonly #groupId: string + + readonly #element: HTMLElement + readonly #scrollUp: HTMLElement + readonly #container: HTMLElement + readonly #scrollDown: HTMLElement + readonly #openTime: number + + #childMenu: Option = Option.None + + #x: int = 0 + #y: int = 0 + + private constructor(parent: Option, item: MenuItem, groupId: string) { + this.#terminator = new Terminator() + + this.#parent = parent + this.#item = item + this.#groupId = groupId + + const {element, scrollUp, container, scrollDown} = this.#createHtml() + this.#element = element + this.#scrollUp = scrollUp + this.#container = container + this.#scrollDown = scrollDown + this.#openTime = Date.now() + + this.#element.onblur = (event) => { + const related = event.relatedTarget + if (related === null) { + // lost focus + this.root.terminate() + } else if (related instanceof HTMLElement && !isDefined(related.getAttribute("data-close-on-blur"))) { + // an unrelated element has been focussed + this.root.terminate() + } + } + } + + own(terminable: T): T {return this.#terminator.own(terminable)} + ownAll(...terminables: Array): void {this.#terminator.ownAll(...terminables)} + spawn(): Terminator {return this.#terminator.spawn()} + + get root(): Menu {return this.#parent.isEmpty() ? this : this.#parent.unwrap().root} + get element(): HTMLElement {return this.#element} + + moveTo(x: int, y: int): void { + this.#x = x | 0 + this.#y = y | 0 + this.#element.style.transform = `translate(${this.#x}px, ${this.#y}px)` + } + + attach(parentElement: Element): void { + parentElement.appendChild(this.#element) + const {right, bottom, width, height} = this.#element.getBoundingClientRect() + const owner = Surface.get(parentElement).owner + const innerWidth = owner.innerWidth + const innerHeight = owner.innerHeight + const offset = parseFloat(getComputedStyle(this.#element).fontSize) / 4 + if (right > innerWidth) { + this.#parent.match({ + none: () => this.moveTo(innerWidth - width - offset, this.#y), + some: () => this.moveTo(offset - width, this.#y) + }) + } + if (height > innerHeight) { + this.#setupScrolling() + this.moveTo(this.#x, -parentElement.getBoundingClientRect().top) + } else if (bottom > innerHeight) { + this.moveTo(this.#x, this.#y - bottom + innerHeight) + } + AnimationFrame.once(() => this.#element.focus()) + } + + terminate(): void { + this.#childMenu.ifSome(menu => menu.terminate()) + this.#childMenu = Option.None + this.#changeSelected() + this.#element.onblur = null + this.#element.remove() + this.#item.removeRuntimeChildren() + this.#terminator.terminate() + } + + #onPointerEnter(item: MenuItem, itemElement: HTMLElement): void { + this.#changeSelected(itemElement) + if (this.#childMenu.nonEmpty()) { + if (item.hasChildren && this.#childMenu.unwrap().#item === item) { + // no need to remove and recreate the same pull-down + return + } + this.#closeChildMenu() + } + if (item.hasChildren) { + const itemRect = itemElement.getBoundingClientRect() + const elementRect = this.#element.getBoundingClientRect() + const childMenu = new Menu(Option.wrap(this), item, this.#groupId) + const em = parseFloat(getComputedStyle(this.#element).fontSize) + childMenu.moveTo(elementRect.width - em / 4, (itemRect.top - elementRect.top) - em) + childMenu.attach(this.#element) + this.#childMenu = Option.wrap(childMenu) + } + } + + #onPointerLeave(_item: MenuItem, itemElement: HTMLElement, event: PointerEvent): void { + if (this.#isChild(event.relatedTarget as Node)) {return} + itemElement.classList.remove("selected") + this.#closeChildMenu() + } + + #onPointerUp(item: MenuItem, _itemElement: HTMLElement, event: PointerEvent): void { + event.preventDefault() + if (this.#childMenu.isEmpty() && this.#openTime + 100 < Date.now()) { + this.root.terminate() + item.trigger() + } + } + + #changeSelected(element: Nullable = null) { + this.#element.querySelector(".selected")?.classList.remove("selected") + element?.classList.add("selected") + } + + #isChild(node: Nullable): boolean { + if (this.#childMenu.isEmpty()) { + return false + } + const childMenu = this.#childMenu.unwrap() + let target: Nullable = node + while (null !== target) { + if (target === this.#element) {return false} + if (target === childMenu.#element) {return true} + target = target.parentNode + } + return false + } + + #closeChildMenu(): void { + if (this.#childMenu.isEmpty()) {return} + this.#element.focus() + this.#childMenu.unwrap().terminate() + this.#childMenu = Option.None + } + + #createHtml(): MenuHtmlStructure { + const scrollUp =
+ const scrollDown =
+ const container =
+ { + this.#item.collectChildren() + .filter((item: MenuItem) => !item.hidden) + .map((item: MenuItem) => { + item.open() + const hasChildren = item.hasChildren + const selectable = item.selectable + const itemElement: HTMLElement = ( +
+ {(() => { + if (item.data === undefined) { + return panic("") + } else if (item.data.type === "default") { + return + } + })()} +
+ ) + if (selectable) { + itemElement.classList.add("selectable") + } + if (hasChildren) { + itemElement.classList.add("has-children") + } + const now = Date.now() + itemElement.onpointerenter = () => this.#onPointerEnter(item, itemElement) + itemElement.onpointerleave = (event: PointerEvent) => this.#onPointerLeave(item, itemElement, event) + itemElement.onpointerup = (event: PointerEvent) => { + if (Date.now() - now < Menu.MIN_TIME_MS) {return} + this.#onPointerUp(item, itemElement, event) + } + return ( + + {item.separatorBefore &&
} + {itemElement} +
+ ) + }) + } +
+ const element = ( + + ) + return {element, scrollUp, container, scrollDown} + } + + #setupScrolling(): void { + const scroll = (direction: int) => this.#container.scrollTop += direction * this.#computeEmInPixels() / 3 + this.element.classList.add("overflowing") + this.#terminator.own(Events.subscribe(this.element, "wheel", (event: WheelEvent) => { + event.preventDefault() + scroll(Math.sign(event.deltaY) * this.#computeEmInPixels() * 1.5) + }, {passive: false})) + const setup = (button: HTMLElement, direction: number) => { + const scrolling = new Terminator() + button.onpointerenter = () => { + if (!this.#canScroll(direction)) {return} + scrolling.own(AnimationFrame.add(() => { + if (this.#canScroll(direction)) { + scroll(direction) + } else { + scrolling.terminate() + } + })) + button.onpointerleave = () => scrolling.terminate() + } + } + setup(this.#scrollUp, -1) + setup(this.#scrollDown, 1) + } + + #canScroll(direction: number): boolean { + return (0 > direction && this.#container.scrollTop > 0) + || (0 < direction && this.#container.scrollTop < this.#container.scrollHeight - this.#container.clientHeight) + } + + #computeEmInPixels(): number {return parseInt(getComputedStyle(this.#element).fontSize)} +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/MenuButton.sass b/packages/app/studio/src/ui/components/MenuButton.sass new file mode 100644 index 00000000..74443aed --- /dev/null +++ b/packages/app/studio/src/ui/components/MenuButton.sass @@ -0,0 +1,60 @@ +component + --color-active: var(--color) + + appearance: none + margin: 0 + padding: 0 + min-width: 1.5em + flex: 0 0 auto + display: flex + place-items: center + place-content: center + outline: none + border: none + background: none + color: var(--color) + font: inherit + pointer-events: all + + > * + pointer-events: none + + .disabled & + opacity: 0.3 + pointer-events: none + + &.stretch + flex: 1 0 auto + + &.pointer + cursor: pointer + + &.tiny-triangle:after + content: "▾" + font-size: 0.5em + position: relative + top: 0.75em + left: 0.125em + + > label + display: flex + align-items: center + justify-content: space-between + font-size: 0.75em + column-gap: 0.25em + + &.framed + border-radius: 0.25em + border: 0.5px solid var(--color) + box-shadow: 0 0 3px -1px var(--color) + min-height: 1.5em + + > label + padding: 0 0.25em 0 0.5em + + &.active + color: var(--color-active) + + &.framed + border: 0.5px solid var(--color-active) + box-shadow: 0 0 3px -1px var(--color-active) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/MenuButton.tsx b/packages/app/studio/src/ui/components/MenuButton.tsx new file mode 100644 index 00000000..f409d7a9 --- /dev/null +++ b/packages/app/studio/src/ui/components/MenuButton.tsx @@ -0,0 +1,79 @@ +import css from "./MenuButton.sass?inline" +import {createElement, JsxValue} from "@opendaw/lib-jsx" +import {MenuItem} from "@/ui/model/menu-item.ts" +import {Menu} from "@/ui/components/Menu.tsx" +import {isDefined, Option} from "@opendaw/lib-std" +import {Surface} from "@/ui/surface/Surface.tsx" +import {Html} from "@opendaw/lib-dom" + +const className = Html.adoptStyleSheet(css, "MenuButton") + +type Appearance = { + color?: string + activeColor?: string + framed?: boolean + tinyTriangle?: boolean + tooltip?: string +} + +type Construct = { + root: MenuItem + style?: Partial + appearance?: Appearance + horizontal?: "left" | "right" + stretch?: boolean + pointer?: boolean + groupId?: string +} + +export const MenuButton = + ({root, style, appearance, horizontal, stretch, pointer, groupId}: Construct, children: JsxValue) => { + let current: Option = Option.None + const button: HTMLButtonElement = ( + + ) + if (isDefined(appearance?.color)) { + button.style.setProperty("--color", appearance.color) + } + if (isDefined(appearance?.activeColor)) { + button.style.setProperty("--color-active", appearance.activeColor) + } + if (isDefined(style)) { + Object.assign(button.style, style) + } + const toggle = () => { + current = current.match({ + none: () => { + button.classList.add("active") + const rect = button.getBoundingClientRect() + const menu = Menu.create(root, groupId) + menu.moveTo(rect[horizontal ?? "left"], rect.bottom + Menu.Padding) + menu.attach(Surface.get(button).flyout) + menu.own({terminate: toggle}) + return Option.wrap(menu) + }, + some: menu => { + button.classList.remove("active") + menu.terminate() + return Option.None + } + }) + } + return button + } \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/NumberInput.sass b/packages/app/studio/src/ui/components/NumberInput.sass new file mode 100644 index 00000000..065df549 --- /dev/null +++ b/packages/app/studio/src/ui/components/NumberInput.sass @@ -0,0 +1,4 @@ +@use "@/mixins" + +component + @include mixins.Input \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/NumberInput.tsx b/packages/app/studio/src/ui/components/NumberInput.tsx new file mode 100644 index 00000000..68381e37 --- /dev/null +++ b/packages/app/studio/src/ui/components/NumberInput.tsx @@ -0,0 +1,121 @@ +import css from "./NumberInput.sass?inline" +import {Events, Html} from "@opendaw/lib-dom" +import {int, isDefined, isInstanceOf, Lifecycle, MutableObservableValue, StringMapping} from "@opendaw/lib-std" +import {createElement} from "@opendaw/lib-jsx" + +const defaultClassName = Html.adoptStyleSheet(css, "NumberInput") + +type Construct = { + lifecycle: Lifecycle + model: MutableObservableValue + mapper?: StringMapping + negativeWarning?: boolean + className?: string + maxChars?: int + step?: number +} + +export const NumberInput = ({lifecycle, model, negativeWarning, className, maxChars, mapper, step}: Construct) => { + step ??= 1.0 + maxChars ??= 3 + mapper ??= StringMapping.numeric({}) + const input: HTMLElement = (
) + const element: HTMLElement = ( +
+ {input} +
+ ) + const updateDigits = () => { + const value = model.getValue() + element.classList.toggle("negative", negativeWarning === true && value < 0) + input.textContent = mapper.x(value).value + } + lifecycle.ownAll( + model.subscribe(updateDigits), + Events.subscribe(element, "focusin", (event: Event) => { + if (!isInstanceOf(event.target, HTMLElement)) {return} + Html.selectContent(event.target) + }), + Events.subscribe(element, "focusout", (event: Event) => { + if (!isInstanceOf(event.target, HTMLElement)) {return} + updateDigits() + Html.unselectContent(event.target) + }), + Events.subscribe(element, "copy", (event: ClipboardEvent) => { + event.preventDefault() + event.clipboardData?.setData("application/json", JSON.stringify({ + app: "openDAW", + content: "number", + value: model.getValue() + })) + }), + Events.subscribe(element, "paste", (event: ClipboardEvent) => { + const data = event.clipboardData?.getData("application/json") + if (isDefined(data)) { + const json = JSON.parse(data) + if (json.app === "openDAW" && json.content === "number") { + event.preventDefault() + model.setValue(json.value) + } + } + }), + Events.subscribe(element, "keydown", (event: KeyboardEvent) => { + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {return} + const target = event.target + if (!isInstanceOf(target, HTMLElement)) {return} + switch (event.code) { + case "ArrowUp": { + event.preventDefault() + const result = mapper.y(target.textContent ?? "") + if (result.type !== "explicit") {return} + const nextValue: int = result.value + model.setValue(nextValue + step) + Html.selectContent(target) + break + } + case "ArrowDown": { + event.preventDefault() + const result = mapper.y(target.textContent ?? "") + if (result.type !== "explicit") {return} + const nextValue: int = result.value + model.setValue(nextValue - step) + Html.selectContent(target) + break + } + case "Enter": { + event.preventDefault() + const result = mapper.y(target.textContent ?? "") + if (result.type !== "explicit") {return} + const nextValue: int = result.value + model.setValue(nextValue) + updateDigits() + Html.selectContent(target) + break + } + case "Digit1": + case "Digit2": + case "Digit3": + case "Digit4": + case "Digit5": + case "Digit6": + case "Digit7": + case "Digit8": + case "Digit9": + case "Digit0": + case "Tab": + case "ArrowLeft": + case "ArrowRight": + case "Minus": + case "Backspace": { + break // Allow + } + default: { + console.debug("ignore", event.code) + event.preventDefault() + } + } + }) + ) + updateDigits() + return element +} \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/ParameterLabel.sass b/packages/app/studio/src/ui/components/ParameterLabel.sass new file mode 100644 index 00000000..c2755426 --- /dev/null +++ b/packages/app/studio/src/ui/components/ParameterLabel.sass @@ -0,0 +1,40 @@ +@use "@/colors" + +component + padding: 0 0.5em + white-space: nowrap + max-width: 100% + color: var(--color-gray) + position: relative + text-align: right + font-size: 8px + overflow: hidden + + &.midi, .midi &, &.automated, .automated & + min-width: 4em + border-radius: 0.125em + background-color: rgba(white, 0.02) + + &.midi, .midi & + box-shadow: 0 0 1px 0 var(--color-orange) + + &.automated, .automated & + box-shadow: 0 0 1px 0 var(--color-blue) + + &:after + content: attr(unit) + color: var(--color-dark) + margin-left: 0.25em + + &.modifying, .modifying & + color: var(--color-blue) + + &.framed + min-width: 44px + border-radius: 0.25em + text-align: right + line-height: 18px + + &.modifying, .modifying & + box-shadow: 0 0 0 0.5px rgba(white, 0.2) + background-color: var(--color-black) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/ParameterLabel.tsx b/packages/app/studio/src/ui/components/ParameterLabel.tsx new file mode 100644 index 00000000..8d7fa43e --- /dev/null +++ b/packages/app/studio/src/ui/components/ParameterLabel.tsx @@ -0,0 +1,45 @@ +import css from "./ParameterLabel.sass?inline" +import {ControlSource, Lifecycle, Terminable} from "@opendaw/lib-std" +import {createElement} from "@opendaw/lib-jsx" +import {attachParameterContextMenu} from "@/ui/menu/automation.ts" +import {AutomatableParameterFieldAdapter, DeviceBoxAdapter} from "@opendaw/studio-adapters" +import {Editing} from "@opendaw/lib-box" +import {Html} from "@opendaw/lib-dom" +import {MIDILearning} from "@/midi/devices/MIDILearning" + +const className = Html.adoptStyleSheet(css, "ParameterLabel") + +type Construct = { + lifecycle: Lifecycle + editing: Editing + midiLearning: MIDILearning + adapter: DeviceBoxAdapter + parameter: AutomatableParameterFieldAdapter + framed?: boolean + standalone?: boolean +} + +export const ParameterLabel = ( + {lifecycle, editing, midiLearning, adapter, parameter, framed, standalone}: Construct): HTMLLabelElement => { + const element: HTMLLabelElement = ( +