diff --git a/.agents/skills/svelte-code-writer/SKILL.md b/.agents/skills/svelte-code-writer/SKILL.md new file mode 100644 index 0000000..b50ccf9 --- /dev/null +++ b/.agents/skills/svelte-code-writer/SKILL.md @@ -0,0 +1,66 @@ +--- +name: svelte-code-writer +description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results. +--- + +# Svelte 5 Code Writer + +## CLI Tools + +You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`: + +### List Documentation Sections + +```bash +npx @sveltejs/mcp list-sections +``` + +Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. + +### Get Documentation + +```bash +npx @sveltejs/mcp get-documentation ",,..." +``` + +Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs. + +**Example:** + +```bash +npx @sveltejs/mcp get-documentation "$state,$derived,$effect" +``` + +### Svelte Autofixer + +```bash +npx @sveltejs/mcp svelte-autofixer "" [options] +``` + +Analyzes Svelte code and suggests fixes for common issues. + +**Options:** + +- `--async` - Enable async Svelte mode (default: false) +- `--svelte-version` - Target version: 4 or 5 (default: 5) + +**Examples:** + +```bash +# Analyze inline code (escape $ as \$) +npx @sveltejs/mcp svelte-autofixer '' + +# Analyze a file +npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte + +# Target Svelte 4 +npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4 +``` + +**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution. + +## Workflow + +1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics +2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues +3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component diff --git a/.agents/skills/svelte-core-bestpractices/SKILL.md b/.agents/skills/svelte-core-bestpractices/SKILL.md new file mode 100644 index 0000000..ffa73ee --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/SKILL.md @@ -0,0 +1,176 @@ +--- +name: svelte-core-bestpractices +description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more. +--- + +## `$state` + +Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable. + +Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example. + +## `$derived` + +To compute something from state, use `$derived` rather than `$effect`: + +```js +// do this +let square = $derived(num * num); + +// don't do this +let square; + +$effect(() => { + square = num * num; +}); +``` + +> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`. + +Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes. + +If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this. + +## `$effect` + +Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects. + +- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md) +- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate +- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md) +- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md) + +Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server. + +## `$props` + +Treat props as though they will change. For example, values that depend on props should usually use `$derived`: + +```js +// @errors: 2451 +let { type } = $props(); + +// do this +let color = $derived(type === 'danger' ? 'red' : 'green'); + +// don't do this — `color` will not update if `type` changes +let color = type === 'danger' ? 'red' : 'green'; +``` + +## `$inspect.trace` + +`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. + +## Events + +Any element attribute starting with `on` is treated as an event listener: + +```svelte + + + + + + + +``` + +If you need to attach listeners to `window` or `document` you can use `` and ``: + +```svelte + + +``` + +Avoid using `onMount` or `$effect` for this. + +## Snippets + +[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template. + +```svelte +{#snippet greeting(name)} +

hello {name}!

+{/snippet} + +{@render greeting('world')} +``` + +> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside ` + + + +``` + +On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations). + +## $inspect(...).with + +`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo: + +```svelte + + + +``` + +## $inspect.trace(...) + +This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire. + +```svelte + +``` + +`$inspect.trace` takes an optional first argument which will be used as the label. diff --git a/.agents/skills/svelte-core-bestpractices/references/@attach.md b/.agents/skills/svelte-core-bestpractices/references/@attach.md new file mode 100644 index 0000000..5c113b1 --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/@attach.md @@ -0,0 +1,166 @@ +Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates. + +Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM. + +> [!NOTE] +> Attachments are available in Svelte 5.29 and newer. + +```svelte + + + +
...
+``` + +An element can have any number of attachments. + +## Attachment factories + +A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo: + +```svelte + + + + + + +``` + +Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).) + +## Inline attachments + +Attachments can also be created inline (demo: + +```svelte + + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> +``` + +> [!NOTE] +> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. + +## Conditional attachments + +Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage: + +```svelte +
...
+``` + +## Passing attachments to components + +When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. + +This allows you to create _wrapper components_ that augment elements (demo: + +```svelte + + + + + +``` + +```svelte + + + + + + +``` + +## Controlling when attachments re-run + +Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`): + +```js +// @errors: 7006 2304 2552 +function foo(bar) { + return (node) => { + veryExpensiveSetupWork(node); + update(node, bar); + }; +} +``` + +In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect: + +```js +// @errors: 7006 2304 2552 +function foo(+++getBar+++) { + return (node) => { + veryExpensiveSetupWork(node); + ++++ $effect(() => { + update(node, getBar()); + });+++ + } +} +``` + +## Creating attachments programmatically + +To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). + +## Converting actions to attachments + +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components. diff --git a/.agents/skills/svelte-core-bestpractices/references/@render.md b/.agents/skills/svelte-core-bestpractices/references/@render.md new file mode 100644 index 0000000..2e60685 --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/@render.md @@ -0,0 +1,35 @@ +To render a [snippet](snippet), use a `{@render ...}` tag. + +```svelte +{#snippet sum(a, b)} +

{a} + {b} = {a + b}

+{/snippet} + +{@render sum(1, 2)} +{@render sum(3, 4)} +{@render sum(5, 6)} +``` + +The expression can be an identifier like `sum`, or an arbitrary JavaScript expression: + +```svelte +{@render (cool ? coolSnippet : lameSnippet)()} +``` + +## Optional snippets + +If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined: + +```svelte +{@render children?.()} +``` + +Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content: + +```svelte +{#if children} + {@render children()} +{:else} +

fallback content

+{/if} +``` diff --git a/.agents/skills/svelte-core-bestpractices/references/await-expressions.md b/.agents/skills/svelte-core-bestpractices/references/await-expressions.md new file mode 100644 index 0000000..18c2231 --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/await-expressions.md @@ -0,0 +1,180 @@ +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

{a} + {b} = {await add(a, b)}

+``` + +...if you increment `a`, the contents of the `

` will _not_ immediately update to read this — + +```html +

2 + 2 = 3

+``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

{await one()}

{await two()}

+``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + +{#if open} + + (open = false)} /> +{/if} +``` + +## Caveats + +As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum. + +## Breaking changes + +Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in very rare situations. diff --git a/.agents/skills/svelte-core-bestpractices/references/bind.md b/.agents/skills/svelte-core-bestpractices/references/bind.md new file mode 100644 index 0000000..80b2f4c --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/bind.md @@ -0,0 +1,16 @@ +## Function bindings + +You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation: + +```svelte + value, (v) => (value = v.toLowerCase())} /> +``` + +In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`: + +```svelte +
...
+``` + +> [!NOTE] +> Function bindings are available in Svelte 5.9.0 and newer. diff --git a/.agents/skills/svelte-core-bestpractices/references/each.md b/.agents/skills/svelte-core-bestpractices/references/each.md new file mode 100644 index 0000000..283b754 --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/each.md @@ -0,0 +1,42 @@ +## Keyed each blocks + +```svelte + +{#each expression as name (key)}...{/each} +``` + +```svelte + +{#each expression as name, index (key)}...{/each} +``` + +If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle. + +The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. + +```svelte +{#each items as item (item.id)} +
  • {item.name} x {item.qty}
  • +{/each} + + +{#each items as item, i (item.id)} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} +``` + +You can freely use destructuring and rest patterns in each blocks. + +```svelte +{#each items as { id, name, qty }, i (id)} +
  • {i + 1}: {name} x {qty}
  • +{/each} + +{#each objects as { id, ...rest }} +
  • {id}
  • +{/each} + +{#each items as [id, ...rest]} +
  • {id}
  • +{/each} +``` diff --git a/.agents/skills/svelte-core-bestpractices/references/hydratable.md b/.agents/skills/svelte-core-bestpractices/references/hydratable.md new file mode 100644 index 0000000..a7baf74 --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/hydratable.md @@ -0,0 +1,100 @@ +In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes: + +```svelte + + +

    {user.name}

    +``` + +That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions). + +To fix the example above: + +```svelte + + +

    {user.name}

    +``` + +This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration: + +```ts +import { hydratable } from 'svelte'; +const rand = hydratable('random', () => Math.random()); +``` + +If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries. + +## Serialization + +All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises: + +```svelte + + +{await promises.one} +{await promises.two} +``` + +## CSP + +`hydratable` adds an inline ` + +{#snippet hello(name)} +

    hello {name}! {message}!

    +{/snippet} + +{@render hello('alice')} +{@render hello('bob')} +``` + +...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): + +```svelte +
    + {#snippet x()} + {#snippet y()}...{/snippet} + + + {@render y()} + {/snippet} + + + {@render y()} +
    + + +{@render x()} +``` + +Snippets can reference themselves and each other (demo: + +```svelte +{#snippet blastoff()} + 🚀 +{/snippet} + +{#snippet countdown(n)} + {#if n > 0} + {n}... + {@render countdown(n - 1)} + {:else} + {@render blastoff()} + {/if} +{/snippet} + +{@render countdown(10)} +``` + +## Passing snippets to components + +### Explicit props + +Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo: + +```svelte + + +{#snippet header()} + fruit + qty + price + total +{/snippet} + +{#snippet row(d)} + {d.name} + {d.qty} + {d.price} + {d.qty * d.price} +{/snippet} + + +``` + +Think about it like passing content instead of data to a component. The concept is similar to slots in web components. + +### Implicit props + +As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo: + +```svelte + +
    + {#snippet header()} + + + + + {/snippet} + + {#snippet row(d)} + + + + + {/snippet} +
    fruitqtypricetotal{d.name}{d.qty}{d.price}{d.qty * d.price}
    +``` + +### Implicit `children` snippet + +Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo: + +```svelte + + +``` + +```svelte + + + + + +``` + +> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name + +### Optional snippet props + +You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set... + +```svelte + + +{@render children?.()} +``` + +...or use an `#if` block to render fallback content: + +```svelte + + +{#if children} + {@render children()} +{:else} + fallback content +{/if} +``` + +## Typing snippets + +Snippets implement the `Snippet` interface imported from `'svelte'`: + +```svelte + +``` + +With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. + +We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: + +```svelte + +``` + +## Exporting snippets + +Snippets declared at the top level of a `.svelte` file can be exported from a ` + +{#snippet add(a, b)} + {a} + {b} = {a + b} +{/snippet} +``` + +> [!NOTE] +> This requires Svelte 5.5.0 or newer + +## Programmatic snippets + +Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases. + +## Snippets and slots + +In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5. diff --git a/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md b/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md new file mode 100644 index 0000000..262e361 --- /dev/null +++ b/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md @@ -0,0 +1,61 @@ +## createSubscriber + +
    + +Available since 5.7.0 + +
    + +Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. +It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. + +If `subscribe` is called inside an effect (including indirectly, for example inside a getter), +the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. + +If `start` returns a cleanup function, it will be called when the effect is destroyed. + +If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects +are active, and the returned teardown function will only be called when all effects are destroyed. + +It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery): + +```js +// @errors: 7031 +import { createSubscriber } from 'svelte/reactivity'; +import { on } from 'svelte/events'; + +export class MediaQuery { + #query; + #subscribe; + + constructor(query) { + this.#query = window.matchMedia(`(${query})`); + + this.#subscribe = createSubscriber((update) => { + // when the `change` event occurs, re-run any effects that read `this.current` + const off = on(this.#query, 'change', update); + + // stop listening when all the effects are destroyed + return () => off(); + }); + } + + get current() { + // This makes the getter reactive, if read in an effect + this.#subscribe(); + + // Return the current state of the query, whether or not we're in an effect + return this.#query.matches; + } +} +``` + +
    + +```dts +function createSubscriber( + start: (update: () => void) => (() => void) | void +): () => void; +``` + +
    diff --git a/.claude/skills/svelte-code-writer/SKILL.md b/.claude/skills/svelte-code-writer/SKILL.md new file mode 100644 index 0000000..b50ccf9 --- /dev/null +++ b/.claude/skills/svelte-code-writer/SKILL.md @@ -0,0 +1,66 @@ +--- +name: svelte-code-writer +description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results. +--- + +# Svelte 5 Code Writer + +## CLI Tools + +You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`: + +### List Documentation Sections + +```bash +npx @sveltejs/mcp list-sections +``` + +Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. + +### Get Documentation + +```bash +npx @sveltejs/mcp get-documentation ",,..." +``` + +Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs. + +**Example:** + +```bash +npx @sveltejs/mcp get-documentation "$state,$derived,$effect" +``` + +### Svelte Autofixer + +```bash +npx @sveltejs/mcp svelte-autofixer "" [options] +``` + +Analyzes Svelte code and suggests fixes for common issues. + +**Options:** + +- `--async` - Enable async Svelte mode (default: false) +- `--svelte-version` - Target version: 4 or 5 (default: 5) + +**Examples:** + +```bash +# Analyze inline code (escape $ as \$) +npx @sveltejs/mcp svelte-autofixer '' + +# Analyze a file +npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte + +# Target Svelte 4 +npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4 +``` + +**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution. + +## Workflow + +1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics +2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues +3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component diff --git a/.claude/skills/svelte-core-bestpractices/SKILL.md b/.claude/skills/svelte-core-bestpractices/SKILL.md new file mode 100644 index 0000000..ffa73ee --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/SKILL.md @@ -0,0 +1,176 @@ +--- +name: svelte-core-bestpractices +description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more. +--- + +## `$state` + +Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable. + +Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example. + +## `$derived` + +To compute something from state, use `$derived` rather than `$effect`: + +```js +// do this +let square = $derived(num * num); + +// don't do this +let square; + +$effect(() => { + square = num * num; +}); +``` + +> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`. + +Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes. + +If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this. + +## `$effect` + +Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects. + +- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md) +- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate +- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md) +- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md) + +Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server. + +## `$props` + +Treat props as though they will change. For example, values that depend on props should usually use `$derived`: + +```js +// @errors: 2451 +let { type } = $props(); + +// do this +let color = $derived(type === 'danger' ? 'red' : 'green'); + +// don't do this — `color` will not update if `type` changes +let color = type === 'danger' ? 'red' : 'green'; +``` + +## `$inspect.trace` + +`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. + +## Events + +Any element attribute starting with `on` is treated as an event listener: + +```svelte + + + + + + + +``` + +If you need to attach listeners to `window` or `document` you can use `` and ``: + +```svelte + + +``` + +Avoid using `onMount` or `$effect` for this. + +## Snippets + +[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template. + +```svelte +{#snippet greeting(name)} +

    hello {name}!

    +{/snippet} + +{@render greeting('world')} +``` + +> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside ` + + + +``` + +On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations). + +## $inspect(...).with + +`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo: + +```svelte + + + +``` + +## $inspect.trace(...) + +This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire. + +```svelte + +``` + +`$inspect.trace` takes an optional first argument which will be used as the label. diff --git a/.claude/skills/svelte-core-bestpractices/references/@attach.md b/.claude/skills/svelte-core-bestpractices/references/@attach.md new file mode 100644 index 0000000..5c113b1 --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/@attach.md @@ -0,0 +1,166 @@ +Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates. + +Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM. + +> [!NOTE] +> Attachments are available in Svelte 5.29 and newer. + +```svelte + + + +
    ...
    +``` + +An element can have any number of attachments. + +## Attachment factories + +A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo: + +```svelte + + + + + + +``` + +Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).) + +## Inline attachments + +Attachments can also be created inline (demo: + +```svelte + + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> +``` + +> [!NOTE] +> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. + +## Conditional attachments + +Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage: + +```svelte +
    ...
    +``` + +## Passing attachments to components + +When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. + +This allows you to create _wrapper components_ that augment elements (demo: + +```svelte + + + + + +``` + +```svelte + + + + + + +``` + +## Controlling when attachments re-run + +Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`): + +```js +// @errors: 7006 2304 2552 +function foo(bar) { + return (node) => { + veryExpensiveSetupWork(node); + update(node, bar); + }; +} +``` + +In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect: + +```js +// @errors: 7006 2304 2552 +function foo(+++getBar+++) { + return (node) => { + veryExpensiveSetupWork(node); + ++++ $effect(() => { + update(node, getBar()); + });+++ + } +} +``` + +## Creating attachments programmatically + +To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). + +## Converting actions to attachments + +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components. diff --git a/.claude/skills/svelte-core-bestpractices/references/@render.md b/.claude/skills/svelte-core-bestpractices/references/@render.md new file mode 100644 index 0000000..2e60685 --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/@render.md @@ -0,0 +1,35 @@ +To render a [snippet](snippet), use a `{@render ...}` tag. + +```svelte +{#snippet sum(a, b)} +

    {a} + {b} = {a + b}

    +{/snippet} + +{@render sum(1, 2)} +{@render sum(3, 4)} +{@render sum(5, 6)} +``` + +The expression can be an identifier like `sum`, or an arbitrary JavaScript expression: + +```svelte +{@render (cool ? coolSnippet : lameSnippet)()} +``` + +## Optional snippets + +If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined: + +```svelte +{@render children?.()} +``` + +Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content: + +```svelte +{#if children} + {@render children()} +{:else} +

    fallback content

    +{/if} +``` diff --git a/.claude/skills/svelte-core-bestpractices/references/await-expressions.md b/.claude/skills/svelte-core-bestpractices/references/await-expressions.md new file mode 100644 index 0000000..18c2231 --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/await-expressions.md @@ -0,0 +1,180 @@ +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

    {a} + {b} = {await add(a, b)}

    +``` + +...if you increment `a`, the contents of the `

    ` will _not_ immediately update to read this — + +```html +

    2 + 2 = 3

    +``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

    {await one()}

    {await two()}

    +``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + +{#if open} + + (open = false)} /> +{/if} +``` + +## Caveats + +As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum. + +## Breaking changes + +Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in very rare situations. diff --git a/.claude/skills/svelte-core-bestpractices/references/bind.md b/.claude/skills/svelte-core-bestpractices/references/bind.md new file mode 100644 index 0000000..80b2f4c --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/bind.md @@ -0,0 +1,16 @@ +## Function bindings + +You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation: + +```svelte + value, (v) => (value = v.toLowerCase())} /> +``` + +In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`: + +```svelte +
    ...
    +``` + +> [!NOTE] +> Function bindings are available in Svelte 5.9.0 and newer. diff --git a/.claude/skills/svelte-core-bestpractices/references/each.md b/.claude/skills/svelte-core-bestpractices/references/each.md new file mode 100644 index 0000000..283b754 --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/each.md @@ -0,0 +1,42 @@ +## Keyed each blocks + +```svelte + +{#each expression as name (key)}...{/each} +``` + +```svelte + +{#each expression as name, index (key)}...{/each} +``` + +If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle. + +The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. + +```svelte +{#each items as item (item.id)} +
  • {item.name} x {item.qty}
  • +{/each} + + +{#each items as item, i (item.id)} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} +``` + +You can freely use destructuring and rest patterns in each blocks. + +```svelte +{#each items as { id, name, qty }, i (id)} +
  • {i + 1}: {name} x {qty}
  • +{/each} + +{#each objects as { id, ...rest }} +
  • {id}
  • +{/each} + +{#each items as [id, ...rest]} +
  • {id}
  • +{/each} +``` diff --git a/.claude/skills/svelte-core-bestpractices/references/hydratable.md b/.claude/skills/svelte-core-bestpractices/references/hydratable.md new file mode 100644 index 0000000..a7baf74 --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/hydratable.md @@ -0,0 +1,100 @@ +In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes: + +```svelte + + +

    {user.name}

    +``` + +That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions). + +To fix the example above: + +```svelte + + +

    {user.name}

    +``` + +This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration: + +```ts +import { hydratable } from 'svelte'; +const rand = hydratable('random', () => Math.random()); +``` + +If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries. + +## Serialization + +All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises: + +```svelte + + +{await promises.one} +{await promises.two} +``` + +## CSP + +`hydratable` adds an inline ` + +{#snippet hello(name)} +

    hello {name}! {message}!

    +{/snippet} + +{@render hello('alice')} +{@render hello('bob')} +``` + +...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): + +```svelte +
    + {#snippet x()} + {#snippet y()}...{/snippet} + + + {@render y()} + {/snippet} + + + {@render y()} +
    + + +{@render x()} +``` + +Snippets can reference themselves and each other (demo: + +```svelte +{#snippet blastoff()} + 🚀 +{/snippet} + +{#snippet countdown(n)} + {#if n > 0} + {n}... + {@render countdown(n - 1)} + {:else} + {@render blastoff()} + {/if} +{/snippet} + +{@render countdown(10)} +``` + +## Passing snippets to components + +### Explicit props + +Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo: + +```svelte + + +{#snippet header()} + fruit + qty + price + total +{/snippet} + +{#snippet row(d)} + {d.name} + {d.qty} + {d.price} + {d.qty * d.price} +{/snippet} + + +``` + +Think about it like passing content instead of data to a component. The concept is similar to slots in web components. + +### Implicit props + +As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo: + +```svelte + +
    + {#snippet header()} + + + + + {/snippet} + + {#snippet row(d)} + + + + + {/snippet} +
    fruitqtypricetotal{d.name}{d.qty}{d.price}{d.qty * d.price}
    +``` + +### Implicit `children` snippet + +Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo: + +```svelte + + +``` + +```svelte + + + + + +``` + +> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name + +### Optional snippet props + +You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set... + +```svelte + + +{@render children?.()} +``` + +...or use an `#if` block to render fallback content: + +```svelte + + +{#if children} + {@render children()} +{:else} + fallback content +{/if} +``` + +## Typing snippets + +Snippets implement the `Snippet` interface imported from `'svelte'`: + +```svelte + +``` + +With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. + +We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: + +```svelte + +``` + +## Exporting snippets + +Snippets declared at the top level of a `.svelte` file can be exported from a ` + +{#snippet add(a, b)} + {a} + {b} = {a + b} +{/snippet} +``` + +> [!NOTE] +> This requires Svelte 5.5.0 or newer + +## Programmatic snippets + +Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases. + +## Snippets and slots + +In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5. diff --git a/.claude/skills/svelte-core-bestpractices/references/svelte-reactivity.md b/.claude/skills/svelte-core-bestpractices/references/svelte-reactivity.md new file mode 100644 index 0000000..262e361 --- /dev/null +++ b/.claude/skills/svelte-core-bestpractices/references/svelte-reactivity.md @@ -0,0 +1,61 @@ +## createSubscriber + +
    + +Available since 5.7.0 + +
    + +Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. +It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. + +If `subscribe` is called inside an effect (including indirectly, for example inside a getter), +the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. + +If `start` returns a cleanup function, it will be called when the effect is destroyed. + +If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects +are active, and the returned teardown function will only be called when all effects are destroyed. + +It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery): + +```js +// @errors: 7031 +import { createSubscriber } from 'svelte/reactivity'; +import { on } from 'svelte/events'; + +export class MediaQuery { + #query; + #subscribe; + + constructor(query) { + this.#query = window.matchMedia(`(${query})`); + + this.#subscribe = createSubscriber((update) => { + // when the `change` event occurs, re-run any effects that read `this.current` + const off = on(this.#query, 'change', update); + + // stop listening when all the effects are destroyed + return () => off(); + }); + } + + get current() { + // This makes the getter reactive, if read in an effect + this.#subscribe(); + + // Return the current state of the query, whether or not we're in an effect + return this.#query.matches; + } +} +``` + +
    + +```dts +function createSubscriber( + start: (update: () => void) => (() => void) | void +): () => void; +``` + +
    diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..c0626d9 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,14 @@ +[mcp_servers.svelte] +command = "npx" +args = [ + "-y", + "@sveltejs/mcp", +] + + +[mcp_servers.mdui] +command = "npx" +args = [ + "-y", + "@mdui/mcp", +] diff --git a/.gitignore b/.gitignore index 6635cf5..8ee8913 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +target \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..1d41a9f --- /dev/null +++ b/.mcp.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "svelte": { + "type": "stdio", + "command": "npx", + "env": { + + }, + "args": [ + "-y", + "@sveltejs/mcp" + ] + } + } +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c56a0f5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 4, + "arrowParens": "avoid", + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md new file mode 100644 index 0000000..76a55c7 --- /dev/null +++ b/.trae/rules/git-commit-message.md @@ -0,0 +1,4 @@ +alwaysApply: true +scene: git_message +--- +使用无序列表逐条描述具体变更,每条以 `- ` 开头,动词使用一般现在时(如 Add, Fix, Update, Merge, Improve)。 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4711cd0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available Svelte MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4711cd0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available Svelte MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..918d64c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6883 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "almost" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" + +[[package]] +name = "alsa" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.13.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" +dependencies = [ + "alsa", + "block2", + "coreaudio-rs", + "dasp_sample", + "jni 0.22.4", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", + "web-sys", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "creek" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3106d374fa70e8a7a8cf3abf6afbb7f3016e923a1e8941a3eee2b3a21583ee" +dependencies = [ + "creek-core", + "creek-decode-symphonia", + "creek-encode-wav", +] + +[[package]] +name = "creek-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8d299b2001167b9a51ac46458cbdcfafc42d6a59125a265d8c1de2e16b28a3" +dependencies = [ + "rtrb", +] + +[[package]] +name = "creek-decode-symphonia" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416063cb8f815ee27036075e544a778770b577e82348f017d145a704df90f246" +dependencies = [ + "creek-core", + "log", + "symphonia 0.5.5", +] + +[[package]] +name = "creek-encode-wav" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cb9ea02c12a68eb40a0576086394b3129b297970612592222284ff060bbf31" +dependencies = [ + "byte-slice-cast", + "creek-core", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" +dependencies = [ + "dbus", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "eq-plugin" +version = "0.1.0" +dependencies = [ + "plugin-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fft-convolver" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb4fbed063c755ecaa4cd1356cd82cc48e9d5c7edd5c30cd144656833fc0661" +dependencies = [ + "realfft", + "rtsan-standalone", + "thiserror 2.0.18", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float_eq" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "hrtf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4de47a84fd55fa33aa5ef337016814fdc869fdad23e7898b5322fa290248e6" +dependencies = [ + "byteorder", + "rubato 0.14.1", + "rustfft", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "llq" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22edcb39a6fc7e511be7ba578a719af09125391ecde44e0bd61ad3c8e003a4a3" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lofty" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca" +dependencies = [ + "byteorder", + "data-encoding", + "flate2", + "lofty_attr", + "log", + "ogg_pager", + "paste", +] + +[[package]] +name = "lofty_attr" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lyrics-plugin" +version = "0.1.0" +dependencies = [ + "lofty", + "plugin-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "my-plugin" +version = "0.1.0" +dependencies = [ + "plugin-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_denormals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bcfe410abc339c9f8c226aceebf946bf26e3e2eca738be3fec9e9ee97d54aa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "ogg_pager" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "piston-float" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad78bf43dcf80e8f950c92b84f938a0fc7590b7f6866fbcbeca781609c115590" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "plugin-sdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rtrb" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" + +[[package]] +name = "rtsan-standalone" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd1b6d61d69481a68e555916d3a52213846f5c2d2140bdc74ebc308e2338fc3" +dependencies = [ + "rtsan-standalone-macros", + "rtsan-standalone-sys", +] + +[[package]] +name = "rtsan-standalone-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8320af894782374141c8e0d4521ee613a8f24d7ab08b6ad359881311e37b9ef" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rtsan-standalone-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362a9c531f731e574a870cdb28ee7d81178ba7a87ed54380eb9b8d8f0c3b5301" +dependencies = [ + "num_cpus", + "tempfile", +] + +[[package]] +name = "rubato" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6dd52e80cfc21894deadf554a5673002938ae4625f7a283e536f9cf7c17b0d5" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rubato" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rust-echo-music" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "eq-plugin", + "libloading 0.9.0", + "lofty", + "lyrics-plugin", + "plugin-sdk", + "rand", + "serde", + "serde_json", + "souvlaki", + "sqlx", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-store", + "tokio", + "trash", + "walkdir", + "web-audio-api", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "souvlaki" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" +dependencies = [ + "base64 0.22.1", + "block", + "cocoa", + "core-graphics 0.22.3", + "dbus", + "dbus-crossroads", + "dispatch", + "objc", + "thiserror 1.0.69", + "windows 0.44.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.118", + "thiserror 2.0.18", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac 0.5.5", + "symphonia-bundle-mp3 0.5.5", + "symphonia-codec-aac 0.5.5", + "symphonia-codec-adpcm 0.5.5", + "symphonia-codec-alac 0.5.5", + "symphonia-codec-pcm 0.5.5", + "symphonia-codec-vorbis 0.5.5", + "symphonia-core 0.5.5", + "symphonia-format-isomp4 0.5.5", + "symphonia-format-mkv 0.5.5", + "symphonia-format-ogg 0.5.5", + "symphonia-format-riff 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1758d6c853020a7244de03cc3e0185eaea3f58715122422dd3cc7452e6d4c16a" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac 0.6.0", + "symphonia-bundle-mp3 0.6.0", + "symphonia-codec-aac 0.6.0", + "symphonia-codec-adpcm 0.6.0", + "symphonia-codec-alac 0.6.0", + "symphonia-codec-pcm 0.6.0", + "symphonia-codec-vorbis 0.6.0", + "symphonia-core 0.6.0", + "symphonia-format-caf", + "symphonia-format-isomp4 0.6.0", + "symphonia-format-mkv 0.6.0", + "symphonia-format-ogg 0.6.0", + "symphonia-format-riff 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee69ad01236a67260b82fd1ff9790dd75ead29f2f46af145e63b7e72273e0e03" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350f1f2f2e19ad4dd315db94304d1eb361b29af070681f94e51b8fdaad769546" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1979c515a76371b186aad2feff5f23e21cbec775bf95de08bf1e3af92a2ad76" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" +dependencies = [ + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebbdfd76d6cc5a601c6292a44357c5b7c82f2cd7cdc0f171421f5c5cff0ea1f" +dependencies = [ + "log", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" +dependencies = [ + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a149cbfc7fb5c405d123a273227d31de17138419552112bf1aa7b73e65827b8" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50baee168f0e9dcf6ba7fc06e8b57eb62072a4490cc7cf13af77e72baae5d328" +dependencies = [ + "log", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b07b4423cd8e0fc472575909a5554b12c2f58e3c190b38c24f042e732fd8de" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8257891ffa7f05e02b58f4761e2abf7e5278c8744fd59e981559e050f86eef55" +dependencies = [ + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ec293b5f288383b72a7bffcade6b2860b642cf66f28b3bd5967349a49938b1" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "lazy_static", + "log", + "num-complex", + "rustfft", + "smallvec", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3ca76633d3400ab57195456c09f8a58d775ff5452329f3f212b6efc8622f5" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d179a01305b3505940135a9f0180d6ef4b487912748fe97554756f120fbd05e" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb17713e134f5ad316c2690fa3104590ccc85842cdbcf82c3cd1a845cb08aa74" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05a67e02b1e4fca1a261ba4fe06910a9357489ad8c36aafdd2960e9c6559433" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17424452a777666d3eaf09a5c651029b15b6a333812fcc5b5474f2a3f0cff3f0" +dependencies = [ + "extended", + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-metadata" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31acf5cd623398a6208e2225d18f4b20f761c55098a796a5247ad516a4a8681" +dependencies = [ + "lazy_static", + "log", + "regex-lite", + "smallvec", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics 0.25.0", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "trash" +version = "5.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7602e0c7d66ec2d92a8c917219fbc7894039efa2063b9064260110828a356f46" +dependencies = [ + "chrono", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vecmath" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ae1e0d85bca567dee1dcf87fb1ca2e792792f66f87dced8381f99cd91156a" +dependencies = [ + "piston-float", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-audio-api" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2bc19aae4c9275d5f8c12f851bcc462630bb3e4996cb9a22b52cbcc8f386bd" +dependencies = [ + "almost", + "arc-swap", + "arrayvec", + "cpal", + "creek", + "crossbeam-channel", + "dasp_sample", + "fft-convolver", + "float_eq", + "futures-channel", + "futures-core", + "futures-util", + "hound", + "hrtf", + "llq", + "log", + "no_denormals", + "num-complex", + "realfft", + "rubato 0.16.2", + "smallvec", + "symphonia 0.6.0", + "vecmath", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5dae22f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["src-tauri", "src-tauri/crates/plugin-sdk", "src-tauri/crates/lyrics-plugin", "plugins/my-plugin"] +resolver = "2" diff --git a/package.json b/package.json index 2f37191..da3f08e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "tauri-app", + "name": "rust-echo-music", "version": "0.1.0", - "description": "", + "description": "A desktop music player built with Rust and Tauri", "type": "module", "scripts": { "dev": "vite dev", @@ -14,21 +14,24 @@ "license": "MIT", "dependencies": { "@mdui/icons": "^1.0.3", - "@tailwindcss/vite": "^4.2.2", - "@tauri-apps/api": "^2.10.1", - "@tauri-apps/plugin-opener": "^2.5.3", - "material-symbols": "^0.43.0", + "@tailwindcss/vite": "^4.3.0", + "@tanstack/svelte-virtual": "^3.13.29", + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-dialog": "~2.7.1", + "@tauri-apps/plugin-opener": "^2.5.4", + "@tauri-apps/plugin-store": "^2.4.3", "mdui": "^2.1.4", - "tailwindcss": "^4.2.2" + "tailwindcss": "^4.3.0" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.55.0", + "@sveltejs/kit": "^2.61.1", "@sveltejs/vite-plugin-svelte": "^5.1.1", - "@tauri-apps/cli": "^2.10.1", - "svelte": "^5.55.1", - "svelte-check": "^4.4.5", + "@tauri-apps/cli": "^2.11.2", + "svelte": "^5.56.0", + "svelte-check": "^4.4.8", "typescript": "~5.6.3", - "vite": "^6.4.1" - } + "vite": "^6.4.2" + }, + "packageManager": "pnpm@11.2.2+sha512.36e6621fad506178936455e70247b8808ef4ec25797a9f437a93281a020484e2607f6a469a22e982987c3dbb8866e3071514ab10a4a1749e06edcd1ec118436f" } diff --git a/plugins/eq/plugin.json b/plugins/eq/plugin.json new file mode 100644 index 0000000..1274561 --- /dev/null +++ b/plugins/eq/plugin.json @@ -0,0 +1,39 @@ +{ + "id": "eq", + "source": "builtin", + "name": "eq", + "displayName": "Equalizer", + "version": "1.0.0", + "author": "RustEchoMusic", + "description": "10-band audio equalizer with presets", + "entry": "eq", + "minAppVersion": "0.1.0", + "permissions": ["audio"], + "activationEvents": [ + "onStartup", + { "onCommand": "eq.apply_preset" }, + { "onCommand": "eq.reset" }, + { "onCommand": "eq.toggle" } + ], + "contributes": { + "commands": [ + { "id": "eq.apply_preset", "title": "应用 EQ 预设" }, + { "id": "eq.reset", "title": "重置 EQ" }, + { "id": "eq.toggle", "title": "切换 EQ 开关" } + ], + "menus": [ + { "command": "eq.apply_preset", "title": "Apply EQ Preset", "location": "TrackContextMenu", "group": "eq" } + ], + "sidebars": [ + { "id": "eq", "title": "均衡器", "icon": "graphic_eq" } + ], + "nativeViews": [ + { "id": "eq-panel-view", "title": "Equalizer", "token": "eq-panel", "icon": "graphic_eq" } + ] + }, + "settings": [ + { "key": "enabled", "title": "启用均衡器", "defaultValue": { "type": "Bool", "value": true } }, + { "key": "preset", "title": "频段预设", "defaultValue": { "type": "Json", "value": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } }, + { "key": "preset_name", "title": "预设名称", "defaultValue": { "type": "Text", "value": "Flat" } } + ] +} diff --git a/plugins/lyrics/plugin.json b/plugins/lyrics/plugin.json new file mode 100644 index 0000000..0ad1825 --- /dev/null +++ b/plugins/lyrics/plugin.json @@ -0,0 +1,42 @@ +{ + "id": "lyrics", + "source": "builtin", + "name": "lyrics", + "displayName": "Lyrics", + "version": "1.0.0", + "author": "RustEchoMusic", + "description": "Lyrics search, load, cache and sync display", + "entry": "lyrics", + "minAppVersion": "0.1.0", + "permissions": [ + "playerRead", + "libraryRead" + ], + "activationEvents": [ + "onStartup", + "onTrackChanged", + {"onCommand": "lyrics.search"}, + {"onCommand": "lyrics.load"}, + {"onCommand": "lyrics.clearCache"} + ], + "contributes": { + "commands": [ + { "id": "lyrics.search", "title": "搜索歌词" }, + { "id": "lyrics.load", "title": "加载歌词" }, + { "id": "lyrics.clearCache", "title": "清除歌词缓存" } + ], + "menus": [], + "sidebars": [ + { "id": "lyrics", "title": "歌词", "icon": "lyrics" } + ], + "nativeViews": [ + { "id": "lyrics-panel-view", "title": "Lyrics", "token": "lyrics-panel", "icon": "lyrics" } + ] + }, + "settings": [ + { "key": "provider", "title": "歌词来源", "defaultValue": { "type": "Text", "value": "local" } }, + { "key": "auto_search", "title": "自动搜索歌词", "defaultValue": { "type": "Bool", "value": true } }, + { "key": "auto_scroll", "title": "自动滚动", "defaultValue": { "type": "Bool", "value": true } }, + { "key": "cache_enabled", "title": "启用缓存", "defaultValue": { "type": "Bool", "value": true } } + ] +} diff --git a/plugins/my-plugin/Cargo.toml b/plugins/my-plugin/Cargo.toml new file mode 100644 index 0000000..8f9edc8 --- /dev/null +++ b/plugins/my-plugin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "my-plugin" +version = "0.1.0" +edition = "2021" + +[lib] +path = "lib.rs" +crate-type = ["cdylib"] + +[dependencies] +plugin-sdk = { path = "../../src-tauri/crates/plugin-sdk" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/plugins/my-plugin/lib.rs b/plugins/my-plugin/lib.rs new file mode 100644 index 0000000..4883888 --- /dev/null +++ b/plugins/my-plugin/lib.rs @@ -0,0 +1,291 @@ +use std::sync::{Arc, Mutex}; + +use plugin_sdk::errors::PluginError; +use plugin_sdk::ffi::*; +use plugin_sdk::traits::Plugin; + +struct FolderPlugin { + folders: Mutex>, + files: Mutex>, +} + +#[derive(serde::Serialize, serde::Deserialize, Default)] +struct FolderPluginState { + folders: Vec, + files: std::collections::HashMap, +} + +impl FolderPlugin { + fn new() -> Self { + let state = Self::load_state(); + Self { + folders: Mutex::new(state.folders), + files: Mutex::new(state.files), + } + } + + fn load_state() -> FolderPluginState { + let path = Self::state_path(); + match std::fs::read_to_string(&path) { + Ok(data) => serde_json::from_str(&data).unwrap_or_default(), + Err(_) => FolderPluginState { + folders: Vec::new(), + files: std::collections::HashMap::new(), + }, + } + } + + fn save_state(&self) { + let path = Self::state_path(); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + let state = FolderPluginState { + folders: self.folders.lock().unwrap().clone(), + files: self.files.lock().unwrap().clone(), + }; + let _ = std::fs::write(&path, serde_json::to_string_pretty(&state).unwrap()); + } + + fn state_path() -> std::path::PathBuf { + std::env::temp_dir().join("my_plugin_state.json") + } + + fn handle_add_folder(&self, path: &str) -> Result { + let mut folders = self.folders.lock().map_err(|e| PluginError::Plugin(e.to_string()))?; + if folders.contains(&path.to_string()) { + return Err(PluginError::Plugin("Folder already added".into())); + } + folders.push(path.to_string()); + drop(folders); + self.save_state(); + Ok(format!("Added folder: {}", path)) + } + + fn handle_remove_folder(&self, path: &str) -> Result { + let mut folders = self.folders.lock().map_err(|e| PluginError::Plugin(e.to_string()))?; + folders.retain(|f| f != path); + drop(folders); + self.files.lock().map_err(|e| PluginError::Plugin(e.to_string()))? + .retain(|k, _| !k.starts_with(path)); + self.save_state(); + Ok(format!("Removed folder: {}", path)) + } + + fn handle_list_folders(&self) -> Result { + let folders = self.folders.lock().map_err(|e| PluginError::Plugin(e.to_string()))?; + Ok(serde_json::to_string(&*folders).unwrap_or_default()) + } + + fn handle_list_files(&self, path: &str) -> Result { + let dir = std::path::Path::new(path); + if !dir.is_dir() { + return Err(PluginError::Plugin(format!("Not a directory: {}", path))); + } + let entries: Vec = std::fs::read_dir(dir) + .map_err(|e| PluginError::Plugin(e.to_string()))? + .filter_map(|e| e.ok()) + .filter_map(|e| e.file_name().into_string().ok()) + .collect(); + Ok(serde_json::to_string(&entries).unwrap_or_default()) + } + + fn handle_read_file(&self, path: &str) -> Result { + if !self.is_allowed_path(path) { + return Err(PluginError::Plugin("Path not in allowed folders".into())); + } + std::fs::read_to_string(path).map_err(|e| PluginError::Plugin(e.to_string())) + } + + fn handle_write_file(&self, path: &str, content: &str) -> Result { + if !self.is_allowed_path(path) { + return Err(PluginError::Plugin("Path not in allowed folders".into())); + } + std::fs::write(path, content).map_err(|e| PluginError::Plugin(e.to_string()))?; + Ok(format!("Written: {}", path)) + } + + fn is_allowed_path(&self, path: &str) -> bool { + let folders = self.folders.lock().unwrap(); + folders.iter().any(|f| path.starts_with(f.as_str())) + } +} + +impl Plugin for FolderPlugin { + fn activate(&self, _ctx: &plugin_sdk::context::PluginContext) -> Result<(), plugin_sdk::errors::PluginError> { + println!("[my-plugin] activated, {} folders loaded", self.folders.lock().unwrap().len()); + Ok(()) + } + + fn deactivate(&self) -> Result<(), plugin_sdk::errors::PluginError> { + println!("[my-plugin] deactivated"); + Ok(()) + } + + fn on_event(&self, _event: &plugin_sdk::events::PluginEvent, _ctx: &plugin_sdk::context::PluginContext) -> Result<(), plugin_sdk::errors::PluginError> { + Ok(()) + } + + fn execute_command(&self, cmd: &str, args: &plugin_sdk::traits::CommandArgs, _ctx: &plugin_sdk::context::PluginContext) -> Result<(), plugin_sdk::errors::PluginError> { + let result = match cmd { + "folder.add" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(path) = args { + self.handle_add_folder(path)? + } else { + return Err(PluginError::Plugin("folder.add requires RawPayload".into())); + } + } + "folder.remove" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(path) = args { + self.handle_remove_folder(path)? + } else { + return Err(PluginError::Plugin("folder.remove requires RawPayload".into())); + } + } + "folder.list" => { + self.handle_list_folders()? + } + "folder.read" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(path) = args { + self.handle_read_file(path)? + } else { + return Err(PluginError::Plugin("folder.read requires RawPayload".into())); + } + } + "folder.write" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(content) = args { + self.handle_write_file("todo", content)? + } else { + return Err(PluginError::Plugin("folder.write requires RawPayload".into())); + } + } + _ => return Err(plugin_sdk::errors::PluginError::Plugin(format!("Unknown command: {}", cmd))), + }; + println!("[my-plugin] {} -> {}", cmd, result); + Ok(()) + } +} + +struct FolderPluginFactory; + +impl plugin_sdk::traits::PluginFactory for FolderPluginFactory { + fn create(&self) -> Arc { + Arc::new(FolderPlugin::new()) + } +} + +static mut PLUGIN: Option> = None; + +unsafe extern "C" fn create() -> PluginHandle { + let plugin = Arc::new(FolderPlugin::new()); + PLUGIN = Some(plugin.clone()); + Arc::into_raw(plugin) as *mut std::ffi::c_void +} + +unsafe extern "C" fn destroy(handle: PluginHandle) { + PLUGIN = None; + let _ = Arc::from_raw(handle as *const FolderPlugin); +} + +unsafe extern "C" fn free_buffer(buf: ByteBuffer) { + if !buf.ptr.is_null() { + Vec::from_raw_parts(buf.ptr, buf.len, buf.len); + } +} + +unsafe extern "C" fn api_activate(_h: PluginHandle) -> i32 { + if let Some(ref p) = PLUGIN { + let _ = p.activate(&plugin_sdk::context::PluginContext::new( + std::sync::Arc::new(NoopHost), + std::sync::Arc::new(NoopPlayer), + std::sync::Arc::new(NoopLibrary), + std::sync::Arc::new(NoopQueue), + std::sync::Arc::new(NoopSettings), + std::sync::Arc::new(NoopPluginSettings), + std::sync::Arc::new(NoopAudio), + )); + } + 0 +} + +unsafe extern "C" fn api_deactivate(_h: PluginHandle) -> i32 { + if let Some(ref p) = PLUGIN { + let _ = p.deactivate(); + } + 0 +} + +unsafe extern "C" fn api_on_event(_h: PluginHandle, _e: EventPayload) -> i32 { 0 } + +unsafe extern "C" fn api_execute_command(_h: PluginHandle, _req: *const CommandRequest) -> CommandResponse { + CommandResponse { success: true, data: ByteBuffer { ptr: std::ptr::null_mut(), len: 0 } } +} + +#[no_mangle] +pub extern "C" fn get_plugin_exports() -> PluginExports { + PluginExports { + abi_version: 1, + create, + destroy, + free_buffer, + api: PluginApi { + activate: api_activate, + deactivate: api_deactivate, + on_event: api_on_event, + execute_command: api_execute_command, + }, + audio_processor: std::ptr::null_mut(), + } +} + +struct NoopHost; +impl plugin_sdk::context::HostContext for NoopHost { + fn plugin_id(&self) -> &str { "my-plugin" } + fn cache_dir(&self) -> Result { + Ok(std::env::temp_dir().join("my-plugin")) + } + fn emit_lyrics_loaded(&self, _event: plugin_sdk::events::LyricsLoadedEvent) -> Result<(), plugin_sdk::errors::PluginError> { + Ok(()) + } +} + +struct NoopPlayer; +impl plugin_sdk::api::PlayerApi for NoopPlayer { + fn play(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn pause(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn next(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn previous(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn current_track_id(&self) -> Result, plugin_sdk::errors::PluginError> { Ok(None) } +} + +struct NoopLibrary; +impl plugin_sdk::api::LibraryApi for NoopLibrary { + fn get_track_path(&self, _track_id: i64) -> Result, plugin_sdk::errors::PluginError> { Ok(None) } + fn exists(&self, _track_id: i64) -> Result { Ok(false) } +} + +struct NoopQueue; +impl plugin_sdk::api::QueueApi for NoopQueue { + fn current_queue(&self) -> Result, plugin_sdk::errors::PluginError> { Ok(vec![]) } + fn remove_track(&self, _track_id: i64) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn clear(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } +} + +struct NoopSettings; +impl plugin_sdk::api::SettingsApi for NoopSettings { + fn theme(&self) -> Result { Ok("light".into()) } + fn set_theme(&self, _theme: String) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } +} + +struct NoopPluginSettings; +impl plugin_sdk::api::PluginSettingsApi for NoopPluginSettings { + fn get_setting(&self, _key: &str) -> Option { None } + fn set_setting(&self, _key: &str, _value: plugin_sdk::settings::SettingValue) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } +} + +struct NoopAudio; +impl plugin_sdk::api::AudioProcessingApi for NoopAudio { + fn set_band_gain(&self, _band: usize, _gain_db: f64) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn apply_preset(&self, _gains: [f64; 10]) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn get_bands(&self) -> Result<[f64; 10], plugin_sdk::errors::PluginError> { Ok([0.0; 10]) } + fn set_enabled(&self, _enabled: bool) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn is_enabled(&self) -> Result { Ok(false) } +} diff --git a/plugins/my-plugin/plugin.json b/plugins/my-plugin/plugin.json new file mode 100644 index 0000000..ff55b70 --- /dev/null +++ b/plugins/my-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "id": "my-plugin", + "source": "packaged", + "name": "my-plugin", + "displayName": "My Plugin", + "version": "1.0.0", + "author": "Developer", + "description": "Folder read/write plugin", + "entry": "my-plugin", + "minAppVersion": "0.1.0", + "permissions": ["libraryRead"], + "activationEvents": [ + { "onCommand": "folder.list" }, + { "onCommand": "folder.read" }, + { "onCommand": "folder.write" }, + { "onCommand": "folder.add" }, + { "onCommand": "folder.remove" } + ], + "contributes": { + "commands": [ + { "id": "folder.list", "title": "List files in folder", "category": null }, + { "id": "folder.read", "title": "Read file content", "category": null }, + { "id": "folder.write", "title": "Write file content", "category": null }, + { "id": "folder.add", "title": "Add a folder", "category": null }, + { "id": "folder.remove", "title": "Remove a folder", "category": null } + ], + "menus": [], + "sidebars": [ + { "id": "folder", "title": "Files", "icon": "folder_open" } + ] + }, + "settings": [] +} diff --git a/plugins/plugin.template.json b/plugins/plugin.template.json new file mode 100644 index 0000000..921c61f --- /dev/null +++ b/plugins/plugin.template.json @@ -0,0 +1,30 @@ +{ + "id": "", + "name": "", + "displayName": "", + "version": "1.0.0", + "author": "", + "description": "", + "entry": "", + "minAppVersion": "0.1.0", + "permissions": [ + "pluginUI" + ], + "activationEvents": [ + "onStartup" + ], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [], + "views": [ + { + "id": "main", + "title": "插件主页面", + "entry": "view/index.html", + "icon": "dashboard" + } + ] + }, + "settings": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99a3523..c3d18bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,48 +12,54 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@tailwindcss/vite': - specifier: ^4.2.2 - version: 4.2.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + specifier: ^4.3.0 + version: 4.3.0(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) + '@tanstack/svelte-virtual': + specifier: ^3.13.29 + version: 3.13.29(svelte@5.56.0) '@tauri-apps/api': - specifier: ^2.10.1 - version: 2.10.1 + specifier: ^2.11.0 + version: 2.11.0 + '@tauri-apps/plugin-dialog': + specifier: ~2.7.1 + version: 2.7.1 '@tauri-apps/plugin-opener': - specifier: ^2.5.3 - version: 2.5.3 - material-symbols: - specifier: ^0.43.0 - version: 0.43.0 + specifier: ^2.5.4 + version: 2.5.4 + '@tauri-apps/plugin-store': + specifier: ^2.4.3 + version: 2.4.3 mdui: specifier: ^2.1.4 version: 2.1.4 tailwindcss: - specifier: ^4.2.2 - version: 4.2.2 + specifier: ^4.3.0 + version: 4.3.0 devDependencies: '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))) + version: 3.0.10(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))) '@sveltejs/kit': - specifier: ^2.55.0 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + specifier: ^2.61.1 + version: 2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + version: 5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) '@tauri-apps/cli': - specifier: ^2.10.1 - version: 2.10.1 + specifier: ^2.11.2 + version: 2.11.2 svelte: - specifier: ^5.55.1 - version: 5.55.1 + specifier: ^5.56.0 + version: 5.56.0 svelte-check: - specifier: ^4.4.5 - version: 4.4.5(picomatch@4.0.4)(svelte@5.55.1)(typescript@5.6.3) + specifier: ^4.4.8 + version: 4.4.8(picomatch@4.0.4)(svelte@5.56.0)(typescript@5.6.3) typescript: specifier: ~5.6.3 version: 5.6.3 vite: - specifier: ^6.4.1 - version: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + specifier: ^6.4.2 + version: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) packages: @@ -232,8 +238,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@lit-labs/ssr-dom-shim@1.5.1': - resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} '@lit/localize@0.12.2': resolution: {integrity: sha512-Qv9kvgJKDq/JVSwXOxuWvQnnOBysHA99ti9im9a4fImCmx+fto+XXcUYQbjZHqiueEEc4V20PcRDPO+1g/6seQ==} @@ -256,149 +262,149 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} cpu: [x64] os: [win32] '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + '@sveltejs/acorn-typescript@1.0.10': + resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} peerDependencies: acorn: ^8.9.0 @@ -407,15 +413,15 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.55.0': - resolution: {integrity: sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==} + '@sveltejs/kit@2.61.1': + resolution: {integrity: sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==} engines: {node: '>=18.13'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 + typescript: ^5.3.3 || ^6.0.0 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: '@opentelemetry/api': @@ -438,69 +444,69 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 - '@tailwindcss/node@4.2.2': - resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} - '@tailwindcss/oxide-android-arm64@4.2.2': - resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.2.2': - resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.2.2': - resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.2.2': - resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -511,108 +517,122 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.2.2': - resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.2.2': - resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tauri-apps/api@2.10.1': - resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + '@tanstack/svelte-virtual@3.13.29': + resolution: {integrity: sha512-9JWVzGK7JN72Fli9xKftpjUcDolOOtH4z/oefs13Xp3C2NGkJUqMjclKGJ4vS3k2EdlLKH0/tj25J7gTYFV2ZA==} + peerDependencies: + svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 + + '@tanstack/virtual-core@3.17.1': + resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==} + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} - '@tauri-apps/cli-darwin-arm64@2.10.1': - resolution: {integrity: sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==} + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.10.1': - resolution: {integrity: sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==} + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': - resolution: {integrity: sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.10.1': - resolution: {integrity: sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==} + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.10.1': - resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': - resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.10.1': - resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.10.1': - resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.10.1': - resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.10.1': - resolution: {integrity: sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==} + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.10.1': - resolution: {integrity: sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==} + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.10.1': - resolution: {integrity: sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==} + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-opener@2.5.3': - resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + '@tauri-apps/plugin-dialog@2.7.1': + resolution: {integrity: sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==} + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@tauri-apps/plugin-store@2.4.3': + resolution: {integrity: sha512-9LWPj9yMphRi9czEtUv87XHbl1b6xgd9EXpPrUnq6nG7+nbtoF84d4Kwz9xhAv/Hf30sr58pq7EOlyI936y8qw==} '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -620,13 +640,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/types@8.57.2': - resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -672,11 +691,11 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} esbuild@0.25.12: @@ -687,8 +706,13 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - esrap@2.2.4: - resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + esrap@2.2.9: + resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -713,8 +737,8 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true kleur@4.1.5: @@ -798,11 +822,11 @@ packages: lit-element@4.2.2: resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} - lit-html@3.3.2: - resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} - lit@3.3.2: - resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -810,9 +834,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - material-symbols@0.43.0: - resolution: {integrity: sha512-40zgnHCv6d+Nx/9WQ6vhZitQoqtntgZNXN1YJGyn1NJRQLoB7lWWD8vpu/i68nKtZs8OxHyMudBEkS2mrUHiqQ==} - mdui@2.1.4: resolution: {integrity: sha512-QtK5xia5HXtVO7yH30QjwvvNruw5JdrJL1MEc1k6S/ZfsbHOj6BxxdYjrdv2HiN5ikkGqt5CIbZdFyq6shaZyw==} @@ -827,8 +848,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -839,16 +860,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -870,27 +891,27 @@ packages: ssr-window@5.0.1: resolution: {integrity: sha512-WVXlhQsm54HC+FnJfEbccEgNF7mKXtnFUB8Xn7rx2dsWHOlBdqezdX88Vjh6pVGaa0ZvL+PoSu7rEcBuNmxt6g==} - svelte-check@4.4.5: - resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==} + svelte-check@4.4.8: + resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte@5.55.1: - resolution: {integrity: sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==} + svelte@5.56.0: + resolution: {integrity: sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==} engines: {node: '>=18'} - tailwindcss@4.2.2: - resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} totalist@3.0.1: @@ -905,8 +926,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - vite@6.4.1: - resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -945,10 +966,10 @@ packages: yaml: optional: true - vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: vite: optional: true @@ -1057,22 +1078,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lit-labs/ssr-dom-shim@1.5.1': {} + '@lit-labs/ssr-dom-shim@1.6.0': {} '@lit/localize@0.12.2': dependencies: - lit: 3.3.2 + lit: 3.3.3 '@lit/reactive-element@2.1.2': dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit-labs/ssr-dom-shim': 1.6.0 '@material/material-color-utilities@0.3.0': {} '@mdui/icons@1.0.3': dependencies: '@mdui/shared': 1.0.8 - lit: 3.3.2 + lit: 3.3.3 tslib: 2.8.1 '@mdui/jq@3.0.3': @@ -1084,267 +1105,282 @@ snapshots: dependencies: '@lit/reactive-element': 2.1.2 '@mdui/jq': 3.0.3 - lit: 3.3.2 + lit: 3.3.3 ssr-window: 5.0.1 tslib: 2.8.1 '@polka/url@1.0.0-next.29': {} - '@rollup/rollup-android-arm-eabi@4.60.1': + '@rollup/rollup-android-arm-eabi@4.60.4': optional: true - '@rollup/rollup-android-arm64@4.60.1': + '@rollup/rollup-android-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-arm64@4.60.1': + '@rollup/rollup-darwin-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-x64@4.60.1': + '@rollup/rollup-darwin-x64@4.60.4': optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': + '@rollup/rollup-freebsd-arm64@4.60.4': optional: true - '@rollup/rollup-freebsd-x64@4.60.1': + '@rollup/rollup-freebsd-x64@4.60.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': + '@rollup/rollup-linux-arm-musleabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': + '@rollup/rollup-linux-arm64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': + '@rollup/rollup-linux-arm64-musl@4.60.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': + '@rollup/rollup-linux-loong64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': + '@rollup/rollup-linux-loong64-musl@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': + '@rollup/rollup-linux-ppc64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': + '@rollup/rollup-linux-ppc64-musl@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': + '@rollup/rollup-linux-riscv64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': + '@rollup/rollup-linux-riscv64-musl@4.60.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': + '@rollup/rollup-linux-s390x-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': + '@rollup/rollup-linux-x64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': + '@rollup/rollup-linux-x64-musl@4.60.4': optional: true - '@rollup/rollup-openbsd-x64@4.60.1': + '@rollup/rollup-openbsd-x64@4.60.4': optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': + '@rollup/rollup-openharmony-arm64@4.60.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.1': + '@rollup/rollup-win32-arm64-msvc@4.60.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.1': + '@rollup/rollup-win32-ia32-msvc@4.60.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.1': + '@rollup/rollup-win32-x64-gnu@4.60.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.1': + '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: acorn: 8.16.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))': dependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/kit': 2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.1.0 sirv: 3.0.2 - svelte: 5.55.1 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + svelte: 5.56.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) optionalDependencies: typescript: 5.6.3 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) debug: 4.4.3 - svelte: 5.55.1 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + svelte: 5.56.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 - svelte: 5.55.1 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) - vitefu: 1.1.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + svelte: 5.56.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) + vitefu: 1.1.3(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) transitivePeerDependencies: - supports-color - '@tailwindcss/node@4.2.2': + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.2.2 + tailwindcss: 4.3.0 - '@tailwindcss/oxide-android-arm64@4.2.2': + '@tailwindcss/oxide-android-arm64@4.3.0': optional: true - '@tailwindcss/oxide-darwin-arm64@4.2.2': + '@tailwindcss/oxide-darwin-arm64@4.3.0': optional: true - '@tailwindcss/oxide-darwin-x64@4.2.2': + '@tailwindcss/oxide-darwin-x64@4.3.0': optional: true - '@tailwindcss/oxide-freebsd-x64@4.2.2': + '@tailwindcss/oxide-freebsd-x64@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.2': + '@tailwindcss/oxide-linux-x64-musl@4.3.0': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.2': + '@tailwindcss/oxide-wasm32-wasi@4.3.0': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': optional: true - '@tailwindcss/oxide@4.2.2': + '@tailwindcss/oxide@4.3.0': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - - '@tailwindcss/vite@4.2.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) + + '@tanstack/svelte-virtual@3.13.29(svelte@5.56.0)': dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + '@tanstack/virtual-core': 3.17.1 + svelte: 5.56.0 + + '@tanstack/virtual-core@3.17.1': {} - '@tauri-apps/api@2.10.1': {} + '@tauri-apps/api@2.11.0': {} - '@tauri-apps/cli-darwin-arm64@2.10.1': + '@tauri-apps/cli-darwin-arm64@2.11.2': optional: true - '@tauri-apps/cli-darwin-x64@2.10.1': + '@tauri-apps/cli-darwin-x64@2.11.2': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.10.1': + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.10.1': + '@tauri-apps/cli-linux-arm64-musl@2.11.2': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.10.1': + '@tauri-apps/cli-linux-x64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-musl@2.10.1': + '@tauri-apps/cli-linux-x64-musl@2.11.2': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.10.1': + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.10.1': + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.10.1': + '@tauri-apps/cli-win32-x64-msvc@2.11.2': optional: true - '@tauri-apps/cli@2.10.1': + '@tauri-apps/cli@2.11.2': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.10.1 - '@tauri-apps/cli-darwin-x64': 2.10.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.10.1 - '@tauri-apps/cli-linux-arm64-gnu': 2.10.1 - '@tauri-apps/cli-linux-arm64-musl': 2.10.1 - '@tauri-apps/cli-linux-riscv64-gnu': 2.10.1 - '@tauri-apps/cli-linux-x64-gnu': 2.10.1 - '@tauri-apps/cli-linux-x64-musl': 2.10.1 - '@tauri-apps/cli-win32-arm64-msvc': 2.10.1 - '@tauri-apps/cli-win32-ia32-msvc': 2.10.1 - '@tauri-apps/cli-win32-x64-msvc': 2.10.1 - - '@tauri-apps/plugin-opener@2.5.3': + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + + '@tauri-apps/plugin-dialog@2.7.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-store@2.4.3': dependencies: - '@tauri-apps/api': 2.10.1 + '@tauri-apps/api': 2.11.0 '@types/cookie@0.6.0': {} '@types/estree@1.0.8': {} - '@types/trusted-types@2.0.7': {} + '@types/estree@1.0.9': {} - '@typescript-eslint/types@8.57.2': {} + '@types/trusted-types@2.0.7': {} acorn@8.16.0: {} @@ -1370,12 +1406,12 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.6.4: {} + devalue@5.8.1: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 esbuild@0.25.12: optionalDependencies: @@ -1408,10 +1444,9 @@ snapshots: esm-env@1.2.2: {} - esrap@2.2.4: + esrap@2.2.9: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.57.2 fdir@6.5.0(picomatch@4.0.4): optionalDependencies: @@ -1426,9 +1461,9 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 - jiti@2.6.1: {} + jiti@2.7.0: {} kleur@4.1.5: {} @@ -1483,19 +1518,19 @@ snapshots: lit-element@4.2.2: dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit-labs/ssr-dom-shim': 1.6.0 '@lit/reactive-element': 2.1.2 - lit-html: 3.3.2 + lit-html: 3.3.3 - lit-html@3.3.2: + lit-html@3.3.3: dependencies: '@types/trusted-types': 2.0.7 - lit@3.3.2: + lit@3.3.3: dependencies: '@lit/reactive-element': 2.1.2 lit-element: 4.2.2 - lit-html: 3.3.2 + lit-html: 3.3.3 locate-character@3.0.0: {} @@ -1503,8 +1538,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - material-symbols@0.43.0: {} - mdui@2.1.4: dependencies: '@floating-ui/utils': 0.2.11 @@ -1515,7 +1548,7 @@ snapshots: '@mdui/shared': 1.0.8 classcat: 5.0.5 is-promise: 4.0.0 - lit: 3.3.2 + lit: 3.3.3 ssr-window: 5.0.1 tslib: 2.8.1 @@ -1525,49 +1558,49 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} + nanoid@3.3.12: {} picocolors@1.1.1: {} picomatch@4.0.4: {} - postcss@8.5.8: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 readdirp@4.1.2: {} - rollup@4.60.1: + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 sade@1.8.1: @@ -1586,42 +1619,44 @@ snapshots: ssr-window@5.0.1: {} - svelte-check@4.4.5(picomatch@4.0.4)(svelte@5.55.1)(typescript@5.6.3): + svelte-check@4.4.8(picomatch@4.0.4)(svelte@5.56.0)(typescript@5.6.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.4) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.55.1 + svelte: 5.56.0 typescript: 5.6.3 transitivePeerDependencies: - picomatch - svelte@5.55.1: + svelte@5.56.0: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 - esrap: 2.2.4 + esrap: 2.2.9 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' - tailwindcss@4.2.2: {} + tailwindcss@4.3.0: {} - tapable@2.3.2: {} + tapable@2.3.3: {} - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -1632,21 +1667,21 @@ snapshots: typescript@5.6.3: {} - vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.8 - rollup: 4.60.1 - tinyglobby: 0.2.15 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.32.0 - vitefu@1.1.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)): + vitefu@1.1.3(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)): optionalDependencies: - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) zimmerframe@1.1.4: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6d60a71..009a825 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -25,13 +25,19 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "almost" version = "0.2.0" @@ -40,21 +46,21 @@ checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" [[package]] name = "alsa" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags 2.11.0", + "bitflags 2.13.0", "cfg-if", "libc", ] [[package]] name = "alsa-sys" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" dependencies = [ "libc", "pkg-config", @@ -77,18 +83,18 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "async-broadcast" @@ -183,14 +189,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -218,7 +224,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -244,6 +250,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -252,9 +267,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -268,24 +283,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -309,13 +306,19 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -325,6 +328,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -349,9 +361,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -360,19 +372,28 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byte-slice-cast" @@ -392,11 +413,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -407,7 +434,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -428,9 +455,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -470,13 +497,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -486,15 +511,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfb" version = "0.7.3" @@ -522,11 +538,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -535,14 +562,39 @@ dependencies = [ ] [[package]] -name = "clang-sys" -version = "1.8.1" +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cocoa" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ - "glob", + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", "libc", - "libloading 0.8.9", + "objc", ] [[package]] @@ -564,12 +616,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.18.1" @@ -580,6 +626,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -596,71 +652,96 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "core-graphics-types", - "foreign-types", + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", "libc", ] [[package]] name = "core-graphics-types" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ - "bitflags 2.11.0", - "core-foundation", + "bitflags 1.3.2", + "core-foundation 0.9.4", "libc", ] [[package]] -name = "coreaudio-rs" -version = "0.11.3" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", ] [[package]] -name = "coreaudio-sys" -version = "0.2.17" +name = "coreaudio-rs" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bindgen", + "bitflags 2.13.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" dependencies = [ "alsa", - "core-foundation-sys", + "block2", "coreaudio-rs", "dasp_sample", - "jni", + "jni 0.22.4", "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", "web-sys", - "windows 0.54.0", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -672,6 +753,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -709,7 +814,7 @@ checksum = "416063cb8f815ee27036075e544a778770b577e82348f017d145a704df90f246" dependencies = [ "creek-core", "log", - "symphonia", + "symphonia 0.5.5", ] [[package]] @@ -731,6 +836,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -739,29 +853,21 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] -name = "cssparser" -version = "0.29.6" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", + "hybrid-array", ] [[package]] @@ -773,7 +879,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.13.1", + "phf", "smallvec", ] @@ -784,17 +890,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] @@ -817,7 +938,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -828,7 +949,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -838,26 +959,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] -name = "deranged" -version = "0.5.8" +name = "data-encoding" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" dependencies = [ - "powerfmt", - "serde_core", + "libc", + "libdbus-sys", + "windows-sys 0.61.2", ] [[package]] -name = "derive_more" -version = "0.99.20" +name = "dbus-crossroads" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", + "dbus", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", ] [[package]] @@ -878,7 +1011,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -887,8 +1020,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -912,13 +1056,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "dispatch2" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -926,13 +1076,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -955,7 +1105,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -965,14 +1115,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", - "cssparser 0.36.0", - "foldhash 0.2.0", - "html5ever 0.38.0", + "cssparser", + "foldhash", + "html5ever", "precomputed-hash", - "selectors 0.36.1", - "tendril 0.5.0", + "selectors", + "tendril", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.2" @@ -997,6 +1153,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -1011,20 +1182,23 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] [[package]] name = "embed-resource" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "vswhom", "winreg", ] @@ -1068,7 +1242,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1098,6 +1272,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1127,9 +1311,9 @@ checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fdeflate" @@ -1142,14 +1326,13 @@ dependencies = [ [[package]] name = "fft-convolver" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdcf0473d3952d710173f7f2acc8ff88065928e01b7d5467ec28449a3abae4fc" +checksum = "ecb4fbed063c755ecaa4cd1356cd82cc48e9d5c7edd5c30cd144656833fc0661" dependencies = [ - "num", "realfft", - "rustfft", - "thiserror 1.0.69", + "rtsan-standalone", + "thiserror 2.0.18", ] [[package]] @@ -1185,16 +1368,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" [[package]] -name = "fnv" -version = "1.0.7" +name = "flume" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] [[package]] -name = "foldhash" -version = "0.1.5" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -1202,6 +1390,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1209,7 +1406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1220,9 +1417,15 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1238,16 +1441,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1275,6 +1468,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1302,7 +1506,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1333,15 +1537,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gdk" version = "0.18.2" @@ -1443,25 +1638,14 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1470,7 +1654,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1487,15 +1671,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", + "rand_core", ] [[package]] @@ -1536,7 +1719,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -1564,7 +1747,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1643,7 +1826,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1654,18 +1837,29 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.1.5", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] [[package]] name = "heck" @@ -1691,6 +1885,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "hound" version = "3.5.1" @@ -1708,18 +1920,6 @@ dependencies = [ "rustfft", ] -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - [[package]] name = "html5ever" version = "0.38.0" @@ -1727,14 +1927,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.38.0", + "markup5ever", ] [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1769,11 +1969,20 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1784,7 +1993,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1844,17 +2052,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1862,9 +2071,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1875,9 +2084,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1889,15 +2098,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1909,15 +2118,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1928,12 +2137,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1953,14 +2156,27 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1974,12 +2190,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1999,16 +2215,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -2028,20 +2234,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -2075,36 +2272,79 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] [[package]] -name = "jobserver" -version = "0.1.34" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ - "getrandom 0.3.4", - "libc", + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", ] [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2136,35 +2376,17 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser 0.29.6", - "html5ever 0.29.1", - "indexmap 2.13.0", - "selectors 0.24.0", -] - [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libappindicator" version = "0.9.0" @@ -2191,9 +2413,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] [[package]] name = "libloading" @@ -2207,9 +2438,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", "windows-link 0.2.1", @@ -2217,13 +2448,24 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2232,9 +2474,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "llq" @@ -2252,38 +2494,60 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.29" +name = "lofty" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca" +dependencies = [ + "byteorder", + "data-encoding", + "flate2", + "lofty_attr", + "log", + "ogg_pager", + "paste", +] [[package]] -name = "mac" -version = "0.1.1" +name = "lofty_attr" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] -name = "mach2" -version = "0.4.3" +name = "log" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lyrics-plugin" +version = "0.1.0" dependencies = [ - "libc", + "lofty", + "plugin-sdk", + "serde", + "serde_json", ] [[package]] -name = "markup5ever" -version = "0.14.1" +name = "mach2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", + "libc", ] [[package]] @@ -2293,32 +2557,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "tendril 0.5.0", + "tendril", "web_atoms", ] [[package]] -name = "match_token" -version = "0.1.0" +name = "md-5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cfg-if", + "digest 0.11.3", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memoffset" @@ -2335,12 +2592,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2353,20 +2604,30 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" -version = "0.17.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -2377,24 +2638,10 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", -] - -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.11.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", + "windows-sys 0.61.2", ] [[package]] @@ -2403,10 +2650,10 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bitflags 2.13.0", + "jni-sys 0.3.1", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -2420,20 +2667,11 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.5.0+25.2.9519653" +version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2444,49 +2682,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "no_denormals" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a63f8a8efd33b4706cecbc4b845b0c63e9c883f8a5404a0fd7ce2a0933421f" - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] +checksum = "b6bcfe410abc339c9f8c226aceebf946bf26e3e2eca738be3fec9e9ee97d54aa" [[package]] name = "num-complex" @@ -2499,9 +2697,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -2511,7 +2709,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2524,34 +2722,22 @@ dependencies = [ ] [[package]] -name = "num-iter" -version = "0.1.45" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "autocfg", + "hermit-abi", + "libc", ] [[package]] @@ -2573,7 +2759,16 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", ] [[package]] @@ -2592,21 +2787,93 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", "objc2", "objc2-core-foundation", "objc2-foundation", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -2616,13 +2883,45 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "dispatch2", "objc2", "objc2-core-foundation", "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -2644,8 +2943,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2656,7 +2956,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", ] @@ -2667,7 +2967,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2679,47 +2979,52 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", + "block2", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", ] [[package]] -name = "objc2-web-kit" +name = "objc2-user-notifications" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.11.0", - "block2", "objc2", - "objc2-app-kit", - "objc2-core-foundation", "objc2-foundation", ] [[package]] -name = "oboe" -version = "0.6.1" +name = "objc2-web-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] -name = "oboe-sys" -version = "0.6.1" +name = "ogg_pager" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216" dependencies = [ - "cc", + "byteorder", ] [[package]] @@ -2730,9 +3035,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.3" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "dunce", "is-wsl", @@ -2810,6 +3115,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2822,105 +3133,25 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros", + "phf_shared", "serde", ] -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", + "phf_generator", + "phf_shared", ] [[package]] @@ -2930,34 +3161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", + "phf_shared", ] [[package]] @@ -2966,38 +3170,11 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.2", + "syn 2.0.118", ] [[package]] @@ -3006,7 +3183,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -3015,12 +3192,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.5" @@ -3040,23 +3211,31 @@ checksum = "ad78bf43dcf80e8f950c92b84f938a0fc7590b7f6866fbcbeca781609c115590" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "quick-xml", "serde", "time", ] +[[package]] +name = "plugin-sdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "png" version = "0.17.16" @@ -3070,6 +3249,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3086,9 +3278,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3099,31 +3291,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "primal-check" version = "0.3.4" @@ -3159,7 +3332,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.5+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -3186,12 +3359,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.106" @@ -3202,115 +3369,57 @@ dependencies = [ ] [[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" +name = "pxfm" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "quick-xml" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "memchr", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "quote" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "proc-macro2", ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand_core" -version = "0.6.4" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rand" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "rand_core 0.5.1", + "chacha20", + "getrandom 0.4.3", + "rand_core", ] [[package]] -name = "rand_pcg" -version = "0.2.1" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "raw-window-handle" @@ -3333,7 +3442,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", ] [[package]] @@ -3364,14 +3473,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -3390,17 +3499,23 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -3430,11 +3545,65 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rtrb" -version = "0.3.3" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" + +[[package]] +name = "rtsan-standalone" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd1b6d61d69481a68e555916d3a52213846f5c2d2140bdc74ebc308e2338fc3" +dependencies = [ + "rtsan-standalone-macros", + "rtsan-standalone-sys", +] + +[[package]] +name = "rtsan-standalone-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8320af894782374141c8e0d4521ee613a8f24d7ab08b6ad359881311e37b9ef" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rtsan-standalone-sys" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" +checksum = "362a9c531f731e574a870cdb28ee7d81178ba7a87ed54380eb9b8d8f0c3b5301" +dependencies = [ + "num_cpus", + "tempfile", +] [[package]] name = "rubato" @@ -3460,11 +3629,36 @@ dependencies = [ "realfft", ] +[[package]] +name = "rust-echo-music" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "libloading 0.9.0", + "lofty", + "lyrics-plugin", + "plugin-sdk", + "rand", + "serde", + "serde_json", + "souvlaki", + "sqlx", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-store", + "tokio", + "trash", + "walkdir", + "web-audio-api", +] + [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3495,7 +3689,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -3565,7 +3759,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3574,48 +3768,30 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser 0.29.6", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc 0.2.0", - "smallvec", -] - [[package]] name = "selectors" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.0", - "cssparser 0.36.0", - "derive_more 2.1.1", + "bitflags 2.13.0", + "cssparser", + "derive_more", "log", "new_debug_unreachable", - "phf 0.13.1", - "phf_codegen 0.13.1", + "phf", + "phf_codegen", "precomputed-hash", "rustc-hash", - "servo_arc 0.4.3", + "servo_arc", "smallvec", ] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -3660,7 +3836,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3671,14 +3847,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3695,7 +3871,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3709,24 +3885,25 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] [[package]] name = "serde_with" -version = "3.18.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -3737,14 +3914,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3766,26 +3943,27 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "servo_arc" -version = "0.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] [[package]] -name = "servo_arc" -version = "0.4.3" +name = "sha1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ - "stable_deref_trait", + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3795,15 +3973,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -3817,21 +4006,31 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "siphasher" -version = "0.3.11" +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3841,15 +4040,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3863,7 +4065,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", "js-sys", - "ndk 0.9.0", + "ndk", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3894,13 +4096,211 @@ dependencies = [ name = "soup3-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "souvlaki" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" +dependencies = [ + "base64 0.22.1", + "block", + "cocoa", + "core-graphics 0.22.3", + "dbus", + "dbus-crossroads", + "dispatch", + "objc", + "thiserror 1.0.69", + "windows 0.44.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.118", + "thiserror 2.0.18", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", ] [[package]] @@ -3915,19 +4315,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -3936,32 +4323,31 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] [[package]] -name = "string_cache_codegen" -version = "0.6.1" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] @@ -3988,19 +4374,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", - "symphonia-bundle-flac", - "symphonia-bundle-mp3", - "symphonia-codec-aac", - "symphonia-codec-adpcm", - "symphonia-codec-alac", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-isomp4", - "symphonia-format-mkv", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", + "symphonia-bundle-flac 0.5.5", + "symphonia-bundle-mp3 0.5.5", + "symphonia-codec-aac 0.5.5", + "symphonia-codec-adpcm 0.5.5", + "symphonia-codec-alac 0.5.5", + "symphonia-codec-pcm 0.5.5", + "symphonia-codec-vorbis 0.5.5", + "symphonia-core 0.5.5", + "symphonia-format-isomp4 0.5.5", + "symphonia-format-mkv 0.5.5", + "symphonia-format-ogg 0.5.5", + "symphonia-format-riff 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1758d6c853020a7244de03cc3e0185eaea3f58715122422dd3cc7452e6d4c16a" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac 0.6.0", + "symphonia-bundle-mp3 0.6.0", + "symphonia-codec-aac 0.6.0", + "symphonia-codec-adpcm 0.6.0", + "symphonia-codec-alac 0.6.0", + "symphonia-codec-pcm 0.6.0", + "symphonia-codec-vorbis 0.6.0", + "symphonia-core 0.6.0", + "symphonia-format-caf", + "symphonia-format-isomp4 0.6.0", + "symphonia-format-mkv 0.6.0", + "symphonia-format-ogg 0.6.0", + "symphonia-format-riff 0.6.0", + "symphonia-metadata 0.6.0", ] [[package]] @@ -4010,11 +4419,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" dependencies = [ "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-bundle-flac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee69ad01236a67260b82fd1ff9790dd75ead29f2f46af145e63b7e72273e0e03" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-bundle-mp3" version = "0.5.5" @@ -4023,8 +4444,19 @@ checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" dependencies = [ "lazy_static", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350f1f2f2e19ad4dd315db94304d1eb361b29af070681f94e51b8fdaad769546" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.6.0", ] [[package]] @@ -4035,7 +4467,19 @@ checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" dependencies = [ "lazy_static", "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1979c515a76371b186aad2feff5f23e21cbec775bf95de08bf1e3af92a2ad76" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", ] [[package]] @@ -4045,7 +4489,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebbdfd76d6cc5a601c6292a44357c5b7c82f2cd7cdc0f171421f5c5cff0ea1f" +dependencies = [ + "log", + "symphonia-core 0.6.0", ] [[package]] @@ -4055,7 +4509,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a149cbfc7fb5c405d123a273227d31de17138419552112bf1aa7b73e65827b8" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", ] [[package]] @@ -4065,7 +4530,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50baee168f0e9dcf6ba7fc06e8b57eb62072a4490cc7cf13af77e72baae5d328" +dependencies = [ + "log", + "symphonia-core 0.6.0", ] [[package]] @@ -4075,10 +4550,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-codec-vorbis" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b07b4423cd8e0fc472575909a5554b12c2f58e3c190b38c24f042e732fd8de" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8257891ffa7f05e02b58f4761e2abf7e5278c8744fd59e981559e050f86eef55" +dependencies = [ + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-core" version = "0.5.5" @@ -4092,6 +4589,32 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ec293b5f288383b72a7bffcade6b2860b642cf66f28b3bd5967349a49938b1" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "lazy_static", + "log", + "num-complex", + "rustfft", + "smallvec", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3ca76633d3400ab57195456c09f8a58d775ff5452329f3f212b6efc8622f5" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + [[package]] name = "symphonia-format-isomp4" version = "0.5.5" @@ -4100,11 +4623,23 @@ checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" dependencies = [ "encoding_rs", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-isomp4" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d179a01305b3505940135a9f0180d6ef4b487912748fe97554756f120fbd05e" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-format-mkv" version = "0.5.5" @@ -4113,11 +4648,23 @@ checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" dependencies = [ "lazy_static", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-mkv" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb17713e134f5ad316c2690fa3104590ccc85842cdbcf82c3cd1a845cb08aa74" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + [[package]] name = "symphonia-format-ogg" version = "0.5.5" @@ -4125,11 +4672,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" dependencies = [ "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-ogg" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05a67e02b1e4fca1a261ba4fe06910a9357489ad8c36aafdd2960e9c6559433" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-format-riff" version = "0.5.5" @@ -4138,8 +4697,20 @@ checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" dependencies = [ "extended", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17424452a777666d3eaf09a5c651029b15b6a333812fcc5b5474f2a3f0cff3f0" +dependencies = [ + "extended", + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", ] [[package]] @@ -4151,7 +4722,20 @@ dependencies = [ "encoding_rs", "lazy_static", "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-metadata" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31acf5cd623398a6208e2225d18f4b20f761c55098a796a5247ad516a4a8681" +dependencies = [ + "lazy_static", + "log", + "regex-lite", + "smallvec", + "symphonia-core 0.6.0", ] [[package]] @@ -4160,8 +4744,8 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" dependencies = [ - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", ] [[package]] @@ -4171,15 +4755,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -4203,7 +4786,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4221,32 +4804,34 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.6" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", - "core-foundation", - "core-graphics", + "core-foundation 0.10.1", + "core-graphics 0.25.0", "crossbeam-channel", + "dbus", "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", - "ndk 0.9.0", - "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk", + "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", @@ -4265,7 +4850,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4276,9 +4861,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" dependencies = [ "anyhow", "bytes", @@ -4291,7 +4876,8 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "image", + "jni 0.21.1", "libc", "log", "mime", @@ -4325,23 +4911,11 @@ dependencies = [ "windows 0.61.3", ] -[[package]] -name = "tauri-app" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-opener", - "web-audio-api", -] - [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -4355,29 +4929,28 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", - "sha2", - "syn 2.0.117", + "sha2 0.10.9", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", "time", @@ -4388,23 +4961,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -4413,15 +4986,56 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.12+spec-1.1.0", "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-opener" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -4439,17 +5053,33 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-store" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -4466,13 +5096,13 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -4492,24 +5122,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever 0.29.1", "http", "infer", "json-patch", - "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf", + "plist", "proc-macro2", "quote", "regex", @@ -4521,7 +5151,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4530,13 +5160,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", ] [[package]] @@ -4546,21 +5176,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -4599,7 +5218,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4610,17 +5229,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -4630,15 +5248,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -4646,28 +5264,66 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4699,15 +5355,30 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -4728,9 +5399,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4741,7 +5412,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4752,7 +5423,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4761,30 +5432,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.0.1+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -4803,20 +5474,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4837,6 +5508,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4850,7 +5522,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4872,11 +5544,29 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "trash" +version = "5.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7602e0c7d66ec2d92a8c917219fbc7894039efa2063b9064260110828a356f46" +dependencies = [ + "chrono", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -4888,10 +5578,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4908,9 +5598,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -4964,6 +5654,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4971,16 +5667,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "url" @@ -4995,6 +5700,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -5021,16 +5732,22 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vecmath" version = "1.0.0" @@ -5091,12 +5808,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -5105,27 +5816,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -5136,23 +5838,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5160,48 +5858,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -5215,23 +5891,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", -] - [[package]] name = "web-audio-api" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "015053d21b75159d00b45f8b862d9c05b783440537d9751d659ad2982911eb7f" +checksum = "6f2bc19aae4c9275d5f8c12f851bcc462630bb3e4996cb9a22b52cbcc8f386bd" dependencies = [ "almost", "arc-swap", @@ -5254,15 +5918,15 @@ dependencies = [ "realfft", "rubato 0.16.2", "smallvec", - "symphonia", + "symphonia 0.6.0", "vecmath", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5270,14 +5934,14 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -5334,8 +5998,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -5346,7 +6010,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5360,6 +6024,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + [[package]] name = "winapi" version = "0.3.9" @@ -5408,11 +6078,20 @@ dependencies = [ [[package]] name = "windows" -version = "0.54.0" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ - "windows-core 0.54.0", + "windows-core 0.56.0", "windows-targets 0.52.6", ] @@ -5422,11 +6101,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -5438,12 +6129,23 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" -version = "0.54.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -5454,8 +6156,8 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -5467,8 +6169,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -5482,7 +6184,29 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -5493,7 +6217,18 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -5504,7 +6239,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5529,6 +6264,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -5667,6 +6412,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -5828,15 +6582,12 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -5853,103 +6604,21 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.54.4" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2", @@ -5963,9 +6632,9 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", - "ndk 0.9.0", + "ndk", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -5975,7 +6644,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -6012,9 +6681,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6023,21 +6692,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zbus" -version = "5.14.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast", "async-executor", @@ -6062,7 +6731,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -6070,14 +6739,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zbus_names", "zvariant", "zvariant_utils", @@ -6085,61 +6754,41 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 0.7.15", + "winnow 1.0.3", "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.8.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5030500cb2d66bdfbb4ebc9563be6ce7005a4b5d0f26be0c523870fe372ca6" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f86989a046a79640b9d8867c823349a139367bda96549794fcc3313ce91f4e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6148,9 +6797,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6159,13 +6808,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6176,40 +6825,40 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.10.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.15", + "winnow 1.0.3", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", - "winnow 0.7.15", + "syn 2.0.118", + "winnow 1.0.3", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 47b049b..00337c1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,26 +1,34 @@ [package] -name = "tauri-app" +name = "rust-echo-music" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "A desktop music player built with Rust and Tauri" +authors = ["RustEchoMusic"] edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] -# The `_lib` suffix may seem redundant but it is necessary -# to make the lib name unique and wouldn't conflict with the bin name. -# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 -name = "tauri_app_lib" +name = "rust_echo_music_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -web-audio-api = "1.2.0" - +web-audio-api = "1.6.0" +lofty = "0.24.0" +base64 = "0.22.1" +walkdir = "2.5.0" +tauri-plugin-dialog = "2" +tauri-plugin-store = "2" +trash = "5.2.6" +souvlaki = "0.8.3" +sqlx = { version = "0.9", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } +rand = "0.10.1" +tokio = { version = "1.52.3", features = ["fs"] } +libloading = "0.9" +plugin-sdk = { path = "crates/plugin-sdk" } +lyrics-plugin = { path = "crates/lyrics-plugin" } +eq-plugin = { path = "crates/eq-plugin" } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 8ff4eff..f6b915d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,6 +12,8 @@ "core:window:allow-start-dragging", "core:window:allow-minimize", "core:window:allow-toggle-maximize", - "core:window:allow-close" + "core:window:allow-close", + "dialog:default", + "store:default" ] } \ No newline at end of file diff --git a/src-tauri/crates/audio-gain/Cargo.lock b/src-tauri/crates/audio-gain/Cargo.lock new file mode 100644 index 0000000..183a387 --- /dev/null +++ b/src-tauri/crates/audio-gain/Cargo.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "audio-gain" +version = "0.1.0" +dependencies = [ + "plugin-sdk", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "plugin-sdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-tauri/crates/audio-gain/Cargo.toml b/src-tauri/crates/audio-gain/Cargo.toml new file mode 100644 index 0000000..693d1f0 --- /dev/null +++ b/src-tauri/crates/audio-gain/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "audio-gain" +version = "0.1.0" +edition = "2021" +description = "Example packaged audio plugin verifying the shared-memory FFI protocol" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +plugin-sdk = { path = "../plugin-sdk" } + +# 独立 workspace:使本 packaged 插件脱离项目根 workspace, +# 避免被 `d:\projects\RustEchoMusic\Cargo.toml` 的 members 声明吞并, +# 从而可独立 `cargo build --manifest-path ... --release` 编译。 +[workspace] diff --git a/src-tauri/crates/audio-gain/plugin.json b/src-tauri/crates/audio-gain/plugin.json new file mode 100644 index 0000000..eced309 --- /dev/null +++ b/src-tauri/crates/audio-gain/plugin.json @@ -0,0 +1,19 @@ +{ + "id": "audio-gain", + "source": "packaged", + "name": "audio-gain", + "displayName": "Audio Gain", + "version": "0.1.0", + "author": "RustEchoMusic", + "description": "示例音频插件:将音量降低 20%(验证共享内存 FFI 协议)", + "entry": "audio-gain", + "minAppVersion": "0.1.0", + "permissions": ["audio"], + "activationEvents": ["onStartup"], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [], + "nativeViews": [] + } +} diff --git a/src-tauri/crates/audio-gain/src/lib.rs b/src-tauri/crates/audio-gain/src/lib.rs new file mode 100644 index 0000000..80bb886 --- /dev/null +++ b/src-tauri/crates/audio-gain/src/lib.rs @@ -0,0 +1,132 @@ +//! audio-gain 示例 packaged 插件:将音频乘以 0.8(音量降低约 1.94 dB)。 +//! +//! 用于验证共享内存 FFI 协议端到端可用: +//! - 宿主通过 `get_plugin_exports` 取得 `PluginExports` +//! - 宿主通过 `rem_audio_processor` 静态符号取得 `*const AudioProcessorApi` +//! - 激活后 `ExternalProcessorNode` 在每个量子调用 `process` 回调 + +use plugin_sdk::ffi::{ + AudioProcessorApi, ByteBuffer, CommandRequest, CommandResponse, EventPayload, PluginApi, + PluginExports, PluginHandle, +}; +use std::ptr; + +/// 增益系数(0.8 ≈ -1.94 dB)。 +const GAIN: f32 = 0.8; + +/// 实时音频处理回调:把交错 PCM 每个样本乘以 GAIN。 +/// +/// - `input` / `output`:交错 PCM,长度 = frames * channels +/// - 返回 0 表示成功,非 0 表示错误(宿主将跳过该插件) +unsafe extern "C" fn process_audio( + _handle: PluginHandle, + input: *const f32, + output: *mut f32, + frames: usize, + channels: usize, +) -> i32 { + let total = frames.checked_mul(channels).unwrap_or(0); + if input.is_null() || output.is_null() || total == 0 { + return -1; + } + let in_slice = std::slice::from_raw_parts(input, total); + let out_slice = std::slice::from_raw_parts_mut(output, total); + for (i, &s) in in_slice.iter().enumerate() { + out_slice[i] = s * GAIN; + } + 0 +} + +static AUDIO_PROCESSOR: AudioProcessorApi = AudioProcessorApi { + init: None, + process: Some(process_audio), + reset: None, +}; + +/// `repr(transparent)` Sync 包装:裸指针 `*const T` 不实现 `Sync`,无法直接作为 +/// `static` 暴露。透明布局保证宿主 `lib.get::<*const AudioProcessorApi>(...)` +/// 读取到的内存即裸指针本身,与无包装时逐字节一致。 +/// +/// 安全性:指向的 `AUDIO_PROCESSOR` 为不可变 `static`,且 `AudioProcessorApi` +/// 由函数指针组成(自动 `Sync`),跨线程共享该指针安全(与 `dynamic_factory` +/// 中 `unsafe impl Send/Sync for DynamicPluginFactory` 同源)。 +#[repr(transparent)] +pub struct SyncAudioProcessorPtr(*const AudioProcessorApi); +unsafe impl Sync for SyncAudioProcessorPtr {} + +/// 音频处理器符号:宿主 `dynamic_factory` 通过 +/// `lib.get::<*const AudioProcessorApi>(b"rem_audio_processor")` 读取此静态指针, +/// 解引用得到 `AudioProcessorApi` 拷贝(结构体 derive Copy)。 +#[no_mangle] +pub static rem_audio_processor: SyncAudioProcessorPtr = SyncAudioProcessorPtr(&AUDIO_PROCESSOR); + +// --- 最小化 PluginExports:本插件不响应命令,但仍需导出符号供 dynamic_factory 加载 --- + +unsafe extern "C" fn create_plugin() -> PluginHandle { + ptr::null_mut() +} + +unsafe extern "C" fn destroy_plugin(_handle: PluginHandle) {} + +unsafe extern "C" fn free_buffer(_buffer: ByteBuffer) {} + +unsafe extern "C" fn activate(_handle: PluginHandle) -> i32 { + 0 +} + +unsafe extern "C" fn deactivate(_handle: PluginHandle) -> i32 { + 0 +} + +unsafe extern "C" fn on_event(_handle: PluginHandle, _payload: EventPayload) -> i32 { + 0 +} + +unsafe extern "C" fn execute_command( + _handle: PluginHandle, + _req: *const CommandRequest, +) -> CommandResponse { + CommandResponse { + success: false, + data: ByteBuffer { + ptr: ptr::null_mut(), + len: 0, + }, + } +} + +/// `repr(transparent)` Sync 包装:`PluginExports` 含 `audio_processor: *const _` +/// 裸指针,编译器不再自动派生 `Sync`,无法直接作为 `static`。透明布局使内存 +/// 与裸 `PluginExports` 一致;宿主从不直接按符号读取本静态(仅通过 +/// `get_plugin_exports` 函数取值),布局仅为保险。 +#[repr(transparent)] +pub struct SyncPluginExports(PluginExports); +unsafe impl Sync for SyncPluginExports {} + +/// 插件导出表(单一真相源)。`audio_processor` 字段按 Task 8 设计直接填充为 +/// `&AudioProcessorApi`(新插件规范);宿主当前实际通过 `rem_audio_processor` +/// 符号读取以兼容旧插件,两者保持一致。 +#[no_mangle] +pub static rem_plugin_exports: SyncPluginExports = SyncPluginExports(PluginExports { + abi_version: 1, + create: create_plugin, + destroy: destroy_plugin, + free_buffer: free_buffer, + api: PluginApi { + activate, + deactivate, + on_event, + execute_command, + }, + audio_processor: &AUDIO_PROCESSOR, +}); + +/// 宿主实际调用的入口:`dynamic_factory` 通过 +/// `lib.get(b"get_plugin_exports")` 查找此函数并调用以取得 `PluginExports` 值。 +/// +/// `PluginExports` 含裸指针与函数指针(均 Copy),但结构体本身未 derive Copy, +/// 无法直接从 static move;用 `ptr::read` 对内部字段做按位拷贝返回。 +#[no_mangle] +pub extern "C" fn get_plugin_exports() -> PluginExports { + unsafe { ptr::read(&rem_plugin_exports.0) } +} diff --git a/src-tauri/crates/eq-plugin/Cargo.toml b/src-tauri/crates/eq-plugin/Cargo.toml new file mode 100644 index 0000000..ff7756e --- /dev/null +++ b/src-tauri/crates/eq-plugin/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "eq-plugin" +version = "0.1.0" +edition = "2021" +description = "Audio equalizer plugin for Rust Echo Music" + +[dependencies] +plugin-sdk = { path = "../plugin-sdk" } +serde_json = "1" +serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/crates/eq-plugin/src/lib.rs b/src-tauri/crates/eq-plugin/src/lib.rs new file mode 100644 index 0000000..57d7432 --- /dev/null +++ b/src-tauri/crates/eq-plugin/src/lib.rs @@ -0,0 +1,166 @@ +use std::sync::{Arc, RwLock}; + +use plugin_sdk::context::PluginContext; +use plugin_sdk::errors::PluginError; +use plugin_sdk::events::PluginEvent; +use plugin_sdk::settings::SettingValue; +use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; + +/// 10 段音频均衡器插件。 +/// +/// 内部缓存当前 bands 与 enabled 状态,方便快速查询; +/// 真正作用于音频引擎的操作全部经由 `ctx.audio` 完成。 +pub struct EqPlugin { + bands: RwLock<[f64; 10]>, + enabled: RwLock, +} + +impl EqPlugin { + pub fn new() -> Self { + Self { + bands: RwLock::new([0.0; 10]), + enabled: RwLock::new(true), + } + } + + /// 从插件设置读取 `preset` 与 `enabled` 并应用到音频引擎。 + /// + /// - `preset` 解析失败(缺失 / 非 Json / 长度不符)时回退到 flat [0;10]。 + /// - `enabled` 缺失或非 Bool 时回退到 true。 + /// 解析结果会同步写入内部缓存。 + fn apply_from_settings(&self, ctx: &PluginContext) -> Result<(), PluginError> { + // 读取预设频段,默认 flat + let bands: [f64; 10] = ctx + .plugin_settings + .get_setting("preset") + .and_then(|v| match v { + SettingValue::Json(v) => serde_json::from_value::<[f64; 10]>(v).ok(), + _ => None, + }) + .unwrap_or([0.0; 10]); + + // 读取启用状态,默认 true + let enabled: bool = ctx + .plugin_settings + .get_setting("enabled") + .and_then(|v| match v { + SettingValue::Bool(b) => Some(b), + _ => None, + }) + .unwrap_or(true); + + // 应用到音频引擎 + ctx.audio.apply_preset(bands)?; + ctx.audio.set_enabled(enabled)?; + + // 同步内部缓存 + if let Ok(mut b) = self.bands.write() { + *b = bands; + } + if let Ok(mut e) = self.enabled.write() { + *e = enabled; + } + Ok(()) + } +} + +impl Plugin for EqPlugin { + fn activate(&self, ctx: &PluginContext) -> Result<(), PluginError> { + self.apply_from_settings(ctx) + } + + fn deactivate(&self) -> Result<(), PluginError> { + // deactivate 无 ctx 参数,仅清内部缓存状态; + // 音频引擎侧的启停由 activate / 事件处理负责。 + if let Ok(mut b) = self.bands.write() { + *b = [0.0; 10]; + } + if let Ok(mut e) = self.enabled.write() { + *e = false; + } + Ok(()) + } + + fn on_event(&self, event: &PluginEvent, ctx: &PluginContext) -> Result<(), PluginError> { + match event { + PluginEvent::Startup => self.apply_from_settings(ctx), + PluginEvent::SettingsChanged => self.apply_from_settings(ctx), + _ => Ok(()), + } + } + + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + ctx: &PluginContext, + ) -> Result<(), PluginError> { + match command_id { + // 应用 EQ 预设:RawPayload 携带 JSON 数组,如 "[0,0,0,0,0,0,0,0,0,0]"。 + // 当从 TrackContextMenu 触发时,args 为 TrackId(EQ 是全局效果,忽略曲目上下文), + // 此时回退到设置中的当前 preset 重新应用,保证菜单项可点击以验证菜单扩展渲染。 + "eq.apply_preset" => { + let bands: [f64; 10] = match args { + CommandArgs::RawPayload(s) => serde_json::from_str(s).map_err(|e| { + PluginError::Plugin(format!("Invalid preset JSON: {}", e)) + })?, + CommandArgs::TrackId(_) => { + // 从已缓存的 bands 重新应用到音频引擎(无操作变化) + self.bands.read().map(|b| *b).unwrap_or([0.0; 10]) + } + _ => { + return Err(PluginError::Plugin( + "eq.apply_preset requires RawPayload or TrackId".into(), + )) + } + }; + ctx.audio.apply_preset(bands)?; + if let Ok(mut b) = self.bands.write() { + *b = bands; + } + let value = serde_json::to_value(&bands) + .map_err(|e| PluginError::Plugin(format!("Failed to serialize preset: {}", e)))?; + ctx.plugin_settings + .set_setting("preset", SettingValue::Json(value))?; + Ok(()) + } + // 重置 EQ 为 flat + "eq.reset" => { + let bands: [f64; 10] = [0.0; 10]; + ctx.audio.apply_preset(bands)?; + if let Ok(mut b) = self.bands.write() { + *b = bands; + } + let value = serde_json::to_value(&bands) + .map_err(|e| PluginError::Plugin(format!("Failed to serialize preset: {}", e)))?; + ctx.plugin_settings + .set_setting("preset", SettingValue::Json(value))?; + Ok(()) + } + // 切换 EQ 开关 + "eq.toggle" => { + let cur = ctx.audio.is_enabled()?; + let next = !cur; + ctx.audio.set_enabled(next)?; + if let Ok(mut e) = self.enabled.write() { + *e = next; + } + ctx.plugin_settings + .set_setting("enabled", SettingValue::Bool(next))?; + Ok(()) + } + _ => Err(PluginError::Plugin(format!( + "Unknown command: {}", + command_id + ))), + } + } +} + +pub struct EqPluginFactory; + +impl PluginFactory for EqPluginFactory { + fn create(&self) -> Arc { + Arc::new(EqPlugin::new()) + } +} diff --git a/src-tauri/crates/lyrics-plugin/Cargo.toml b/src-tauri/crates/lyrics-plugin/Cargo.toml new file mode 100644 index 0000000..a354368 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "lyrics-plugin" +version = "0.1.0" +edition = "2021" +description = "Lyrics plugin for Rust Echo Music" + +[dependencies] +plugin-sdk = { path = "../plugin-sdk" } +lofty = "0.24.0" +serde_json = "1" +serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/crates/lyrics-plugin/src/cache.rs b/src-tauri/crates/lyrics-plugin/src/cache.rs new file mode 100644 index 0000000..31244cc --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/cache.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use super::models::LyricDocument; + +pub struct LyricsCacheService { + cache_dir: PathBuf, +} + +impl LyricsCacheService { + pub fn new(cache_dir: PathBuf) -> Self { + Self { cache_dir } + } + + pub fn init(&self) -> Result<(), String> { + std::fs::create_dir_all(&self.cache_dir) + .map_err(|e| format!("Failed to create lyrics cache dir: {}", e)) + } + + fn cache_path(&self, song_id: i64) -> PathBuf { + self.cache_dir.join(format!("{}.json", song_id)) + } + + pub fn save(&self, song_id: i64, document: &LyricDocument) -> Result<(), String> { + let content = serde_json::to_string_pretty(document) + .map_err(|e| format!("Failed to serialize lyrics: {}", e))?; + std::fs::write(self.cache_path(song_id), content) + .map_err(|e| format!("Failed to write lyrics cache: {}", e)) + } + + pub fn load(&self, song_id: i64) -> Option { + let path = self.cache_path(song_id); + if !path.exists() { + return None; + } + let content = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&content).ok() + } + + pub fn exists(&self, song_id: i64) -> bool { + self.cache_path(song_id).exists() + } + + pub fn clear(&self) -> Result<(), String> { + if !self.cache_dir.exists() { + return Ok(()); + } + let entries = std::fs::read_dir(&self.cache_dir) + .map_err(|e| format!("Failed to read cache dir: {}", e))?; + for entry in entries.flatten() { + let _ = std::fs::remove_file(entry.path()); + } + Ok(()) + } +} diff --git a/src-tauri/crates/lyrics-plugin/src/lib.rs b/src-tauri/crates/lyrics-plugin/src/lib.rs new file mode 100644 index 0000000..48e62f6 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/lib.rs @@ -0,0 +1,196 @@ +use std::sync::{Arc, RwLock}; + +use plugin_sdk::context::PluginContext; +use plugin_sdk::errors::PluginError; +use plugin_sdk::events::{LyricsLoadedEvent, PluginEvent}; +use plugin_sdk::settings::SettingValue; +use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; + +mod cache; +mod models; +mod service; + +use cache::LyricsCacheService; +use models::LyricDocument; +use service::LyricsService; + +struct LyricsState { + song_id: Option, + lyrics: Option, +} + +pub struct LyricsPlugin { + cache: RwLock>, + state: RwLock, +} + +impl LyricsPlugin { + pub fn new() -> Self { + Self { + cache: RwLock::new(None), + state: RwLock::new(LyricsState { song_id: None, lyrics: None }), + } + } + + fn get_bool_setting(ctx: &PluginContext, key: &str) -> bool { + ctx.plugin_settings + .get_setting(key) + .and_then(|v| match v { + SettingValue::Bool(b) => Some(b), + _ => None, + }) + .unwrap_or(true) + } + + fn init_cache(&self, ctx: &PluginContext) { + let mut cache_lock = match self.cache.write() { + Ok(guard) => guard, + Err(_) => return, + }; + if cache_lock.is_some() { + return; + } + let cache_dir = match ctx.cache_dir() { + Ok(dir) => dir, + Err(_) => return, + }; + let svc = LyricsCacheService::new(cache_dir); + if let Err(e) = svc.init() { + eprintln!("[Lyrics] Failed to init cache: {}", e); + } + *cache_lock = Some(svc); + } + + fn handle_track_changed(&self, track_id: i64, file_path: &str, ctx: &PluginContext) { + { + let mut state = match self.state.write() { + Ok(guard) => guard, + Err(_) => return, + }; + if state.song_id == Some(track_id) { + return; + } + state.song_id = Some(track_id); + state.lyrics = None; + } + + let auto_search = Self::get_bool_setting(ctx, "auto_search"); + if !auto_search { + return; + } + + let cache_guard = match self.cache.read() { + Ok(guard) => guard, + Err(_) => return, + }; + let cache = match cache_guard.as_ref() { + Some(c) => c, + None => return, + }; + + let path = std::path::Path::new(file_path); + if let Some(doc) = LyricsService::load_or_fetch(cache, track_id, path) { + let event = LyricsLoadedEvent { + song_id: doc.song_id, + timestamp_ms_list: doc.lines.iter().map(|l| l.timestamp_ms).collect(), + text_list: doc.lines.iter().map(|l| l.text.clone()).collect(), + }; + if let Err(e) = ctx.emit_lyrics_loaded(event) { + eprintln!("[Lyrics] Failed to emit lyrics: {}", e); + } + if let Ok(mut state) = self.state.write() { + if state.song_id == Some(track_id) { + state.lyrics = Some(doc); + } + } + } + } +} + +impl Plugin for LyricsPlugin { + fn activate(&self, ctx: &PluginContext) -> Result<(), PluginError> { + self.init_cache(ctx); + Ok(()) + } + + fn deactivate(&self) -> Result<(), PluginError> { + if let Ok(mut state) = self.state.write() { + state.song_id = None; + state.lyrics = None; + } + Ok(()) + } + + fn on_event(&self, event: &PluginEvent, ctx: &PluginContext) -> Result<(), PluginError> { + if let PluginEvent::TrackChanged { track, .. } = event { + let path = ctx.library.get_track_path(track.id)?; + if let Some(path) = path { + self.handle_track_changed(track.id, &path.to_string_lossy(), ctx); + } + } + Ok(()) + } + + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + ctx: &PluginContext, + ) -> Result<(), PluginError> { + match command_id { + "lyrics.search" => { + if let CommandArgs::LyricsSearch { title, artist } = args { + println!("[Lyrics] search '{} - {}' (local only)", title, artist); + Ok(()) + } else { + Err(PluginError::Plugin("lyrics.search requires LyricsSearch args".into())) + } + } + "lyrics.load" => { + if let CommandArgs::TrackId(song_id) = args { + let path = ctx.library.get_track_path(*song_id)? + .ok_or_else(|| PluginError::Plugin(format!("Track path not found: {}", song_id)))?; + + let cache_guard = self.cache.read().map_err(|e| PluginError::Plugin(e.to_string()))?; + let cache = cache_guard.as_ref() + .ok_or_else(|| PluginError::Plugin("Cache not initialized".into()))?; + + if let Some(doc) = LyricsService::load_or_fetch(cache, *song_id, &path) { + let event = LyricsLoadedEvent { + song_id: doc.song_id, + timestamp_ms_list: doc.lines.iter().map(|l| l.timestamp_ms).collect(), + text_list: doc.lines.iter().map(|l| l.text.clone()).collect(), + }; + ctx.emit_lyrics_loaded(event)?; + if let Ok(mut state) = self.state.write() { + state.song_id = Some(*song_id); + state.lyrics = Some(doc); + } + return Ok(()); + } + + Err(PluginError::Plugin(format!("No lyrics found for song_id: {}", song_id))) + } else { + Err(PluginError::Plugin("lyrics.load requires TrackId".into())) + } + } + "lyrics.clear_cache" => { + if let Ok(cache_guard) = self.cache.read() { + if let Some(ref cache) = *cache_guard { + cache.clear().map_err(|e| PluginError::Plugin(e.to_string()))?; + } + } + Ok(()) + } + _ => Err(PluginError::Plugin(format!("Unknown command: {}", command_id))), + } + } +} + +pub struct LyricsPluginFactory; + +impl PluginFactory for LyricsPluginFactory { + fn create(&self) -> Arc { + Arc::new(LyricsPlugin::new()) + } +} diff --git a/src-tauri/crates/lyrics-plugin/src/models.rs b/src-tauri/crates/lyrics-plugin/src/models.rs new file mode 100644 index 0000000..4cca026 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/models.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LyricLine { + pub timestamp_ms: u64, + pub text: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LyricDocument { + pub song_id: i64, + pub lines: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LyricsSearchResult { + pub song_id: i64, + pub title: String, + pub artist: String, + pub source: String, +} diff --git a/src-tauri/crates/lyrics-plugin/src/service.rs b/src-tauri/crates/lyrics-plugin/src/service.rs new file mode 100644 index 0000000..659de51 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/service.rs @@ -0,0 +1,113 @@ +use std::path::Path; + +use lofty::prelude::*; +use lofty::probe::Probe; +use lofty::tag::ItemKey; + +use super::cache::LyricsCacheService; +use super::models::{LyricDocument, LyricLine}; + +pub struct LyricsService; + +impl LyricsService { + pub fn read_from_file(file_path: &Path, song_id: i64) -> Option { + let tagged_file = Probe::open(file_path).ok()?.read().ok()?; + let tag = tagged_file + .primary_tag() + .or_else(|| tagged_file.first_tag())?; + + let lyrics_text = tag + .get_items(ItemKey::Lyrics) + .next() + .or_else(|| tag.get_items(ItemKey::UnsyncLyrics).next()) + .and_then(|item| item.value().text())?; + + let lines = Self::parse_lrc(lyrics_text); + + if lines.is_empty() { + return None; + } + + Some(LyricDocument { song_id, lines }) + } + + pub fn load_or_fetch( + cache: &LyricsCacheService, + song_id: i64, + file_path: &Path, + ) -> Option { + if let Some(doc) = cache.load(song_id) { + return Some(doc); + } + + let doc = Self::read_from_file(file_path, song_id)?; + + if let Err(e) = cache.save(song_id, &doc) { + eprintln!( + "[Lyrics] Failed to cache lyrics for song {}: {}", + song_id, e + ); + } + + Some(doc) + } + + pub fn parse_lrc(content: &str) -> Vec { + let mut lines = Vec::new(); + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if let Some(parsed) = Self::parse_lrc_line(trimmed) { + lines.push(parsed); + } + } + lines.sort_by(|a, b| a.timestamp_ms.cmp(&b.timestamp_ms)); + lines + } + + fn parse_lrc_line(line: &str) -> Option { + if !line.starts_with('[') { + return None; + } + let close = line.find(']')?; + let tag = &line[1..close]; + let text = line[close + 1..].trim().to_string(); + + let timestamp_ms = Self::parse_lrc_timestamp(tag)?; + Some(LyricLine { timestamp_ms, text }) + } + + fn parse_lrc_timestamp(tag: &str) -> Option { + let parts: Vec<&str> = tag.split(':').collect(); + + let (hours, minutes, sec_str) = match parts.len() { + 2 => (0u64, parts[0].parse::().ok()?, parts[1]), + 3 => ( + parts[0].parse::().ok()?, + parts[1].parse::().ok()?, + parts[2], + ), + _ => return None, + }; + + let sec_parts: Vec<&str> = sec_str.split('.').collect(); + if sec_parts.len() != 2 { + return None; + } + + let seconds: u64 = sec_parts[0].parse().ok()?; + let frac_str = sec_parts[1]; + let frac_value: u64 = frac_str.parse().ok()?; + + let milliseconds = match frac_str.len() { + 1 => frac_value * 100, + 2 => frac_value * 10, + 3 => frac_value, + _ => frac_value, + }; + + Some(hours * 3_600_000 + minutes * 60_000 + seconds * 1_000 + milliseconds) + } +} diff --git a/src-tauri/crates/plugin-sdk/Cargo.toml b/src-tauri/crates/plugin-sdk/Cargo.toml new file mode 100644 index 0000000..7d87273 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "plugin-sdk" +version = "0.1.0" +edition = "2021" +description = "Plugin SDK for Rust Echo Music" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/src-tauri/crates/plugin-sdk/src/api.rs b/src-tauri/crates/plugin-sdk/src/api.rs new file mode 100644 index 0000000..32b98a4 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/api.rs @@ -0,0 +1,46 @@ +use std::path::PathBuf; + +use crate::errors::PluginError; + +pub trait PlayerApi: Send + Sync { + fn play(&self) -> Result<(), PluginError>; + fn pause(&self) -> Result<(), PluginError>; + fn next(&self) -> Result<(), PluginError>; + fn previous(&self) -> Result<(), PluginError>; + fn current_track_id(&self) -> Result, PluginError>; +} + +pub trait LibraryApi: Send + Sync { + fn get_track_path(&self, track_id: i64) -> Result, PluginError>; + fn exists(&self, track_id: i64) -> Result; +} + +pub trait QueueApi: Send + Sync { + fn current_queue(&self) -> Result, PluginError>; + fn remove_track(&self, track_id: i64) -> Result<(), PluginError>; + fn clear(&self) -> Result<(), PluginError>; +} + +pub trait SettingsApi: Send + Sync { + fn theme(&self) -> Result; + fn set_theme(&self, theme: String) -> Result<(), PluginError>; +} + +pub trait PluginSettingsApi: Send + Sync { + fn get_setting(&self, key: &str) -> Option; + fn set_setting(&self, key: &str, value: crate::settings::SettingValue) -> Result<(), PluginError>; +} + +/// 音频处理 API:暴露 10 段 EQ 给插件控制。 +pub trait AudioProcessingApi: Send + Sync { + /// 设置指定频段(0..10)的增益(dB)。 + fn set_band_gain(&self, band: usize, gain_db: f64) -> Result<(), PluginError>; + /// 一次性应用 10 段 EQ 预设(dB)。 + fn apply_preset(&self, gains: [f64; 10]) -> Result<(), PluginError>; + /// 读取当前 10 段增益(dB)。 + fn get_bands(&self) -> Result<[f64; 10], PluginError>; + /// 启用 / 旁路 EQ。 + fn set_enabled(&self, enabled: bool) -> Result<(), PluginError>; + /// 查询 EQ 是否启用。 + fn is_enabled(&self) -> Result; +} diff --git a/src-tauri/crates/plugin-sdk/src/context.rs b/src-tauri/crates/plugin-sdk/src/context.rs new file mode 100644 index 0000000..81df525 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/context.rs @@ -0,0 +1,56 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use crate::api::{AudioProcessingApi, LibraryApi, PlayerApi, PluginSettingsApi, QueueApi, SettingsApi}; +use crate::errors::PluginError; +use crate::events::LyricsLoadedEvent; + +pub trait HostContext: Send + Sync { + fn plugin_id(&self) -> &str; + fn cache_dir(&self) -> Result; + fn emit_lyrics_loaded(&self, event: LyricsLoadedEvent) -> Result<(), PluginError>; +} + +pub struct PluginContext { + pub host: Arc, + pub player: Arc, + pub library: Arc, + pub queue: Arc, + pub settings: Arc, + pub plugin_settings: Arc, + pub audio: Arc, +} + +impl PluginContext { + pub fn new( + host: Arc, + player: Arc, + library: Arc, + queue: Arc, + settings: Arc, + plugin_settings: Arc, + audio: Arc, + ) -> Self { + Self { + host, + player, + library, + queue, + settings, + plugin_settings, + audio, + } + } + + pub fn plugin_id(&self) -> &str { + self.host.plugin_id() + } + + pub fn cache_dir(&self) -> Result { + self.host.cache_dir() + } + + pub fn emit_lyrics_loaded(&self, event: LyricsLoadedEvent) -> Result<(), PluginError> { + self.host.emit_lyrics_loaded(event) + } +} diff --git a/src-tauri/crates/plugin-sdk/src/contributes.rs b/src-tauri/crates/plugin-sdk/src/contributes.rs new file mode 100644 index 0000000..24fdb60 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/contributes.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CommandContribution { + pub id: String, + pub title: String, + pub category: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum MenuLocation { + TrackContextMenu, + SidebarActions, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MenuContribution { + pub command: String, + pub title: String, + pub location: MenuLocation, + pub group: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SidebarContribution { + pub id: String, + pub title: String, + pub icon: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ViewContribution { + pub id: String, + pub title: String, + pub entry: String, + pub icon: Option, +} + +/// 原生视图贡献:插件声明它要使用应用预置的哪个原生组件。 +/// 应用按 `token` 查找对应原生组件并注入渲染。 +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NativeViewContribution { + pub id: String, + pub title: String, + /// 组件令牌,应用按此查找组件 + pub token: String, + pub icon: Option, +} diff --git a/src-tauri/crates/plugin-sdk/src/errors.rs b/src-tauri/crates/plugin-sdk/src/errors.rs new file mode 100644 index 0000000..766f67c --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/errors.rs @@ -0,0 +1,43 @@ +use crate::permissions::PluginPermission; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PluginError { + Plugin(String), + PermissionDenied { + plugin_id: String, + permission: PluginPermission, + }, + Io(String), +} + +impl std::fmt::Display for PluginError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginError::Plugin(msg) => write!(f, "Plugin error: {}", msg), + PluginError::PermissionDenied { plugin_id, permission } => { + write!(f, "Plugin '{}' denied permission {:?}", plugin_id, permission) + } + PluginError::Io(msg) => write!(f, "Plugin IO error: {}", msg), + } + } +} + +impl std::error::Error for PluginError {} + +impl From for PluginError { + fn from(e: std::io::Error) -> Self { + PluginError::Io(e.to_string()) + } +} + +impl From for PluginError { + fn from(s: String) -> Self { + PluginError::Plugin(s) + } +} + +impl From<&str> for PluginError { + fn from(s: &str) -> Self { + PluginError::Plugin(s.to_string()) + } +} diff --git a/src-tauri/crates/plugin-sdk/src/events.rs b/src-tauri/crates/plugin-sdk/src/events.rs new file mode 100644 index 0000000..77878ac --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/events.rs @@ -0,0 +1,33 @@ +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +pub struct TrackSnapshot { + pub id: i64, + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, +} + +#[derive(Clone, Debug, Serialize)] +pub struct LyricsLoadedEvent { + pub song_id: i64, + pub timestamp_ms_list: Vec, + pub text_list: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub enum PluginEvent { + Startup, + TrackChanged { + track: TrackSnapshot, + index: usize, + }, + PlaybackStateChanged { + playing: bool, + current_time: f64, + }, + QueueChanged, + SettingsChanged, + LyricsLoaded(LyricsLoadedEvent), +} diff --git a/src-tauri/crates/plugin-sdk/src/ffi.rs b/src-tauri/crates/plugin-sdk/src/ffi.rs new file mode 100644 index 0000000..d33e43e --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/ffi.rs @@ -0,0 +1,74 @@ +use std::ffi::c_void; + +pub type PluginHandle = *mut c_void; +pub type EventPayload = *const c_void; +pub type CommandRequest = *const c_void; + +#[repr(C)] +pub struct ByteBuffer { + pub ptr: *mut u8, + pub len: usize, +} + +#[repr(C)] +pub struct CommandResponse { + pub success: bool, + pub data: ByteBuffer, +} + +#[repr(C)] +pub struct PluginApi { + pub activate: unsafe extern "C" fn(PluginHandle) -> i32, + pub deactivate: unsafe extern "C" fn(PluginHandle) -> i32, + pub on_event: unsafe extern "C" fn(PluginHandle, EventPayload) -> i32, + pub execute_command: + unsafe extern "C" fn(PluginHandle, *const CommandRequest) -> CommandResponse, +} + +#[repr(C)] +pub struct PluginExports { + pub abi_version: u32, + pub create: unsafe extern "C" fn() -> PluginHandle, + pub destroy: unsafe extern "C" fn(PluginHandle), + pub free_buffer: unsafe extern "C" fn(ByteBuffer), + pub api: PluginApi, + /// 音频处理器指针。null 表示插件不导出音频处理能力。 + /// + /// 注意:为了 ABI 兼容,**不**在 PluginExports 中内嵌 AudioProcessorApi, + /// 而是用单独导出的全局符号 `rem_audio_processor` 暴露。 + /// 旧插件(abi_version=1 但无此字段)在 dynamic_factory 读取时: + /// - 用 dlsym 单独查找 `rem_audio_processor` 符号;找到则构造 AudioProcessorApi 指针 + /// - 找不到则置 None + /// 新插件在 PluginExports 中直接填充此字段为 `&AudioProcessorApi` 或 null。 + pub audio_processor: *const AudioProcessorApi, +} + +/// 音频处理器 FFI 协议:packaged 插件通过此协议接收 PCM 帧、处理后写回。 +/// +/// - `init`: 可选初始化回调(sample_rate, channels)。返回 0 表示成功,非 0 失败。 +/// - `process`: 实时处理回调。 +/// - `handle`: PluginHandle +/// - `input`: *const f32,交错 PCM 输入(frames * channels 个 f32) +/// - `output`: *mut f32,交错 PCM 输出(同长度) +/// - `frames`: 帧数(每通道样本数,通常 128 或 256) +/// - `channels`: 通道数(1=mono, 2=stereo) +/// 返回 0 表示成功,非 0 表示错误(宿主应跳过该插件)。 +/// - `reset`: 可选状态重置回调(如清理延迟线 buffer)。 +/// +/// 3 个字段都用 `Option`,因为旧插件可能不导出音频处理能力。 +/// `repr(C)` 的 `Option` 等价 nullable pointer,能被 libloading 正确读取。 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AudioProcessorApi { + pub init: Option i32>, + pub process: Option< + unsafe extern "C" fn( + PluginHandle, + input: *const f32, + output: *mut f32, + frames: usize, + channels: usize, + ) -> i32, + >, + pub reset: Option i32>, +} diff --git a/src-tauri/crates/plugin-sdk/src/lib.rs b/src-tauri/crates/plugin-sdk/src/lib.rs new file mode 100644 index 0000000..efa2457 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/lib.rs @@ -0,0 +1,25 @@ +pub mod api; +pub mod contributes; +pub mod context; +pub mod errors; +pub mod events; +pub mod ffi; +pub mod manifest; +pub mod model; +pub mod permissions; +pub mod settings; +pub mod traits; + +pub use api::{LibraryApi, PlayerApi, PluginSettingsApi, QueueApi, SettingsApi}; +pub use contributes::{NativeViewContribution, ViewContribution}; +pub use context::{HostContext, PluginContext}; +pub use errors::PluginError; +pub use events::{LyricsLoadedEvent, PluginEvent, TrackSnapshot}; +pub use manifest::{ + ActivationEvent, PluginContribution, PluginManifest, PluginSource, ResolvedPluginManifest, + SettingDefinition, +}; +pub use model::PluginIdentity; +pub use permissions::PluginPermission; +pub use settings::{PluginSetting, PluginSettingMeta, PluginSettings, SettingValue}; +pub use traits::{CommandArgs, Plugin, PluginFactory}; diff --git a/src-tauri/crates/plugin-sdk/src/manifest.rs b/src-tauri/crates/plugin-sdk/src/manifest.rs new file mode 100644 index 0000000..f67b563 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/manifest.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +use crate::contributes::{ + CommandContribution, MenuContribution, NativeViewContribution, SidebarContribution, + ViewContribution, +}; +use crate::permissions::PluginPermission; +use crate::settings::SettingValue; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum ActivationEvent { + OnStartup, + OnTrackChanged, + OnPlaybackStateChanged, + OnQueueChanged, + OnSettingsChanged, + OnCommand(String), +} + +/// Where a plugin's manifest and code come from. +/// +/// `id` never carries a source prefix; `source` is the single, +/// independent field that classifies provenance. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum PluginSource { + Builtin, + Packaged, + User, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PluginContribution { + pub commands: Vec, + pub menus: Vec, + pub sidebars: Vec, + #[serde(default)] + pub views: Vec, + #[serde(default, rename = "nativeViews")] + pub native_views: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SettingDefinition { + pub key: String, + pub title: String, + pub default_value: SettingValue, +} + +/// Raw manifest as authored in `plugin.json`. +/// +/// `route` is an optional override of the slug segment under +/// `/plugins/view/`. When absent, the slug defaults to `id`. +/// The raw manifest is loader-internal only; consumers must use +/// [`ResolvedPluginManifest`], which has every field resolved. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginManifest { + pub id: String, + pub source: PluginSource, + #[serde(default)] + pub route: Option, + pub name: String, + pub display_name: String, + pub version: String, + pub author: String, + pub description: String, + pub entry: String, + pub min_app_version: String, + pub permissions: Vec, + pub activation_events: Vec, + pub contributes: PluginContribution, + #[serde(default)] + pub settings: Vec, +} + +/// Fully resolved, normalized manifest — the only shape exposed to +/// the frontend. No `Option`s; `route` is already normalized to an +/// absolute `/plugins/view/` path. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvedPluginManifest { + pub id: String, + pub source: PluginSource, + pub route: String, + pub name: String, + pub display_name: String, + pub version: String, + pub author: String, + pub description: String, + pub entry: String, + pub min_app_version: String, + pub permissions: Vec, + pub activation_events: Vec, + pub contributes: PluginContribution, + pub settings: Vec, +} diff --git a/src-tauri/crates/plugin-sdk/src/model.rs b/src-tauri/crates/plugin-sdk/src/model.rs new file mode 100644 index 0000000..58509ee --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/model.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +use crate::errors::PluginError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] +pub struct PluginIdentity { + pub namespace: Option, + pub name: String, +} + +impl PluginIdentity { + pub fn bare(name: &str) -> Self { + Self { + namespace: None, + name: name.to_string(), + } + } +} + +impl std::fmt::Display for PluginIdentity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ref ns) = self.namespace { + write!(f, "{}/{}", ns, self.name) + } else { + write!(f, "{}", self.name) + } + } +} + +impl std::str::FromStr for PluginIdentity { + type Err = crate::errors::PluginError; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split('/').collect(); + match parts.len() { + 2 if !parts[0].is_empty() && !parts[1].is_empty() => Ok(Self { + namespace: Some(parts[0].to_string()), + name: parts[1].to_string(), + }), + 1 if !parts[0].is_empty() => Ok(Self { + namespace: None, + name: parts[0].to_string(), + }), + _ => Err(PluginError::Plugin(format!("Invalid identity: {}", s))), + } + } +} diff --git a/src-tauri/crates/plugin-sdk/src/permissions.rs b/src-tauri/crates/plugin-sdk/src/permissions.rs new file mode 100644 index 0000000..a1131db --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/permissions.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub enum PluginPermission { + PlayerRead, + PlayerControl, + QueueRead, + QueueWrite, + LibraryRead, + LibraryWrite, + SettingsRead, + SettingsWrite, + PluginUI, + /// 音频处理(EQ 等)读写权限,序列化为 "audio" + Audio, +} diff --git a/src-tauri/crates/plugin-sdk/src/settings.rs b/src-tauri/crates/plugin-sdk/src/settings.rs new file mode 100644 index 0000000..d809003 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/settings.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value")] +pub enum SettingValue { + Bool(bool), + Integer(i64), + Float(f64), + Text(String), + List(Vec), + Json(serde_json::Value), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginSetting { + pub key: String, + pub title: String, + pub value: SettingValue, + pub default_value: SettingValue, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginSettingMeta { + pub key: String, + pub title: String, + pub value: SettingValue, + pub default_value: SettingValue, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginSettings { + pub plugin_id: String, + pub settings: HashMap, +} diff --git a/src-tauri/crates/plugin-sdk/src/traits.rs b/src-tauri/crates/plugin-sdk/src/traits.rs new file mode 100644 index 0000000..60c5d39 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/traits.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; + +use crate::context::PluginContext; +use crate::errors::PluginError; +use crate::events::PluginEvent; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum CommandArgs { + None, + TrackId(i64), + TrackIds(Vec), + RawPayload(String), + LyricsSearch { title: String, artist: String }, +} + +/// Runtime behaviour of a plugin. Manifest is NOT part of this trait — +/// it lives in the `ManifestRegistry`, sourced from `plugin.json`. +/// This keeps `plugin.json` as the single source of truth and removes +/// the dual-maintenance problem of `build_manifest()`. +pub trait Plugin: Send + Sync { + fn activate(&self, ctx: &PluginContext) -> Result<(), PluginError>; + fn deactivate(&self) -> Result<(), PluginError>; + fn on_event(&self, event: &PluginEvent, ctx: &PluginContext) -> Result<(), PluginError>; + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + ctx: &PluginContext, + ) -> Result<(), PluginError>; +} + +/// Constructs plugin instances. Knows nothing about manifests. +/// The association `id -> factory` lives in `FactoryRegistry`; +/// the association `id -> manifest` lives in `ManifestRegistry`. +pub trait PluginFactory: Send + Sync { + fn create(&self) -> Arc; +} diff --git a/src-tauri/migrations/0001_initial.sql b/src-tauri/migrations/0001_initial.sql new file mode 100644 index 0000000..923e76a --- /dev/null +++ b/src-tauri/migrations/0001_initial.sql @@ -0,0 +1,55 @@ +CREATE TABLE tracks ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT, + album TEXT, + duration INTEGER NOT NULL, + path TEXT NOT NULL UNIQUE, + cover TEXT, + file_size INTEGER, + play_count INTEGER DEFAULT 0, + last_played_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE playlists ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE TABLE playlist_tracks ( + id INTEGER PRIMARY KEY, + playlist_id INTEGER NOT NULL, + track_id INTEGER NOT NULL, + position INTEGER NOT NULL, + FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, + FOREIGN KEY (track_id) REFERENCES tracks(id) ON DELETE CASCADE, + UNIQUE (playlist_id, track_id), + UNIQUE (playlist_id, position) +); + +CREATE TABLE recent_played ( + track_id INTEGER PRIMARY KEY, + played_at TEXT NOT NULL, + FOREIGN KEY (track_id) REFERENCES tracks(id) ON DELETE CASCADE +); + +CREATE TABLE settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + theme TEXT NOT NULL, + volume INTEGER NOT NULL, + scan_on_startup INTEGER NOT NULL, + reduce_motion INTEGER NOT NULL, + library_dirs TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE INDEX idx_tracks_title ON tracks(title); +CREATE INDEX idx_tracks_artist ON tracks(artist); +CREATE INDEX idx_tracks_album ON tracks(album); +CREATE INDEX idx_playlist_tracks_playlist_id ON playlist_tracks(playlist_id); +CREATE INDEX idx_playlist_tracks_track_id ON playlist_tracks(track_id); +CREATE INDEX idx_recent_played_played_at ON recent_played(played_at); diff --git a/src-tauri/migrations/0002_extend_settings.sql b/src-tauri/migrations/0002_extend_settings.sql new file mode 100644 index 0000000..6cc93a1 --- /dev/null +++ b/src-tauri/migrations/0002_extend_settings.sql @@ -0,0 +1,5 @@ +ALTER TABLE settings ADD COLUMN use_album_artist_grouping INTEGER NOT NULL DEFAULT 0; +ALTER TABLE settings ADD COLUMN plugin_dirs TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE settings ADD COLUMN plugin_dev_mode INTEGER NOT NULL DEFAULT 0; +ALTER TABLE settings ADD COLUMN plugin_scan_on_startup INTEGER NOT NULL DEFAULT 1; +ALTER TABLE settings ADD COLUMN plugin_log_level TEXT NOT NULL DEFAULT 'warn'; diff --git a/src-tauri/plugins/demo-view/plugin.json b/src-tauri/plugins/demo-view/plugin.json new file mode 100644 index 0000000..02b8b5b --- /dev/null +++ b/src-tauri/plugins/demo-view/plugin.json @@ -0,0 +1,28 @@ +{ + "id": "demo-view", + "name": "demo-view", + "displayName": "Demo View Plugin", + "version": "0.1.0", + "author": "RustEchoMusic", + "description": "临时示例插件,用于验证 plugin:// scheme 与 view 字段结构(不会被注册为内置插件)。", + "entry": "view/index.html", + "minAppVersion": "0.1.0", + "permissions": [ + "pluginUI" + ], + "activationEvents": [], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [], + "views": [ + { + "id": "main", + "title": "Demo View", + "entry": "view/index.html", + "icon": "dashboard" + } + ] + }, + "settings": [] +} diff --git a/src-tauri/plugins/demo-view/view/index.html b/src-tauri/plugins/demo-view/view/index.html new file mode 100644 index 0000000..edd4353 --- /dev/null +++ b/src-tauri/plugins/demo-view/view/index.html @@ -0,0 +1,102 @@ + + + + + + demo-view + + + +

    demo-view

    +
    等待宿主握手...
    +
    (尚未收到 ready)
    +
    pluginId:
    +
    capabilities:
    + + + + + + diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs new file mode 100644 index 0000000..912bafd --- /dev/null +++ b/src-tauri/src/audio/engine.rs @@ -0,0 +1,39 @@ +use crate::errors::AppError; + +pub trait AudioEngine: Send { + fn play(&mut self) -> Result<(), AppError>; + fn pause(&mut self); + fn seek(&mut self, time: f64); + fn set_volume(&self, volume: f32); + fn set_pan(&self, pan: f32); + fn current_time(&self) -> f64; + fn paused(&self) -> bool; + + // ===== 均衡器(EQ)扩展方法 ===== + // 以下方法均带默认 no-op 实现,保证现有实现 / mock 不被破坏。 + + /// 设置指定频段(0..10)的增益(dB)。 + fn set_eq_band_gain(&mut self, _band_index: usize, _gain_db: f64) -> Result<(), AppError> { + Ok(()) + } + + /// 一次性应用 10 段 EQ 预设(dB)。 + fn apply_eq_preset(&mut self, _gains: &[f64; 10]) -> Result<(), AppError> { + Ok(()) + } + + /// 启用 / 旁路 EQ。 + fn set_eq_enabled(&mut self, _enabled: bool) -> Result<(), AppError> { + Ok(()) + } + + /// 读取当前 10 段增益(dB)。 + fn get_eq_bands(&self) -> Result<[f64; 10], AppError> { + Ok([0.0; 10]) + } + + /// 查询 EQ 是否启用。 + fn is_eq_enabled(&self) -> Result { + Ok(false) + } +} diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs new file mode 100644 index 0000000..d833d70 --- /dev/null +++ b/src-tauri/src/audio/mod.rs @@ -0,0 +1,12 @@ +pub mod engine; +pub mod state; +pub mod web_audio; + +pub use engine::AudioEngine; +pub use state::{init_audio_state, lock_audio_state, AudioState}; +pub use web_audio::WebAudioEngine; +use web_audio_api::context::AudioContext; + +pub fn get_audio_context() -> AudioContext { + AudioContext::default() +} diff --git a/src-tauri/src/audio/state.rs b/src-tauri/src/audio/state.rs new file mode 100644 index 0000000..81adab3 --- /dev/null +++ b/src-tauri/src/audio/state.rs @@ -0,0 +1,69 @@ +use crate::audio::engine::AudioEngine; +use crate::errors::AppError; +use crate::models::PlaybackQueue; +use std::sync::{Mutex, MutexGuard, OnceLock}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum PlayMode { + ListLoop, + SingleLoop, + Shuffle, +} + +pub struct AudioState { + pub engine: Option>, + pub volume: f32, + pub pan: f32, + pub playing: bool, + pub current_track_id: Option, + pub playback_queue: PlaybackQueue, + /// 10 段 EQ 增益(dB),与引擎同步 + pub eq_bands: [f64; 10], + /// EQ 是否启用 + pub eq_enabled: bool, +} + +impl AudioState { + pub fn with_engine(&mut self, f: F) -> Result + where + F: FnOnce(&mut Box) -> R, + { + if let Some(ref mut engine) = self.engine { + Ok(f(engine)) + } else { + Err(AppError::from("Audio engine is missing")) + } + } + + pub fn play_mode(&self) -> PlayMode { + self.playback_queue.play_mode + } + pub fn set_play_mode(&mut self, mode: PlayMode) { + self.playback_queue.play_mode = mode; + } +} + +static AUDIO_STATE: OnceLock> = OnceLock::new(); + +pub fn init_audio_state(queue: PlaybackQueue) { + AUDIO_STATE.get_or_init(|| { + Mutex::new(AudioState { + engine: None, + volume: 1.0, + pan: 0.0, + playing: false, + current_track_id: None, + playback_queue: queue, + eq_bands: [0.0; 10], + eq_enabled: false, + }) + }); +} + +pub fn lock_audio_state() -> Result, AppError> { + AUDIO_STATE + .get() + .ok_or_else(|| AppError::from("Audio state not initialized"))? + .lock() + .map_err(|_| AppError::from("Failed to lock audio state")) +} diff --git a/src-tauri/src/audio/web_audio.rs b/src-tauri/src/audio/web_audio.rs new file mode 100644 index 0000000..dfecde2 --- /dev/null +++ b/src-tauri/src/audio/web_audio.rs @@ -0,0 +1,320 @@ +use crate::audio::engine::AudioEngine; +use crate::audio::get_audio_context; +use crate::errors::AppError; +use crate::services::plugin::audio_processor_registry::AudioProcessorRegistry; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use web_audio_api::{ + context::BaseAudioContext, + node::{AudioNode, AudioNodeOptions, BiquadFilterNode, BiquadFilterType, GainNode, MediaElementAudioSourceNode, StereoPannerNode}, + worklet::{AudioParamValues, AudioWorkletGlobalScope, AudioWorkletNode, AudioWorkletNodeOptions, AudioWorkletProcessor}, + MediaElement, +}; + +/// 10 段 EQ 标准中心频率(Hz) +const EQ_FREQUENCIES: [f32; 10] = [ + 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0, +]; +/// EQ 标准 Q 值(约 √2) +const EQ_Q: f32 = 1.41; +const EQ_BAND_COUNT: usize = 10; + +/// 用户态 AudioWorklet 处理器:把渲染量子的 PCM 交错后依次喂给 +/// `AudioProcessorRegistry` 中所有已激活的 packaged 插件,再把结果解交错 +/// 写回输出。 +/// +/// 节点常驻于音频图(方案 A):无插件注册时透传 input → output,仅一次 +/// `copy_from_slice`,开销可忽略。这避免了在有/无插件之间频繁重建音频图。 +/// +/// 缓冲区作为字段复用(Task 9.3):`AudioWorkletProcessor` 实例的 `process` +/// 在单线程音频回调中调用,`Vec` 字段可安全持有。 +struct ExternalProcessorNode { + registry: Arc, + in_buf: Vec, + out_buf: Vec, +} + +impl AudioWorkletProcessor for ExternalProcessorNode { + type ProcessorOptions = Arc; + + fn constructor(registry: Self::ProcessorOptions) -> Self + where + Self: Sized, + { + Self { + registry, + in_buf: Vec::new(), + out_buf: Vec::new(), + } + } + + fn process<'a, 'b>( + &mut self, + inputs: &'b [&'a [&'a [f32]]], + outputs: &'b mut [&'a mut [&'a mut [f32]]], + _params: AudioParamValues<'b>, + _scope: &'b AudioWorkletGlobalScope, + ) -> bool { + // 单输入单输出节点:inputs[0] / outputs[0] 为各通道的样本切片。 + let input: &[&[f32]] = inputs.get(0).copied().unwrap_or(&[]); + let channels = input.len(); + let frames = input.first().map(|c| c.len()).unwrap_or(0); + + let processors = self.registry.iter_active(); + if processors.is_empty() { + // 无插件:透传(input → output)。 + if let Some(output) = outputs.get_mut(0) { + for (i, out_chan) in output.iter_mut().enumerate() { + if let Some(in_chan) = input.get(i) { + let len = in_chan.len().min(out_chan.len()); + if len > 0 { + out_chan[..len].copy_from_slice(&in_chan[..len]); + } + } + } + } + return true; + } + + if channels == 0 || frames == 0 { + // 无输入数据;输出保持静默。 + return true; + } + + // 构造交错缓冲区:[L0, R0, L1, R1, ...](frames * channels 个 f32)。 + let total = frames * channels; + self.in_buf.clear(); + self.in_buf.reserve(total); + for f in 0..frames { + for c in 0..channels { + let v = input + .get(c) + .and_then(|ch| ch.get(f)) + .copied() + .unwrap_or(0.0); + self.in_buf.push(v); + } + } + // out_buf 初值 = in_buf(透传给首个插件作为输出缓冲)。 + self.out_buf.clear(); + self.out_buf.extend_from_slice(&self.in_buf); + + // 依次调用各 processor 的 process FFI。成功后用 out_buf 作为下一轮输入。 + for (_plugin_id, api, handle) in processors { + let Some(process_fn) = api.process else { + continue; + }; + let rc = unsafe { + process_fn( + handle, + self.in_buf.as_ptr(), + self.out_buf.as_mut_ptr(), + frames, + channels, + ) + }; + if rc == 0 { + // 处理成功:用 out_buf 作为下一轮插件输入。 + self.in_buf[..total].copy_from_slice(&self.out_buf); + } + // 失败:跳过该插件,保留 out_buf / in_buf 不变。 + } + + // 解交错写回 outputs。 + if let Some(output) = outputs.get_mut(0) { + for c in 0..channels { + if let Some(out_chan) = output.get_mut(c) { + let n = out_chan.len().min(frames); + for f in 0..n { + out_chan[f] = self.out_buf[f * channels + c]; + } + } + } + } + + // 返回 true:节点常驻(即使输入断开,仍保留在图中以便随时接收插件输出)。 + true + } +} + +pub struct WebAudioEngine { + media: MediaElement, + src: MediaElementAudioSourceNode, + gain: GainNode, + panner: StereoPannerNode, + /// 10 段 BiquadFilter(Peaking)节点链,串联在 source 与 gain 之间 + eq_filters: Vec, + /// 外部处理器节点(packaged 插件音频处理入口),串联在 eq_chain 与 gain 之间。 + external_processor_node: AudioWorkletNode, + /// EQ 是否启用(false = 旁路,所有 biquad gain 设为 0dB) + eq_enabled: bool, + /// 各频段目标增益(dB),启用时同步到对应 biquad + eq_bands: [f64; 10], +} + +impl WebAudioEngine { + pub fn new( + file_path: &Path, + volume: f32, + pan: f32, + audio_processor_registry: Arc, + ) -> Result { + let context = get_audio_context(); + let _ = context.resume(); + + // 把当前音频输出格式同步给 registry,供插件 init 回调使用。 + audio_processor_registry.set_audio_format(context.sample_rate() as u32, 2); + + let mut media = web_audio_api::MediaElement::new(file_path) + .map_err(|e| format!("Failed to create media element: {}", e))?; + + let src = context.create_media_element_source(&mut media); + let gain = context.create_gain(); + let panner = context.create_stereo_panner(); + + // 构建 10 段 EQ BiquadFilter 链(始终串联,bypass 时 gain 全为 0dB,不改变频响) + let mut eq_filters: Vec = Vec::with_capacity(EQ_BAND_COUNT); + for &freq in EQ_FREQUENCIES.iter() { + let mut filter = context.create_biquad_filter(); + filter.set_type(BiquadFilterType::Peaking); + filter.frequency().set_value(freq); + filter.q().set_value(EQ_Q); + // 初始处于旁路状态:gain = 0 dB + filter.gain().set_value(0.0); + eq_filters.push(filter); + } + + // 外部处理器节点(AudioWorklet)。1 入 1 出,output_channel_count 为空时 + // 继承输入通道数(stereo)。 + let worklet_options = AudioWorkletNodeOptions { + number_of_inputs: 1, + number_of_outputs: 1, + output_channel_count: Vec::new(), + parameter_data: HashMap::new(), + processor_options: audio_processor_registry, + audio_node_options: AudioNodeOptions::default(), + }; + let external_processor_node = + AudioWorkletNode::new::(&context, worklet_options); + + // 节点链:src → biquad[0] → ... → biquad[9] → external_processor_node → gain → panner → destination + src.connect(&eq_filters[0]); + for i in 0..(EQ_BAND_COUNT - 1) { + eq_filters[i].connect(&eq_filters[i + 1]); + } + eq_filters[EQ_BAND_COUNT - 1].connect(&external_processor_node); + external_processor_node.connect(&gain); + gain.connect(&panner); + panner.connect(&context.destination()); + + media.set_loop(false); + media.set_current_time(0.0); + gain.gain().set_value(volume); + panner.pan().set_value(pan); + + Ok(WebAudioEngine { + media, + src, + gain, + panner, + eq_filters, + external_processor_node, + eq_enabled: false, + eq_bands: [0.0; 10], + }) + } +} + +impl AudioEngine for WebAudioEngine { + fn play(&mut self) -> Result<(), AppError> { + self.media.play(); + Ok(()) + } + + fn pause(&mut self) { + self.media.pause(); + } + + fn seek(&mut self, time: f64) { + self.media.set_current_time(time); + } + + fn set_volume(&self, volume: f32) { + self.gain.gain().set_value(volume); + } + + fn set_pan(&self, pan: f32) { + self.panner.pan().set_value(pan); + } + + fn current_time(&self) -> f64 { + self.media.current_time() + } + + fn paused(&self) -> bool { + self.media.paused() + } + + fn set_eq_band_gain(&mut self, band_index: usize, gain_db: f64) -> Result<(), AppError> { + if band_index >= EQ_BAND_COUNT { + return Err(AppError::Service(format!( + "EQ band index {} out of range (0..{})", + band_index, EQ_BAND_COUNT + ))); + } + self.eq_bands[band_index] = gain_db; + // 仅在 EQ 启用时同步到节点,旁路时保持 0dB + if self.eq_enabled { + self.eq_filters[band_index].gain().set_value(gain_db as f32); + } + Ok(()) + } + + fn apply_eq_preset(&mut self, gains: &[f64; 10]) -> Result<(), AppError> { + self.eq_bands = *gains; + if self.eq_enabled { + for (i, &g) in gains.iter().enumerate() { + self.eq_filters[i].gain().set_value(g as f32); + } + } + Ok(()) + } + + fn set_eq_enabled(&mut self, enabled: bool) -> Result<(), AppError> { + self.eq_enabled = enabled; + if enabled { + // 启用时恢复各频段目标增益 + for (i, &g) in self.eq_bands.iter().enumerate() { + self.eq_filters[i].gain().set_value(g as f32); + } + } else { + // 旁路时所有 biquad gain 设为 0dB(不改变频响) + for filter in &self.eq_filters { + filter.gain().set_value(0.0); + } + } + Ok(()) + } + + fn get_eq_bands(&self) -> Result<[f64; 10], AppError> { + Ok(self.eq_bands) + } + + fn is_eq_enabled(&self) -> Result { + Ok(self.eq_enabled) + } +} + +impl Drop for WebAudioEngine { + fn drop(&mut self) { + let _ = self.panner.disconnect(); + let _ = self.gain.disconnect(); + let _ = self.external_processor_node.disconnect(); + for filter in &self.eq_filters { + let _ = filter.disconnect(); + } + let _ = self.src.disconnect(); + self.media.pause(); + } +} diff --git a/src-tauri/src/commands/equalizer.rs b/src-tauri/src/commands/equalizer.rs new file mode 100644 index 0000000..7c51495 --- /dev/null +++ b/src-tauri/src/commands/equalizer.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use tauri::{command, State}; + +use plugin_sdk::settings::{PluginSettingMeta, SettingValue}; + +use crate::errors::AppError; +use crate::services::plugin::manager::PluginManager; +use crate::state::playback_state::with_audio_state; + +/// EQ 插件 ID,与 `plugins/eq/plugin.json` 中的 `id` 一致 +const EQ_PLUGIN_ID: &str = "eq"; + +/// 对外暴露的 EQ 状态快照,前端通过 `get_eq_state` 读取 +#[derive(serde::Serialize)] +pub struct EqState { + pub bands: [f64; 10], + pub enabled: bool, + pub preset_name: String, +} + +/// 读取持久化的预设名称;缺失或非 Text 时回退到 "Flat" +fn read_preset_name(manager: &PluginManager) -> String { + manager + .get_plugin_settings(EQ_PLUGIN_ID) + .into_iter() + .find(|m: &PluginSettingMeta| m.key == "preset_name") + .and_then(|m| match m.value { + SettingValue::Text(s) => Some(s), + _ => None, + }) + .unwrap_or_else(|| "Flat".to_string()) +} + +/// 把当前 audio state 的 eq_bands 序列化为 Json 写回 "preset" key +fn persist_preset(manager: &PluginManager) -> Result<(), AppError> { + let bands = with_audio_state(|s| s.eq_bands)?; + manager.update_plugin_setting( + EQ_PLUGIN_ID, + "preset", + SettingValue::Json(serde_json::to_value(&bands).unwrap_or_default()), + )?; + Ok(()) +} + +#[command] +pub async fn get_eq_state( + manager: State<'_, Arc>, +) -> Result { + let (bands, enabled) = with_audio_state(|s| (s.eq_bands, s.eq_enabled))?; + let preset_name = read_preset_name(&manager); + Ok(EqState { + bands, + enabled, + preset_name, + }) +} + +#[command] +pub async fn set_eq_band( + band: usize, + gain: f64, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + if band >= 10 { + return Err(AppError::from("EQ band index out of range")); + } + with_audio_state(|state| { + state.eq_bands[band] = gain; + if let Some(ref mut engine) = state.engine { + engine.set_eq_band_gain(band, gain)?; + } + Ok::<(), AppError>(()) + })??; + persist_preset(&manager)?; + Ok(()) +} + +#[command] +pub async fn apply_eq_preset( + preset_name: String, + bands: [f64; 10], + manager: State<'_, Arc>, +) -> Result<(), AppError> { + with_audio_state(|state| { + state.eq_bands = bands; + if let Some(ref mut engine) = state.engine { + engine.apply_eq_preset(&bands)?; + } + Ok::<(), AppError>(()) + })??; + manager.update_plugin_setting( + EQ_PLUGIN_ID, + "preset", + SettingValue::Json(serde_json::to_value(&bands).unwrap_or_default()), + )?; + manager.update_plugin_setting( + EQ_PLUGIN_ID, + "preset_name", + SettingValue::Text(preset_name), + )?; + Ok(()) +} + +#[command] +pub async fn set_eq_enabled( + enabled: bool, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + with_audio_state(|state| { + state.eq_enabled = enabled; + if let Some(ref mut engine) = state.engine { + engine.set_eq_enabled(enabled)?; + } + Ok::<(), AppError>(()) + })??; + manager.update_plugin_setting(EQ_PLUGIN_ID, "enabled", SettingValue::Bool(enabled))?; + Ok(()) +} diff --git a/src-tauri/src/commands/library.rs b/src-tauri/src/commands/library.rs new file mode 100644 index 0000000..9c11524 --- /dev/null +++ b/src-tauri/src/commands/library.rs @@ -0,0 +1,264 @@ +use std::collections::HashSet; +use std::path::Path; +use std::process::Command; + +use base64::{engine::general_purpose, Engine as _}; +use lofty::prelude::*; +use lofty::probe::Probe; +use tauri::{command, State}; +use walkdir::WalkDir; + +use crate::commands::playback::remove_track_from_queue; +use crate::errors::AppError; +use crate::metadata::parse_single_track; +use crate::models::{NewTrack, Track}; +use crate::state::AppState; + +const SUPPORTED_EXT: [&str; 5] = ["mp3", "flac", "m4a", "wav", "ogg"]; + +fn is_supported_audio_file(path: &Path) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| SUPPORTED_EXT.contains(&ext.to_lowercase().as_str())) + .unwrap_or(false) +} + +fn scan_single_directory(dir: &str) -> Result, AppError> { + let root_path = Path::new(dir); + + if !root_path.exists() || !root_path.is_dir() { + return Err(format!("无效目录: {}", dir).into()); + } + + let mut tracks = Vec::new(); + + for entry in WalkDir::new(root_path) + .follow_links(false) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + + if !path.is_file() || !is_supported_audio_file(path) { + continue; + } + + if let Some(mut track) = parse_single_track(path) { + track.cover = None; + tracks.push(track); + } + } + + Ok(tracks) +} + +fn dedupe_new_tracks(tracks: &mut Vec) { + let mut seen = HashSet::new(); + tracks.retain(|track| seen.insert(track.path.clone())); +} + +pub fn execute_scan(dirs: Vec) -> Result, AppError> { + let mut all_tracks = Vec::new(); + + for dir in dirs { + if let Ok(mut tracks) = scan_single_directory(&dir) { + all_tracks.append(&mut tracks); + } + } + + dedupe_new_tracks(&mut all_tracks); + Ok(all_tracks) +} + +async fn upsert_scanned_tracks( + state: &AppState, + tracks: Vec, +) -> Result, AppError> { + state + .tracks + .upsert_tracks(tracks) + .await +} + +#[command] +pub async fn get_track_cover(track_id: i64, state: State<'_, AppState>) -> Result, AppError> { + let track = state.tracks.get_track(track_id).await?; + let track = match track { + Some(t) => t, + None => return Err("曲目不存在".into()), + }; + + let file_path = Path::new(&track.path); + + if !file_path.exists() { + return Err("音频文件不存在".into()); + } + + let tagged_file = Probe::open(file_path) + .map_err(|e| e.to_string())? + .read() + .map_err(|e| e.to_string())?; + + let tag = tagged_file + .primary_tag() + .or_else(|| tagged_file.first_tag()); + + let cover = tag.and_then(|t| t.pictures().first()).map(|pic| { + let b64_encoded = general_purpose::STANDARD.encode(pic.data()); + let mime_type = pic.mime_type().map(|m| m.as_str()).unwrap_or("image/jpeg"); + + format!("data:{};base64,{}", mime_type, b64_encoded) + }); + + Ok(cover) +} + +fn validate_audio_path(path: &str) -> Result<&Path, AppError> { + let file_path = Path::new(path); + + if !file_path.exists() { + return Err("音频文件不存在".into()); + } + + if !file_path.is_file() || !is_supported_audio_file(file_path) { + return Err(format!("拒绝操作非音频文件: {}", path).into()); + } + + Ok(file_path) +} + +#[command] +pub async fn show_in_folder(track_id: i64, state: State<'_, AppState>) -> Result<(), AppError> { + let track = state.tracks.get_track(track_id).await?; + let track = track.ok_or_else(|| AppError::from("曲目不存在"))?; + let file_path = Path::new(&track.path); + + if !file_path.exists() { + return Err("文件不存在".into()); + } + + #[cfg(target_os = "linux")] + let parent = file_path + .parent() + .ok_or_else(|| AppError::from("无法定位文件所在目录"))?; + + #[cfg(target_os = "windows")] + let status = Command::new("explorer.exe") + .arg(format!("/select,{}", file_path.display())) + .status(); + + #[cfg(target_os = "macos")] + let status = Command::new("open").arg("-R").arg(file_path).status(); + + #[cfg(target_os = "linux")] + let status = Command::new("xdg-open").arg(parent).status(); + + let status = status?; + + if status.success() { + Ok(()) + } else { + Err("打开文件所在目录失败".into()) + } +} + +#[command] +pub async fn delete_track_file( + app_handle: tauri::AppHandle, + track_id: i64, + state: State<'_, AppState>, +) -> Result<(), AppError> { + let track = state.tracks.get_track(track_id).await?; + let track = track.ok_or_else(|| AppError::from("曲目不存在"))?; + + let file_path = validate_audio_path(&track.path)?; + trash::delete(file_path).map_err(|e| format!("移至回收站失败 {}: {}", track.path, e))?; + + remove_track_from_queue(app_handle, track_id).await?; + + Ok(()) +} + +#[command] +pub async fn trash_track_files(track_ids: Vec, state: State<'_, AppState>) -> Result<(), AppError> { + for track_id in track_ids { + let track = state.tracks.get_track(track_id).await?; + let track = match track { + Some(t) => t, + None => continue, + }; + + let file_path = Path::new(&track.path); + if !file_path.exists() { + continue; + } + + if !file_path.is_file() || !is_supported_audio_file(file_path) { + return Err(format!("拒绝移至回收站非音频文件: {}", track.path).into()); + } + + trash::delete(file_path).map_err(|e| format!("移至回收站失败 {}: {}", track.path, e))?; + } + + Ok(()) +} + +#[command] +pub async fn delete_track_files(track_ids: Vec, state: State<'_, AppState>) -> Result<(), AppError> { + for track_id in track_ids { + let track = state.tracks.get_track(track_id).await?; + let track = match track { + Some(t) => t, + None => continue, + }; + + let file_path = Path::new(&track.path); + if !file_path.exists() { + continue; + } + + if !file_path.is_file() || !is_supported_audio_file(file_path) { + return Err(format!("拒绝删除非音频文件: {}", track.path).into()); + } + + tokio::fs::remove_file(file_path) + .await + .map_err(|e| format!("删除文件失败 {}: {}", track.path, e))?; + } + + Ok(()) +} + +#[command] +pub async fn scan_track_directories( + state: State<'_, AppState>, + dirs: Vec, +) -> Result, AppError> { + let scanned = execute_scan(dirs)?; + let tracks = upsert_scanned_tracks(&state, scanned).await?; + Ok(tracks) +} + +#[command] +pub async fn load_track_library(state: State<'_, AppState>) -> Result, AppError> { + let tracks = state.tracks.list_tracks().await?; + + if tracks.is_empty() { + return rebuild_and_get_library(&state).await; + } + + Ok(tracks) +} + +async fn rebuild_and_get_library(state: &AppState) -> Result, AppError> { + let settings = state + .settings + .get_settings() + .await + .map_err(|error| error.to_string())?; + let scanned = tauri::async_runtime::spawn_blocking(move || execute_scan(settings.library_dirs)) + .await + .map_err(|e| e.to_string())??; + let tracks = upsert_scanned_tracks(state, scanned).await?; + Ok(tracks) +} diff --git a/src-tauri/src/commands/menu.rs b/src-tauri/src/commands/menu.rs new file mode 100644 index 0000000..879f3ee --- /dev/null +++ b/src-tauri/src/commands/menu.rs @@ -0,0 +1,13 @@ +use tauri::{command, menu::MenuBuilder, Manager, Window}; + +#[command] +pub fn show_context_menu(window: Window) { + let handle = window.app_handle(); + let menu = MenuBuilder::new(handle) + .text("quit", "退出程序") + .separator() + .text("play", "播放") + .build() + .unwrap(); + let _ = window.popup_menu(&menu); +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..903d4b1 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,10 @@ +pub mod equalizer; +pub mod library; +pub mod menu; +pub mod playback; +pub mod playback_queue; +pub mod playlists; +pub mod plugin_extensions; +pub mod recent; +pub mod settings; +pub mod tracks; diff --git a/src-tauri/src/commands/playback.rs b/src-tauri/src/commands/playback.rs new file mode 100644 index 0000000..cb7d886 --- /dev/null +++ b/src-tauri/src/commands/playback.rs @@ -0,0 +1,139 @@ +use crate::audio::state::PlayMode; +use crate::errors::AppError; +use crate::events::{AppEvent, EventBus}; +use crate::models::playback::PlaybackStatusSnapshot; +use crate::models::Track; +use crate::services::playback_service::PlaybackService; +use crate::state::playback_state::{ + current_playback_snapshot, current_time_from_state, lock_audio_state, sanitize_track, + with_audio_state, +}; +use tauri::{command, AppHandle}; + +pub use crate::services::media_control_service::handle_media_control_event; + +#[command] +pub async fn sync_playback_queue( + app_handle: AppHandle, + playlist: Vec, + current_index: Option, + play_mode: PlayMode, + history: Vec, +) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + + state.playback_queue.sync( + playlist.into_iter().map(sanitize_track).collect(), + current_index, + play_mode, + history, + ); + } + + PlaybackService::emit_queue_changed(&app_handle)?; + + Ok(()) +} + +#[command] +pub async fn remove_track_from_queue(app_handle: AppHandle, track_id: i64) -> Result<(), AppError> { + PlaybackService::new(app_handle) + .remove_track_from_queue(track_id) + .await +} + +#[command] +pub async fn insert_track_as_next(app_handle: AppHandle, track: Track) -> Result<(), AppError> { + with_audio_state(|state| { + state.playback_queue.insert_next(track); + })?; + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub async fn play_queue_track(app_handle: AppHandle, index: usize) -> Result<(), AppError> { + PlaybackService::new(app_handle).play_queue_index(index)?; + Ok(()) +} + +#[command] +pub async fn play_next_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).next().await?; + Ok(()) +} + +#[command] +pub async fn play_previous_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).previous().await?; + Ok(()) +} + +#[command] +pub async fn stop_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).stop().await?; + Ok(()) +} + +#[command] +pub async fn play_track( + app_handle: AppHandle, + track_id: i64, + state: tauri::State<'_, crate::state::AppState>, +) -> Result { + let track = state.tracks.get_track(track_id).await?; + let track = track.ok_or_else(|| AppError::from("曲目不存在"))?; + let service = PlaybackService::new(app_handle); + service.play_track(track, None)?; + Ok(format!("Playing track: {}", track_id)) +} + +#[command] +pub async fn resume_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).resume().await?; + Ok(()) +} + +#[command] +pub async fn pause_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).pause().await?; + Ok(()) +} + +#[command] +pub async fn toggle_track(app_handle: AppHandle) -> Result { + let status = PlaybackService::new(app_handle).toggle().await?; + Ok(status) +} + +#[command] +pub async fn current_time() -> f64 { + current_time_from_state() +} + +#[command] +pub async fn get_current_status() -> Result { + current_playback_snapshot() +} + +#[command] +pub async fn set_current_time(app_handle: AppHandle, time: f64) -> Result<(), AppError> { + PlaybackService::new(app_handle).seek(time).await +} + +#[command] +pub async fn set_volume(app_handle: tauri::AppHandle, volume: f32) -> Result<(), AppError> { + let mut state = lock_audio_state()?; + let safe_volume = (volume / 100.0).clamp(0.0, 1.0); + + state.volume = safe_volume; + + if let Some(ref engine) = state.engine { + engine.set_volume(safe_volume); + } + + EventBus::emit(&app_handle, AppEvent::VolumeChanged(safe_volume))?; + + Ok(()) +} diff --git a/src-tauri/src/commands/playback_queue.rs b/src-tauri/src/commands/playback_queue.rs new file mode 100644 index 0000000..3ab2a5f --- /dev/null +++ b/src-tauri/src/commands/playback_queue.rs @@ -0,0 +1,58 @@ +use crate::audio::state::PlayMode; +use crate::errors::AppError; +use crate::models::{PlaybackQueue, Track}; +use crate::services::playback_service::PlaybackService; +use crate::state::playback_state::{lock_audio_state, with_audio_state}; +use tauri::{command, AppHandle}; + +#[command] +pub fn get_playback_queue() -> Result { + with_audio_state(|state| state.playback_queue.clone()) +} + +#[command] +pub async fn clear_queue(app_handle: AppHandle) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + state.playback_queue.clear(); + } + + PlaybackService::new(app_handle.clone()).stop().await?; + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub fn set_play_mode(app_handle: AppHandle, mode: PlayMode) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + state.playback_queue.play_mode = mode; + } + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub fn insert_tracks_as_next(app_handle: AppHandle, tracks: Vec) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + state.playback_queue.insert_tracks_as_next(tracks); + } + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub async fn replace_playlist_and_play( + app_handle: AppHandle, + tracks: Vec, + target_id: i64, +) -> Result<(), AppError> { + let play_index = { + let mut state = lock_audio_state()?; + state.playback_queue.replace_playlist(tracks, target_id)? + }; + PlaybackService::emit_queue_changed(&app_handle)?; + PlaybackService::new(app_handle).play_queue_index(play_index)?; + Ok(()) +} diff --git a/src-tauri/src/commands/playlists.rs b/src-tauri/src/commands/playlists.rs new file mode 100644 index 0000000..7d72d8b --- /dev/null +++ b/src-tauri/src/commands/playlists.rs @@ -0,0 +1,94 @@ +use tauri::{command, State}; + +use crate::errors::AppError; +use crate::models::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, +}; +use crate::state::AppState; + +#[command] +pub async fn create_playlist( + state: State<'_, AppState>, + playlist: NewPlaylist, +) -> Result { + state.playlists.create_playlist(playlist).await +} + +#[command] +pub async fn rename_playlist( + state: State<'_, AppState>, + playlist: RenamePlaylist, +) -> Result { + state.playlists.rename_playlist(playlist).await +} + +#[command] +pub async fn delete_playlist(state: State<'_, AppState>, id: i64) -> Result<(), AppError> { + state.playlists.delete_playlist(id).await +} + +#[command] +pub async fn get_playlist( + state: State<'_, AppState>, + id: i64, +) -> Result, AppError> { + state.playlists.get_playlist(id).await +} + +#[command] +pub async fn list_playlists(state: State<'_, AppState>) -> Result, AppError> { + state.playlists.list_playlists().await +} + +#[command] +pub async fn list_playlists_with_tracks( + state: State<'_, AppState>, +) -> Result, AppError> { + state.playlists.list_playlists_with_tracks().await +} + +#[command] +pub async fn get_playlist_with_tracks( + state: State<'_, AppState>, + id: i64, +) -> Result, AppError> { + state.playlists.get_playlist_with_tracks(id).await +} + +#[command] +pub async fn add_track_to_playlist( + state: State<'_, AppState>, + track: AddPlaylistTrack, +) -> Result { + state.playlists.add_track(track).await +} + +#[command] +pub async fn remove_track_from_playlist( + state: State<'_, AppState>, + playlist_id: i64, + track_id: i64, +) -> Result<(), AppError> { + state.playlists.remove_track(playlist_id, track_id).await +} + +#[command] +pub async fn clear_playlist_tracks( + state: State<'_, AppState>, + playlist_id: i64, +) -> Result<(), AppError> { + state.playlists.clear_tracks(playlist_id).await +} + +#[command] +pub async fn reorder_playlist_track( + state: State<'_, AppState>, + playlist_id: i64, + track_id: i64, + position: i64, +) -> Result<(), AppError> { + state + .playlists + .reorder_track(playlist_id, track_id, position) + .await +} diff --git a/src-tauri/src/commands/plugin_extensions.rs b/src-tauri/src/commands/plugin_extensions.rs new file mode 100644 index 0000000..fa8b964 --- /dev/null +++ b/src-tauri/src/commands/plugin_extensions.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use tauri::{command, State}; + +use crate::errors::AppError; +use plugin_sdk::contributes::MenuLocation; +use plugin_sdk::manifest::ResolvedPluginManifest; +use crate::services::plugin::extension::menu_extension::MenuExtension; +use crate::services::plugin::extension::native_view_extension::NativeViewExtension; +use crate::services::plugin::extension::sidebar_extension::SidebarExtension; +use crate::services::plugin::manager::{PluginManager, PluginViewResolution}; +use plugin_sdk::settings::{PluginSettingMeta, SettingValue}; +use plugin_sdk::traits::CommandArgs; + +#[command] +pub async fn get_menu_extensions( + location: MenuLocation, + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.menu_extensions(location)) +} + +#[command] +pub async fn get_sidebar_extensions( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.sidebar_extensions()) +} + +#[command] +pub async fn get_all_sidebar_extensions( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.all_sidebar_extensions()) +} + +#[command] +pub async fn get_native_view_extensions( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.native_view_extensions()) +} + +/// All resolved plugin manifests — the only plugin-identity shape the +/// frontend consumes. Routes are already normalized to +/// `/plugins/view/`; no `Option`s leak across the IPC boundary. +#[command] +pub async fn get_plugin_manifests( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.all_manifests()) +} + +#[command] +pub async fn get_plugin_view( + plugin_id: String, + view_id: String, + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.plugin_view(&plugin_id, &view_id)) +} + +#[command] +pub async fn get_plugin_settings( + plugin_id: String, + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.get_plugin_settings(&plugin_id)) +} + +#[command] +pub async fn update_plugin_setting( + plugin_id: String, + key: String, + value: SettingValue, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.update_plugin_setting(&plugin_id, &key, value) +} + +#[command] +pub async fn enable_plugin_command( + plugin_id: String, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.enable_plugin(&plugin_id) +} + +#[command] +pub async fn disable_plugin_command( + plugin_id: String, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.disable_plugin(&plugin_id) +} + +#[command] +pub async fn execute_plugin_command( + command_id: String, + args: CommandArgs, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.execute_plugin_command(&command_id, args) +} diff --git a/src-tauri/src/commands/recent.rs b/src-tauri/src/commands/recent.rs new file mode 100644 index 0000000..343a14c --- /dev/null +++ b/src-tauri/src/commands/recent.rs @@ -0,0 +1,28 @@ +use tauri::{command, State}; + +use crate::errors::AppError; +use crate::models::RecentPlayedWithTrack; +use crate::state::AppState; + +#[command] +pub async fn load_recently_played( + state: State<'_, AppState>, + limit: i64, + offset: i64, +) -> Result, AppError> { + state.recent.load(limit, offset).await +} + +#[command] +pub async fn add_recently_played( + state: State<'_, AppState>, + track_id: i64, + played_at: String, +) -> Result<(), AppError> { + state.recent.add(track_id, played_at).await +} + +#[command] +pub async fn clear_recently_played(state: State<'_, AppState>) -> Result<(), AppError> { + state.recent.clear().await +} diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs new file mode 100644 index 0000000..9a43fd1 --- /dev/null +++ b/src-tauri/src/commands/settings.rs @@ -0,0 +1,47 @@ +use crate::errors::AppError; +use crate::events::{AppEvent, EventBus}; +use crate::models::AppSettings; +use crate::state::AppState; +use tauri::{command, AppHandle, State}; + +#[command] +pub async fn get_settings(state: State<'_, AppState>) -> Result { + state.settings.get_settings().await +} + +#[command] +pub async fn update_settings( + app_handle: AppHandle, + state: State<'_, AppState>, + settings: AppSettings, +) -> Result { + let updated_settings = state.settings.update_settings(settings).await?; + + EventBus::emit( + &app_handle, + AppEvent::SettingsChanged(updated_settings.clone()), + )?; + + Ok(updated_settings) +} + +#[command] +pub async fn load_settings(state: State<'_, AppState>) -> Result { + state.settings.get_settings().await +} + +#[command] +pub async fn save_settings( + app_handle: AppHandle, + state: State<'_, AppState>, + settings: AppSettings, +) -> Result { + let updated_settings = state.settings.update_settings(settings).await?; + + EventBus::emit( + &app_handle, + AppEvent::SettingsChanged(updated_settings.clone()), + )?; + + Ok(updated_settings) +} diff --git a/src-tauri/src/commands/tracks.rs b/src-tauri/src/commands/tracks.rs new file mode 100644 index 0000000..0d35e5b --- /dev/null +++ b/src-tauri/src/commands/tracks.rs @@ -0,0 +1,71 @@ +use tauri::{command, State}; + +use crate::errors::AppError; +use crate::models::{NewTrack, Track, TrackSearchQuery, UpdateTrack}; +use crate::state::AppState; + +#[command] +pub async fn create_track(state: State<'_, AppState>, track: NewTrack) -> Result { + state.tracks.create_track(track).await +} + +#[command] +pub async fn upsert_track(state: State<'_, AppState>, track: NewTrack) -> Result { + state.tracks.upsert_track(track).await +} + +#[command] +pub async fn update_track( + state: State<'_, AppState>, + track: UpdateTrack, +) -> Result { + state.tracks.update_track(track).await +} + +#[command] +pub async fn delete_track(state: State<'_, AppState>, id: i64) -> Result<(), AppError> { + state.tracks.delete_track(id).await +} + +#[command] +pub async fn delete_track_by_path( + state: State<'_, AppState>, + path: String, +) -> Result<(), AppError> { + state.tracks.delete_track_by_path(&path).await +} + +#[command] +pub async fn get_track(state: State<'_, AppState>, id: i64) -> Result, AppError> { + state.tracks.get_track(id).await +} + +#[command] +pub async fn get_track_by_path( + state: State<'_, AppState>, + path: String, +) -> Result, AppError> { + state.tracks.get_track_by_path(&path).await +} + +#[command] +pub async fn list_tracks(state: State<'_, AppState>) -> Result, AppError> { + state.tracks.list_tracks().await +} + +#[command] +pub async fn search_tracks( + state: State<'_, AppState>, + query: TrackSearchQuery, +) -> Result, AppError> { + state.tracks.search_tracks(query).await +} + +#[command] +pub async fn mark_track_played( + state: State<'_, AppState>, + id: i64, + played_at: String, +) -> Result<(), AppError> { + state.tracks.mark_track_played(id, played_at).await +} diff --git a/src-tauri/src/db/manager.rs b/src-tauri/src/db/manager.rs new file mode 100644 index 0000000..bc64f8a --- /dev/null +++ b/src-tauri/src/db/manager.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous}; +use sqlx::{ConnectOptions, SqlitePool}; +use tauri::Manager; + +use crate::errors::AppError; + +#[derive(Clone)] +pub struct DatabaseManager { + pool: SqlitePool, +} + +impl DatabaseManager { + pub async fn new(app_handle: &tauri::AppHandle) -> Result { + let database_path = database_path(app_handle)?; + + if let Some(parent) = database_path.parent() { + let exists = tokio::fs::try_exists(parent).await.unwrap_or(false); + if !exists { + tokio::fs::create_dir_all(parent) + .await + .map_err(AppError::from)?; + } + } + + let options = SqliteConnectOptions::new() + .filename(&database_path) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .foreign_keys(true) + .disable_statement_logging(); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(options) + .await + .map_err(AppError::from)?; + + sqlx::migrate!("./migrations") + .run(&pool) + .await + .map_err(AppError::from)?; + + Ok(Self { pool }) + } + + pub fn pool(&self) -> &SqlitePool { + &self.pool + } +} + +fn database_path(app_handle: &tauri::AppHandle) -> Result { + let app_data_path = app_handle.path().app_data_dir().map_err(AppError::from)?; + + Ok(app_data_path.join("music.sqlite")) +} diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs new file mode 100644 index 0000000..ac04603 --- /dev/null +++ b/src-tauri/src/db/mod.rs @@ -0,0 +1,3 @@ +pub mod manager; + +pub use manager::DatabaseManager; diff --git a/src-tauri/src/errors/app_error.rs b/src-tauri/src/errors/app_error.rs new file mode 100644 index 0000000..da6a493 --- /dev/null +++ b/src-tauri/src/errors/app_error.rs @@ -0,0 +1,96 @@ +use serde::Serialize; + +use plugin_sdk::permissions::PluginPermission; +use plugin_sdk::errors::PluginError; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase", tag = "kind", content = "message")] +pub enum AppError { + Database(String), + Io(String), + Migration(String), + Service(String), + Command(String), + Platform(String), + Domain(String), + Plugin(String), + PluginPermissionDenied { + plugin_id: String, + permission: PluginPermission, + }, +} + +impl std::fmt::Display for AppError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppError::Database(message) + | AppError::Io(message) + | AppError::Migration(message) + | AppError::Service(message) + | AppError::Command(message) + | AppError::Platform(message) + | AppError::Domain(message) + | AppError::Plugin(message) => formatter.write_str(message), + AppError::PluginPermissionDenied { + plugin_id, + permission, + } => { + write!( + formatter, + "Plugin '{}' denied permission {:?}", + plugin_id, permission + ) + } + } + } +} + +impl std::error::Error for AppError {} + +impl From for AppError { + fn from(error: sqlx::Error) -> Self { + AppError::Database(error.to_string()) + } +} + +impl From for AppError { + fn from(error: sqlx::migrate::MigrateError) -> Self { + AppError::Migration(error.to_string()) + } +} + +impl From for AppError { + fn from(error: std::io::Error) -> Self { + AppError::Io(error.to_string()) + } +} + +impl From for AppError { + fn from(error: String) -> Self { + AppError::Service(error) + } +} + +impl From<&str> for AppError { + fn from(error: &str) -> Self { + AppError::Service(error.to_string()) + } +} + +impl From for AppError { + fn from(error: tauri::Error) -> Self { + AppError::Platform(error.to_string()) + } +} + +impl From for AppError { + fn from(error: PluginError) -> Self { + match error { + PluginError::Plugin(msg) => AppError::Plugin(msg), + PluginError::PermissionDenied { plugin_id, permission } => { + AppError::PluginPermissionDenied { plugin_id, permission } + } + PluginError::Io(msg) => AppError::Io(msg), + } + } +} diff --git a/src-tauri/src/errors/mod.rs b/src-tauri/src/errors/mod.rs new file mode 100644 index 0000000..eaeaca9 --- /dev/null +++ b/src-tauri/src/errors/mod.rs @@ -0,0 +1,3 @@ +pub mod app_error; + +pub use app_error::AppError; diff --git a/src-tauri/src/events/bus.rs b/src-tauri/src/events/bus.rs new file mode 100644 index 0000000..cd42476 --- /dev/null +++ b/src-tauri/src/events/bus.rs @@ -0,0 +1,50 @@ +use std::sync::{LazyLock, RwLock}; + +use tauri::{AppHandle, Emitter}; + +use crate::errors::AppError; +use crate::events::payloads::{AppEvent, LyricsLoadedPayload}; + +pub type EventListener = Box; + +static LISTENERS: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +pub struct EventBus; + +impl EventBus { + pub const CHANNEL: &'static str = "global-app-event"; + + pub fn subscribe(listener: EventListener) { + if let Ok(mut lock) = LISTENERS.write() { + lock.push(listener); + } + } + + pub fn emit(app_handle: &AppHandle, event: AppEvent) -> Result<(), AppError> { + Self::notify_local(&event); + app_handle.emit(Self::CHANNEL, event)?; + Ok(()) + } + + pub fn emit_local_only(event: &AppEvent) { + Self::notify_local(event); + } + + pub fn emit_lyrics_loaded( + app_handle: &AppHandle, + payload: LyricsLoadedPayload, + ) -> Result<(), AppError> { + Self::emit(app_handle, AppEvent::LyricsLoaded(payload)) + } + + fn notify_local(event: &AppEvent) { + let listeners = match LISTENERS.read() { + Ok(lock) => lock, + Err(_) => return, + }; + for listener in listeners.iter() { + listener(event); + } + } +} diff --git a/src-tauri/src/events/mod.rs b/src-tauri/src/events/mod.rs new file mode 100644 index 0000000..125b679 --- /dev/null +++ b/src-tauri/src/events/mod.rs @@ -0,0 +1,5 @@ +pub mod bus; +pub mod payloads; + +pub use bus::*; +pub use payloads::*; diff --git a/src-tauri/src/events/payloads.rs b/src-tauri/src/events/payloads.rs new file mode 100644 index 0000000..e699c54 --- /dev/null +++ b/src-tauri/src/events/payloads.rs @@ -0,0 +1,55 @@ +use serde::Serialize; + +use crate::models::{AppSettings, PlaybackQueue, Track}; + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackStatePayload { + pub playing: bool, + pub current_time: f64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackProgressPayload { + pub current_time: f64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackStartedPayload { + pub track: Track, + pub index: usize, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LyricLinePayload { + pub timestamp_ms: u64, + pub text: String, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LyricsLoadedPayload { + pub song_id: i64, + pub lines: Vec, +} + +#[derive(Clone, Serialize)] +#[serde(tag = "type", content = "payload")] +pub enum AppEvent { + VolumeChanged(f32), + + SettingsChanged(AppSettings), + + QueueChanged(PlaybackQueue), + + PlaybackStateChanged(PlaybackStatePayload), + + PlaybackProgress(PlaybackProgressPayload), + + TrackStarted(TrackStartedPayload), + + LyricsLoaded(LyricsLoadedPayload), +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e7f1264..fc4ef53 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,46 +1,369 @@ -use std::fs::File; +mod audio; +mod commands; +mod db; +mod errors; +mod events; +mod media_controls; +mod metadata; +mod models; +mod repositories; +mod services; +mod state; -use tauri::command; +use std::sync::Arc; -use web_audio_api::context::{AudioContext, BaseAudioContext}; -use web_audio_api::node::{AudioNode, AudioScheduledSourceNode}; +use tauri::{ + menu::{Menu, MenuBuilder, MenuItem, SubmenuBuilder}, + tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, + Emitter, Manager, +}; -#[command] -fn play_music(name: &str) -> String { - let context = AudioContext::default(); +use commands::library::{ + delete_track_file, delete_track_files, execute_scan, get_track_cover, load_track_library, + scan_track_directories, show_in_folder, trash_track_files, +}; - let current_dir = std::env::current_dir().unwrap(); - let file_path = current_dir.join("..").join("static").join(name); +use commands::playback::{ + current_time, get_current_status, insert_track_as_next, pause_track, play_next_track, + play_previous_track, play_queue_track, play_track, remove_track_from_queue, resume_track, + set_current_time, set_volume, stop_track, sync_playback_queue, toggle_track, +}; +use services::playback_service::PlaybackService; - let file = File::open(file_path).unwrap(); - let buffer = context.decode_audio_data_sync(file).unwrap(); +use commands::recent::{add_recently_played, clear_recently_played, load_recently_played}; - let mut src = context.create_buffer_source(); - src.set_buffer(buffer); - src.set_loop(false); +use commands::settings::{get_settings, load_settings, save_settings, update_settings}; - // // create a biquad filter - // let biquad = context.create_biquad_filter(); - // biquad.frequency().set_value(125.); - // connect the audio nodes - // src.connect(&biquad); - // biquad.connect(&context.destination()); +use commands::menu::show_context_menu; +use commands::playlists::{ + add_track_to_playlist, clear_playlist_tracks, create_playlist, delete_playlist, get_playlist, + get_playlist_with_tracks, list_playlists, list_playlists_with_tracks, + remove_track_from_playlist, rename_playlist, reorder_playlist_track, +}; +use commands::tracks::{ + create_track, delete_track, delete_track_by_path, get_track, get_track_by_path, list_tracks, + mark_track_played, search_tracks, update_track, upsert_track, +}; +use db::DatabaseManager; +use media_controls::init_media_controls; +use repositories::sqlite::{ + SqlitePlaylistRepository, SqliteRecentRepository, SqliteSettingsRepository, + SqliteTrackRepository, +}; +use services::plugin::capability::permission_checker::PermissionChecker; +use services::plugin::capability::CapabilityRegistry; +use services::plugin::provider::{BuiltinManifestProvider, ManifestProvider}; +use services::plugin::registry::{FactoryRegistry, ManifestRegistry}; +use services::plugin::runtime::context_factory::PluginContextFactory; +use services::plugin::settings::persistence::JsonPluginStorage; +use services::plugin::settings::settings_registry::SettingsRegistry; +use services::plugin::PluginManager; +use services::{PlaylistService, RecentService, SettingsService, TrackService}; +use state::AppState; - src.connect(&context.destination()); +use commands::playback_queue::{ + clear_queue, get_playback_queue, insert_tracks_as_next, replace_playlist_and_play, + set_play_mode, +}; - src.start(); +use commands::plugin_extensions::{ + disable_plugin_command, enable_plugin_command, execute_plugin_command, + get_all_sidebar_extensions, get_menu_extensions, get_native_view_extensions, + get_plugin_manifests, get_plugin_settings, get_plugin_view, get_sidebar_extensions, + update_plugin_setting, +}; - let buffer_duration = src.buffer().unwrap().duration() as f64; - let sleep_duration = std::time::Duration::from_secs_f64(buffer_duration); +use commands::equalizer::{apply_eq_preset, get_eq_state, set_eq_band, set_eq_enabled}; - format!("Playing music: {}", name) -} +use audio::init_audio_state; + +use crate::errors::AppError; + +pub fn init_startup_scan(_app_handle: tauri::AppHandle) {} #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - // .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![play_music]) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_store::Builder::new().build()) + .register_uri_scheme_protocol("plugin", crate::services::plugin::asset::scheme::handle) + .setup(|app| { + println!("[Timer] setup start"); + let app_handle = app.handle().clone(); + + println!("[Timer] creating database..."); + let database = tauri::async_runtime::block_on(DatabaseManager::new(&app_handle))?; + let pool = database.pool().clone(); + + let track_repository = Arc::new(SqliteTrackRepository::new(pool.clone())); + let playlist_repository = Arc::new(SqlitePlaylistRepository::new(pool.clone())); + let setting_repository = Arc::new(SqliteSettingsRepository::new(pool.clone())); + let recent_repository = Arc::new(SqliteRecentRepository::new(pool)); + + println!("[Timer] loading settings..."); + let setting_service = Arc::new(SettingsService::new(setting_repository)); + tauri::async_runtime::block_on(setting_service.get_settings())?; + + println!("[Timer] creating services..."); + let track_service = Arc::new(TrackService::new(track_repository)); + let playlist_service = Arc::new(PlaylistService::new(playlist_repository)); + let recent_service = Arc::new(RecentService::new(recent_repository)); + + let capability_registry = Arc::new(CapabilityRegistry::new()); + let permission_checker = + Arc::new(PermissionChecker::new(Arc::clone(&capability_registry))); + let settings_registry = Arc::new(SettingsRegistry::new()); + + // 共享音频处理器 registry:由 PluginManager(激活/失活插件时写入) + // 与 WebAudioEngine 的 ExternalProcessorNode(渲染量子时读取)共同持有。 + let audio_processor_registry = Arc::new( + crate::services::plugin::AudioProcessorRegistry::new(), + ); + + let app_data_dir = app_handle + .path() + .app_data_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")); + let storage = Arc::new(JsonPluginStorage::new(app_data_dir)); + + // Plugin manifest aggregation: collect → normalize → validate → + // resolve → cache. Builtin manifests are compile-time embedded; + // packaged/user providers are reserved for future expansion. + let manifest_registry = Arc::new(ManifestRegistry::new()); + let providers: Vec> = + vec![Box::new(BuiltinManifestProvider)]; + if let Err(e) = manifest_registry.register_from_providers(providers) { + eprintln!("[startup] Failed to load plugin manifests: {}", e); + } + let factory_registry = Arc::new(FactoryRegistry::new()); + + let context_factory = PluginContextFactory::new( + app_handle.clone(), + permission_checker, + Arc::new(PlaybackService::new(app.handle().clone())), + track_service.clone(), + setting_service.clone(), + Arc::clone(&settings_registry), + ); + + let plugin_manager = Arc::new(PluginManager::new( + app.handle().clone(), + manifest_registry.clone(), + factory_registry.clone(), + context_factory, + capability_registry, + settings_registry, + storage, + Arc::clone(&audio_processor_registry), + )); + plugin_manager.load_persisted_state(); + plugin_manager.load_builtin_plugins(); + let pm = plugin_manager.clone(); + let handle_for_plugins = app.handle().clone(); + tauri::async_runtime::spawn_blocking(move || { + pm.load_packaged_plugins(); + let _ = handle_for_plugins.emit("plugins:loaded", ()); + }); + + let app_state = AppState::new( + track_service, + playlist_service, + setting_service, + recent_service, + plugin_manager.clone(), + ); + + app.manage(app_state); + app.manage(plugin_manager); + + init_audio_state(models::PlaybackQueue::default()); + + println!("[Timer] init media controls..."); + let _ = init_media_controls(app.handle().clone()); + println!("[Timer] starting playback progress task..."); + PlaybackService::spawn_playback_progress_task(app.handle().clone()); + + println!("[Timer] setup complete, WebView should render now"); + + let settings_item = MenuItem::with_id(app, "settings", "设置", true, None::<&str>)?; + let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?; + let tray_menu = Menu::with_items(app, &[&settings_item, &quit_item])?; + let file_menu = SubmenuBuilder::new(app, "文件") + .text("quit", "退出") + .build()?; + + let app_menu = MenuBuilder::new(app).items(&[&file_menu]).build()?; + app.set_menu(app_menu)?; + + let icon = app + .default_window_icon() + .cloned() + .ok_or_else(|| AppError::from("Application default window icon is missing"))?; + + let _tray = TrayIconBuilder::new() + .icon(icon) + .menu(&tray_menu) + .show_menu_on_left_click(false) + .on_menu_event( + |app_handle: &tauri::AppHandle, event| match event.id.as_ref() { + "settings" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.emit("tray:navigate", "settings"); + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + "quit" => { + app_handle.exit(0); + } + _ => {} + }, + ) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + .. + } = event + { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .build(app)?; + + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let state = handle.state::(); + if let Ok(settings) = state.settings.get_settings().await { + if settings.scan_on_startup { + let library_dirs = settings.library_dirs; + let scanned_result = tauri::async_runtime::spawn_blocking(move || { + execute_scan(library_dirs) + }) + .await; + + match scanned_result { + Ok(Ok(scanned)) => { + let mut tracks = Vec::with_capacity(scanned.len()); + for track in scanned { + if let Ok(saved) = state.tracks.upsert_track(track).await { + tracks.push(saved); + } + } + let _ = handle.emit("library:refreshed", tracks); + } + Ok(Err(error)) => { + eprintln!("{}", error); + } + Err(error) => { + eprintln!("{}", error); + } + } + } + } + }); + + app.on_menu_event(move |app_handle: &tauri::AppHandle, event| { + match event.id().0.as_str() { + "quit" => { + app_handle.exit(0); + } + "play" => { + let handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = PlaybackService::new(handle).toggle().await; + }); + } + "next" => { + let handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = PlaybackService::new(handle).next().await; + }); + } + _ => {} + } + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + show_context_menu, + create_track, + upsert_track, + update_track, + delete_track, + delete_track_by_path, + get_track, + get_track_by_path, + list_tracks, + search_tracks, + mark_track_played, + create_playlist, + rename_playlist, + delete_playlist, + get_playlist, + list_playlists, + list_playlists_with_tracks, + get_playlist_with_tracks, + add_track_to_playlist, + remove_track_from_playlist, + clear_playlist_tracks, + reorder_playlist_track, + play_track, + play_queue_track, + insert_track_as_next, + remove_track_from_queue, + play_next_track, + play_previous_track, + stop_track, + sync_playback_queue, + resume_track, + pause_track, + toggle_track, + current_time, + get_current_status, + set_current_time, + set_volume, + scan_track_directories, + show_in_folder, + delete_track_file, + delete_track_files, + trash_track_files, + load_track_library, + get_track_cover, + load_recently_played, + add_recently_played, + clear_recently_played, + get_settings, + update_settings, + load_settings, + save_settings, + get_playback_queue, + replace_playlist_and_play, + insert_tracks_as_next, + set_play_mode, + clear_queue, + get_menu_extensions, + get_sidebar_extensions, + get_all_sidebar_extensions, + get_native_view_extensions, + get_plugin_manifests, + get_plugin_settings, + update_plugin_setting, + enable_plugin_command, + disable_plugin_command, + execute_plugin_command, + get_plugin_view, + get_eq_state, + set_eq_band, + apply_eq_preset, + set_eq_enabled, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2abccd9..4a233fd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - tauri_app_lib::run() + rust_echo_music_lib::run() } diff --git a/src-tauri/src/media_controls.rs b/src-tauri/src/media_controls.rs new file mode 100644 index 0000000..dc510d7 --- /dev/null +++ b/src-tauri/src/media_controls.rs @@ -0,0 +1,107 @@ +use std::ffi::c_void; +use std::sync::mpsc::{self, Sender}; +use std::sync::OnceLock; +use std::thread; +use std::time::Duration; + +use souvlaki::{MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig}; +use tauri::{AppHandle, Manager, WebviewWindow}; + +use crate::commands::playback::handle_media_control_event; +use crate::events::PlaybackStatePayload; +use crate::models::playback::NativeTrackMetadata; + +#[derive(Clone)] +enum MediaControlMessage { + Metadata(NativeTrackMetadata), + Playback(PlaybackStatePayload), +} + +static MEDIA_CONTROL_SENDER: OnceLock> = OnceLock::new(); + +#[cfg(target_os = "windows")] +fn window_handle(window: &WebviewWindow) -> Option { + window.hwnd().ok().map(|handle| handle.0 as isize) +} + +#[cfg(not(target_os = "windows"))] +fn window_handle(_window: &WebviewWindow) -> Option { + None +} + +pub fn init_media_controls(app_handle: AppHandle) -> Result<(), String> { + let window = app_handle + .get_webview_window("main") + .ok_or_else(|| "main window not found".to_string())?; + + let hwnd = window_handle(&window); + let (tx, rx) = mpsc::channel::(); + let app_for_events = app_handle.clone(); + + thread::spawn(move || { + #[cfg(target_os = "windows")] + if hwnd.is_none() { + return; + } + + let config = PlatformConfig { + display_name: "RustEchoMusic", + dbus_name: "rust_echo_music", + hwnd: hwnd.map(|value| value as *mut c_void), + }; + + let Ok(mut controls) = MediaControls::new(config) else { + return; + }; + + let _ = controls.attach(move |event| { + let app = app_for_events.clone(); + tauri::async_runtime::spawn(async move { + handle_media_control_event(app, event).await; + }); + }); + + while let Ok(message) = rx.recv() { + match message { + MediaControlMessage::Metadata(track) => { + let duration = track.duration.map(Duration::from_secs_f64); + let metadata = MediaMetadata { + title: Some(track.title.as_str()), + album: Some(track.album.as_str()), + artist: Some(track.artist.as_str()), + cover_url: None, + duration, + }; + let _ = controls.set_metadata(metadata); + } + MediaControlMessage::Playback(state) => { + let progress = Some(MediaPosition(Duration::from_secs_f64(state.current_time))); + let playback = if state.playing { + MediaPlayback::Playing { progress } + } else { + MediaPlayback::Paused { progress } + }; + let _ = controls.set_playback(playback); + } + } + } + }); + + let _ = MEDIA_CONTROL_SENDER.set(tx); + + Ok(()) +} + +fn send(message: MediaControlMessage) { + if let Some(sender) = MEDIA_CONTROL_SENDER.get() { + let _ = sender.send(message); + } +} + +pub fn update_media_controls_metadata(track: NativeTrackMetadata) { + send(MediaControlMessage::Metadata(track)); +} + +pub fn update_media_controls_playback(state: PlaybackStatePayload) { + send(MediaControlMessage::Playback(state)); +} diff --git a/src-tauri/src/metadata.rs b/src-tauri/src/metadata.rs new file mode 100644 index 0000000..f4960ae --- /dev/null +++ b/src-tauri/src/metadata.rs @@ -0,0 +1,53 @@ +use std::fs; +use std::path::Path; + +use lofty::prelude::*; +use lofty::probe::Probe; + +use crate::models::NewTrack; + +pub fn parse_single_track(file_path: &Path) -> Option { + let tagged_file = Probe::open(file_path).ok()?.read().ok()?; + + let tag = tagged_file + .primary_tag() + .or_else(|| tagged_file.first_tag()); + + let title = tag + .and_then(|t| t.title()) + .map(|s| s.to_string()) + .unwrap_or_else(|| { + file_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("Unknown Track") + .to_string() + }); + + let artist = tag + .and_then(|t| t.artist()) + .map(|s| s.to_string()) + .or_else(|| Some("Unknown Artist".to_string())); + + let album = tag + .and_then(|t| t.album()) + .map(|s| s.to_string()) + .or_else(|| Some("Unknown Album".to_string())); + + let props = tagged_file.properties(); + let duration = (props.duration().as_secs_f64() * 1000.0).round() as i64; + let path = file_path.to_string_lossy().into_owned(); + let file_size = fs::metadata(file_path) + .ok() + .and_then(|metadata| i64::try_from(metadata.len()).ok()); + + Some(NewTrack { + title, + artist, + album, + duration, + path, + cover: None, + file_size, + }) +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs new file mode 100644 index 0000000..b4d7a0f --- /dev/null +++ b/src-tauri/src/models/mod.rs @@ -0,0 +1,14 @@ +pub mod playback; +pub mod playback_queue; +pub mod playlist; +pub mod recent; +pub mod settings; +pub mod track; + +pub use playback_queue::PlaybackQueue; +pub use playlist::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, +}; +pub use recent::RecentPlayedWithTrack; +pub use settings::{AppSettings, PluginLogLevel, SettingRow, ThemeMode}; +pub use track::{NewTrack, SortDirection, Track, TrackSearchQuery, TrackSortBy, UpdateTrack}; diff --git a/src-tauri/src/models/playback.rs b/src-tauri/src/models/playback.rs new file mode 100644 index 0000000..b7be620 --- /dev/null +++ b/src-tauri/src/models/playback.rs @@ -0,0 +1,31 @@ +use serde::Serialize; + +use crate::models::Track; + +#[derive(Clone)] +pub struct NativeTrackMetadata { + pub title: String, + pub album: String, + pub artist: String, + pub duration: Option, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackStatusSnapshot { + pub playing: bool, + pub current_time: f64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackTrackStartedPayload { + pub track: Track, + pub index: usize, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackTrackInfo { + pub id: Option, +} diff --git a/src-tauri/src/models/playback_queue.rs b/src-tauri/src/models/playback_queue.rs new file mode 100644 index 0000000..4509d07 --- /dev/null +++ b/src-tauri/src/models/playback_queue.rs @@ -0,0 +1,263 @@ +use rand::RngExt; +use serde::{Deserialize, Serialize}; + +use crate::audio::state::PlayMode; +use crate::models::Track; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackQueue { + pub tracks: Vec, + pub current_index: Option, + pub play_mode: PlayMode, + pub history: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct QueueRemoveResult { + pub play_index: Option, + pub should_stop: bool, +} + +impl Default for PlaybackQueue { + fn default() -> Self { + Self { + tracks: Vec::new(), + current_index: None, + play_mode: PlayMode::ListLoop, + history: Vec::new(), + } + } +} + +impl PlaybackQueue { + pub fn sync( + &mut self, + tracks: Vec, + current_index: Option, + play_mode: PlayMode, + history: Vec, + ) { + self.tracks = tracks; + self.current_index = current_index.filter(|i| *i < self.tracks.len()); + self.play_mode = play_mode; + self.history = history; + } + + pub fn clear(&mut self) { + self.tracks.clear(); + self.current_index = None; + self.history.clear(); + } + + pub fn is_empty(&self) -> bool { + self.tracks.is_empty() + } + + pub fn len(&self) -> usize { + self.tracks.len() + } + + pub fn current_index(&self) -> Option { + self.current_index + } + + pub fn set_current_index(&mut self, index: Option) { + self.current_index = index.filter(|i| *i < self.tracks.len()); + } + + pub fn set_play_mode(&mut self, mode: PlayMode) { + self.play_mode = mode; + } + + pub fn current_track(&self) -> Option<&Track> { + self.current_index.and_then(|i| self.tracks.get(i)) + } + + pub fn current_track_cloned(&self) -> Option { + self.current_track().cloned() + } + + pub fn track(&self, index: usize) -> Option<&Track> { + self.tracks.get(index) + } + + pub fn require_track(&self, index: usize) -> Result { + self.tracks + .get(index) + .cloned() + .ok_or_else(|| "Invalid queue index".to_string()) + } + + pub fn contains_track(&self, track_id: i64) -> bool { + self.tracks.iter().any(|t| t.id == track_id) + } + + pub fn find_track(&self, track_id: i64) -> Option<&Track> { + self.tracks.iter().find(|t| t.id == track_id) + } + + pub fn remove_track(&mut self, track_id: i64) -> Option { + let remove_index = self.tracks.iter().position(|track| track.id == track_id)?; + + let was_current = self.current_index == Some(remove_index); + + self.tracks.remove(remove_index); + self.history.retain(|id| *id != track_id); + + if self.tracks.is_empty() { + self.current_index = None; + return Some(QueueRemoveResult { + play_index: None, + should_stop: true, + }); + } + + if was_current { + let next_index = remove_index.min(self.tracks.len() - 1); + self.current_index = Some(next_index); + return Some(QueueRemoveResult { + play_index: Some(next_index), + should_stop: false, + }); + } + + if let Some(current_index) = self.current_index { + if remove_index < current_index { + self.current_index = Some(current_index - 1); + } + } + + Some(QueueRemoveResult { + play_index: None, + should_stop: false, + }) + } + + pub fn insert_next(&mut self, mut track: Track) { + track.cover = None; + + if let Some(existing_index) = self.tracks.iter().position(|item| item.id == track.id) { + if self.current_index == Some(existing_index) { + return; + } + + self.tracks.remove(existing_index); + + if let Some(current_index) = self.current_index { + if existing_index < current_index { + self.current_index = Some(current_index - 1); + } + } + } + + if self.tracks.is_empty() || self.current_index.is_none() { + self.tracks.push(track); + self.current_index = Some(0); + return; + } + + let insert_index = self.current_index.unwrap() + 1; + + self.tracks.insert(insert_index, track); + } + + pub fn insert_tracks_as_next(&mut self, tracks: Vec) { + for track in tracks.into_iter().rev() { + self.insert_next(track); + } + } + + pub fn replace_playlist( + &mut self, + tracks: Vec, + target_id: i64, + ) -> Result { + self.tracks = tracks; + self.history.clear(); + let index = self + .tracks + .iter() + .position(|t| t.id == target_id) + .unwrap_or(0); + self.current_index = Some(index); + Ok(index) + } + + pub fn next_index(&self) -> Option { + if self.tracks.is_empty() { + return None; + } + + let current = self.current_index.unwrap_or(0); + + match self.play_mode { + PlayMode::SingleLoop => Some(current), + + PlayMode::ListLoop => Some((current + 1) % self.tracks.len()), + + PlayMode::Shuffle => { + if self.tracks.len() <= 1 { + return Some(0); + } + + let mut rng = rand::rng(); + + loop { + let next = rng.random_range(0..self.tracks.len()); + + if next != current { + return Some(next); + } + } + } + } + } + + pub fn previous_index(&self) -> Option { + if self.tracks.is_empty() { + return None; + } + + if let Some(last_id) = self.history.last() { + if let Some(index) = self.tracks.iter().position(|track| track.id == *last_id) { + return Some(index); + } + } + + let current = self.current_index.unwrap_or(0); + + Some(if current == 0 { + self.tracks.len() - 1 + } else { + current - 1 + }) + } + + pub fn move_next(&mut self) -> Option { + if let Some(track) = self.current_track() { + if self.history.last() != Some(&track.id) { + self.history.push(track.id); + } + } + + self.next_index() + } + + pub fn move_previous(&mut self) -> Option { + let index = self.previous_index()?; + + if let Some(track) = self.tracks.get(index) { + if self.history.last() == Some(&track.id) { + self.history.pop(); + } + } + + Some(index) + } + + pub fn payload(&self) -> Self { + self.clone() + } +} diff --git a/src-tauri/src/models/playlist.rs b/src-tauri/src/models/playlist.rs new file mode 100644 index 0000000..2692373 --- /dev/null +++ b/src-tauri/src/models/playlist.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +use super::track::Track; + +#[derive(Clone, Debug, Deserialize, FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Playlist { + pub id: i64, + pub name: String, + pub created_at: String, +} + +#[derive(Clone, Debug, Deserialize, FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaylistTrack { + pub id: i64, + pub playlist_id: i64, + pub track_id: i64, + pub position: i64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaylistWithTracks { + pub playlist: Playlist, + pub tracks: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NewPlaylist { + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenamePlaylist { + pub id: i64, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AddPlaylistTrack { + pub playlist_id: i64, + pub track_id: i64, + pub position: Option, +} diff --git a/src-tauri/src/models/recent.rs b/src-tauri/src/models/recent.rs new file mode 100644 index 0000000..8105007 --- /dev/null +++ b/src-tauri/src/models/recent.rs @@ -0,0 +1,8 @@ +use crate::models::Track; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecentPlayedWithTrack { + pub track: Track, + pub played_at: String, +} diff --git a/src-tauri/src/models/settings.rs b/src-tauri/src/models/settings.rs new file mode 100644 index 0000000..7373041 --- /dev/null +++ b/src-tauri/src/models/settings.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSettings { + pub theme: ThemeMode, + pub volume: u8, + pub library_dirs: Vec, + pub scan_on_startup: bool, + pub reduce_motion: bool, + pub use_album_artist_grouping: bool, + pub plugin_dirs: Vec, + pub plugin_dev_mode: bool, + pub plugin_scan_on_startup: bool, + pub plugin_log_level: PluginLogLevel, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + theme: ThemeMode::Auto, + volume: 80, + library_dirs: Vec::new(), + scan_on_startup: false, + reduce_motion: false, + use_album_artist_grouping: false, + plugin_dirs: Vec::new(), + plugin_dev_mode: false, + plugin_scan_on_startup: true, + plugin_log_level: PluginLogLevel::Warn, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ThemeMode { + Auto, + Light, + Dark, +} + +impl ThemeMode { + pub fn as_str(&self) -> &'static str { + match self { + ThemeMode::Auto => "auto", + ThemeMode::Light => "light", + ThemeMode::Dark => "dark", + } + } +} + +impl TryFrom for ThemeMode { + type Error = String; + + fn try_from(value: String) -> Result>::Error> { + match value.as_str() { + "auto" => Ok(ThemeMode::Auto), + "light" => Ok(ThemeMode::Light), + "dark" => Ok(ThemeMode::Dark), + _ => Err(format!("无效主题: {}", value)), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PluginLogLevel { + Off, + Error, + Warn, + Info, + Debug, +} + +impl PluginLogLevel { + pub fn as_str(&self) -> &'static str { + match self { + PluginLogLevel::Off => "off", + PluginLogLevel::Error => "error", + PluginLogLevel::Warn => "warn", + PluginLogLevel::Info => "info", + PluginLogLevel::Debug => "debug", + } + } +} + +impl TryFrom for PluginLogLevel { + type Error = String; + + fn try_from(value: String) -> Result>::Error> { + match value.as_str() { + "off" => Ok(PluginLogLevel::Off), + "error" => Ok(PluginLogLevel::Error), + "warn" => Ok(PluginLogLevel::Warn), + "info" => Ok(PluginLogLevel::Info), + "debug" => Ok(PluginLogLevel::Debug), + _ => Err(format!("无效插件日志级别: {}", value)), + } + } +} + +#[derive(Clone, Debug, FromRow)] +pub struct SettingRow { + pub id: i64, + pub theme: String, + pub volume: i64, + pub scan_on_startup: i64, + pub reduce_motion: i64, + pub library_dirs: String, + pub use_album_artist_grouping: i64, + pub plugin_dirs: String, + pub plugin_dev_mode: i64, + pub plugin_scan_on_startup: i64, + pub plugin_log_level: String, + pub created_at: String, + pub updated_at: String, +} diff --git a/src-tauri/src/models/track.rs b/src-tauri/src/models/track.rs new file mode 100644 index 0000000..d771da4 --- /dev/null +++ b/src-tauri/src/models/track.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +#[derive(Clone, Debug, Deserialize, FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Track { + pub id: i64, + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, + pub path: String, + pub cover: Option, + pub file_size: Option, + pub play_count: i64, + pub last_played_at: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NewTrack { + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, + pub path: String, + pub cover: Option, + pub file_size: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateTrack { + pub id: i64, + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, + pub path: String, + pub cover: Option, + pub file_size: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TrackSortBy { + Title, + Artist, + Album, + Duration, + CreatedAt, + UpdatedAt, + PlayCount, + LastPlayedAt, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SortDirection { + Asc, + Desc, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackSearchQuery { + pub keyword: Option, + pub sort_by: Option, + pub sort_direction: Option, + pub limit: Option, + pub offset: Option, +} diff --git a/src-tauri/src/repositories/mod.rs b/src-tauri/src/repositories/mod.rs new file mode 100644 index 0000000..6b1c108 --- /dev/null +++ b/src-tauri/src/repositories/mod.rs @@ -0,0 +1 @@ +pub mod sqlite; diff --git a/src-tauri/src/repositories/sqlite/mod.rs b/src-tauri/src/repositories/sqlite/mod.rs new file mode 100644 index 0000000..2230893 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/mod.rs @@ -0,0 +1,9 @@ +pub mod playlist_repository; +pub mod recent_repository; +pub mod settings_repository; +pub mod track_repository; + +pub use playlist_repository::SqlitePlaylistRepository; +pub use recent_repository::SqliteRecentRepository; +pub use settings_repository::SqliteSettingsRepository; +pub use track_repository::SqliteTrackRepository; diff --git a/src-tauri/src/repositories/sqlite/playlist_repository.rs b/src-tauri/src/repositories/sqlite/playlist_repository.rs new file mode 100644 index 0000000..d6d9791 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/playlist_repository.rs @@ -0,0 +1,178 @@ +use sqlx::SqlitePool; + +use crate::errors::AppError; +use crate::models::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, + Track, +}; + +#[derive(Clone)] +pub struct SqlitePlaylistRepository { + pool: SqlitePool, +} + +impl SqlitePlaylistRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn create(&self, playlist: NewPlaylist) -> Result { + let res = sqlx::query_as::<_, Playlist>( + "INSERT INTO playlists (name, created_at) + VALUES (?1, datetime('now')) + RETURNING id, name, created_at", + ) + .bind(playlist.name) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn rename(&self, playlist: RenamePlaylist) -> Result { + let res = sqlx::query_as::<_, Playlist>( + "UPDATE playlists SET name = ?1 WHERE id = ?2 RETURNING id, name, created_at", + ) + .bind(playlist.name) + .bind(playlist.id) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn delete(&self, id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM playlists WHERE id = ?1") + .bind(id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn find_by_id(&self, id: i64) -> Result, AppError> { + let res = sqlx::query_as::<_, Playlist>( + "SELECT id, name, created_at FROM playlists WHERE id = ?1", + ) + .bind(id) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn list_all(&self) -> Result, AppError> { + let rows = sqlx::query_as::<_, Playlist>( + "SELECT id, name, created_at FROM playlists ORDER BY created_at DESC, id DESC", + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + pub async fn list_with_tracks(&self) -> Result, AppError> { + let playlists = self.list_all().await?; + let mut result = Vec::with_capacity(playlists.len()); + + for playlist in playlists { + let tracks = tracks_for_playlist(&self.pool, playlist.id).await?; + result.push(PlaylistWithTracks { playlist, tracks }); + } + + Ok(result) + } + + pub async fn get_with_tracks(&self, id: i64) -> Result, AppError> { + let Some(playlist) = self.find_by_id(id).await? else { + return Ok(None); + }; + + let tracks = tracks_for_playlist(&self.pool, playlist.id).await?; + Ok(Some(PlaylistWithTracks { playlist, tracks })) + } + + pub async fn add_track(&self, track: AddPlaylistTrack) -> Result { + let position = match track.position { + Some(position) => position, + None => next_position(&self.pool, track.playlist_id).await?, + }; + + let res = sqlx::query_as::<_, PlaylistTrack>( + "INSERT INTO playlist_tracks (playlist_id, track_id, position) + VALUES (?1, ?2, ?3) + ON CONFLICT(playlist_id, track_id) DO UPDATE SET position = excluded.position + RETURNING id, playlist_id, track_id, position", + ) + .bind(track.playlist_id) + .bind(track.track_id) + .bind(position) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn remove_track(&self, playlist_id: i64, track_id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM playlist_tracks WHERE playlist_id = ?1 AND track_id = ?2") + .bind(playlist_id) + .bind(track_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn clear_tracks(&self, playlist_id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM playlist_tracks WHERE playlist_id = ?1") + .bind(playlist_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn reorder_track( + &self, + playlist_id: i64, + track_id: i64, + position: i64, + ) -> Result<(), AppError> { + sqlx::query( + "UPDATE playlist_tracks SET position = ?1 WHERE playlist_id = ?2 AND track_id = ?3", + ) + .bind(position) + .bind(playlist_id) + .bind(track_id) + .execute(&self.pool) + .await?; + + Ok(()) + } +} + +async fn next_position(pool: &SqlitePool, playlist_id: i64) -> Result { + let row: (i64,) = sqlx::query_as( + "SELECT coalesce(max(position), -1) + 1 FROM playlist_tracks WHERE playlist_id = ?1", + ) + .bind(playlist_id) + .fetch_one(pool) + .await?; + + Ok(row.0) +} + +async fn tracks_for_playlist(pool: &SqlitePool, playlist_id: i64) -> Result, AppError> { + let rows = sqlx::query_as::<_, Track>( + "SELECT t.id, t.title, t.artist, t.album, t.duration, t.path, t.cover, t.file_size, t.play_count, t.last_played_at, t.created_at, t.updated_at + FROM tracks t + INNER JOIN playlist_tracks pt ON pt.track_id = t.id + WHERE pt.playlist_id = ?1 + ORDER BY pt.position ASC, pt.id ASC", + ) + .bind(playlist_id) + .fetch_all(pool) + .await?; + + Ok(rows) +} diff --git a/src-tauri/src/repositories/sqlite/recent_repository.rs b/src-tauri/src/repositories/sqlite/recent_repository.rs new file mode 100644 index 0000000..0941ff4 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/recent_repository.rs @@ -0,0 +1,114 @@ +use sqlx::SqlitePool; + +use crate::errors::AppError; +use crate::models::{RecentPlayedWithTrack, Track}; + +#[derive(Clone)] +pub struct SqliteRecentRepository { + pool: SqlitePool, +} + +impl SqliteRecentRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn list_with_tracks( + &self, + limit: i64, + offset: i64, + ) -> Result, AppError> { + let rows = sqlx::query_as::<_, RecentRow>( + "SELECT t.id, t.title, t.artist, t.album, t.duration, t.path, t.cover, t.file_size, t.play_count, t.last_played_at, t.created_at, t.updated_at, r.played_at + FROM recent_played r + INNER JOIN tracks t ON t.id = r.track_id + ORDER BY r.played_at DESC + LIMIT ?1 OFFSET ?2", + ) + .bind(limit) + .bind(offset) + .fetch_all(&self.pool) + .await?; + + Ok(rows + .into_iter() + .map(|row| RecentPlayedWithTrack { + track: Track { + id: row.id, + title: row.title, + artist: row.artist, + album: row.album, + duration: row.duration, + path: row.path, + cover: row.cover, + file_size: row.file_size, + play_count: row.play_count, + last_played_at: row.last_played_at, + created_at: row.created_at, + updated_at: row.updated_at, + }, + played_at: row.played_at, + }) + .collect()) + } + + pub async fn upsert(&self, track_id: i64, played_at: String) -> Result<(), AppError> { + sqlx::query( + "INSERT INTO recent_played (track_id, played_at) + VALUES (?1, ?2) + ON CONFLICT(track_id) DO UPDATE SET played_at = excluded.played_at", + ) + .bind(track_id) + .bind(played_at) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn clear(&self) -> Result<(), AppError> { + sqlx::query("DELETE FROM recent_played") + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn count(&self) -> Result { + let row: (i64,) = sqlx::query_as("SELECT count(*) FROM recent_played") + .fetch_one(&self.pool) + .await?; + + Ok(row.0) + } + + pub async fn remove_oldest(&self, keep: i64) -> Result<(), AppError> { + sqlx::query( + "DELETE FROM recent_played WHERE track_id NOT IN ( + SELECT track_id FROM recent_played ORDER BY played_at DESC LIMIT ?1 + )", + ) + .bind(keep) + .execute(&self.pool) + .await?; + + Ok(()) + } +} + +#[derive(sqlx::FromRow)] +struct RecentRow { + id: i64, + title: String, + artist: Option, + album: Option, + duration: i64, + path: String, + cover: Option, + file_size: Option, + play_count: i64, + last_played_at: Option, + created_at: String, + updated_at: String, + played_at: String, +} diff --git a/src-tauri/src/repositories/sqlite/settings_repository.rs b/src-tauri/src/repositories/sqlite/settings_repository.rs new file mode 100644 index 0000000..f1651f8 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/settings_repository.rs @@ -0,0 +1,102 @@ +use sqlx::SqlitePool; + +use crate::errors::AppError; +use crate::models::SettingRow; + +#[derive(Clone)] +pub struct SqliteSettingsRepository { + pool: SqlitePool, +} + +impl SqliteSettingsRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn get(&self) -> Result, AppError> { + let res = sqlx::query_as::<_, SettingRow>( + "SELECT + id, + theme, + volume, + scan_on_startup, + reduce_motion, + library_dirs, + use_album_artist_grouping, + plugin_dirs, + plugin_dev_mode, + plugin_scan_on_startup, + plugin_log_level, + created_at, + updated_at + FROM settings + WHERE id = 1", + ) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn update(&self, row: SettingRow) -> Result<(), AppError> { + sqlx::query( + "INSERT INTO settings ( + id, + theme, + volume, + scan_on_startup, + reduce_motion, + library_dirs, + use_album_artist_grouping, + plugin_dirs, + plugin_dev_mode, + plugin_scan_on_startup, + plugin_log_level, + created_at, + updated_at + ) + VALUES ( + 1, + ?1, + ?2, + ?3, + ?4, + ?5, + ?6, + ?7, + ?8, + ?9, + ?10, + coalesce(nullif(?11, ''), datetime('now')), + datetime('now') + ) + ON CONFLICT(id) DO UPDATE SET + theme = excluded.theme, + volume = excluded.volume, + scan_on_startup = excluded.scan_on_startup, + reduce_motion = excluded.reduce_motion, + library_dirs = excluded.library_dirs, + use_album_artist_grouping = excluded.use_album_artist_grouping, + plugin_dirs = excluded.plugin_dirs, + plugin_dev_mode = excluded.plugin_dev_mode, + plugin_scan_on_startup = excluded.plugin_scan_on_startup, + plugin_log_level = excluded.plugin_log_level, + updated_at = datetime('now')", + ) + .bind(row.theme) + .bind(row.volume) + .bind(row.scan_on_startup) + .bind(row.reduce_motion) + .bind(row.library_dirs) + .bind(row.use_album_artist_grouping) + .bind(row.plugin_dirs) + .bind(row.plugin_dev_mode) + .bind(row.plugin_scan_on_startup) + .bind(row.plugin_log_level) + .bind(row.created_at) + .execute(&self.pool) + .await?; + + Ok(()) + } +} diff --git a/src-tauri/src/repositories/sqlite/track_repository.rs b/src-tauri/src/repositories/sqlite/track_repository.rs new file mode 100644 index 0000000..564aafb --- /dev/null +++ b/src-tauri/src/repositories/sqlite/track_repository.rs @@ -0,0 +1,274 @@ +use sqlx::{QueryBuilder, Sqlite, SqlitePool}; + +use crate::errors::AppError; +use crate::models::{NewTrack, SortDirection, Track, TrackSearchQuery, TrackSortBy, UpdateTrack}; + +#[derive(Clone)] +pub struct SqliteTrackRepository { + pool: SqlitePool, +} + +impl SqliteTrackRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn create(&self, track: NewTrack) -> Result { + let res = sqlx::query_as::<_, Track>( + "INSERT INTO tracks (title, artist, album, duration, path, cover, file_size, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'), datetime('now')) + RETURNING id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn upsert_by_path(&self, track: NewTrack) -> Result { + let res = sqlx::query_as::<_, Track>( + "INSERT INTO tracks (title, artist, album, duration, path, cover, file_size, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'), datetime('now')) + ON CONFLICT(path) DO UPDATE SET + title = excluded.title, + artist = excluded.artist, + album = excluded.album, + duration = excluded.duration, + cover = excluded.cover, + file_size = excluded.file_size, + updated_at = datetime('now') + RETURNING id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn upsert_tracks(&self, tracks: Vec) -> Result, AppError> { + let mut tx = self.pool.begin().await?; + + let mut saved = Vec::with_capacity(tracks.len()); + + for track in tracks { + let res = sqlx::query_as::<_, Track>( + " + INSERT INTO tracks ( + title, + artist, + album, + duration, + path, + cover, + file_size, + created_at, + updated_at + ) + VALUES ( + ?1, ?2, ?3, + ?4, ?5, ?6, + ?7, + datetime('now'), + datetime('now') + ) + + ON CONFLICT(path) + DO UPDATE SET + title=excluded.title, + artist=excluded.artist, + album=excluded.album, + duration=excluded.duration, + cover=excluded.cover, + file_size=excluded.file_size, + updated_at=datetime('now') + + RETURNING + id, + title, + artist, + album, + duration, + path, + cover, + file_size, + play_count, + last_played_at, + created_at, + updated_at + ", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .fetch_one(&mut *tx) + .await?; + + saved.push(res); + } + + tx.commit().await?; + + Ok(saved) + } + + pub async fn update(&self, track: UpdateTrack) -> Result { + let res = sqlx::query_as::<_, Track>( + "UPDATE tracks SET + title = ?1, + artist = ?2, + album = ?3, + duration = ?4, + path = ?5, + cover = ?6, + file_size = ?7, + updated_at = datetime('now') + WHERE id = ?8 + RETURNING id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .bind(track.id) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn delete(&self, id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM tracks WHERE id = ?1") + .bind(id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_by_path(&self, path: &str) -> Result<(), AppError> { + sqlx::query("DELETE FROM tracks WHERE path = ?1") + .bind(path) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn find_by_id(&self, id: i64) -> Result, AppError> { + let res = sqlx::query_as::<_, Track>( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at + FROM tracks WHERE id = ?1", + ) + .bind(id) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn find_by_path(&self, path: &str) -> Result, AppError> { + let res = sqlx::query_as::<_, Track>( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at + FROM tracks WHERE path = ?1", + ) + .bind(path) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn list_all(&self) -> Result, AppError> { + let rows = sqlx::query_as::<_, Track>( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at + FROM tracks ORDER BY title COLLATE NOCASE ASC", + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + pub async fn search(&self, query: TrackSearchQuery) -> Result, AppError> { + let mut builder = QueryBuilder::::new( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at FROM tracks", + ); + + if let Some(keyword) = query + .keyword + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + let pattern = format!("%{}%", keyword.to_lowercase()); + builder.push(" WHERE lower(title) LIKE "); + builder.push_bind(pattern.clone()); + builder.push(" OR lower(coalesce(artist, '')) LIKE "); + builder.push_bind(pattern.clone()); + builder.push(" OR lower(coalesce(album, '')) LIKE "); + builder.push_bind(pattern); + } + + builder.push(" ORDER BY "); + match query.sort_by.unwrap_or(TrackSortBy::Title) { + TrackSortBy::Title => builder.push("title COLLATE NOCASE"), + TrackSortBy::Artist => builder.push("artist COLLATE NOCASE"), + TrackSortBy::Album => builder.push("album COLLATE NOCASE"), + TrackSortBy::Duration => builder.push("duration"), + TrackSortBy::CreatedAt => builder.push("created_at"), + TrackSortBy::UpdatedAt => builder.push("updated_at"), + TrackSortBy::PlayCount => builder.push("play_count"), + TrackSortBy::LastPlayedAt => builder.push("last_played_at"), + }; + + match query.sort_direction.unwrap_or(SortDirection::Asc) { + SortDirection::Asc => builder.push(" ASC"), + SortDirection::Desc => builder.push(" DESC"), + }; + + builder.push(" LIMIT "); + builder.push_bind(query.limit.unwrap_or(100).clamp(1, 500)); + builder.push(" OFFSET "); + builder.push_bind(query.offset.unwrap_or(0).max(0)); + + let rows = builder + .build_query_as::() + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + pub async fn increment_play_count(&self, id: i64, played_at: String) -> Result<(), AppError> { + sqlx::query( + "UPDATE tracks SET play_count = play_count + 1, last_played_at = ?1, updated_at = datetime('now') WHERE id = ?2", + ) + .bind(played_at) + .bind(id) + .execute(&self.pool) + .await?; + + Ok(()) + } +} diff --git a/src-tauri/src/services/media_control_service.rs b/src-tauri/src/services/media_control_service.rs new file mode 100644 index 0000000..e1db4dc --- /dev/null +++ b/src-tauri/src/services/media_control_service.rs @@ -0,0 +1,50 @@ +use crate::services::playback_service::PlaybackService; +use crate::state::playback_state::current_time_from_state; +use souvlaki::{MediaControlEvent, SeekDirection}; +use tauri::AppHandle; + +pub async fn handle_media_control_event(app_handle: AppHandle, event: MediaControlEvent) { + let service = PlaybackService::new(app_handle); + + match event { + MediaControlEvent::Play => { + let _ = service.resume().await; + } + MediaControlEvent::Pause => { + let _ = service.pause().await; + } + MediaControlEvent::Toggle => { + let _ = service.toggle().await; + } + MediaControlEvent::Next => { + let _ = service.next().await; + } + MediaControlEvent::Previous => { + let _ = service.previous().await; + } + MediaControlEvent::Stop => { + let _ = service.stop().await; + } + MediaControlEvent::SetPosition(position) => { + let _ = service.seek(position.0.as_secs_f64()).await; + } + MediaControlEvent::Seek(direction) => { + let current_time = current_time_from_state(); + let offset = match direction { + SeekDirection::Forward => 10.0, + SeekDirection::Backward => -10.0, + }; + let _ = service.seek((current_time + offset).max(0.0)).await; + } + MediaControlEvent::SeekBy(direction, duration) => { + let current_time = current_time_from_state(); + let offset = duration.as_secs_f64(); + let next_time = match direction { + SeekDirection::Forward => current_time + offset, + SeekDirection::Backward => current_time - offset, + }; + let _ = service.seek(next_time.max(0.0)).await; + } + _ => {} + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 0000000..89ac128 --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,12 @@ +pub mod media_control_service; +pub mod playback_service; +pub mod playlist_service; +pub mod plugin; +pub mod recent_service; +pub mod settings_service; +pub mod track_service; + +pub use playlist_service::PlaylistService; +pub use recent_service::RecentService; +pub use settings_service::SettingsService; +pub use track_service::TrackService; diff --git a/src-tauri/src/services/playback_service.rs b/src-tauri/src/services/playback_service.rs new file mode 100644 index 0000000..f75d4d4 --- /dev/null +++ b/src-tauri/src/services/playback_service.rs @@ -0,0 +1,291 @@ +use crate::audio::{lock_audio_state, AudioEngine, WebAudioEngine}; +use crate::errors::AppError; +use crate::events::{ + AppEvent, EventBus, PlaybackProgressPayload, PlaybackStatePayload, TrackStartedPayload, +}; +use crate::media_controls::{update_media_controls_metadata, update_media_controls_playback}; +use crate::models::Track; +use crate::state::app_state::AppState; +use crate::state::playback_state::{ + current_playback_snapshot, metadata_from_track, should_advance_track, with_audio_state, +}; +use std::path::Path; +use std::time::Duration; +use tauri::{AppHandle, Manager}; + +fn emit_and_dispatch(app_handle: &AppHandle, event: AppEvent) -> Result<(), AppError> { + EventBus::emit(app_handle, event.clone())?; + if let Some(state) = app_handle.try_state::() { + state.plugins.dispatch_event(&event); + } + Ok(()) +} + +pub struct PlaybackService { + app_handle: AppHandle, +} + +impl PlaybackService { + pub fn new(app_handle: AppHandle) -> Self { + Self { app_handle } + } + + pub fn play_track(&self, track: Track, index: Option) -> Result<(), AppError> { + let file_path = Path::new(&track.path); + + if !file_path.exists() { + return Err("音频文件不存在或已被移动".into()); + } + + let mut state = lock_audio_state()?; + + let volume = state.volume; + let pan = state.pan; + state.engine = None; + + // 取共享的音频处理器 registry(来自 PluginManager),喂给渲染图中的 + // ExternalProcessorNode。AppState 未就绪时退化为空 registry — 此时 + // 插件尚未注册,透传行为不受影响。 + let audio_processor_registry = self + .app_handle + .try_state::() + .map(|s| s.plugins.audio_processor_registry()) + .unwrap_or_else(|| std::sync::Arc::new(crate::services::plugin::AudioProcessorRegistry::new())); + + let mut engine = match WebAudioEngine::new(file_path, volume, pan, audio_processor_registry) { + Ok(eng) => eng, + Err(e) => { + state.current_track_id = None; + state.playing = false; + return Err(e); + } + }; + + if let Err(e) = engine.play() { + state.current_track_id = None; + state.playing = false; + return Err(e); + } + + state.engine = Some(Box::new(engine)); + state.current_track_id = Some(track.id); + + if let Some(next_index) = index { + state.playback_queue.current_index = Some(next_index); + } + + state.playing = true; + let target_index = index.or(state.playback_queue.current_index).unwrap_or(0); + + drop(state); + + Self::emit_track_started(&self.app_handle, track, target_index)?; + + Ok(()) + } + + pub fn play_queue_index(&self, index: usize) -> Result<(), AppError> { + let track = with_audio_state(|state| state.playback_queue.require_track(index))??; + self.play_track(track, Some(index)) + } + + pub async fn next(&self) -> Result<(), AppError> { + let (track, index) = with_audio_state(|state| { + let index = state + .playback_queue + .move_next() + .ok_or_else(|| AppError::from("Queue is empty"))?; + + let track = state.playback_queue.require_track(index)?; + + Ok::<(Track, usize), AppError>((track, index)) + })??; + + self.play_track(track, Some(index)) + } + + pub async fn previous(&self) -> Result<(), AppError> { + let (track, index) = with_audio_state(|state| { + let index = state + .playback_queue + .move_previous() + .ok_or_else(|| AppError::from("Queue is empty"))?; + + let track = state.playback_queue.require_track(index)?; + + Ok::<(Track, usize), AppError>((track, index)) + })??; + + self.play_track(track, Some(index)) + } + + pub async fn resume(&self) -> Result<(), AppError> { + let current_time = with_audio_state(|state| { + let res = state + .with_engine(|engine| { + engine.play()?; + Ok(engine.current_time()) + }) + .and_then(|inner| inner); + + if res.is_ok() { + state.playing = true; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, true, current_time)?; + Ok(()) + } + + pub async fn pause(&self) -> Result<(), AppError> { + let current_time = with_audio_state(|state| { + let res = state.with_engine(|engine| { + engine.pause(); + engine.current_time() + }); + if res.is_ok() { + state.playing = false; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, false, current_time)?; + Ok(()) + } + + pub async fn toggle(&self) -> Result { + let (playing, current_time) = with_audio_state(|state| { + let res = state + .with_engine(|engine| { + if engine.paused() { + engine.play()?; + Ok((true, engine.current_time())) + } else { + engine.pause(); + Ok((false, engine.current_time())) + } + }) + .and_then(|i| i); + if let Ok((p, _)) = res { + state.playing = p; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, playing, current_time)?; + Ok(playing) + } + + pub async fn stop(&self) -> Result<(), AppError> { + with_audio_state(|state| { + let _ = state.with_engine(|engine| { + engine.pause(); + engine.seek(0.0); + }); + state.playing = false; + })?; + + Self::sync_playback_state(&self.app_handle, false, 0.0)?; + Ok(()) + } + + pub async fn seek(&self, time: f64) -> Result<(), AppError> { + let playing = with_audio_state(|state| { + let res = state.with_engine(|engine| { + engine.seek(time); + !engine.paused() + }); + if let Ok(p) = res { + state.playing = p; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, playing, time)?; + Ok(()) + } + + pub fn emit_track_started( + app_handle: &AppHandle, + track: Track, + index: usize, + ) -> Result<(), AppError> { + update_media_controls_metadata(metadata_from_track(&track)); + + emit_and_dispatch( + app_handle, + AppEvent::TrackStarted(TrackStartedPayload { track, index }), + ) + } + + pub fn sync_playback_state( + app_handle: &AppHandle, + playing: bool, + current_time: f64, + ) -> Result<(), AppError> { + let payload = PlaybackStatePayload { + playing, + current_time, + }; + + update_media_controls_playback(payload.clone()); + + emit_and_dispatch(app_handle, AppEvent::PlaybackStateChanged(payload)) + } + + pub fn emit_progress(app_handle: &AppHandle) -> Result<(), AppError> { + if let Ok(snapshot) = current_playback_snapshot() { + emit_and_dispatch( + app_handle, + AppEvent::PlaybackProgress(PlaybackProgressPayload { + current_time: snapshot.current_time, + }), + )?; + } + Ok(()) + } + + pub fn spawn_playback_progress_task(app_handle: AppHandle) { + tauri::async_runtime::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(500)); + + loop { + interval.tick().await; + + let _ = Self::emit_progress(&app_handle); + + if should_advance_track() { + let _ = PlaybackService::new(app_handle.clone()).next().await; + } + } + }); + } + + pub(crate) async fn remove_track_from_queue(&self, track_id: i64) -> Result<(), AppError> { + let result = { + let mut state = lock_audio_state()?; + state.playback_queue.remove_track(track_id) + }; + + let Some(remove_result) = result else { + return Ok(()); + }; + + if remove_result.should_stop { + self.stop().await?; + } else if let Some(index) = remove_result.play_index { + self.play_queue_index(index)?; + } + + Self::emit_queue_changed(&self.app_handle)?; + + Ok(()) + } + + pub(crate) fn emit_queue_changed(app_handle: &tauri::AppHandle) -> Result<(), AppError> { + let queue = with_audio_state(|state| state.playback_queue.clone())?; + + emit_and_dispatch(app_handle, AppEvent::QueueChanged(queue)) + } +} diff --git a/src-tauri/src/services/playlist_service.rs b/src-tauri/src/services/playlist_service.rs new file mode 100644 index 0000000..8a640f8 --- /dev/null +++ b/src-tauri/src/services/playlist_service.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::models::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, +}; +use crate::repositories::sqlite::playlist_repository::SqlitePlaylistRepository; + +#[derive(Clone)] +pub struct PlaylistService { + playlists: Arc, +} + +impl PlaylistService { + pub fn new(playlists: Arc) -> Self { + Self { playlists } + } + + pub async fn create_playlist(&self, playlist: NewPlaylist) -> Result { + self.playlists.create(playlist).await + } + + pub async fn rename_playlist(&self, playlist: RenamePlaylist) -> Result { + self.playlists.rename(playlist).await + } + + pub async fn delete_playlist(&self, id: i64) -> Result<(), AppError> { + self.playlists.delete(id).await + } + + pub async fn get_playlist(&self, id: i64) -> Result, AppError> { + self.playlists.find_by_id(id).await + } + + pub async fn list_playlists(&self) -> Result, AppError> { + self.playlists.list_all().await + } + + pub async fn list_playlists_with_tracks(&self) -> Result, AppError> { + self.playlists.list_with_tracks().await + } + + pub async fn get_playlist_with_tracks( + &self, + id: i64, + ) -> Result, AppError> { + self.playlists.get_with_tracks(id).await + } + + pub async fn add_track(&self, track: AddPlaylistTrack) -> Result { + self.playlists.add_track(track).await + } + + pub async fn remove_track(&self, playlist_id: i64, track_id: i64) -> Result<(), AppError> { + self.playlists.remove_track(playlist_id, track_id).await + } + + pub async fn clear_tracks(&self, playlist_id: i64) -> Result<(), AppError> { + self.playlists.clear_tracks(playlist_id).await + } + + pub async fn reorder_track( + &self, + playlist_id: i64, + track_id: i64, + position: i64, + ) -> Result<(), AppError> { + self.playlists + .reorder_track(playlist_id, track_id, position) + .await + } +} diff --git a/src-tauri/src/services/plugin/adapters/audio_adapter.rs b/src-tauri/src/services/plugin/adapters/audio_adapter.rs new file mode 100644 index 0000000..114aa35 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/audio_adapter.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use plugin_sdk::api::AudioProcessingApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; + +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::state::playback_state::with_audio_state; + +/// EQ 频段数量(与 trait 的 [f64; 10] 对应) +const EQ_BAND_COUNT: usize = 10; + +/// 将 AppError 转换为 PluginError,规则与 player_adapter 一致。 +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct AudioAdapter { + plugin_id: String, + checker: Arc, +} + +impl AudioAdapter { + pub fn new(plugin_id: String, checker: Arc) -> Self { + Self { plugin_id, checker } + } +} + +impl AudioProcessingApi for AudioAdapter { + fn set_band_gain(&self, band: usize, gain_db: f64) -> Result<(), PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + if band >= EQ_BAND_COUNT { + return Err(PluginError::Plugin("EQ band index out of range".into())); + } + with_audio_state(|state| -> Result<(), crate::errors::AppError> { + state.eq_bands[band] = gain_db; + if let Some(ref mut engine) = state.engine { + engine.set_eq_band_gain(band, gain_db)?; + } + Ok(()) + }) + .and_then(|inner| inner) + .map_err(sdk_err) + } + + fn apply_preset(&self, gains: [f64; 10]) -> Result<(), PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + with_audio_state(|state| -> Result<(), crate::errors::AppError> { + state.eq_bands = gains; + if let Some(ref mut engine) = state.engine { + engine.apply_eq_preset(&gains)?; + } + Ok(()) + }) + .and_then(|inner| inner) + .map_err(sdk_err) + } + + fn get_bands(&self) -> Result<[f64; 10], PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + // state.eq_bands 在所有写操作中与引擎同步,直接读取即可 + with_audio_state(|state| state.eq_bands).map_err(sdk_err) + } + + fn set_enabled(&self, enabled: bool) -> Result<(), PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + with_audio_state(|state| -> Result<(), crate::errors::AppError> { + state.eq_enabled = enabled; + if let Some(ref mut engine) = state.engine { + engine.set_eq_enabled(enabled)?; + } + Ok(()) + }) + .and_then(|inner| inner) + .map_err(sdk_err) + } + + fn is_enabled(&self) -> Result { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + with_audio_state(|state| state.eq_enabled).map_err(sdk_err) + } +} diff --git a/src-tauri/src/services/plugin/adapters/library_adapter.rs b/src-tauri/src/services/plugin/adapters/library_adapter.rs new file mode 100644 index 0000000..6c6b8fe --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/library_adapter.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use plugin_sdk::api::LibraryApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::services::track_service::TrackService; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct LibraryAdapter { + plugin_id: String, + checker: Arc, + track_service: Arc, +} + +impl LibraryAdapter { + pub fn new(plugin_id: String, checker: Arc, track_service: Arc) -> Self { + Self { plugin_id, checker, track_service } + } +} + +impl LibraryApi for LibraryAdapter { + fn get_track_path(&self, track_id: i64) -> Result, PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::LibraryRead).map_err(sdk_err)?; + let track = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.track_service.get_track(track_id)) + }).map_err(sdk_err)?; + Ok(track.map(|t| PathBuf::from(t.path))) + } + + fn exists(&self, track_id: i64) -> Result { + self.checker.require(&self.plugin_id, PluginPermission::LibraryRead).map_err(sdk_err)?; + let track = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.track_service.get_track(track_id)) + }).map_err(sdk_err)?; + Ok(track.is_some()) + } +} diff --git a/src-tauri/src/services/plugin/adapters/mod.rs b/src-tauri/src/services/plugin/adapters/mod.rs new file mode 100644 index 0000000..21fee2c --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/mod.rs @@ -0,0 +1,13 @@ +pub mod audio_adapter; +pub mod library_adapter; +pub mod player_adapter; +pub mod plugin_settings_adapter; +pub mod queue_adapter; +pub mod settings_adapter; + +pub use audio_adapter::AudioAdapter; +pub use library_adapter::LibraryAdapter; +pub use player_adapter::PlayerAdapter; +pub use plugin_settings_adapter::PluginSettingsAdapter; +pub use queue_adapter::QueueAdapter; +pub use settings_adapter::SettingsAdapter; diff --git a/src-tauri/src/services/plugin/adapters/player_adapter.rs b/src-tauri/src/services/plugin/adapters/player_adapter.rs new file mode 100644 index 0000000..4b5688a --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/player_adapter.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use crate::services::playback_service::PlaybackService; +use plugin_sdk::api::PlayerApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::state::playback_state::with_audio_state; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct PlayerAdapter { + plugin_id: String, + checker: Arc, + playback_service: Arc, +} + +impl PlayerAdapter { + pub fn new( + plugin_id: String, + checker: Arc, + playback_service: Arc, + ) -> Self { + Self { plugin_id, checker, playback_service } + } +} + +impl PlayerApi for PlayerAdapter { + fn play(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.resume()) + }).map_err(sdk_err)?; + Ok(()) + } + + fn pause(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.pause()) + }).map_err(sdk_err) + } + + fn next(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.next()) + }).map_err(sdk_err) + } + + fn previous(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.previous()) + }).map_err(sdk_err) + } + + fn current_track_id(&self) -> Result, PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerRead).map_err(sdk_err)?; + with_audio_state(|state| state.current_track_id).map_err(sdk_err) + } +} diff --git a/src-tauri/src/services/plugin/adapters/plugin_settings_adapter.rs b/src-tauri/src/services/plugin/adapters/plugin_settings_adapter.rs new file mode 100644 index 0000000..6107e88 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/plugin_settings_adapter.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use plugin_sdk::api::PluginSettingsApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::settings::SettingValue; +use crate::services::plugin::settings::settings_registry::SettingsRegistry; + +pub struct PluginSettingsAdapter { + plugin_id: String, + registry: Arc, +} + +impl PluginSettingsAdapter { + pub fn new(plugin_id: String, registry: Arc) -> Self { + Self { plugin_id, registry } + } +} + +impl PluginSettingsApi for PluginSettingsAdapter { + fn get_setting(&self, key: &str) -> Option { + self.registry.get_setting(&self.plugin_id, key) + } + + fn set_setting(&self, key: &str, value: SettingValue) -> Result<(), PluginError> { + self.registry.update_setting(&self.plugin_id, key, value) + .map_err(|e| PluginError::Plugin(e.to_string())) + } +} diff --git a/src-tauri/src/services/plugin/adapters/queue_adapter.rs b/src-tauri/src/services/plugin/adapters/queue_adapter.rs new file mode 100644 index 0000000..cb8d30c --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/queue_adapter.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use plugin_sdk::api::QueueApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::state::playback_state::with_audio_state; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct QueueAdapter { + plugin_id: String, + checker: Arc, +} + +impl QueueAdapter { + pub fn new(plugin_id: String, checker: Arc) -> Self { + Self { plugin_id, checker } + } +} + +impl QueueApi for QueueAdapter { + fn current_queue(&self) -> Result, PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::QueueRead).map_err(sdk_err)?; + with_audio_state(|state| state.playback_queue.tracks.iter().map(|t| t.id).collect()).map_err(sdk_err) + } + + fn remove_track(&self, track_id: i64) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::QueueWrite).map_err(sdk_err)?; + with_audio_state(|state| { state.playback_queue.remove_track(track_id); }).map_err(sdk_err)?; + Ok(()) + } + + fn clear(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::QueueWrite).map_err(sdk_err)?; + with_audio_state(|state| { state.playback_queue.clear(); }).map_err(sdk_err)?; + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/adapters/settings_adapter.rs b/src-tauri/src/services/plugin/adapters/settings_adapter.rs new file mode 100644 index 0000000..6dbd565 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/settings_adapter.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use crate::models::ThemeMode; +use plugin_sdk::api::SettingsApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::services::settings_service::SettingsService; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct SettingsAdapter { + plugin_id: String, + checker: Arc, + settings_service: Arc, +} + +impl SettingsAdapter { + pub fn new(plugin_id: String, checker: Arc, settings_service: Arc) -> Self { + Self { plugin_id, checker, settings_service } + } +} + +impl SettingsApi for SettingsAdapter { + fn theme(&self) -> Result { + self.checker.require(&self.plugin_id, PluginPermission::SettingsRead).map_err(sdk_err)?; + let settings = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.settings_service.get_settings()) + }).map_err(sdk_err)?; + Ok(settings.theme.as_str().to_string()) + } + + fn set_theme(&self, theme: String) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::SettingsWrite).map_err(sdk_err)?; + let theme_mode = ThemeMode::try_from(theme).map_err(|e| PluginError::Plugin(e.to_string()))?; + let mut settings = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.settings_service.get_settings()) + }).map_err(sdk_err)?; + settings.theme = theme_mode; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.settings_service.update_settings(settings)) + }).map_err(sdk_err)?; + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/asset/mod.rs b/src-tauri/src/services/plugin/asset/mod.rs new file mode 100644 index 0000000..5453ae6 --- /dev/null +++ b/src-tauri/src/services/plugin/asset/mod.rs @@ -0,0 +1 @@ +pub mod scheme; diff --git a/src-tauri/src/services/plugin/asset/scheme.rs b/src-tauri/src/services/plugin/asset/scheme.rs new file mode 100644 index 0000000..0558c27 --- /dev/null +++ b/src-tauri/src/services/plugin/asset/scheme.rs @@ -0,0 +1,167 @@ +//! `plugin://` URI scheme handler. +//! +//! Exposes plugin asset files to sandboxed iframes via +//! `plugin:///`. The special `__host__` plugin id +//! serves the compile-time embedded host SDK so plugin HTML can reference it +//! without a runtime file dependency. + +use std::path::{Component, Path, PathBuf}; + +use tauri::http::{Request, Response, StatusCode}; +use tauri::{AppHandle, Manager, Runtime, UriSchemeContext}; + +/// Compile-time embedded host SDK. Path is relative to this source file: +/// `scheme.rs` lives at `src-tauri/src/services/plugin/asset/scheme.rs`, so +/// five `..` segments reach the project root where `static/` lives. +const HOST_SDK_JS: &str = + include_str!("../../../../../static/plugin-sdk/plugin-host-sdk.js"); + +/// Tauri `register_uri_scheme_protocol` handler for the `plugin` scheme. +pub fn handle( + ctx: UriSchemeContext<'_, R>, + request: Request>, +) -> Response> { + let app = ctx.app_handle(); + let uri = request.uri(); + let host = uri.host().unwrap_or(""); + let path = uri.path().trim_start_matches('/'); + + // Special segment: `plugin://__host__/plugin-sdk/plugin-host-sdk.js` + // serves the embedded host SDK without touching the filesystem. + if host == "__host__" && path == "plugin-sdk/plugin-host-sdk.js" { + return Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/javascript; charset=utf-8") + .body(HOST_SDK_JS.as_bytes().to_vec()) + .unwrap(); + } + + // Validate the plugin id: must be non-empty and contain no path + // separators or traversal segments. + let plugin_id = match host { + "" => return not_found("missing plugin id"), + id if id.contains('/') || id.contains("..") || id.contains('\0') => { + return forbidden("invalid plugin id") + } + id => id, + }; + + // Cross-plugin isolation: when a sub-resource request carries a + // `Referer` whose plugin id differs from the requested plugin id, + // reject with 403. Entry HTML loads have no Referer and are allowed. + if let Some(referer) = request.headers().get("referer").and_then(|v| v.to_str().ok()) { + if let Some(referer_plugin_id) = parse_plugin_referer(referer) { + if referer_plugin_id != plugin_id { + return forbidden("cross-plugin access denied"); + } + } + } + + // Path safety: reject absolute paths, traversal, and NUL bytes up front. + if path.is_empty() || path.starts_with('/') || path.contains("..") || path.contains('\0') { + return forbidden("invalid path"); + } + + // Component-level check: only `Normal` components are allowed. This + // rejects `RootDir`, `ParentDir`, `Prefix` (Windows drive), and + // `CurDir` (`.`). + let rel = Path::new(path); + for comp in rel.components() { + match comp { + Component::Normal(_) => {} + _ => return forbidden("invalid path component"), + } + } + + // Locate the plugin's resource root directory. + let root = match resolve_plugin_root(app, plugin_id) { + Some(p) => p, + None => return not_found("plugin root not found"), + }; + + let file_path = root.join(rel); + + // Canonicalize both the file and the root to defend against symlinks or + // other filesystem tricks that could escape the plugin sandbox. + let canonical = match file_path.canonicalize() { + Ok(p) => p, + Err(_) => return not_found("file not found"), + }; + let canonical_root = match root.canonicalize() { + Ok(p) => p, + Err(_) => return not_found("plugin root invalid"), + }; + if !canonical.starts_with(&canonical_root) { + return forbidden("path escapes plugin root"); + } + + match std::fs::read(&canonical) { + Ok(bytes) => Response::builder() + .status(StatusCode::OK) + .header("Content-Type", content_type_for(&canonical)) + .body(bytes) + .unwrap(), + Err(_) => not_found("file read error"), + } +} + +/// Parse the plugin id from a `Referer` header value of the form +/// `plugin:///...`. Returns `None` for non-`plugin:` URIs. +fn parse_plugin_referer(referer: &str) -> Option<&str> { + let stripped = referer.strip_prefix("plugin://")?; + let plugin_id = stripped.split('/').next()?; + if plugin_id.is_empty() || plugin_id.contains("..") || plugin_id.contains('\0') { + return None; + } + Some(plugin_id) +} + +/// Resolve a plugin's resource root by searching the same candidate +/// directories used by `PluginManager::load_packaged_plugins`: app data, +/// the Tauri resource dir, and the exe's parent directory. +fn resolve_plugin_root(app: &AppHandle, plugin_id: &str) -> Option { + let mut candidates = Vec::new(); + + if let Ok(app_data) = app.path().app_data_dir() { + candidates.push(app_data.join("plugins").join(plugin_id)); + } + + if let Ok(resource_dir) = app.path().resource_dir() { + candidates.push(resource_dir.join("plugins").join(plugin_id)); + } + + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + candidates.push(parent.join(plugin_id)); + } + } + + candidates.into_iter().find(|p| p.exists()) +} + +fn content_type_for(path: &Path) -> &'static str { + match path.extension().and_then(|e| e.to_str()) { + Some("html") => "text/html; charset=utf-8", + Some("js") => "application/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("json") => "application/json; charset=utf-8", + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("svg") => "image/svg+xml", + _ => "application/octet-stream", + } +} + +fn not_found(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(msg.as_bytes().to_vec()) + .unwrap() +} + +fn forbidden(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::FORBIDDEN) + .body(msg.as_bytes().to_vec()) + .unwrap() +} diff --git a/src-tauri/src/services/plugin/audio_processor_registry.rs b/src-tauri/src/services/plugin/audio_processor_registry.rs new file mode 100644 index 0000000..2549405 --- /dev/null +++ b/src-tauri/src/services/plugin/audio_processor_registry.rs @@ -0,0 +1,141 @@ +//! Audio processor registry shared between the plugin manager and the +//! audio render graph. +//! +//! Packaged plugins that export the `rem_audio_processor` symbol register +//! an [`AudioProcessorApi`] here at activation time. The +//! [`crate::audio::web_audio::ExternalProcessorNode`] (an +//! `AudioWorkletProcessor`) holds an `Arc` and, on +//! every render quantum, iterates the active processors, calling each one's +//! `process` FFI callback with interleaved PCM. +//! +//! # Design notes +//! +//! - `AudioProcessorApi` is `Copy` (composed of `Option`), +//! so it is stored by value and returned by value — no lifetime juggling. +//! - `sample_rate` / `channels` are stored as atomics so the render thread +//! (worklet) and the control thread (plugin manager) can read them without +//! locking. They are populated by `WebAudioEngine` when it is constructed. +//! - The raw `PluginHandle` is currently stored as `null_mut()`: exposing +//! the FFI handle through the `Plugin` trait requires SDK changes that are +//! out of scope for this task (Task 8 forbade `ffi.rs` edits). The registry +//! plumbing is in place so a future trait extension can flow the real +//! handle through. + +use std::collections::HashMap; +use std::ffi::c_void; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Mutex, RwLock}; + +use plugin_sdk::ffi::AudioProcessorApi; + +pub struct AudioProcessorRegistry { + /// Active audio processors keyed by plugin id. `AudioProcessorApi` is + /// `Copy`, so by-value storage keeps `iter_active` allocation-free per + /// entry. + processors: RwLock>, + /// Per-plugin FFI handle (`*mut c_void`) to hand back to `process` / + /// `reset` callbacks. Stored separately so `iter_active` only needs a + /// short-lived `Mutex` guard to fetch the handle map. + handles: Mutex>, + /// Output sample rate (Hz). Set by `WebAudioEngine` at construction. + sample_rate: AtomicU32, + /// Output channel count. Set by `WebAudioEngine` at construction. + channels: AtomicU32, +} + +impl Default for AudioProcessorRegistry { + fn default() -> Self { + Self::new() + } +} + +impl AudioProcessorRegistry { + pub fn new() -> Self { + Self { + processors: RwLock::new(HashMap::new()), + handles: Mutex::new(HashMap::new()), + sample_rate: AtomicU32::new(44100), + channels: AtomicU32::new(2), + } + } + + /// Update the audio format the render graph is running at. Called by + /// `WebAudioEngine` when it is constructed so plugin `init` callbacks + /// receive accurate values. + pub fn set_audio_format(&self, sample_rate: u32, channels: u32) { + self.sample_rate.store(sample_rate, Ordering::Relaxed); + self.channels.store(channels, Ordering::Relaxed); + } + + pub fn sample_rate(&self) -> u32 { + self.sample_rate.load(Ordering::Relaxed) + } + + pub fn channels(&self) -> u32 { + self.channels.load(Ordering::Relaxed) + } + + /// Register an activated plugin's audio processor. + /// + /// `handle` is the FFI `PluginHandle` to pass to `process` / `reset`. + /// Currently `null_mut()` until the SDK exposes the raw handle. + pub fn register(&self, plugin_id: String, api: AudioProcessorApi, handle: *mut c_void) { + if let Ok(mut lock) = self.processors.write() { + lock.insert(plugin_id.clone(), api); + } + if let Ok(mut lock) = self.handles.lock() { + lock.insert(plugin_id, handle); + } + } + + /// Remove a plugin's audio processor, returning its api + handle so the + /// caller can invoke `reset` before dropping. + pub fn unregister(&self, plugin_id: &str) -> Option<(AudioProcessorApi, *mut c_void)> { + let api = self + .processors + .write() + .ok() + .and_then(|mut lock| lock.remove(plugin_id)); + let handle = self + .handles + .lock() + .ok() + .and_then(|mut lock| lock.remove(plugin_id)); + match (api, handle) { + (Some(api), Some(handle)) => Some((api, handle)), + (Some(api), None) => Some((api, std::ptr::null_mut())), + _ => None, + } + } + + /// Snapshot of all active processors. Called once per render quantum + /// (128 frames) on the audio thread. + /// + /// Returns `(plugin_id, api, handle)` triples so the worklet can pass + /// the handle straight to the `process` FFI callback without a second + /// lookup. + pub fn iter_active(&self) -> Vec<(String, AudioProcessorApi, *mut c_void)> { + let processors = match self.processors.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + // Acquire the handle map only for the duration of the snapshot build. + let handles = match self.handles.lock() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + processors + .iter() + .map(|(id, api)| { + let handle = handles.get(id).copied().unwrap_or(std::ptr::null_mut()); + (id.clone(), *api, handle) + }) + .collect() + } +} + +// SAFETY: `AudioProcessorApi` is `Copy` (function pointers / null), `*mut +// c_void` raw pointers are wrapped behind `RwLock`/`Mutex`. The registry is +// shared (`Arc`) between the control thread and the audio render thread. +unsafe impl Send for AudioProcessorRegistry {} +unsafe impl Sync for AudioProcessorRegistry {} diff --git a/src-tauri/src/services/plugin/capability/capability.rs b/src-tauri/src/services/plugin/capability/capability.rs new file mode 100644 index 0000000..c2fd4d0 --- /dev/null +++ b/src-tauri/src/services/plugin/capability/capability.rs @@ -0,0 +1,8 @@ +use std::collections::HashSet; + +use plugin_sdk::permissions::PluginPermission; + +pub struct PluginCapability { + pub plugin_id: String, + pub permissions: HashSet, +} diff --git a/src-tauri/src/services/plugin/capability/mod.rs b/src-tauri/src/services/plugin/capability/mod.rs new file mode 100644 index 0000000..a7b47bd --- /dev/null +++ b/src-tauri/src/services/plugin/capability/mod.rs @@ -0,0 +1,7 @@ +pub mod capability; +pub mod permission_checker; +pub mod plugin_capability; + +pub use capability::PluginCapability; +pub use permission_checker::PermissionChecker; +pub use plugin_capability::CapabilityRegistry; diff --git a/src-tauri/src/services/plugin/capability/permission_checker.rs b/src-tauri/src/services/plugin/capability/permission_checker.rs new file mode 100644 index 0000000..1b99258 --- /dev/null +++ b/src-tauri/src/services/plugin/capability/permission_checker.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::services::plugin::capability::plugin_capability::CapabilityRegistry; +use plugin_sdk::permissions::PluginPermission; + +pub struct PermissionChecker { + capability_registry: Arc, +} + +impl PermissionChecker { + pub fn new(capability_registry: Arc) -> Self { + Self { + capability_registry, + } + } + + pub fn require(&self, plugin_id: &str, permission: PluginPermission) -> Result<(), AppError> { + if self + .capability_registry + .has_permission(plugin_id, permission) + { + Ok(()) + } else { + Err(AppError::PluginPermissionDenied { + plugin_id: plugin_id.to_string(), + permission, + }) + } + } +} diff --git a/src-tauri/src/services/plugin/capability/plugin_capability.rs b/src-tauri/src/services/plugin/capability/plugin_capability.rs new file mode 100644 index 0000000..0e48701 --- /dev/null +++ b/src-tauri/src/services/plugin/capability/plugin_capability.rs @@ -0,0 +1,44 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::services::plugin::capability::capability::PluginCapability; + +pub struct CapabilityRegistry { + capabilities: RwLock>>, +} + +impl CapabilityRegistry { + pub fn new() -> Self { + Self { + capabilities: RwLock::new(HashMap::new()), + } + } + + pub fn register(&self, capability: Arc) { + let plugin_id = capability.plugin_id.clone(); + if let Ok(mut lock) = self.capabilities.write() { + lock.insert(plugin_id, capability); + } + } + + pub fn remove(&self, plugin_id: &str) { + if let Ok(mut lock) = self.capabilities.write() { + lock.remove(plugin_id); + } + } + + pub fn has_permission( + &self, + plugin_id: &str, + permission: plugin_sdk::permissions::PluginPermission, + ) -> bool { + let lock = match self.capabilities.read() { + Ok(guard) => guard, + Err(_) => return false, + }; + + lock.get(plugin_id) + .map(|cap| cap.permissions.contains(&permission)) + .unwrap_or(false) + } +} diff --git a/src-tauri/src/services/plugin/dynamic_factory.rs b/src-tauri/src/services/plugin/dynamic_factory.rs new file mode 100644 index 0000000..3f5fd07 --- /dev/null +++ b/src-tauri/src/services/plugin/dynamic_factory.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; + +use plugin_sdk::errors::PluginError; +use plugin_sdk::ffi::{AudioProcessorApi, ByteBuffer, CommandResponse, PluginExports, PluginHandle}; +use plugin_sdk::traits::{CommandArgs, Plugin}; + +pub struct DynamicPluginFactory { + lib: Arc, + exports: PluginExports, + manifest: plugin_sdk::manifest::ResolvedPluginManifest, + /// 可选音频处理器协议。来自 `rem_audio_processor` 符号查找; + /// 旧插件未导出此符号时为 None,行为与改动前一致。 + pub audio_processor: Option, +} + +// `PluginExports` 现含 `audio_processor: *const AudioProcessorApi` 裸指针, +// 编译器不再自动派生 Send/Sync。工厂从不解引用该字段(音频处理器权威值 +// 存于 `audio_processor: Option`,由 Copy 的函数指针组成, +// 天然 Send+Sync),因此与 DynamicPluginInstance 一致地手动声明安全。 +unsafe impl Send for DynamicPluginFactory {} +unsafe impl Sync for DynamicPluginFactory {} + +pub struct DynamicPluginInstance { + handle: PluginHandle, + lib: Arc, + exports: PluginExports, +} + +unsafe impl Send for DynamicPluginInstance {} +unsafe impl Sync for DynamicPluginInstance {} + +impl DynamicPluginFactory { + pub fn new( + lib: Arc, + exports: PluginExports, + manifest: plugin_sdk::manifest::ResolvedPluginManifest, + audio_processor: Option, + ) -> Self { + Self { + lib, + exports, + manifest, + audio_processor, + } + } +} + +impl plugin_sdk::traits::PluginFactory for DynamicPluginFactory { + fn create(&self) -> Arc { + let handle = unsafe { (self.exports.create)() }; + Arc::new(DynamicPluginInstance { + handle, + lib: Arc::clone(&self.lib), + exports: PluginExports { + abi_version: self.exports.abi_version, + create: self.exports.create, + destroy: self.exports.destroy, + free_buffer: self.exports.free_buffer, + api: plugin_sdk::ffi::PluginApi { + activate: self.exports.api.activate, + deactivate: self.exports.api.deactivate, + on_event: self.exports.api.on_event, + execute_command: self.exports.api.execute_command, + }, + // 实例不直接使用 audio_processor;权威值在工厂的 audio_processor 字段中。 + // 置 null 以避免读取旧插件 exports 中可能未初始化的字段。 + audio_processor: std::ptr::null(), + }, + }) + } +} + +impl Drop for DynamicPluginInstance { + fn drop(&mut self) { + unsafe { (self.exports.destroy)(self.handle) } + } +} + +fn serialize_to_buffer(value: &T) -> Result { + let json = serde_json::to_vec(value) + .map_err(|e| PluginError::Plugin(format!("Failed to serialize: {}", e)))?; + let len = json.len(); + let ptr = Box::into_raw(json.into_boxed_slice()) as *mut u8; + Ok(ByteBuffer { ptr, len }) +} + +fn deserialize_from_buffer serde::Deserialize<'de>>( + buffer: &ByteBuffer, +) -> Result { + let slice = unsafe { std::slice::from_raw_parts(buffer.ptr, buffer.len) }; + serde_json::from_slice(slice).map_err(|e| PluginError::Plugin(format!("Failed to deserialize: {}", e))) +} + +impl Plugin for DynamicPluginInstance { + fn activate( + &self, + _ctx: &plugin_sdk::context::PluginContext, + ) -> Result<(), PluginError> { + let result = unsafe { (self.exports.api.activate)(self.handle) }; + if result == 0 { + Ok(()) + } else { + Err(PluginError::Plugin(format!( + "activate failed with code {}", + result + ))) + } + } + + fn deactivate(&self) -> Result<(), PluginError> { + let result = unsafe { (self.exports.api.deactivate)(self.handle) }; + if result == 0 { + Ok(()) + } else { + Err(PluginError::Plugin(format!( + "deactivate failed with code {}", + result + ))) + } + } + + fn on_event( + &self, + event: &plugin_sdk::events::PluginEvent, + _ctx: &plugin_sdk::context::PluginContext, + ) -> Result<(), PluginError> { + let buffer = serialize_to_buffer(event)?; + let result = unsafe { (self.exports.api.on_event)(self.handle, buffer.ptr as *const std::ffi::c_void) }; + unsafe { (self.exports.free_buffer)(buffer); } + if result == 0 { + Ok(()) + } else { + Err(PluginError::Plugin(format!( + "on_event failed with code {}", + result + ))) + } + } + + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + _ctx: &plugin_sdk::context::PluginContext, + ) -> Result<(), PluginError> { + let request_payload = serde_json::json!({ + "command_id": command_id, + "args": args + }); + + let request_buffer = serialize_to_buffer(&request_payload)?; + let ptr_to_ptr = &request_buffer.ptr as *const *mut u8 as *const *const std::ffi::c_void; + let response: CommandResponse = unsafe { + (self.exports.api.execute_command)(self.handle, ptr_to_ptr) + }; + unsafe { (self.exports.free_buffer)(request_buffer); } + + if response.success { + if response.data.ptr.is_null() || response.data.len == 0 { + return Ok(()); + } + let _response_data: serde_json::Value = deserialize_from_buffer(&response.data)?; + unsafe { (self.exports.free_buffer)(response.data); } + Ok(()) + } else { + let msg = if response.data.ptr.is_null() || response.data.len == 0 { + "execute_command failed".to_string() + } else { + let data: serde_json::Value = deserialize_from_buffer(&response.data).unwrap_or_default(); + unsafe { (self.exports.free_buffer)(response.data); } + data.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error").to_string() + }; + Err(PluginError::Plugin(msg)) + } + } +} diff --git a/src-tauri/src/services/plugin/embedded.rs b/src-tauri/src/services/plugin/embedded.rs new file mode 100644 index 0000000..1d1d813 --- /dev/null +++ b/src-tauri/src/services/plugin/embedded.rs @@ -0,0 +1,24 @@ +//! Compile-time embedding of all builtin plugin manifests. +//! +//! This is the **single** place that owns `include_str!` paths to +//! `plugin.json` files. Providers reference these constants instead of +//! reaching across the directory tree themselves, so moving a file only +//! requires changing the path in one location. +//! +//! Path math (relative to *this* file, +//! `src-tauri/src/services/plugin/embedded.rs`): +//! - `..` → `src-tauri/src/services/plugin/` +//! - `../..` → `src-tauri/src/services/` +//! - `../../..` → `src-tauri/src/` +//! - `../../../..` → `src-tauri/` +//! - `../../../../plugins/lyrics/plugin.json` → target (repo root) + +/// Embedded `plugins/lyrics/plugin.json` — the single source of truth +/// for the lyrics plugin manifest (no Rust `build_manifest()` needed). +pub const LYRICS_MANIFEST: &str = + include_str!("../../../../plugins/lyrics/plugin.json"); + +/// Embedded `plugins/eq/plugin.json` — the single source of truth +/// for the EQ plugin manifest. +pub const EQ_MANIFEST: &str = + include_str!("../../../../plugins/eq/plugin.json"); diff --git a/src-tauri/src/services/plugin/extension/command_extension.rs b/src-tauri/src/services/plugin/extension/command_extension.rs new file mode 100644 index 0000000..12b0d58 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/command_extension.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +use crate::services::plugin::extension::state::ExtensionState; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandExtension { + pub id: String, + pub plugin_id: String, + pub title: String, + pub category: Option, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/menu_extension.rs b/src-tauri/src/services/plugin/extension/menu_extension.rs new file mode 100644 index 0000000..9d89f1d --- /dev/null +++ b/src-tauri/src/services/plugin/extension/menu_extension.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use plugin_sdk::contributes::MenuLocation; +use crate::services::plugin::extension::state::ExtensionState; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MenuExtension { + pub id: String, + pub plugin_id: String, + pub command: String, + pub location: MenuLocation, + pub group: Option, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/mod.rs b/src-tauri/src/services/plugin/extension/mod.rs new file mode 100644 index 0000000..d14dadf --- /dev/null +++ b/src-tauri/src/services/plugin/extension/mod.rs @@ -0,0 +1,13 @@ +pub mod command_extension; +pub mod menu_extension; +pub mod native_view_extension; +pub mod registry; +pub mod sidebar_extension; +pub mod state; + +pub use command_extension::CommandExtension; +pub use menu_extension::MenuExtension; +pub use native_view_extension::NativeViewExtension; +pub use registry::ExtensionRegistry; +pub use sidebar_extension::SidebarExtension; +pub use state::ExtensionState; diff --git a/src-tauri/src/services/plugin/extension/native_view_extension.rs b/src-tauri/src/services/plugin/extension/native_view_extension.rs new file mode 100644 index 0000000..a059361 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/native_view_extension.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::services::plugin::extension::state::ExtensionState; + +/// 原生视图扩展:插件声明的原生组件贡献在运行期的具象形态。 +/// 应用按 `token` 查找对应的预置原生组件并注入渲染。 +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeViewExtension { + pub id: String, + pub plugin_id: String, + pub title: String, + /// 组件令牌,应用按此查找组件 + pub token: String, + pub icon: Option, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/registry.rs b/src-tauri/src/services/plugin/extension/registry.rs new file mode 100644 index 0000000..ca062a4 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/registry.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use plugin_sdk::contributes::MenuLocation; +use crate::services::plugin::extension::command_extension::CommandExtension; +use crate::services::plugin::extension::menu_extension::MenuExtension; +use crate::services::plugin::extension::native_view_extension::NativeViewExtension; +use crate::services::plugin::extension::sidebar_extension::SidebarExtension; +use crate::services::plugin::extension::state::ExtensionState; + +pub struct ExtensionRegistry { + menus: RwLock>>, + sidebars: RwLock>>, + commands: RwLock>>, + native_views: RwLock>>, +} + +impl ExtensionRegistry { + pub fn new() -> Self { + Self { + menus: RwLock::new(HashMap::new()), + sidebars: RwLock::new(HashMap::new()), + commands: RwLock::new(HashMap::new()), + native_views: RwLock::new(HashMap::new()), + } + } + + pub fn register_menu(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.menus.write() { + lock.insert(key, extension); + } + } + + pub fn register_sidebar(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.sidebars.write() { + lock.insert(key, extension); + } + } + + pub fn register_command(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.commands.write() { + lock.insert(key, extension); + } + } + + pub fn register_native_view(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.native_views.write() { + lock.insert(key, extension); + } + } + + pub fn unregister_plugin(&self, plugin_id: &str) { + if let Ok(mut lock) = self.menus.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + if let Ok(mut lock) = self.sidebars.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + if let Ok(mut lock) = self.commands.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + if let Ok(mut lock) = self.native_views.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + } + + pub fn menu_extensions(&self, location: MenuLocation) -> Vec { + let lock = match self.menus.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.location == location && ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } + + pub fn sidebar_extensions(&self) -> Vec { + let lock = match self.sidebars.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } + + pub fn command_extensions(&self) -> Vec { + let lock = match self.commands.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } + + pub fn native_view_extensions(&self) -> Vec { + let lock = match self.native_views.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } +} diff --git a/src-tauri/src/services/plugin/extension/sidebar_extension.rs b/src-tauri/src/services/plugin/extension/sidebar_extension.rs new file mode 100644 index 0000000..0a1cd47 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/sidebar_extension.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use crate::services::plugin::extension::state::ExtensionState; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SidebarExtension { + pub id: String, + pub plugin_id: String, + pub title: String, + pub icon: String, + pub route: String, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/state.rs b/src-tauri/src/services/plugin/extension/state.rs new file mode 100644 index 0000000..4345198 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/state.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum ExtensionState { + Enabled, + Disabled, +} diff --git a/src-tauri/src/services/plugin/loader.rs b/src-tauri/src/services/plugin/loader.rs new file mode 100644 index 0000000..35ace79 --- /dev/null +++ b/src-tauri/src/services/plugin/loader.rs @@ -0,0 +1,118 @@ +use crate::errors::AppError; +use plugin_sdk::manifest::PluginManifest; + +/// Manifest validation utilities. +/// +/// Builtin manifest loading (compile-time embed) now lives in the +/// [`provider`] module; packaged/user discovery will live there too in +/// the future. This module keeps only the cross-cutting validation used +/// by [`ManifestRegistry`] before a manifest is resolved. +/// +/// [`provider`]: crate::services::plugin::provider +/// [`ManifestRegistry`]: crate::services::plugin::registry::ManifestRegistry +pub struct PluginLoader; + +impl PluginLoader { + /// Validate a raw manifest before it is normalized and cached. + /// + /// The `id` must be a pure logical identifier — it must NOT carry a + /// source/provenance prefix (e.g. `builtin/lyrics`). Provenance lives + /// in the separate `source` field, not in the id. + pub fn validate_manifest(manifest: &PluginManifest) -> Result<(), AppError> { + if manifest.id.is_empty() { + return Err(AppError::Plugin("Manifest 'id' cannot be empty".into())); + } + if manifest.name.is_empty() { + return Err(AppError::Plugin("Manifest 'name' cannot be empty".into())); + } + if manifest.entry.is_empty() { + return Err(AppError::Plugin("Manifest 'entry' cannot be empty".into())); + } + // Reject legacy "source/name" ids — provenance is now `source`, not a + // prefix. This guards against regressing to the old `builtin/lyrics` + // convention. + if manifest.id.contains('/') { + return Err(AppError::Plugin(format!( + "Manifest id '{}' must not contain '/'; provenance belongs in the 'source' field, not the id", + manifest.id + ))); + } + for view in &manifest.contributes.views { + if view.id.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' has a view with empty id", + manifest.id + ))); + } + if !view + .id + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' id may only contain [a-zA-Z0-9_-]", + manifest.id, view.id + ))); + } + if view.entry.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' has empty entry", + manifest.id, view.id + ))); + } + if !view.entry.ends_with(".html") { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' entry '{}' must end with .html", + manifest.id, view.id, view.entry + ))); + } + if view.entry.contains("..") || view.entry.starts_with('/') { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' entry '{}' must not contain '..' or start with '/'", + manifest.id, view.id, view.entry + ))); + } + if view.title.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' has empty title", + manifest.id, view.id + ))); + } + } + for nv in &manifest.contributes.native_views { + if nv.id.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' has a nativeView with empty id", + manifest.id + ))); + } + if !nv + .id + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin '{}' nativeView '{}' id may only contain [a-zA-Z0-9_-]", + manifest.id, nv.id + ))); + } + if nv.token.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' nativeView '{}' has empty token", + manifest.id, nv.id + ))); + } + if !nv + .token + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin '{}' nativeView '{}' token may only contain [a-zA-Z0-9_-]", + manifest.id, nv.id + ))); + } + } + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/manager.rs b/src-tauri/src/services/plugin/manager.rs new file mode 100644 index 0000000..4a5bc10 --- /dev/null +++ b/src-tauri/src/services/plugin/manager.rs @@ -0,0 +1,1071 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +use serde::{Deserialize, Serialize}; +use crate::errors::AppError; +use crate::events::AppEvent; +use crate::services::plugin::audio_processor_registry::AudioProcessorRegistry; +use crate::services::plugin::capability::capability::PluginCapability; +use crate::services::plugin::capability::plugin_capability::CapabilityRegistry; +use plugin_sdk::contributes::{MenuContribution, MenuLocation}; +use plugin_sdk::events::PluginEvent; +use plugin_sdk::ffi::AudioProcessorApi; +use crate::services::plugin::extension::command_extension::CommandExtension; +use crate::services::plugin::extension::menu_extension::MenuExtension; +use crate::services::plugin::extension::native_view_extension::NativeViewExtension; +use crate::services::plugin::extension::registry::ExtensionRegistry; +use crate::services::plugin::extension::sidebar_extension::SidebarExtension; +use crate::services::plugin::extension::state::ExtensionState; +use plugin_sdk::manifest::{ActivationEvent, PluginSource, ResolvedPluginManifest}; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::registry::command_registry::CommandRegistry; +use crate::services::plugin::registry::factory_registry::FactoryRegistry; +use crate::services::plugin::registry::manifest_registry::ManifestRegistry; +use crate::services::plugin::runtime::context_factory::PluginContextFactory; +use crate::services::plugin::runtime::runtime_instance::RuntimePluginInstance; +use crate::services::plugin::runtime::state::{PluginLifecycleState, PluginState}; +use crate::services::plugin::settings::persistence::PluginStorage; +use plugin_sdk::settings::SettingValue; +use crate::services::plugin::settings::settings_registry::SettingsRegistry; +use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; +use tauri::Manager; + +pub struct PluginManager { + app_handle: tauri::AppHandle, + manifest_registry: Arc, + factory_registry: Arc, + command_registry: Arc, + extension_registry: Arc, + capability_registry: Arc, + settings_registry: Arc, + storage: Arc, + context_factory: PluginContextFactory, + instances: RwLock>, + lifecycle_states: RwLock>, + /// Shared audio processor registry. Populated when packaged plugins that + /// export `rem_audio_processor` are activated; read by the render graph's + /// `ExternalProcessorNode` on every quantum. + audio_processor_registry: Arc, + /// Known `AudioProcessorApi` per packaged plugin (regardless of lifecycle + /// state). Stored separately from the registry's *active* set so we can + /// look up the api at activate time without downcasting the + /// `Arc` stored in `FactoryRegistry`. + audio_processor_apis: RwLock>, +} + +impl PluginManager { + pub fn new( + app_handle: tauri::AppHandle, + manifest_registry: Arc, + factory_registry: Arc, + context_factory: PluginContextFactory, + capability_registry: Arc, + settings_registry: Arc, + storage: Arc, + audio_processor_registry: Arc, + ) -> Self { + Self { + app_handle, + manifest_registry, + factory_registry, + command_registry: Arc::new(CommandRegistry::new()), + extension_registry: Arc::new(ExtensionRegistry::new()), + capability_registry, + settings_registry, + storage, + context_factory, + instances: RwLock::new(HashMap::new()), + lifecycle_states: RwLock::new(HashMap::new()), + audio_processor_registry, + audio_processor_apis: RwLock::new(HashMap::new()), + } + } + + /// Shared audio processor registry — also held by the audio render graph. + pub fn audio_processor_registry(&self) -> Arc { + Arc::clone(&self.audio_processor_registry) + } + + pub fn manifest_registry(&self) -> &Arc { + &self.manifest_registry + } + + pub fn factory_registry(&self) -> &Arc { + &self.factory_registry + } + + pub fn command_registry(&self) -> &Arc { + &self.command_registry + } + + pub fn extension_registry(&self) -> &Arc { + &self.extension_registry + } + + pub fn capability_registry(&self) -> &Arc { + &self.capability_registry + } + + pub fn settings_registry(&self) -> &Arc { + &self.settings_registry + } + + pub fn load_persisted_state(&self) { + match self.storage.load_config() { + Ok((states, settings)) => { + // One-time id migration: ids used to carry a source prefix + // (e.g. `builtin/lyrics`). They are now pure logical ids + // (`lyrics`). Strip any `/` prefix so previously stored + // lifecycle/settings aren't orphaned, then persist the fixup. + let migrated = migrate_prefixed_ids(states, settings); + if let Ok(mut lock) = self.lifecycle_states.write() { + *lock = migrated.states; + } + self.settings_registry.load_all(migrated.settings); + if migrated.changed { + self.persist_state(); + } + } + Err(e) => { + eprintln!("[PluginManager] Failed to load persisted state: {}", e); + } + } + } + + pub fn persist_state(&self) { + let states = match self.lifecycle_states.read() { + Ok(guard) => guard.clone(), + Err(_) => return, + }; + let settings = self.settings_registry.dump_all(); + if let Err(e) = self.storage.save_config(&states, &settings) { + eprintln!("[PluginManager] Failed to persist state: {}", e); + } + } + + /// Register all builtin plugins whose manifests were aggregated by + /// `ManifestRegistry`. Each builtin manifest is explicitly linked to + /// its factory via [`builtin_factory_for`]; this is an explicit + /// association (not an implicit hard-coded factory list), so a + /// builtin manifest with no matching factory is surfaced loudly + /// instead of silently half-loading. + pub fn load_builtin_plugins(&self) { + for manifest in self.manifest_registry.by_source(PluginSource::Builtin) { + let factory = match builtin_factory_for(&manifest.id) { + Some(f) => f, + None => { + eprintln!( + "[PluginManager] No builtin factory for plugin '{}'; skipping", + manifest.id + ); + continue; + } + }; + self.register_static_plugin(manifest, factory); + } + } + + pub fn load_packaged_plugins(&self) { + use crate::services::plugin::provider::PackagedManifestProvider; + use crate::services::plugin::provider::ManifestProvider; + + let mut search_dirs = Vec::new(); + + if let Ok(app_data) = self.app_handle.path().app_data_dir() { + search_dirs.push(app_data.join("plugins")); + } + + if let Ok(exe_dir) = std::env::current_exe() { + if let Some(parent) = exe_dir.parent() { + let exe_plugin_dir = parent.to_path_buf(); + if exe_plugin_dir.exists() && !search_dirs.contains(&exe_plugin_dir) { + search_dirs.push(exe_plugin_dir); + } + } + } + + println!("[PluginManager] Packaged search dirs: {:?}", search_dirs); + + if search_dirs.is_empty() { + println!("[PluginManager] No search dirs, skipping packaged plugins"); + return; + } + + let provider = PackagedManifestProvider::new(search_dirs.clone()); + let raw_manifests = match provider.load() { + Ok(m) => { + println!("[PluginManager] Found {} packaged manifests", m.len()); + m + } + Err(e) => { + eprintln!("[PluginManager] Failed to load packaged manifests: {}", e); + return; + } + }; + + for raw in raw_manifests { + println!("[PluginManager] Registering manifest: {}", raw.id); + if let Err(e) = self.manifest_registry.register_one(raw) { + eprintln!("[PluginManager] Failed to register manifest: {}", e); + continue; + } + } + + for manifest in self.manifest_registry.by_source(plugin_sdk::manifest::PluginSource::Packaged) { + let plugin_id = manifest.id.clone(); + + println!("[PluginManager] Loading dynamic factory for '{}'...", plugin_id); + match self.load_dynamic_factory_for(plugin_id.as_str(), &search_dirs) { + Ok(factory) => { + println!("[PluginManager] Dynamic factory loaded for '{}', calling register_static_plugin", plugin_id); + self.register_static_plugin(manifest, factory); + println!("[PluginManager] register_static_plugin completed for '{}'", plugin_id); + println!("[PluginManager] instance count: {}", self.instances.read().map(|m| m.len()).unwrap_or(0)); + } + Err(e) => { + eprintln!("[PluginManager] Failed to load dynamic factory for '{}': {}", plugin_id, e); + } + } + } + } + + fn load_dynamic_factory_for( + &self, + plugin_id: &str, + search_dirs: &[std::path::PathBuf], + ) -> Result, AppError> { + use crate::services::plugin::dynamic_factory::DynamicPluginFactory; + + let platform_suffix = if cfg!(target_os = "windows") { + ".dll" + } else if cfg!(target_os = "macos") { + ".dylib" + } else { + ".so" + }; + + let mut lib_path = None; + for dir in search_dirs { + let plugin_dir = dir.join(plugin_id); + if !plugin_dir.exists() { + println!("[PluginManager] {} does not exist, skipping", plugin_dir.display()); + continue; + } + + let candidates = [ + plugin_dir.join(format!("lib{}", plugin_id)), + plugin_dir.join(format!("lib{}{}", plugin_id, platform_suffix)), + plugin_dir.join(format!("{}{}", plugin_id, platform_suffix)), + ]; + + for candidate in &candidates { + println!("[PluginManager] checking: {}", candidate.display()); + if candidate.exists() { + lib_path = Some(candidate.clone()); + break; + } + } + + if lib_path.is_none() { + let alt = plugin_dir.join(plugin_id); + if alt.exists() { + lib_path = Some(alt); + } + } + + if lib_path.is_some() { + break; + } + } + + let lib_path = lib_path.ok_or_else(|| AppError::Plugin(format!("No library found for plugin '{}'", plugin_id)))?; + + let lib = unsafe { libloading::Library::new(&lib_path) } + .map_err(|e| AppError::Plugin(format!("Failed to load library '{}': {}", lib_path.display(), e)))?; + + let lib = Arc::new(lib); + + let get_exports: libloading::Symbol plugin_sdk::ffi::PluginExports> = + unsafe { lib.get(b"get_plugin_exports") } + .map_err(|e| AppError::Plugin(format!("Symbol 'get_plugin_exports' not found: {}", e)))?; + + let exports = unsafe { get_exports() }; + + const CURRENT_ABI_VERSION: u32 = 1; + if exports.abi_version != CURRENT_ABI_VERSION { + return Err(AppError::Plugin(format!( + "Plugin '{}' ABI version mismatch: expected {}, got {}", + plugin_id, CURRENT_ABI_VERSION, exports.abi_version + ))); + } + + let manifest = self.manifest_registry.manifest(&plugin_id) + .ok_or_else(|| AppError::Plugin(format!("No manifest for plugin '{}'", plugin_id)))?; + + // 查找可选的 `rem_audio_processor` 符号。旧插件未导出此符号时为 None, + // 行为与改动前一致(不破坏现有 packaged 插件加载逻辑)。 + // 注意:不从 exports.audio_processor 字段读取(旧插件结构体更短,读取为 UB), + // 而是用单独的全局符号查找。 + let audio_processor: Option = match unsafe { + lib.get::<*const plugin_sdk::ffi::AudioProcessorApi>(b"rem_audio_processor") + } { + Ok(sym) => { + let ptr: *const plugin_sdk::ffi::AudioProcessorApi = *sym; + if ptr.is_null() { + None + } else { + // 拷贝 AudioProcessorApi(Copy),脱离符号借用,之后 lib 可安全 move 进工厂。 + Some(unsafe { *ptr }) + } + } + Err(_) => None, + }; + + // 记下 api 以便 activate 时无需 downcast `Arc`。 + if let Some(api) = audio_processor { + if let Ok(mut lock) = self.audio_processor_apis.write() { + lock.insert(plugin_id.to_string(), api); + } + } + + Ok(Arc::new(DynamicPluginFactory::new( + lib, + exports, + manifest, + audio_processor, + ))) + } + + /// Link one resolved manifest to a factory and register all of the + /// plugin's contributed extensions, capabilities, settings defaults, + /// and lifecycle entry. Manifest comes from `ManifestRegistry`; + /// factory is registered with `FactoryRegistry`. + pub fn register_static_plugin( + &self, + manifest: ResolvedPluginManifest, + factory: Arc, + ) { + let plugin_id = manifest.id.clone(); + + let mut defaults = HashMap::new(); + for def in &manifest.settings { + defaults.insert(def.key.clone(), def.default_value.clone()); + } + if !defaults.is_empty() { + self.settings_registry + .register_defaults(&plugin_id, defaults); + } + + for cmd in &manifest.contributes.commands { + self.command_registry + .register_command(cmd.id.clone(), plugin_id.clone()); + + self.extension_registry + .register_command(Arc::new(CommandExtension { + id: cmd.id.clone(), + plugin_id: plugin_id.clone(), + title: cmd.title.clone(), + category: cmd.category.clone(), + state: ExtensionState::Enabled, + })); + } + + for menu in &manifest.contributes.menus { + self.extension_registry + .register_menu(Arc::new(MenuExtension { + id: menu.command.clone(), + plugin_id: plugin_id.clone(), + command: menu.command.clone(), + location: menu.location.clone(), + group: menu.group.clone(), + state: ExtensionState::Enabled, + })); + } + + for sidebar in &manifest.contributes.sidebars { + self.extension_registry + .register_sidebar(Arc::new(SidebarExtension { + id: sidebar.id.clone(), + plugin_id: plugin_id.clone(), + title: sidebar.title.clone(), + icon: sidebar.icon.clone(), + // Single source of truth: route comes from the resolved + // manifest, never duplicated on the sidebar contribution. + route: manifest.route.clone(), + state: ExtensionState::Enabled, + })); + } + + for nv in &manifest.contributes.native_views { + self.extension_registry + .register_native_view(Arc::new(NativeViewExtension { + id: nv.id.clone(), + plugin_id: plugin_id.clone(), + title: nv.title.clone(), + token: nv.token.clone(), + icon: nv.icon.clone(), + state: ExtensionState::Enabled, + })); + } + + let permissions: HashSet = manifest.permissions.iter().cloned().collect(); + + self.capability_registry + .register(Arc::new(PluginCapability { + plugin_id: plugin_id.clone(), + permissions, + })); + + self.factory_registry.register(&plugin_id, factory); + + let lifecycle = { + let states = match self.lifecycle_states.read() { + Ok(guard) => guard, + Err(_) => return, + }; + states + .get(&plugin_id) + .cloned() + .unwrap_or(PluginLifecycleState::Enabled) + }; + + if lifecycle == PluginLifecycleState::Enabled { + if let Ok(mut instances) = self.instances.write() { + instances.insert(plugin_id, RuntimePluginInstance::Registered); + } + } + } + + pub fn enable_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + { + let mut states = self + .lifecycle_states + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + states.insert(plugin_id.to_string(), PluginLifecycleState::Enabled); + } + + { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + if !instances.contains_key(plugin_id) { + if self.factory_registry.contains(plugin_id) { + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Registered); + } + } + } + + self.persist_state(); + Ok(()) + } + + pub fn disable_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + if !self.factory_registry.contains(plugin_id) { + return Err(AppError::Plugin(format!( + "Plugin '{}' is not registered", + plugin_id + ))); + } + + if self.plugin_state(plugin_id) == PluginLifecycleState::Disabled { + return Ok(()); + } + + { + let mut states = self + .lifecycle_states + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + states.insert(plugin_id.to_string(), PluginLifecycleState::Disabled); + } + + { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + if let Some(RuntimePluginInstance::Active(plugin)) = instances.remove(plugin_id) { + let _ = plugin.deactivate(); + // 失活的同时从音频图中摘除其处理器,避免回调已停用插件。 + self.unregister_audio_processor(plugin_id); + } + } + + self.persist_state(); + Ok(()) + } + + pub fn plugin_state(&self, plugin_id: &str) -> PluginLifecycleState { + let states = match self.lifecycle_states.read() { + Ok(guard) => guard, + Err(_) => return PluginLifecycleState::Enabled, + }; + states + .get(plugin_id) + .cloned() + .unwrap_or(PluginLifecycleState::Enabled) + } + + fn is_plugin_enabled(&self, plugin_id: &str) -> bool { + self.plugin_state(plugin_id) == PluginLifecycleState::Enabled + } + + pub fn execute_plugin_command( + &self, + command_id: &str, + args: CommandArgs, + ) -> Result<(), AppError> { + let plugin_id = self + .command_registry + .find_plugin(command_id) + .ok_or_else(|| { + AppError::Plugin(format!("No plugin registered for command '{}'", command_id)) + })?; + + if !self.is_plugin_enabled(&plugin_id) { + return Err(AppError::Plugin(format!( + "Plugin '{}' is disabled", + plugin_id + ))); + } + + let plugin = self.ensure_active(&plugin_id)?; + let ctx = self.context_factory.create_context(&plugin_id); + plugin.execute_command(command_id, &args, &ctx).map_err(|e| e.into()) + } + + fn ensure_active(&self, plugin_id: &str) -> Result, AppError> { + { + let instances = self + .instances + .read() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match instances.get(plugin_id) { + Some(RuntimePluginInstance::Active(plugin)) => { + return Ok(Arc::clone(plugin)); + } + Some(RuntimePluginInstance::Activating) => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is currently activating", + plugin_id + ))); + } + Some(RuntimePluginInstance::Deactivating(_)) => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is currently deactivating", + plugin_id + ))); + } + Some(RuntimePluginInstance::Disabled) => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is disabled", + plugin_id + ))); + } + Some(RuntimePluginInstance::Registered) => {} + None => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is not registered", + plugin_id + ))); + } + } + } + + self.activate_instance(plugin_id) + } + + fn activate_instance(&self, plugin_id: &str) -> Result, AppError> { + { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match instances.get(plugin_id) { + Some(RuntimePluginInstance::Registered) => { + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Activating); + } + _ => { + return Err(AppError::Plugin(format!( + "Plugin '{}' cannot be activated from its current state", + plugin_id + ))); + } + } + } + + let factory = self + .factory_registry + .get(plugin_id) + .ok_or_else(|| AppError::Plugin(format!("No factory for plugin '{}'", plugin_id)))?; + + let plugin = factory.create(); + let ctx = self.context_factory.create_context(plugin_id); + + match plugin.activate(&ctx) { + Ok(()) => { + // 若该 packaged 插件导出了音频处理器,注册到音频图共享的 registry。 + // 仅在插件声明 `Audio` 权限时生效;缺少权限则记录告警并跳过。 + self.maybe_register_audio_processor(plugin_id); + + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + instances.insert( + plugin_id.to_string(), + RuntimePluginInstance::Active(Arc::clone(&plugin)), + ); + + Ok(plugin) + } + Err(e) => { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Registered); + + Err(e.into()) + } + } + } + + /// Activate-time audio processor registration (Task 10.2 + 10.4). + /// + /// If the plugin exported `rem_audio_processor`, verify it declared the + /// `Audio` permission, call its `init` callback, then add it to the shared + /// registry so the render graph's `ExternalProcessorNode` starts feeding + /// it PCM. Lacking the permission only logs a warning — plugin activation + /// itself is unaffected. + fn maybe_register_audio_processor(&self, plugin_id: &str) { + let api = match self + .audio_processor_apis + .read() + .ok() + .and_then(|lock| lock.get(plugin_id).copied()) + { + Some(api) => api, + None => return, // 该插件未导出音频处理能力 + }; + + let has_audio_perm = self + .manifest_registry + .manifest(plugin_id) + .map(|m| m.permissions.iter().any(|p| matches!(p, PluginPermission::Audio))) + .unwrap_or(false); + + if !has_audio_perm { + eprintln!( + "[PluginManager] plugin '{}' exports audio_processor but lacks 'audio' permission, skipping", + plugin_id + ); + return; + } + + let sample_rate = self.audio_processor_registry.sample_rate(); + let channels = self.audio_processor_registry.channels(); + if let Some(init_fn) = api.init { + // handle 暂用 null:FFI PluginHandle 当前封存在 DynamicPluginInstance + // 内部,需 SDK trait 扩展才能透出(本任务范围内不修改 ffi.rs)。 + let _ = unsafe { init_fn(std::ptr::null_mut(), sample_rate, channels) }; + } + + self.audio_processor_registry + .register(plugin_id.to_string(), api, std::ptr::null_mut()); + } + + /// Deactivate-time cleanup (Task 10.3). Removes the processor from the + /// shared registry and invokes its `reset` callback so the plugin can + /// release delay lines / internal buffers. + fn unregister_audio_processor(&self, plugin_id: &str) { + let Some((api, handle)) = self.audio_processor_registry.unregister(plugin_id) else { + return; + }; + if let Some(reset_fn) = api.reset { + let _ = unsafe { reset_fn(handle) }; + } + } + + pub fn activate_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + self.ensure_active(plugin_id)?; + Ok(()) + } + + pub fn deactivate_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + let plugin = { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match instances.remove(plugin_id) { + Some(RuntimePluginInstance::Active(p)) => { + instances.insert( + plugin_id.to_string(), + RuntimePluginInstance::Deactivating(Arc::clone(&p)), + ); + p + } + other => { + if let Some(original) = other { + instances.insert(plugin_id.to_string(), original); + } + return Err(AppError::Plugin(format!( + "Plugin '{}' is not in Active state", + plugin_id + ))); + } + } + }; + + let result = plugin.deactivate(); + + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match result { + Ok(()) => { + // 失活成功后从音频图中摘除其处理器并触发 reset。 + self.unregister_audio_processor(plugin_id); + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Registered); + } + Err(e) => { + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Active(plugin)); + return Err(e.into()); + } + } + + Ok(()) + } + + pub fn get_active_plugin(&self, id: &str) -> Option> { + let instances = self.instances.read().ok()?; + match instances.get(id)? { + RuntimePluginInstance::Active(p) => Some(Arc::clone(p)), + _ => None, + } + } + + pub fn list_active_plugins(&self) -> Vec<(String, Arc)> { + let instances = match self.instances.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + instances + .iter() + .filter_map(|(id, inst)| match inst { + RuntimePluginInstance::Active(p) => Some((id.clone(), Arc::clone(p))), + _ => None, + }) + .collect() + } + + pub fn active_count(&self) -> usize { + self.instances + .read() + .ok() + .map(|l| { + l.values() + .filter(|i| matches!(i, RuntimePluginInstance::Active(_))) + .count() + }) + .unwrap_or(0) + } + + pub fn dispatch_event(&self, app_event: &AppEvent) { + let plugin_event = match Self::bridge_app_event(app_event) { + Some(event) => event, + None => return, + }; + + let required_activation = match &plugin_event { + PluginEvent::Startup => ActivationEvent::OnStartup, + PluginEvent::TrackChanged { .. } => ActivationEvent::OnTrackChanged, + PluginEvent::PlaybackStateChanged { .. } => ActivationEvent::OnPlaybackStateChanged, + PluginEvent::QueueChanged => ActivationEvent::OnQueueChanged, + PluginEvent::SettingsChanged => ActivationEvent::OnSettingsChanged, + PluginEvent::LyricsLoaded { .. } => return, + }; + + let pending_activation: Vec = { + let instances = match self.instances.read() { + Ok(guard) => guard, + Err(_) => return, + }; + + instances + .iter() + .filter(|(id, inst)| { + self.is_plugin_enabled(id) && matches!(inst, RuntimePluginInstance::Registered) + }) + .filter_map(|(id, _)| { + let manifest = self.manifest_registry.manifest(id)?; + if manifest.activation_events.contains(&required_activation) { + Some(id.clone()) + } else { + None + } + }) + .collect() + }; + + for plugin_id in &pending_activation { + let _ = self.ensure_active(plugin_id); + } + + let instances = match self.instances.read() { + Ok(guard) => guard, + Err(_) => return, + }; + + for (plugin_id, instance) in instances.iter() { + if !self.is_plugin_enabled(plugin_id) { + continue; + } + + if let RuntimePluginInstance::Active(plugin) = instance { + // Manifest is sourced from ManifestRegistry (single source of + // truth), not from the plugin instance. + let Some(manifest) = self.manifest_registry.manifest(plugin_id) else { + continue; + }; + if manifest.activation_events.contains(&required_activation) { + let ctx = self.context_factory.create_context(plugin_id); + let _ = plugin.on_event(&plugin_event, &ctx); + } + } + } + } + + pub fn bridge_app_event(app_event: &AppEvent) -> Option { + match app_event { + AppEvent::TrackStarted(payload) => Some(PluginEvent::TrackChanged { + track: plugin_sdk::events::TrackSnapshot { + id: payload.track.id, + title: payload.track.title.clone(), + artist: payload.track.artist.clone(), + album: payload.track.album.clone(), + duration: payload.track.duration, + }, + index: payload.index, + }), + AppEvent::PlaybackStateChanged(payload) => Some(PluginEvent::PlaybackStateChanged { + playing: payload.playing, + current_time: payload.current_time, + }), + AppEvent::QueueChanged(_) => Some(PluginEvent::QueueChanged), + AppEvent::SettingsChanged(_) => Some(PluginEvent::SettingsChanged), + AppEvent::VolumeChanged(_) => None, + AppEvent::PlaybackProgress(_) => None, + AppEvent::LyricsLoaded(_) => None, + } + } + + pub fn menu_extensions(&self, location: MenuLocation) -> Vec { + self.extension_registry + .menu_extensions(location) + .into_iter() + .filter(|ext| self.is_plugin_enabled(&ext.plugin_id)) + .collect() + } + + pub fn sidebar_extensions(&self) -> Vec { + self.extension_registry + .sidebar_extensions() + .into_iter() + .filter(|ext| self.is_plugin_enabled(&ext.plugin_id)) + .collect() + } + + pub fn native_view_extensions(&self) -> Vec { + self.extension_registry + .native_view_extensions() + .into_iter() + .filter(|ext| self.is_plugin_enabled(&ext.plugin_id)) + .collect() + } + + pub fn all_sidebar_extensions(&self) -> Vec { + self.extension_registry + .sidebar_extensions() + .into_iter() + .map(|mut ext| { + if !self.is_plugin_enabled(&ext.plugin_id) { + ext.state = ExtensionState::Disabled; + } + ext + }) + .collect() + } + + pub fn get_plugin_settings( + &self, + plugin_id: &str, + ) -> Vec { + let definitions = self + .manifest_registry + .manifest(plugin_id) + .map(|m| m.settings) + .unwrap_or_default(); + + let current = self.settings_registry.get_all(plugin_id); + + definitions + .into_iter() + .map(|def| { + let value = current + .get(&def.key) + .cloned() + .unwrap_or_else(|| def.default_value.clone()); + plugin_sdk::settings::PluginSettingMeta { + key: def.key, + title: def.title, + value, + default_value: def.default_value, + } + }) + .collect() + } + + pub fn update_plugin_setting( + &self, + plugin_id: &str, + key: &str, + value: SettingValue, + ) -> Result<(), AppError> { + self.settings_registry + .update_setting(plugin_id, key, value)?; + self.persist_state(); + Ok(()) + } + + /// All resolved manifests, the only plugin-identity shape the frontend + /// ever consumes. Exposed via the `get_plugin_manifests` command. + pub fn all_manifests(&self) -> Vec { + self.manifest_registry.all() + } + + /// Resolve the view entry URL for a plugin's view. + /// Returns None if: + /// - plugin not found + /// - plugin has no views + /// - view_id not found among the plugin's views + /// - plugin lacks PluginUI permission + pub fn plugin_view(&self, plugin_id: &str, view_id: &str) -> Option { + let manifest = self.manifest_registry.manifest(plugin_id)?; + + // 校验权限 + let has_ui_permission = manifest + .permissions + .iter() + .any(|p| matches!(p, PluginPermission::PluginUI)); + if !has_ui_permission { + return None; + } + + // 查找 view + let view = manifest + .contributes + .views + .iter() + .find(|v| v.id == view_id) + .cloned()?; + + Some(PluginViewResolution { + plugin_id: plugin_id.to_string(), + view_id: view.id.clone(), + title: view.title.clone(), + entry_url: format!("plugin://{}/{}", plugin_id, view.entry), + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginViewResolution { + pub plugin_id: String, + pub view_id: String, + pub title: String, + pub entry_url: String, +} + +/// Explicit association from a builtin plugin id to its Rust factory. +/// +/// This is the single, intentional place where a builtin manifest (sourced +/// from `plugin.json`) is wired to the crate that implements its logic. +/// Adding a new builtin plugin means adding one line here plus one embedded +/// manifest in [`embedded`]. +/// +/// [`embedded`]: crate::services::plugin::embedded +fn builtin_factory_for(id: &str) -> Option> { + match id { + "lyrics" => Some(Arc::new(lyrics_plugin::LyricsPluginFactory)), + "eq" => Some(Arc::new(eq_plugin::EqPluginFactory)), + _ => None, + } +} + +/// Result of stripping legacy `/` prefixes from persisted ids. +struct MigratedState { + states: HashMap, + settings: HashMap, + changed: bool, +} + +/// Strip any `/` prefix from persisted plugin ids so configs written +/// under the old `builtin/lyrics` convention keep working under the new +/// pure-id convention (`lyrics`). +fn migrate_prefixed_ids( + states: HashMap, + mut settings: HashMap, +) -> MigratedState { + let mut changed = false; + + // Migrate state keys. + let migrated_states = states + .into_iter() + .map(|(id, state)| { + if let Some(stripped) = id.rsplit('/').next() { + if stripped != id { + changed = true; + return (stripped.to_string(), state); + } + } + (id, state) + }) + .collect::>(); + + // Migrate settings keys, and fix the `plugin_id` field carried inside + // each `PluginSettings` so it stays consistent with the map key. + let migrated_settings = std::mem::take(&mut settings); + let mut new_settings = HashMap::with_capacity(migrated_settings.len()); + for (id, mut ps) in migrated_settings { + if let Some(stripped) = id.rsplit('/').next() { + if stripped != id { + changed = true; + ps.plugin_id = stripped.to_string(); + new_settings.insert(stripped.to_string(), ps); + continue; + } + } + new_settings.insert(id, ps); + } + + MigratedState { + states: migrated_states, + settings: new_settings, + changed, + } +} diff --git a/src-tauri/src/services/plugin/mod.rs b/src-tauri/src/services/plugin/mod.rs new file mode 100644 index 0000000..2b84863 --- /dev/null +++ b/src-tauri/src/services/plugin/mod.rs @@ -0,0 +1,39 @@ +pub mod adapters; +pub mod asset; +pub mod audio_processor_registry; +pub mod capability; +pub mod dynamic_factory; +pub mod embedded; +pub mod extension; +pub mod loader; +pub mod manager; +pub mod provider; +pub mod registry; +pub mod runtime; +pub mod sdk_adapters; +pub mod settings; + +pub use audio_processor_registry::AudioProcessorRegistry; +pub use capability::{CapabilityRegistry, PermissionChecker, PluginCapability}; +pub use extension::{ + CommandExtension, ExtensionRegistry, ExtensionState, MenuExtension, SidebarExtension, +}; +pub use loader::PluginLoader; +pub use manager::PluginManager; +pub use provider::{BuiltinManifestProvider, ManifestProvider}; +pub use registry::{CommandRegistry, FactoryRegistry, ManifestRegistry}; +pub use runtime::{PluginContextFactory, PluginLifecycleState, PluginState, RuntimePluginInstance}; +pub use settings::{PluginStorage, SettingsRegistry}; + +pub use plugin_sdk::api::{LibraryApi, PlayerApi, PluginSettingsApi, QueueApi, SettingsApi}; +pub use plugin_sdk::context::{HostContext, PluginContext}; +pub use plugin_sdk::errors::PluginError; +pub use plugin_sdk::events::PluginEvent; +pub use plugin_sdk::manifest::{ + ActivationEvent, PluginContribution, PluginManifest, PluginSource, ResolvedPluginManifest, + SettingDefinition, +}; +pub use plugin_sdk::model::PluginIdentity; +pub use plugin_sdk::permissions::PluginPermission; +pub use plugin_sdk::settings::{PluginSetting, PluginSettingMeta, PluginSettings, SettingValue}; +pub use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; diff --git a/src-tauri/src/services/plugin/provider/builtin.rs b/src-tauri/src/services/plugin/provider/builtin.rs new file mode 100644 index 0000000..c298a7b --- /dev/null +++ b/src-tauri/src/services/plugin/provider/builtin.rs @@ -0,0 +1,40 @@ +//! Provider for builtin plugins. +//! +//! Builtin manifests are embedded at compile time via [`embedded`] and +//! their logic ships as Rust crates. Each builtin manifest is parsed +//! from its embedded JSON and force-stamped `source = Builtin` so the +//! source field can never lie about provenance. +//! +//! [`embedded`]: crate::services::plugin::embedded + +use crate::errors::AppError; +use crate::services::plugin::embedded; +use plugin_sdk::manifest::{PluginManifest, PluginSource}; + +use super::ManifestProvider; + +/// Loads builtin plugin manifests embedded at compile time. +pub struct BuiltinManifestProvider; + +impl ManifestProvider for BuiltinManifestProvider { + fn source(&self) -> PluginSource { + PluginSource::Builtin + } + + fn load(&self) -> Result, AppError> { + let raw_entries: &[&str] = &[embedded::LYRICS_MANIFEST, embedded::EQ_MANIFEST]; + let source = self.source(); + + let mut manifests = Vec::with_capacity(raw_entries.len()); + for raw in raw_entries { + let mut manifest: PluginManifest = serde_json::from_str(raw).map_err(|e| { + AppError::Plugin(format!("Failed to parse builtin manifest: {}", e)) + })?; + // Force-correct provenance: a builtin provider can only ever + // publish builtin manifests, regardless of what the json says. + manifest.source = source; + manifests.push(manifest); + } + Ok(manifests) + } +} diff --git a/src-tauri/src/services/plugin/provider/mod.rs b/src-tauri/src/services/plugin/provider/mod.rs new file mode 100644 index 0000000..e68b01a --- /dev/null +++ b/src-tauri/src/services/plugin/provider/mod.rs @@ -0,0 +1,35 @@ +//! Manifest source providers. +//! +//! Each provider knows how to load raw [`PluginManifest`]s from one +//! provenance (builtin / packaged / user). The [`ManifestRegistry`] +//! collects from all providers, normalizes, validates, and resolves +//! them into [`ResolvedPluginManifest`]s. +//! +//! [`ManifestRegistry`]: crate::services::plugin::registry::ManifestRegistry +//! [`ResolvedPluginManifest`]: plugin_sdk::manifest::ResolvedPluginManifest + +pub mod builtin; +pub mod packaged; + +pub use builtin::BuiltinManifestProvider; +pub use packaged::PackagedManifestProvider; + +use crate::errors::AppError; +use plugin_sdk::manifest::{PluginManifest, PluginSource}; + +/// Loads raw plugin manifests from a single provenance. +/// +/// Implementations must NOT normalize or validate beyond what is needed +/// to deserialize; the registry owns normalization/validation. They MAY, +/// however, override `source` to reflect their own provenance — this is +/// how builtin manifests stamped with the wrong `source` in their json +/// get corrected. +pub trait ManifestProvider: Send + Sync { + /// The provenance this provider represents. Used to (re)stamp each + /// loaded manifest's `source` field so a builtin provider can never + /// accidentally publish a `user`/`packaged` manifest and vice versa. + fn source(&self) -> PluginSource; + + /// Load every manifest this provider can supply. + fn load(&self) -> Result, AppError>; +} diff --git a/src-tauri/src/services/plugin/provider/packaged.rs b/src-tauri/src/services/plugin/provider/packaged.rs new file mode 100644 index 0000000..fca27cd --- /dev/null +++ b/src-tauri/src/services/plugin/provider/packaged.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; + +use crate::errors::AppError; +use crate::services::plugin::loader::PluginLoader; +use crate::services::plugin::provider::ManifestProvider; +use plugin_sdk::manifest::{PluginManifest, PluginSource}; + +pub struct PackagedManifestProvider { + search_dirs: Vec, +} + +impl PackagedManifestProvider { + pub fn new(search_dirs: Vec) -> Self { + Self { search_dirs } + } +} + +impl ManifestProvider for PackagedManifestProvider { + fn source(&self) -> PluginSource { + PluginSource::Packaged + } + + fn load(&self) -> Result, AppError> { + let mut manifests = Vec::new(); + + for dir in &self.search_dirs { + if !dir.exists() { + continue; + } + + let entries = std::fs::read_dir(dir).map_err(|e| { + AppError::Io(format!("Failed to read '{}': {}", dir.display(), e)) + })?; + + for entry in entries { + let entry = entry.map_err(|e| AppError::Io(format!("Failed to read entry: {}", e)))?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let manifest_path = path.join("plugin.json"); + if !manifest_path.exists() { + continue; + } + + let content = std::fs::read_to_string(&manifest_path).map_err(|e| { + AppError::Io(format!("Failed to read '{}': {}", manifest_path.display(), e)) + })?; + + let mut manifest: PluginManifest = serde_json::from_str(&content).map_err(|e| { + AppError::Plugin(format!("Failed to parse '{}': {}", manifest_path.display(), e)) + })?; + + PluginLoader::validate_manifest(&manifest)?; + + manifest.source = PluginSource::Packaged; + manifests.push(manifest); + } + } + + Ok(manifests) + } +} diff --git a/src-tauri/src/services/plugin/registry/command_registry.rs b/src-tauri/src/services/plugin/registry/command_registry.rs new file mode 100644 index 0000000..b05d710 --- /dev/null +++ b/src-tauri/src/services/plugin/registry/command_registry.rs @@ -0,0 +1,38 @@ +use std::collections::HashMap; +use std::sync::RwLock; + +/// Maps command ids to their owning plugin ids. +/// +/// Populated during factory registration, consulted during +/// `execute_plugin_command` dispatch. +pub struct CommandRegistry { + commands: RwLock>, +} + +impl CommandRegistry { + pub fn new() -> Self { + Self { + commands: RwLock::new(HashMap::new()), + } + } + + pub fn register_command(&self, command_id: String, plugin_id: String) { + if let Ok(mut lock) = self.commands.write() { + lock.insert(command_id, plugin_id); + } + } + + pub fn find_plugin(&self, command_id: &str) -> Option { + self.commands.read().ok()?.get(command_id).cloned() + } + + pub fn unregister_plugin(&self, plugin_id: &str) { + if let Ok(mut lock) = self.commands.write() { + lock.retain(|_, v| v != plugin_id); + } + } + + pub fn count(&self) -> usize { + self.commands.read().ok().map(|l| l.len()).unwrap_or(0) + } +} diff --git a/src-tauri/src/services/plugin/registry/factory_registry.rs b/src-tauri/src/services/plugin/registry/factory_registry.rs new file mode 100644 index 0000000..365c96a --- /dev/null +++ b/src-tauri/src/services/plugin/registry/factory_registry.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use plugin_sdk::traits::PluginFactory; + +/// Maps plugin id → `PluginFactory`. **Only** owns factories — never +/// manifests (those live in [`ManifestRegistry`]) and never live +/// instances (those live in `PluginManager`). +/// +/// Keeping factory registration separate from manifest aggregation +/// avoids the half-loaded state where a manifest exists but its factory +/// was never registered (or vice versa): `PluginManager` is the only +/// component that links the two, by id. +/// +/// [`ManifestRegistry`]: super::ManifestRegistry +pub struct FactoryRegistry { + factories: RwLock>>, +} + +impl FactoryRegistry { + pub fn new() -> Self { + Self { + factories: RwLock::new(HashMap::new()), + } + } + + pub fn register(&self, id: &str, factory: Arc) { + if let Ok(mut lock) = self.factories.write() { + lock.insert(id.to_string(), factory); + } + } + + pub fn get(&self, id: &str) -> Option> { + self.factories.read().ok()?.get(id).cloned() + } + + pub fn contains(&self, id: &str) -> bool { + self.factories + .read() + .map(|l| l.contains_key(id)) + .unwrap_or(false) + } + + pub fn list_ids(&self) -> Vec { + self.factories + .read() + .map(|l| l.keys().cloned().collect()) + .unwrap_or_default() + } +} diff --git a/src-tauri/src/services/plugin/registry/manifest_registry.rs b/src-tauri/src/services/plugin/registry/manifest_registry.rs new file mode 100644 index 0000000..b44b5b0 --- /dev/null +++ b/src-tauri/src/services/plugin/registry/manifest_registry.rs @@ -0,0 +1,179 @@ +use std::collections::HashMap; +use std::sync::RwLock; + +use crate::errors::AppError; +use crate::services::plugin::provider::ManifestProvider; +use plugin_sdk::manifest::{ + PluginManifest, PluginSource, ResolvedPluginManifest, +}; + +/// Prefix prepended to every plugin slug to form an absolute route. +const VIEW_PREFIX: &str = "/plugins/view/"; + +/// Collects, normalizes, validates, resolves, and caches plugin +/// manifests. **Only** owns manifests — never factories (those live in +/// [`FactoryRegistry`]) and never live plugin instances (those live in +/// `PluginManager`). +/// +/// Pipeline: collect (providers) → normalize → validate → resolve → cache +/// → expose [`ResolvedPluginManifest`] (the only shape with no `Option`s). +/// +/// [`FactoryRegistry`]: super::FactoryRegistry +pub struct ManifestRegistry { + resolved: RwLock>, + by_route: RwLock>, // route -> id +} + +impl ManifestRegistry { + pub fn new() -> Self { + Self { + resolved: RwLock::new(HashMap::new()), + by_route: RwLock::new(HashMap::new()), + } + } + + /// Run the full collect → normalize → validate → resolve → cache + /// pipeline over every provider. Already-registered ids/routes are + /// overwritten by later providers in the slice order. + pub fn register_from_providers( + &self, + providers: Vec>, + ) -> Result<(), AppError> { + for provider in providers { + let manifests = provider.load()?; + for raw in manifests { + self.register_one(raw)?; + } + } + Ok(()) + } + + /// Normalize, validate, resolve and cache a single raw manifest. + pub fn register_one(&self, raw: PluginManifest) -> Result<(), AppError> { + crate::services::plugin::loader::PluginLoader::validate_manifest(&raw)?; + + let id = raw.id.clone(); + let resolved = resolve(raw)?; + + // Deduplicate: unique id AND unique normalized route. + { + let by_id = self + .resolved + .read() + .map_err(|e| AppError::Plugin(e.to_string()))?; + if let Some(existing) = by_id.get(&id) { + return Err(AppError::Plugin(format!( + "Duplicate plugin id '{}', already registered with route '{}'", + id, existing.route + ))); + } + if by_id + .values() + .any(|m| m.route == resolved.route && m.id != id) + { + return Err(AppError::Plugin(format!( + "Duplicate normalized route '{}' from plugin '{}'", + resolved.route, id + ))); + } + } + + let mut by_id = self + .resolved + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + let mut by_route = self + .by_route + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + by_route.insert(resolved.route.clone(), id.clone()); + by_id.insert(id, resolved); + Ok(()) + } + + pub fn manifest(&self, id: &str) -> Option { + self.resolved.read().ok()?.get(id).cloned() + } + + pub fn manifest_by_route(&self, route: &str) -> Option { + let id = self.by_route.read().ok()?.get(route)?.clone(); + self.manifest(&id) + } + + pub fn all(&self) -> Vec { + self.resolved + .read() + .map(|l| l.values().cloned().collect()) + .unwrap_or_default() + } + + pub fn contains(&self, id: &str) -> bool { + self.resolved + .read() + .map(|l| l.contains_key(id)) + .unwrap_or(false) + } + + /// All manifests whose provenance is builtin. Used by `PluginManager` + /// to wire builtin manifests to their factories. + pub fn by_source(&self, source: PluginSource) -> Vec { + self.resolved + .read() + .map(|l| l.values().filter(|m| m.source == source).cloned().collect()) + .unwrap_or_default() + } +} + +/// Validate a slug segment and turn it into an absolute route. +/// +/// The slug must be a single path segment (no `/`, `..`, `?`) and may +/// only contain `[a-zA-Z0-9_-]`. The result is always `VIEW_PREFIX + slug`. +fn normalize_route(id: &str, override_route: Option<&str>) -> Result { + let slug = override_route.unwrap_or(id); + validate_slug(slug)?; + Ok(format!("{}{}", VIEW_PREFIX, slug)) +} + +fn validate_slug(slug: &str) -> Result<(), AppError> { + if slug.is_empty() { + return Err(AppError::Plugin("Plugin route slug cannot be empty".into())); + } + if slug.starts_with('/') || slug.contains("..") || slug.contains('?') { + return Err(AppError::Plugin(format!( + "Plugin route slug '{}' must not start with '/', contain '..', or include '?'", + slug + ))); + } + if !slug + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin route slug '{}' may only contain [a-zA-Z0-9_-]", + slug + ))); + } + Ok(()) +} + +/// Promote a raw (loader-internal) manifest to a fully resolved one. +fn resolve(raw: PluginManifest) -> Result { + let route = normalize_route(&raw.id, raw.route.as_deref())?; + Ok(ResolvedPluginManifest { + id: raw.id, + source: raw.source, + route, + name: raw.name, + display_name: raw.display_name, + version: raw.version, + author: raw.author, + description: raw.description, + entry: raw.entry, + min_app_version: raw.min_app_version, + permissions: raw.permissions, + activation_events: raw.activation_events, + contributes: raw.contributes, + settings: raw.settings, + }) +} diff --git a/src-tauri/src/services/plugin/registry/mod.rs b/src-tauri/src/services/plugin/registry/mod.rs new file mode 100644 index 0000000..93269e9 --- /dev/null +++ b/src-tauri/src/services/plugin/registry/mod.rs @@ -0,0 +1,7 @@ +pub mod command_registry; +pub mod factory_registry; +pub mod manifest_registry; + +pub use command_registry::CommandRegistry; +pub use factory_registry::FactoryRegistry; +pub use manifest_registry::ManifestRegistry; diff --git a/src-tauri/src/services/plugin/runtime/context_factory.rs b/src-tauri/src/services/plugin/runtime/context_factory.rs new file mode 100644 index 0000000..c19a5bb --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/context_factory.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use tauri::AppHandle; + +use plugin_sdk::context::PluginContext; +use crate::services::playback_service::PlaybackService; +use crate::services::plugin::adapters::{ + AudioAdapter, LibraryAdapter, PlayerAdapter, PluginSettingsAdapter, QueueAdapter, SettingsAdapter, +}; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::services::plugin::sdk_adapters::SdkHostContext; +use crate::services::plugin::settings::settings_registry::SettingsRegistry; +use crate::services::settings_service::SettingsService; +use crate::services::track_service::TrackService; + +pub struct PluginContextFactory { + app_handle: AppHandle, + checker: Arc, + playback_service: Arc, + track_service: Arc, + settings_service: Arc, + settings_registry: Arc, +} + +impl PluginContextFactory { + pub fn new( + app_handle: AppHandle, + checker: Arc, + playback_service: Arc, + track_service: Arc, + settings_service: Arc, + settings_registry: Arc, + ) -> Self { + Self { + app_handle, + checker, + playback_service, + track_service, + settings_service, + settings_registry, + } + } + + pub fn create_context(&self, plugin_id: &str) -> PluginContext { + let host = Arc::new(SdkHostContext::new( + plugin_id.to_string(), + self.app_handle.clone(), + )); + let player = Arc::new(PlayerAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + Arc::clone(&self.playback_service), + )); + let library = Arc::new(LibraryAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + Arc::clone(&self.track_service), + )); + let queue = Arc::new(QueueAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + )); + let settings = Arc::new(SettingsAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + Arc::clone(&self.settings_service), + )); + let plugin_settings = Arc::new(PluginSettingsAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.settings_registry), + )); + let audio = Arc::new(AudioAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + )); + + PluginContext::new(host, player, library, queue, settings, plugin_settings, audio) + } +} diff --git a/src-tauri/src/services/plugin/runtime/mod.rs b/src-tauri/src/services/plugin/runtime/mod.rs new file mode 100644 index 0000000..c413d12 --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/mod.rs @@ -0,0 +1,7 @@ +pub mod context_factory; +pub mod runtime_instance; +pub mod state; + +pub use context_factory::PluginContextFactory; +pub use runtime_instance::RuntimePluginInstance; +pub use state::{PluginLifecycleState, PluginState}; diff --git a/src-tauri/src/services/plugin/runtime/runtime_instance.rs b/src-tauri/src/services/plugin/runtime/runtime_instance.rs new file mode 100644 index 0000000..ad5d99a --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/runtime_instance.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use plugin_sdk::traits::Plugin; + +pub enum RuntimePluginInstance { + Registered, + Activating, + Active(Arc), + Deactivating(Arc), + Disabled, +} + +impl RuntimePluginInstance { + pub fn plugin(&self) -> Option<&Arc> { + match self { + Self::Active(p) | Self::Deactivating(p) => Some(p), + _ => None, + } + } + + pub fn is_active(&self) -> bool { + matches!(self, Self::Active(_)) + } +} diff --git a/src-tauri/src/services/plugin/runtime/state.rs b/src-tauri/src/services/plugin/runtime/state.rs new file mode 100644 index 0000000..d47b292 --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/state.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PluginLifecycleState { + Enabled, + Disabled, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginState { + pub plugin_id: String, + pub state: PluginLifecycleState, +} diff --git a/src-tauri/src/services/plugin/sdk_adapters.rs b/src-tauri/src/services/plugin/sdk_adapters.rs new file mode 100644 index 0000000..6586053 --- /dev/null +++ b/src-tauri/src/services/plugin/sdk_adapters.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use plugin_sdk::context::HostContext; +use plugin_sdk::errors::PluginError; +use plugin_sdk::events::LyricsLoadedEvent; + +use crate::events::{EventBus, LyricLinePayload, LyricsLoadedPayload}; +use tauri::{AppHandle, Emitter, Manager}; + +pub struct SdkHostContext { + plugin_id: String, + app_handle: AppHandle, +} + +impl SdkHostContext { + pub fn new(plugin_id: String, app_handle: AppHandle) -> Self { + Self { plugin_id, app_handle } + } +} + +impl HostContext for SdkHostContext { + fn plugin_id(&self) -> &str { + &self.plugin_id + } + + fn cache_dir(&self) -> Result { + let app_data = self + .app_handle + .path() + .app_data_dir() + .map_err(|e| PluginError::Io(e.to_string()))?; + Ok(app_data.join("plugins").join(&self.plugin_id).join("cache")) + } + + fn emit_lyrics_loaded(&self, event: LyricsLoadedEvent) -> Result<(), PluginError> { + let payload = LyricsLoadedPayload { + song_id: event.song_id, + lines: event + .timestamp_ms_list + .into_iter() + .zip(event.text_list.into_iter()) + .map(|(ts, text)| LyricLinePayload { + timestamp_ms: ts, + text, + }) + .collect(), + }; + EventBus::emit_lyrics_loaded(&self.app_handle, payload) + .map_err(|e| PluginError::Plugin(e.to_string())) + } +} diff --git a/src-tauri/src/services/plugin/settings/mod.rs b/src-tauri/src/services/plugin/settings/mod.rs new file mode 100644 index 0000000..e408d17 --- /dev/null +++ b/src-tauri/src/services/plugin/settings/mod.rs @@ -0,0 +1,5 @@ +pub mod persistence; +pub mod settings_registry; + +pub use persistence::{JsonPluginStorage, PluginStorage}; +pub use settings_registry::SettingsRegistry; diff --git a/src-tauri/src/services/plugin/settings/persistence.rs b/src-tauri/src/services/plugin/settings/persistence.rs new file mode 100644 index 0000000..674382a --- /dev/null +++ b/src-tauri/src/services/plugin/settings/persistence.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::errors::AppError; +use crate::services::plugin::runtime::state::PluginLifecycleState; +use plugin_sdk::settings::PluginSettings; + +pub trait PluginStorage: Send + Sync { + fn load_config( + &self, + ) -> Result< + ( + HashMap, + HashMap, + ), + AppError, + >; + fn save_config( + &self, + states: &HashMap, + settings: &HashMap, + ) -> Result<(), AppError>; +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +struct PluginConfigFile { + states: HashMap, + settings: HashMap, +} + +pub struct JsonPluginStorage { + path: PathBuf, +} + +impl JsonPluginStorage { + pub fn new(app_data_dir: PathBuf) -> Self { + let dir = app_data_dir.join("plugins"); + Self { + path: dir.join("plugins.config.json"), + } + } +} + +impl PluginStorage for JsonPluginStorage { + fn load_config( + &self, + ) -> Result< + ( + HashMap, + HashMap, + ), + AppError, + > { + if !self.path.exists() { + return Ok((HashMap::new(), HashMap::new())); + } + + let content = std::fs::read_to_string(&self.path) + .map_err(|e| AppError::Io(format!("Failed to read plugin config: {}", e)))?; + + let file: PluginConfigFile = serde_json::from_str(&content) + .map_err(|e| AppError::Plugin(format!("Failed to parse plugin config: {}", e)))?; + + Ok((file.states, file.settings)) + } + + fn save_config( + &self, + states: &HashMap, + settings: &HashMap, + ) -> Result<(), AppError> { + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + AppError::Io(format!("Failed to create plugin config directory: {}", e)) + })?; + } + + let file = PluginConfigFile { + states: states.clone(), + settings: settings.clone(), + }; + + let content = serde_json::to_string_pretty(&file) + .map_err(|e| AppError::Plugin(format!("Failed to serialize plugin config: {}", e)))?; + + std::fs::write(&self.path, content) + .map_err(|e| AppError::Io(format!("Failed to write plugin config: {}", e)))?; + + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/settings/settings_registry.rs b/src-tauri/src/services/plugin/settings/settings_registry.rs new file mode 100644 index 0000000..f9b580b --- /dev/null +++ b/src-tauri/src/services/plugin/settings/settings_registry.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::errors::AppError; +use plugin_sdk::settings::{PluginSettings, SettingValue}; + +pub struct SettingsRegistry { + settings: RwLock>, +} + +impl SettingsRegistry { + pub fn new() -> Self { + Self { + settings: RwLock::new(HashMap::new()), + } + } + + pub fn register_defaults(&self, plugin_id: &str, defaults: HashMap) { + let mut lock = match self.settings.write() { + Ok(guard) => guard, + Err(_) => return, + }; + + let entry = lock + .entry(plugin_id.to_string()) + .or_insert_with(|| PluginSettings { + plugin_id: plugin_id.to_string(), + settings: HashMap::new(), + }); + + for (key, value) in defaults { + entry.settings.entry(key).or_insert(value); + } + } + + pub fn get_setting(&self, plugin_id: &str, key: &str) -> Option { + let lock = self.settings.read().ok()?; + lock.get(plugin_id)?.settings.get(key).cloned() + } + + pub fn get_all(&self, plugin_id: &str) -> HashMap { + let lock = match self.settings.read() { + Ok(guard) => guard, + Err(_) => return HashMap::new(), + }; + lock.get(plugin_id) + .map(|ps| ps.settings.clone()) + .unwrap_or_default() + } + + pub fn update_setting( + &self, + plugin_id: &str, + key: &str, + value: SettingValue, + ) -> Result<(), AppError> { + let mut lock = self + .settings + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + let entry = lock + .entry(plugin_id.to_string()) + .or_insert_with(|| PluginSettings { + plugin_id: plugin_id.to_string(), + settings: HashMap::new(), + }); + + entry.settings.insert(key.to_string(), value); + Ok(()) + } + + pub fn dump_all(&self) -> HashMap { + let lock = match self.settings.read() { + Ok(guard) => guard, + Err(_) => return HashMap::new(), + }; + lock.clone() + } + + pub fn load_all(&self, data: HashMap) { + let mut lock = match self.settings.write() { + Ok(guard) => guard, + Err(_) => return, + }; + *lock = data; + } +} diff --git a/src-tauri/src/services/recent_service.rs b/src-tauri/src/services/recent_service.rs new file mode 100644 index 0000000..d57914b --- /dev/null +++ b/src-tauri/src/services/recent_service.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::models::RecentPlayedWithTrack; +use crate::repositories::sqlite::recent_repository::SqliteRecentRepository; + +const MAX_RECENT_RECORDS: i64 = 100; + +#[derive(Clone)] +pub struct RecentService { + recent: Arc, +} + +impl RecentService { + pub fn new(recent: Arc) -> Self { + Self { recent } + } + + pub async fn load( + &self, + limit: i64, + offset: i64, + ) -> Result, AppError> { + self.recent.list_with_tracks(limit, offset).await + } + + pub async fn add(&self, track_id: i64, played_at: String) -> Result<(), AppError> { + self.recent.upsert(track_id, played_at).await?; + + let count = self.recent.count().await?; + + if count > MAX_RECENT_RECORDS { + self.recent.remove_oldest(MAX_RECENT_RECORDS).await?; + } + + Ok(()) + } + + pub async fn clear(&self) -> Result<(), AppError> { + self.recent.clear().await + } +} diff --git a/src-tauri/src/services/settings_service.rs b/src-tauri/src/services/settings_service.rs new file mode 100644 index 0000000..d2ca0b0 --- /dev/null +++ b/src-tauri/src/services/settings_service.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::errors::AppError; +use crate::models::{AppSettings, PluginLogLevel, SettingRow, ThemeMode}; +use crate::repositories::sqlite::settings_repository::SqliteSettingsRepository; + +#[derive(Clone)] +pub struct SettingsService { + settings: Arc, +} + +impl SettingsService { + pub fn new(settings: Arc) -> Self { + Self { settings } + } + + pub async fn get_settings(&self) -> Result { + if let Some(row) = self.settings.get().await? { + return row_to_settings(row); + } + + let settings = AppSettings::default(); + self.update_settings(settings.clone()).await?; + Ok(settings) + } + + pub async fn update_settings(&self, settings: AppSettings) -> Result { + let row = settings_to_row(settings.clone())?; + self.settings.update(row).await?; + Ok(settings) + } +} + +fn row_to_settings(row: SettingRow) -> Result { + Ok(AppSettings { + theme: ThemeMode::try_from(row.theme).map_err(AppError::Service)?, + volume: row.volume.clamp(0, 100) as u8, + scan_on_startup: row.scan_on_startup != 0, + reduce_motion: row.reduce_motion != 0, + library_dirs: serde_json::from_str(&row.library_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + use_album_artist_grouping: row.use_album_artist_grouping != 0, + plugin_dirs: serde_json::from_str(&row.plugin_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + plugin_dev_mode: row.plugin_dev_mode != 0, + plugin_scan_on_startup: row.plugin_scan_on_startup != 0, + plugin_log_level: PluginLogLevel::try_from(row.plugin_log_level) + .map_err(AppError::Service)?, + }) +} + +fn settings_to_row(settings: AppSettings) -> Result { + Ok(SettingRow { + id: 1, + theme: settings.theme.as_str().to_string(), + volume: settings.volume as i64, + scan_on_startup: i64::from(settings.scan_on_startup), + reduce_motion: i64::from(settings.reduce_motion), + library_dirs: serde_json::to_string(&settings.library_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + use_album_artist_grouping: i64::from(settings.use_album_artist_grouping), + plugin_dirs: serde_json::to_string(&settings.plugin_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + plugin_dev_mode: i64::from(settings.plugin_dev_mode), + plugin_scan_on_startup: i64::from(settings.plugin_scan_on_startup), + plugin_log_level: settings.plugin_log_level.as_str().to_string(), + created_at: now_string(), + updated_at: now_string(), + }) +} + +fn now_string() -> String { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|value| value.as_secs().to_string()) + .unwrap_or_else(|_| "0".to_string()) +} diff --git a/src-tauri/src/services/track_service.rs b/src-tauri/src/services/track_service.rs new file mode 100644 index 0000000..997ae9f --- /dev/null +++ b/src-tauri/src/services/track_service.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::models::{NewTrack, Track, TrackSearchQuery, UpdateTrack}; +use crate::repositories::sqlite::track_repository::SqliteTrackRepository; + +#[derive(Clone)] +pub struct TrackService { + tracks: Arc, +} + +impl TrackService { + pub fn new(tracks: Arc) -> Self { + Self { tracks } + } + + pub async fn create_track(&self, track: NewTrack) -> Result { + self.tracks.create(track).await + } + + pub async fn upsert_track(&self, track: NewTrack) -> Result { + self.tracks.upsert_by_path(track).await + } + + pub async fn upsert_tracks(&self, tracks: Vec) -> Result, AppError> { + let mut saved = Vec::with_capacity(tracks.len()); + for track in tracks { + saved.push(self.tracks.upsert_by_path(track).await?); + } + Ok(saved) + } + + pub async fn update_track(&self, track: UpdateTrack) -> Result { + self.tracks.update(track).await + } + + pub async fn delete_track(&self, id: i64) -> Result<(), AppError> { + self.tracks.delete(id).await + } + + pub async fn delete_track_by_path(&self, path: &str) -> Result<(), AppError> { + self.tracks.delete_by_path(path).await + } + + pub async fn get_track(&self, id: i64) -> Result, AppError> { + self.tracks.find_by_id(id).await + } + + pub async fn get_track_by_path(&self, path: &str) -> Result, AppError> { + self.tracks.find_by_path(path).await + } + + pub async fn list_tracks(&self) -> Result, AppError> { + self.tracks.list_all().await + } + + pub async fn search_tracks(&self, query: TrackSearchQuery) -> Result, AppError> { + self.tracks.search(query).await + } + + pub async fn mark_track_played(&self, id: i64, played_at: String) -> Result<(), AppError> { + self.tracks.increment_play_count(id, played_at).await + } +} diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs new file mode 100644 index 0000000..341b7cd --- /dev/null +++ b/src-tauri/src/state/app_state.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use crate::services::plugin::PluginManager; +use crate::services::{PlaylistService, RecentService, SettingsService, TrackService}; + +#[derive(Clone)] +pub struct AppState { + pub tracks: Arc, + pub playlists: Arc, + pub settings: Arc, + pub recent: Arc, + pub plugins: Arc, +} + +impl AppState { + pub fn new( + tracks: Arc, + playlists: Arc, + settings: Arc, + recent: Arc, + plugins: Arc, + ) -> Self { + Self { + tracks, + playlists, + settings, + recent, + plugins, + } + } +} diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs new file mode 100644 index 0000000..21772f9 --- /dev/null +++ b/src-tauri/src/state/mod.rs @@ -0,0 +1,4 @@ +pub mod app_state; +pub mod playback_state; + +pub use app_state::AppState; diff --git a/src-tauri/src/state/playback_state.rs b/src-tauri/src/state/playback_state.rs new file mode 100644 index 0000000..7dbcdcd --- /dev/null +++ b/src-tauri/src/state/playback_state.rs @@ -0,0 +1,90 @@ +pub use crate::audio::lock_audio_state; +use crate::audio::AudioState; +use crate::errors::AppError; +use crate::models::playback::{NativeTrackMetadata, PlaybackStatusSnapshot}; +use crate::models::Track; + +pub fn with_audio_state(f: impl FnOnce(&mut AudioState) -> T) -> Result { + let value = { + let mut state = lock_audio_state()?; + f(&mut state) + }; + + Ok(value) +} + +pub fn current_playback_state(state: &mut AudioState) -> PlaybackStatusSnapshot { + match state.with_engine(|engine| (!engine.paused(), engine.current_time())) { + Ok((playing, current_time)) => { + state.playing = playing; + + PlaybackStatusSnapshot { + playing, + current_time, + } + } + + Err(_) => { + state.playing = false; + + PlaybackStatusSnapshot { + playing: false, + current_time: 0.0, + } + } + } +} + +pub fn current_playback_snapshot() -> Result { + with_audio_state(current_playback_state) +} + +pub fn current_time_from_state() -> f64 { + current_playback_snapshot() + .map(|state| state.current_time) + .unwrap_or(0.0) +} + +pub fn current_track_duration(state: &AudioState) -> Option { + let index = state.playback_queue.current_index?; + state + .playback_queue + .tracks + .get(index) + .map(|track| track.duration as f64 / 1000.0) +} + +pub fn should_advance_track() -> bool { + let Ok((snapshot, duration)) = with_audio_state(|state| { + let snapshot = current_playback_state(state); + let duration = current_track_duration(state); + (snapshot, duration) + }) else { + return false; + }; + + snapshot.playing + && duration + .map(|duration| duration > 0.0 && snapshot.current_time >= duration - 0.5) + .unwrap_or(false) +} + +pub fn metadata_from_track(track: &Track) -> NativeTrackMetadata { + NativeTrackMetadata { + title: track.title.clone(), + album: track + .album + .clone() + .unwrap_or_else(|| "未知专辑".to_string()), + artist: track + .artist + .clone() + .unwrap_or_else(|| "未知歌手".to_string()), + duration: Some(track.duration as f64 / 1000.0), + } +} + +pub fn sanitize_track(mut track: Track) -> Track { + track.cover = None; + track +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f3fcfe9..aaf6d3f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "tauri-app", + "productName": "Rust Echo Music", "version": "0.1.0", "identifier": "com.rustechomusic.app", "build": { @@ -12,19 +12,19 @@ "app": { "windows": [ { - "title": "tauri-app", + "title": "Rust Echo Music", "decorations": false, "width": 1200, "height": 900 } ], "security": { - "csp": null + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost; frame-src 'self' plugin:" } }, "bundle": { "active": true, - "targets": "all", + "targets": ["nsis", "msi", "dmg", "appimage"], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -33,7 +33,15 @@ "icons/icon.ico" ], "resources": [ - "../static/**/*" - ] + "../static/*" + ], + "windows": { + "wix": { + "language": "zh-CN" + }, + "nsis": { + "installMode": "both" + } + } } -} \ No newline at end of file +} diff --git a/src/app.css b/src/app.css index 348e34e..cfd3d65 100644 --- a/src/app.css +++ b/src/app.css @@ -3,404 +3,413 @@ @import "tailwindcss/theme.css" layer(theme); /* @import "tailwindcss/preflight.css" layer(base); */ @import "tailwindcss/utilities.css" layer(utilities); -@import 'material-symbols'; - -* { - transition: all 0.2s ease; -} - -/* - 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) - 2. Remove default margins and padding - 3. Reset all borders. -*/ - -/* *, */ -html, -body, -::after, -::before, -::backdrop, -::file-selector-button { - box-sizing: border-box; /* 1 */ - margin: 0; /* 2 */ - padding: 0; /* 2 */ - border: 0 solid; /* 3 */ -} - -/* - 1. Use a consistent sensible line-height in all browsers. - 2. Prevent adjustments of font size after orientation changes in iOS. - 3. Use a more readable tab size. - 4. Use the user's configured `sans` font-family by default. - 5. Use the user's configured `sans` font-feature-settings by default. - 6. Use the user's configured `sans` font-variation-settings by default. - 7. Disable tap highlights on iOS. -*/ - -html, -:host { - line-height: 1.5; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - tab-size: 4; /* 3 */ - font-family: --theme( - --default-font-family, - ui-sans-serif, - system-ui, - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - 'Noto Color Emoji' - ); /* 4 */ - font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ - font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ - -webkit-tap-highlight-color: transparent; /* 7 */ -} - -/* - 1. Add the correct height in Firefox. - 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) - 3. Reset the default border style to a 1px solid border. -*/ - -hr { - height: 0; /* 1 */ - color: inherit; /* 2 */ - border-top-width: 1px; /* 3 */ -} - -/* - Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* - Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* - Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; -} - -/* - Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* - 1. Use the user's configured `mono` font-family by default. - 2. Use the user's configured `mono` font-feature-settings by default. - 3. Use the user's configured `mono` font-variation-settings by default. - 4. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: --theme( - --default-mono-font-family, - ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - 'Liberation Mono', - 'Courier New', - monospace - ); /* 1 */ - font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ - font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ - font-size: 1em; /* 4 */ -} - -/* - Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* - Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* - 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) - 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) - 3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; /* 1 */ - border-color: inherit; /* 2 */ - border-collapse: collapse; /* 3 */ -} - -/* - Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* - Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* - Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* - Make lists unstyled by default. -*/ - -ol, -ul, -menu { - list-style: none; -} - -/* - 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) - 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; /* 1 */ - vertical-align: middle; /* 2 */ -} - -/* - Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* - 1. Inherit font styles in all browsers. - 2. Remove border radius in all browsers. - 3. Remove background color in all browsers. - 4. Ensure consistent opacity for disabled states in all browsers. -*/ - -button, -input, -select, -optgroup, -textarea, -::file-selector-button { - font: inherit; /* 1 */ - font-feature-settings: inherit; /* 1 */ - font-variation-settings: inherit; /* 1 */ - letter-spacing: inherit; /* 1 */ - color: inherit; /* 1 */ - border-radius: 0; /* 2 */ - background-color: transparent; /* 3 */ - opacity: 1; /* 4 */ -} - -/* - Restore default font weight. -*/ - -:where(select:is([multiple], [size])) optgroup { - font-weight: bolder; -} - -/* - Restore indentation. -*/ - -:where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; -} - -/* - Restore space after button. -*/ - -::file-selector-button { - margin-inline-end: 4px; -} - -/* - Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -*/ - -::placeholder { - opacity: 1; -} - -/* - Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not - crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) -*/ - -@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or - (contain-intrinsic-size: 1px) /* Safari 17+ */ { +@import "./fonts.css" layer(theme); + +@layer base { + + * { + transition: all 0.2s ease; + } + + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. + */ + + /* *, */ + html, + body, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ + } + + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + 5. Use the user's configured `sans` font-feature-settings by default. + 6. Use the user's configured `sans` font-variation-settings by default. + 7. Disable tap highlights on iOS. + */ + + html, + :host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: --theme( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ); /* 4 */ + font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ + background-color: theme(--color-gray-100); + } + + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. + */ + + hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. + */ + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + /* + Remove the default font size and weight for headings. + */ + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + p { + margin: 0; + padding: 0; + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. + */ + + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + /* + Add the correct font weight in Edge and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /* + 1. Use the user's configured `mono` font-family by default. + 2. Use the user's configured `mono` font-feature-settings by default. + 3. Use the user's configured `mono` font-variation-settings by default. + 4. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp, + pre { + font-family: --theme( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); /* 1 */ + font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ + font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ + font-size: 1em; /* 4 */ + } + + /* + Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. + */ + + table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ + } + + /* + Use the modern Firefox focus style for all focusable elements. + */ + + :-moz-focusring { + outline: auto; + } + + /* + Add the correct vertical alignment in Chrome and Firefox. + */ + + progress { + vertical-align: baseline; + } + + /* + Add the correct display in Chrome and Safari. + */ + + summary { + display: list-item; + } + + /* + Make lists unstyled by default. + */ + + ol, + ul, + menu { + list-style: none; + } + + /* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. + */ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) + */ + + img, + video { + max-width: 100%; + height: auto; + } + + /* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. + */ + + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ + } + + /* + Restore default font weight. + */ + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + /* + Restore indentation. + */ + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + /* + Restore space after button. + */ + + ::file-selector-button { + margin-inline-end: 4px; + } + + /* + Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + */ + ::placeholder { - color: color-mix(in oklab, currentcolor 50%, transparent); - } -} - -/* - Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* - Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* - 1. Ensure date/time inputs have the same height when empty in iOS Safari. - 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. -*/ - -::-webkit-date-and-time-value { - min-height: 1lh; /* 1 */ - text-align: inherit; /* 2 */ -} - -/* - Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. -*/ - -::-webkit-datetime-edit { - display: inline-flex; -} - -/* - Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. -*/ - -::-webkit-datetime-edit-fields-wrapper { - padding: 0; -} - -::-webkit-datetime-edit, -::-webkit-datetime-edit-year-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-minute-field, -::-webkit-datetime-edit-second-field, -::-webkit-datetime-edit-millisecond-field, -::-webkit-datetime-edit-meridiem-field { - padding-block: 0; -} - -/* - Center dropdown marker shown on inputs with paired ``s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) -*/ - -::-webkit-calendar-picker-indicator { - line-height: 1; -} - -/* - Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* - Correct the inability to style the border radius in iOS Safari. -*/ - -button, -input:where([type='button'], [type='reset'], [type='submit']), -::file-selector-button { - appearance: button; -} - -/* - Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* - Make elements with the HTML hidden attribute stay hidden by default. -*/ - -[hidden]:where(:not([hidden='until-found'])) { - display: none !important; + opacity: 1; + } + + /* + Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not + crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) + */ + + @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or + (contain-intrinsic-size: 1px) /* Safari 17+ */ { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + + /* + Prevent resizing textareas horizontally by default. + */ + + textarea { + resize: vertical; + } + + /* + Remove the inner padding in Chrome and Safari on macOS. + */ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. + */ + + ::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ + } + + /* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. + */ + + ::-webkit-datetime-edit { + display: inline-flex; + } + + /* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. + */ + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + /* + Center dropdown marker shown on inputs with paired ``s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) + */ + + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + + /* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) + */ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* + Correct the inability to style the border radius in iOS Safari. + */ + + button, + input:where([type='button'], [type='reset'], [type='submit']), + ::file-selector-button { + appearance: button; + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. + */ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* + Make elements with the HTML hidden attribute stay hidden by default. + */ + + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } } \ No newline at end of file diff --git a/src/app.html b/src/app.html index d8b86cf..7a8b585 100644 --- a/src/app.html +++ b/src/app.html @@ -6,36 +6,7 @@ - Tauri + SvelteKit + Typescript App - - - - - - - - - - - - - - + Rust Echo Music %sveltekit.head% diff --git a/src/fonts.css b/src/fonts.css new file mode 100644 index 0000000..45bd03b --- /dev/null +++ b/src/fonts.css @@ -0,0 +1,71 @@ +/* fallback */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(/fonts/Material_Icons.woff2) format('woff2'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} + +/* fallback */ +@font-face { + font-family: 'Material Icons Outlined'; + font-style: normal; + font-weight: 400; + src: url(/fonts/Material_Icons_Outlined.woff2) format('woff2'); +} + +.material-icons-outlined { + font-family: 'Material Icons Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} + +/* fallback */ +@font-face { + font-family: 'Material Icons Round'; + font-style: normal; + font-weight: 400; + src: url(/fonts/Material_Icons_Round.woff2) format('woff2'); +} + +.material-icons-round { + font-family: 'Material Icons Round'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} \ No newline at end of file diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte deleted file mode 100644 index d9319b5..0000000 --- a/src/lib/components/Header.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -
    -
    Header PlaceHolder
    - { - if (e.key === 'Enter' || e.key === ' ') playMusic() - }} - class="px-4 py-2" - role="none" - > - 播放音乐 - -
    diff --git a/src/lib/components/NavRail.svelte b/src/lib/components/NavRail.svelte deleted file mode 100644 index ef952e9..0000000 --- a/src/lib/components/NavRail.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - Recent - Library - Track - Artist - - - diff --git a/src/lib/components/PlayerBar.svelte b/src/lib/components/PlayerBar.svelte deleted file mode 100644 index 1e37292..0000000 --- a/src/lib/components/PlayerBar.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - - -
    -
    {playerState.current?.title}
    -
    {playerState.current?.artist}
    -
    - -
    -
    -
    - {playerState.current?.title ?? '未在播放'} -
    -
    - {playerState.current?.artist ?? '未知艺术家'} -
    -
    - -
    - e.key === 'Enter' && prev()} - > - e.key === 'Enter' && toggle()} - > - - e.key === 'Enter' && next()} - > -
    - -
    - - - -
    -
    -
    diff --git a/src/lib/components/PlaylistCard.svelte b/src/lib/components/PlaylistCard.svelte deleted file mode 100644 index eed8668..0000000 --- a/src/lib/components/PlaylistCard.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
    - -
    - {playlist.name} -
    -
    -

    {playlist.name}

    -

    播放列表

    -
    -
    -
    \ No newline at end of file diff --git a/src/lib/components/Playlists.svelte b/src/lib/components/Playlists.svelte deleted file mode 100644 index 7020867..0000000 --- a/src/lib/components/Playlists.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
    -
    - {#each playlists as playlist} - - {/each} -
    -
    \ No newline at end of file diff --git a/src/lib/components/Progress.svelte b/src/lib/components/Progress.svelte deleted file mode 100644 index 0d01747..0000000 --- a/src/lib/components/Progress.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - -
    -
    -
    -
    - -
    -
    - - \ No newline at end of file diff --git a/src/lib/components/RecentSongs.svelte b/src/lib/components/RecentSongs.svelte deleted file mode 100644 index 1b9b6b2..0000000 --- a/src/lib/components/RecentSongs.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
    -
    - {#each songs as song} - - {/each} -
    -
    \ No newline at end of file diff --git a/src/lib/components/Slider.svelte b/src/lib/components/Slider.svelte deleted file mode 100644 index eebc193..0000000 --- a/src/lib/components/Slider.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/src/lib/components/SongCard.svelte b/src/lib/components/SongCard.svelte deleted file mode 100644 index adbe7c7..0000000 --- a/src/lib/components/SongCard.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - -
    - {song.title} -
    -
    -

    {song.title}

    -

    - {song.artist} - {song.album} -

    -
    -
    - - play_arrow - - - more_vert - -
    -
    \ No newline at end of file diff --git a/src/lib/components/base/Button.svelte b/src/lib/components/base/Button.svelte new file mode 100644 index 0000000..c59c789 --- /dev/null +++ b/src/lib/components/base/Button.svelte @@ -0,0 +1,14 @@ + + + onclick()} + onkeydown={(e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && onclick()} + role="button" + tabindex="0" + {...props} +> + {@render children()} + diff --git a/src/lib/components/base/Heading.svelte b/src/lib/components/base/Heading.svelte new file mode 100644 index 0000000..ac1b195 --- /dev/null +++ b/src/lib/components/base/Heading.svelte @@ -0,0 +1,30 @@ + + +
    +
    +

    + {eyebrow} +

    + +

    + {title} +

    +
    +
    diff --git a/src/lib/components/base/IconButton.svelte b/src/lib/components/base/IconButton.svelte new file mode 100644 index 0000000..bc094df --- /dev/null +++ b/src/lib/components/base/IconButton.svelte @@ -0,0 +1,18 @@ + + + onclick(event)} + onkeydown={(event: KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + onclick(event) + } + }} + role="button" + tabindex="0" + {...props} +> \ No newline at end of file diff --git a/src/lib/components/base/MduiSlider.svelte b/src/lib/components/base/MduiSlider.svelte new file mode 100644 index 0000000..673e1bd --- /dev/null +++ b/src/lib/components/base/MduiSlider.svelte @@ -0,0 +1,109 @@ + + + \ No newline at end of file diff --git a/src/lib/components/base/PlayingIndicator.svelte b/src/lib/components/base/PlayingIndicator.svelte new file mode 100644 index 0000000..b4ef748 --- /dev/null +++ b/src/lib/components/base/PlayingIndicator.svelte @@ -0,0 +1,32 @@ + + +
    + + + +
    + + \ No newline at end of file diff --git a/src/lib/components/base/SearchBar.svelte b/src/lib/components/base/SearchBar.svelte new file mode 100644 index 0000000..3a4e61a --- /dev/null +++ b/src/lib/components/base/SearchBar.svelte @@ -0,0 +1,32 @@ + + +
    e.stopPropagation()} + role="searchbox" + tabindex="0" +> + + + {#if value} +
    value = ''} + onkeydown={e => { if (e.key === 'Enter' || e.key === ' ') value = '' }} + role="button" + tabindex="0" + > + +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/components/base/SearchField.svelte b/src/lib/components/base/SearchField.svelte new file mode 100644 index 0000000..79df8ad --- /dev/null +++ b/src/lib/components/base/SearchField.svelte @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/src/lib/components/base/Select.svelte b/src/lib/components/base/Select.svelte new file mode 100644 index 0000000..7aced8f --- /dev/null +++ b/src/lib/components/base/Select.svelte @@ -0,0 +1,73 @@ + + +
    + (opened = false)} + aria-expanded={opened} + {...props} + > + {#each options as option (option.value)} + + {option.label} + + {/each} + +
    diff --git a/src/lib/components/base/Slider.svelte b/src/lib/components/base/Slider.svelte new file mode 100644 index 0000000..33553c8 --- /dev/null +++ b/src/lib/components/base/Slider.svelte @@ -0,0 +1,338 @@ + + +
    +
    +
    +
    + +
    + +
    + {Math.round(value)} +
    +
    + + diff --git a/src/lib/components/base/TextField.svelte b/src/lib/components/base/TextField.svelte new file mode 100644 index 0000000..a4fe110 --- /dev/null +++ b/src/lib/components/base/TextField.svelte @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/src/lib/components/media/MediaGrid.svelte b/src/lib/components/media/MediaGrid.svelte new file mode 100644 index 0000000..8af3cce --- /dev/null +++ b/src/lib/components/media/MediaGrid.svelte @@ -0,0 +1,227 @@ + + +{#snippet mediaCardContent(item: MediaGridItem, isSelected: boolean)} +
    + {#if item.image} + {item.title} + {:else} +
    + {#if item.shape === 'circle'} + 👤 + {:else} + 🎵 + {/if} +
    + {/if} + + {#if onplay} +
    + handlePlay(item, event)} + onkeydown={(event: KeyboardEvent) => handlePlayKeydown(item, event)} + > +
    + {/if} +
    + +
    +

    + {item.title} +

    + + {#if item.subtitle} +

    + {item.subtitle} +

    + {/if} +
    +{/snippet} + +{#snippet mediaCard(item: MediaGridItem)} + {@const isSelected = selectedId === item.id} + {#if onselect} + handleSelect(item)} + onkeydown={(event: KeyboardEvent) => handleKeydown(item, event)} + role="button" + tabindex="0" + > + {@render mediaCardContent(item, isSelected)} + + {:else} + + {@render mediaCardContent(item, isSelected)} + + {/if} +{/snippet} + +{#if items.length === 0} +
    +
    🎧
    +
    {emptyTitle}
    +
    + {emptyDescription} +
    +
    +{:else} +
    +
    + {#each $virtualizer.getVirtualItems() as row (row.index)} + {@const startIndex = row.index * columns} + {@const rowItems = items.slice(startIndex, startIndex + columns)} + +
    + {#each rowItems as item (item.id)} + {@render mediaCard(item)} + {/each} +
    + {/each} +
    +
    +{/if} \ No newline at end of file diff --git a/src/lib/features/EqPanel.svelte b/src/lib/features/EqPanel.svelte new file mode 100644 index 0000000..b45b652 --- /dev/null +++ b/src/lib/features/EqPanel.svelte @@ -0,0 +1,62 @@ + + +
    +
    + 均衡器 + + equalizer.setEnabled((e.currentTarget as HTMLInputElement).checked)} + > +
    + +
    \ No newline at end of file diff --git a/src/lib/features/LyricsPanel.svelte b/src/lib/features/LyricsPanel.svelte new file mode 100644 index 0000000..75458f5 --- /dev/null +++ b/src/lib/features/LyricsPanel.svelte @@ -0,0 +1,80 @@ + + +
    +
    + + 歌词 + + (pluginState.activeNativePanel = null)} + /> +
    + +
    + {#if lyrics.currentLyrics && lyrics.currentLyrics.lines.length > 0} +
    + {#each lyrics.currentLyrics.lines as line, i (line.timestampMs + i)} +
    + {line.text || '···'} +
    + {/each} +
    + {:else} +
    + 暂无歌词 +
    + {/if} +
    +
    diff --git a/src/lib/features/PlayerBar.svelte b/src/lib/features/PlayerBar.svelte new file mode 100644 index 0000000..c23c00e --- /dev/null +++ b/src/lib/features/PlayerBar.svelte @@ -0,0 +1,164 @@ + + +{#snippet progressArea()} +
    + + {formatTime(player.currentTime)} + + +
    + +
    + + + {formatTime((player.currentTrack?.duration ?? 0) / 1000)} + +
    +{/snippet} + +{#snippet nowPlayingInfo()} +
    + Album Cover + +
    +
    + {player.currentTrack?.title ?? '未在播放'} +
    + +
    + {player.currentTrack?.artist ?? '未知艺术家'} +
    +
    +
    +{/snippet} + +{#snippet transportControls()} +
    + player.prev()} + /> + + player.toggle()} + /> + + player.next()} /> +
    +{/snippet} + +{#snippet volumeControls()} +
    + player.cyclePlayMode()} + class="opacity-70 hover:opacity-100" + /> + + {#each pluginState.nativeViewExtensions.filter(nv => nv.state === 'Enabled') as nv (nv.pluginId + nv.id)} + toggleNativePanel(nv.pluginId)} + class={pluginState.activeNativePanel === nv.pluginId ? 'text-[rgb(var(--mdui-color-primary))]' : 'opacity-70 hover:opacity-100'} + /> + {/each} + + player.toggleQueue()} + class={player.queueOpen ? 'text-[rgb(var(--mdui-color-primary))]' : 'opacity-70 hover:opacity-100'} + /> + + (player.muted = !player.muted)} + class="text-lg opacity-70 hover:opacity-100" + /> + + +
    +{/snippet} + + + {@render progressArea()} +
    + {@render nowPlayingInfo()} + {@render transportControls()} + {@render volumeControls()} +
    +
    diff --git a/src/lib/features/PlaylistGrid.svelte b/src/lib/features/PlaylistGrid.svelte new file mode 100644 index 0000000..b710cac --- /dev/null +++ b/src/lib/features/PlaylistGrid.svelte @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/src/lib/features/PluginNativeViewHost.svelte b/src/lib/features/PluginNativeViewHost.svelte new file mode 100644 index 0000000..a4ce671 --- /dev/null +++ b/src/lib/features/PluginNativeViewHost.svelte @@ -0,0 +1,34 @@ + + +{#if Comp} + +{:else if loadError} +
    + {loadError} +
    +{:else} +
    + +
    +{/if} diff --git a/src/lib/features/QueueDrawer.svelte b/src/lib/features/QueueDrawer.svelte new file mode 100644 index 0000000..fde4644 --- /dev/null +++ b/src/lib/features/QueueDrawer.svelte @@ -0,0 +1,107 @@ + + + { + player.queueOpen = false + }} + class={[ + 'mt-32', + 'h-[calc(100vh-var(--spacing)*(24+14+20))]', + '[&::part(panel)]:mt-20', + '[&::part(overlay)]:bg-transparent', + ]} +> +
    +
    +
    + + 播放队列 + + + {player.queue.tracks.length} + +
    +
    + + { + player.queueOpen = false + }} + /> +
    +
    + +
    + {#if player.queue.tracks.length === 0} +
    + 🎵 +

    队列为空

    +
    + {:else} + + {/if} +
    +
    +
    + + diff --git a/src/lib/features/settings/ListEditor.svelte b/src/lib/features/settings/ListEditor.svelte new file mode 100644 index 0000000..3c73fd7 --- /dev/null +++ b/src/lib/features/settings/ListEditor.svelte @@ -0,0 +1,61 @@ + + +
    + {#each value as item, i (i)} +
    + { + const target = e.currentTarget as HTMLInputElement + updateItem(i, target.value) + }} + role="textbox" + tabindex="0" + style="flex: 1;" + > + removeItem(i)} + onkeydown={(e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') removeItem(i) + }} + role="button" + tabindex="0" + > +
    + {/each} + { + if (e.key === 'Enter' || e.key === ' ') addItem() + }} + role="button" + tabindex="0">添加 +
    diff --git a/src/lib/features/settings/SettingsListItem.svelte b/src/lib/features/settings/SettingsListItem.svelte new file mode 100644 index 0000000..8497606 --- /dev/null +++ b/src/lib/features/settings/SettingsListItem.svelte @@ -0,0 +1,41 @@ + + +
    +
    +
    + {title} +
    + + {#if description} +
    + {description} +
    + {/if} +
    + + {#if children} +
    + {@render children()} +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/features/settings/SettingsRow.svelte b/src/lib/features/settings/SettingsRow.svelte new file mode 100644 index 0000000..ec9f3ea --- /dev/null +++ b/src/lib/features/settings/SettingsRow.svelte @@ -0,0 +1,37 @@ + + +
    +
    +
    {title}
    + + {#if description} +
    + {description} +
    + {/if} +
    + + {#if children} +
    + {@render children()} +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/features/settings/SettingsSection.svelte b/src/lib/features/settings/SettingsSection.svelte new file mode 100644 index 0000000..003da9a --- /dev/null +++ b/src/lib/features/settings/SettingsSection.svelte @@ -0,0 +1,33 @@ + + +
    +

    + {title} +

    + + {#if children} +
    + {@render children()} +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/components/Appbar.svelte b/src/lib/features/shell/Appbar.svelte similarity index 88% rename from src/lib/components/Appbar.svelte rename to src/lib/features/shell/Appbar.svelte index 6cfcf1f..6c2477d 100644 --- a/src/lib/components/Appbar.svelte +++ b/src/lib/features/shell/Appbar.svelte @@ -1,4 +1,6 @@ + + + { + if (e.key === 'Enter' || e.key === ' ') { + scanDirectory() + } + }} + role="button" + tabindex="0" + > + + {#each navItems as item (item.href)} + + {item.label} + + {/each} + diff --git a/src/lib/features/track-list/TrackContextMenu.svelte b/src/lib/features/track-list/TrackContextMenu.svelte new file mode 100644 index 0000000..02571c9 --- /dev/null +++ b/src/lib/features/track-list/TrackContextMenu.svelte @@ -0,0 +1,231 @@ + + +{#if open && track} + +{/if} + + diff --git a/src/lib/features/track-list/TrackList.svelte b/src/lib/features/track-list/TrackList.svelte new file mode 100644 index 0000000..16ebca8 --- /dev/null +++ b/src/lib/features/track-list/TrackList.svelte @@ -0,0 +1,235 @@ + + + + + + + {#if !hideHeader} + + {#if selectable} +
    + {/if} + + {#if columns.includes("index")} +
    #
    + {/if} + + {#if columns.includes("title")} +
    标题
    + {/if} + + {#if columns.includes("album")} + + {/if} + + {#if columns.includes("duration")} +
    时长
    + {/if} +
    + {/if} + + +
    + +
    + {#each $virtualizer.getVirtualItems() as virtualItem (virtualItem.key)} + {@const track = tracks[virtualItem.index]} +
    + setTrackSelected(track, checked)} + {ondblclicktrack} + {onremovetrack} + onplay={() => playTrack(track, virtualItem.index)} + oncontextmenu={(e) => openTrackContextMenu(e, track, virtualItem.index)} + /> +
    + {/each} +
    +
    +
    + + + + \ No newline at end of file diff --git a/src/lib/features/track-list/TrackListRow.svelte b/src/lib/features/track-list/TrackListRow.svelte new file mode 100644 index 0000000..7a9d1c5 --- /dev/null +++ b/src/lib/features/track-list/TrackListRow.svelte @@ -0,0 +1,308 @@ + + +{#snippet trackIndexCell()} +
    + {#if isCurrent && playing} + + {:else} + + {index + 1} + + {/if} +
    +{/snippet} + +{#snippet trackCoverCell(cover: string | null)} +
    + {#if cover} + cover + {:else} +
    + 🎵 +
    + {/if} + + {#if !columns.includes("index") && isCurrent} +
    + {#if playing} + + {:else} + + {/if} +
    + {/if} +
    +{/snippet} + +{#snippet trackTitleCell(cover: string | null)} +
    + {@render trackCoverCell(cover)} + +
    + + {track.title} + + + + {track.artist ?? "未知歌手"} + +
    +
    +{/snippet} + +{#snippet trackAlbumCell()} + +{/snippet} + +{#snippet trackDurationCell()} +
    +
    + {formatDuration(track.duration / 1000)} +
    + {#if onremovetrack} + { + e.stopPropagation(); + onremovetrack?.(track, index); + }} + /> + {/if} +
    +{/snippet} + + + + +
    + {#if selectable} +
    + +
    + {/if} + + {#if columns.includes("index")} + {@render trackIndexCell()} + {/if} + + {#if columns.includes("title")} + {@render trackTitleCell(cover)} + {/if} + + {#if columns.includes("album")} + {@render trackAlbumCell()} + {/if} + + {#if columns.includes("duration")} + {@render trackDurationCell()} + {/if} +
    +
    + + diff --git a/src/lib/player.svelte.ts b/src/lib/player.svelte.ts deleted file mode 100644 index 20ebdda..0000000 --- a/src/lib/player.svelte.ts +++ /dev/null @@ -1,19 +0,0 @@ -type PlayerState = { - current: Track | null - isPlaying: boolean - playlist: Track[] - progress: number -} - -type Track = { - title: string - artist: string - path: string -} - -export const playerState = $state({ - current: null, - isPlaying: false, - playlist: [], - progress: 0, -}) diff --git a/src/lib/plugins/component-registry.ts b/src/lib/plugins/component-registry.ts new file mode 100644 index 0000000..6b92800 --- /dev/null +++ b/src/lib/plugins/component-registry.ts @@ -0,0 +1,18 @@ +import type { Component } from 'svelte' + +type ComponentLoader = () => Promise<{ default: Component }> + +const registry = new Map() + +export function registerNativeView(token: string, loader: ComponentLoader): void { + if (registry.has(token)) { + console.warn(`[component-registry] token "${token}" already registered, overriding`) + } + registry.set(token, loader) +} + +export function resolveNativeView(token: string): Promise<{ default: Component } | null> { + const loader = registry.get(token) + if (!loader) return Promise.resolve(null) + return loader() +} diff --git a/src/lib/plugins/host-bridge.ts b/src/lib/plugins/host-bridge.ts new file mode 100644 index 0000000..611a752 --- /dev/null +++ b/src/lib/plugins/host-bridge.ts @@ -0,0 +1,311 @@ +/** + * Plugin Host Bridge + * + * 宿主端 postMessage 桥:负责将沙箱 iframe 内的插件 SDK 请求路由到对应的 + * Tauri 命令/前端状态,并把宿主事件广播给所有已挂载的插件 iframe。 + * + * 协议信封与 `static/plugin-sdk/plugin-host-sdk.js` 完全对齐: + * { source: 'rem-plugin-host' | 'rem-plugin', type: string, id?: string, payload?: unknown } + */ + +import { invoke } from '@tauri-apps/api/core' +import { listen, type UnlistenFn } from '@tauri-apps/api/event' +import { pluginState } from '$lib/state/plugins.svelte' +import { player } from '$lib/state/player.svelte' +import type { GlobalAppEvent, PluginSetting } from '$lib/types' + +const HOST_SOURCE = 'rem-plugin-host' +const PLUGIN_SOURCE = 'rem-plugin' + +export type PluginEventKind = + | 'trackChanged' + | 'playbackStateChanged' + | 'queueChanged' + | 'settingsChanged' + +type StateKind = 'playback' | 'track' | 'queue' + +interface PluginEnvelope { + source: 'rem-plugin' | 'rem-plugin-host' + type: string + id?: string + payload?: unknown +} + +interface Attachment { + pluginId: string + messageListener: (e: MessageEvent) => void + loadListener: () => void +} + +class PluginHostBridge { + #attachments = new Map() + #eventUnlisten: UnlistenFn | null = null + #started = false + + constructor() { + this.#startEventSubscription() + } + + /** + * 模块加载时启动一次 Tauri 事件订阅,将后端事件桥接为 + * 插件侧的 `event` 广播。单例,全程存活。 + */ + #startEventSubscription(): void { + if (this.#started) return + this.#started = true + + void listen('global-app-event', event => { + const { type, payload } = event.payload + switch (type) { + case 'TrackStarted': + this.broadcastEvent('trackChanged', payload) + break + case 'PlaybackStateChanged': + this.broadcastEvent('playbackStateChanged', payload) + break + case 'QueueChanged': + this.broadcastEvent('queueChanged', payload) + break + case 'SettingsChanged': + this.broadcastEvent('settingsChanged', payload) + break + // PlaybackProgress / VolumeChanged / LyricsLoaded 不广播给插件 + } + }).then(unlisten => { + this.#eventUnlisten = unlisten + }).catch(err => { + console.error('[host-bridge] 订阅 global-app-event 失败:', err) + }) + } + + /** + * 绑定 iframe,开始监听其 postMessage。返回清理函数。 + * 在 iframe `load` 时主动发送 `ready` 握手。 + */ + attach(iframe: HTMLIFrameElement, pluginId: string): () => void { + // 已绑定过则先解绑,避免重复监听 + if (this.#attachments.has(iframe)) { + this.detach(iframe) + } + + const messageListener = (e: MessageEvent) => { + // 来源校验:只接受本 iframe 内发出的消息(防越权) + if (e.source !== iframe.contentWindow) return + const data = e.data as PluginEnvelope | undefined + // 信封校验:必须是插件信封 + if (!data || data.source !== PLUGIN_SOURCE) return + void this.#handleMessage(iframe, pluginId, data) + } + + const loadListener = () => { + const capabilities = this.#getCapabilities(pluginId) + this.sendReady(iframe, pluginId, capabilities) + } + + window.addEventListener('message', messageListener) + iframe.addEventListener('load', loadListener) + + this.#attachments.set(iframe, { + pluginId, + messageListener, + loadListener, + }) + + // 兜底:若 iframe 已加载完成(load 事件不会再触发), + // 立即发一次 ready。SDK 对重复 ready 幂等处理。 + if (iframe.contentWindow) { + loadListener() + } + + return () => this.detach(iframe) + } + + /** 解绑 iframe,移除所有监听 */ + detach(iframe: HTMLIFrameElement): void { + const att = this.#attachments.get(iframe) + if (!att) return + window.removeEventListener('message', att.messageListener) + iframe.removeEventListener('load', att.loadListener) + this.#attachments.delete(iframe) + } + + /** 向指定 iframe 发送 ready 握手消息 */ + sendReady( + iframe: HTMLIFrameElement, + pluginId: string, + capabilities: string[], + ): void { + const win = iframe.contentWindow + if (!win) return + const envelope: PluginEnvelope = { + source: HOST_SOURCE, + type: 'ready', + payload: { pluginId, capabilities }, + } + win.postMessage(envelope, '*') + } + + /** 向所有已 attach 的 iframe 广播事件 */ + broadcastEvent(kind: PluginEventKind, data: unknown): void { + if (this.#attachments.size === 0) return + const envelope: PluginEnvelope = { + source: HOST_SOURCE, + type: 'event', + payload: { kind, data }, + } + for (const iframe of this.#attachments.keys()) { + const win = iframe.contentWindow + if (!win) continue + win.postMessage(envelope, '*') + } + } + + /** 从 manifest 中读取插件 permissions 作为 capabilities */ + #getCapabilities(pluginId: string): string[] { + const manifest = pluginState.manifests.find(m => m.id === pluginId) + return manifest?.permissions ?? [] + } + + /** 路由插件请求到对应 Tauri 命令或前端状态,并回写响应 */ + async #handleMessage( + iframe: HTMLIFrameElement, + pluginId: string, + env: PluginEnvelope, + ): Promise { + const { type, id, payload } = env + + switch (type) { + case 'command': { + try { + const p = (payload ?? {}) as { command: string; args?: unknown } + const result = await invoke('execute_plugin_command', { + commandId: p.command, + args: p.args ?? null, + }) + this.#sendResult(iframe, 'command:result', id, { + ok: true, + data: result, + }) + } catch (e) { + this.#sendResult(iframe, 'command:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'settings:get': { + try { + const p = (payload ?? {}) as { key: string } + const settings = await invoke( + 'get_plugin_settings', + { pluginId }, + ) + const found = settings.find(s => s.key === p.key) + if (!found) { + this.#sendResult(iframe, 'settings:get:result', id, { + ok: false, + error: 'key not found', + }) + } else { + // 裸值:直接返回 SettingValue(tagged union) + this.#sendResult( + iframe, + 'settings:get:result', + id, + found.value, + ) + } + } catch (e) { + this.#sendResult(iframe, 'settings:get:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'settings:set': { + try { + const p = (payload ?? {}) as { key: string; value: unknown } + await invoke('update_plugin_setting', { + pluginId, + key: p.key, + value: p.value, + }) + this.#sendResult(iframe, 'settings:set:result', id, { + ok: true, + }) + } catch (e) { + this.#sendResult(iframe, 'settings:set:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'state:get': { + try { + const p = (payload ?? {}) as { kind: StateKind } + const snapshot = this.#getStateSnapshot(p.kind) + this.#sendResult(iframe, 'state:get:result', id, snapshot) + } catch (e) { + this.#sendResult(iframe, 'state:get:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'subscribe': + case 'unsubscribe': + // 广播默认发给所有 iframe,无需精确订阅记账 + break + + default: + console.warn( + `[host-bridge] 未识别的插件消息类型: ${type}(plugin=${pluginId})`, + ) + break + } + } + + /** 从 player 状态读取快照 */ + #getStateSnapshot(kind: StateKind): unknown { + switch (kind) { + case 'playback': + return { + playing: player.playing, + currentTime: player.currentTime, + } + case 'track': + return { currentTrack: player.currentTrack } + case 'queue': + return { queue: player.queue } + } + } + + /** 向 iframe 回写响应消息 */ + #sendResult( + iframe: HTMLIFrameElement, + type: string, + id: string | undefined, + payload: unknown, + ): void { + const win = iframe.contentWindow + if (!win) return + const envelope: PluginEnvelope = { + source: HOST_SOURCE, + type, + payload, + } + if (id !== undefined) envelope.id = id + win.postMessage(envelope, '*') + } +} + +export const hostBridge = new PluginHostBridge() diff --git a/src/lib/plugins/register-builtin-views.ts b/src/lib/plugins/register-builtin-views.ts new file mode 100644 index 0000000..d5229ea --- /dev/null +++ b/src/lib/plugins/register-builtin-views.ts @@ -0,0 +1,11 @@ +import { registerNativeView } from './component-registry' + +let registered = false + +export function registerBuiltinViews(): void { + if (registered) return + registered = true + + registerNativeView('eq-panel', () => import('$lib/features/EqPanel.svelte')) + registerNativeView('lyrics-panel', () => import('$lib/features/LyricsPanel.svelte')) +} diff --git a/src/lib/state/covers.svelte.ts b/src/lib/state/covers.svelte.ts new file mode 100644 index 0000000..857c629 --- /dev/null +++ b/src/lib/state/covers.svelte.ts @@ -0,0 +1,44 @@ +import { invoke } from '@tauri-apps/api/core' +import type { Track } from '../types/music' + +class TrackCovers { + covers = $state>({}) + private promises = new Map>() + + async load(track: Track | null | undefined): Promise { + if (!track) return null + + if (this.covers[track.id] !== undefined) { + return this.covers[track.id] + } + + if (this.promises.has(track.id)) { + return await this.promises.get(track.id)! + } + + const promise = invoke('get_track_cover', { trackId: track.id }) + .then((cover) => { + this.covers[track.id] = cover + return cover + }) + .catch((err) => { + console.error('加载封面失败:', track.id, err) + this.covers[track.id] = null + return null + }) + .finally(() => { + this.promises.delete(track.id) + }) + + this.promises.set(track.id, promise) + + return await promise + } + + get(track: Track | null | undefined): string | null { + if (!track) return null + return this.covers[track.id] ?? null + } +} + +export const trackCovers = new TrackCovers() diff --git a/src/lib/state/equalizer.svelte.ts b/src/lib/state/equalizer.svelte.ts new file mode 100644 index 0000000..6684d45 --- /dev/null +++ b/src/lib/state/equalizer.svelte.ts @@ -0,0 +1,73 @@ +import { invoke } from '@tauri-apps/api/core' + +// 10 段 EQ 的中心频率(Hz) +const EQ_BANDS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000] +// 与 EQ_BANDS 对应的显示标签 +const BAND_LABELS = ['31', '62', '125', '250', '500', '1k', '2k', '4k', '8k', '16k'] + +export type EqPreset = { + name: string + bands: number[] +} + +export const EQ_PRESETS: EqPreset[] = [ + { name: 'Flat', bands: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { name: 'Bass Boost', bands: [6, 5, 4, 2, 0, 0, 0, 0, 0, 0] }, + { name: 'Treble Boost', bands: [0, 0, 0, 0, 0, 2, 4, 5, 6, 6] }, + { name: 'Vocal', bands: [-3, -2, 0, 2, 4, 4, 3, 1, 0, -1] }, +] + +class EqualizerState { + bands = $state([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + enabled = $state(true) + presetName = $state('Flat') + isOpen = $state(false) + loaded = $state(false) + + async load() { + try { + const state = await invoke<{ bands: number[]; enabled: boolean; preset_name: string }>('get_eq_state') + this.bands = state.bands + this.enabled = state.enabled + this.presetName = state.preset_name + this.loaded = true + } catch (err) { + console.error('Failed to load EQ state:', err) + } + } + + async setBand(index: number, gain: number) { + this.bands[index] = gain + try { + await invoke('set_eq_band', { band: index, gain }) + } catch (err) { + console.error(err) + } + } + + async applyPreset(preset: EqPreset) { + this.bands = [...preset.bands] + this.presetName = preset.name + try { + await invoke('apply_eq_preset', { presetName: preset.name, bands: preset.bands }) + } catch (err) { + console.error(err) + } + } + + async setEnabled(enabled: boolean) { + this.enabled = enabled + try { + await invoke('set_eq_enabled', { enabled }) + } catch (err) { + console.error(err) + } + } + + toggle() { + this.isOpen = !this.isOpen + } +} + +export const equalizer = new EqualizerState() +export { EQ_BANDS, BAND_LABELS } diff --git a/src/lib/state/library.svelte.ts b/src/lib/state/library.svelte.ts new file mode 100644 index 0000000..a0a9211 --- /dev/null +++ b/src/lib/state/library.svelte.ts @@ -0,0 +1,207 @@ +import { invoke } from '@tauri-apps/api/core' +import { listen, type UnlistenFn } from '@tauri-apps/api/event' +import { SvelteMap } from 'svelte/reactivity' +import type { Album, Artist, Track } from '../types/music' +import { settings } from './settings.svelte' + +class MusicLibrary { + tracks = $state([]) + isLoading = $state(false) + error = $state(null) + initialized = false + + get useAlbumArtistGrouping() { + return settings.data.useAlbumArtistGrouping ?? false + } + + #unlisten: UnlistenFn | null = null + + #refreshPromise: Promise | null = null + #albumsMap = $derived.by(() => { + const albumMap = new SvelteMap() + const useAlbumArtistGrouping = this.useAlbumArtistGrouping + + for (const track of this.tracks) { + const albumTitle = track.album?.trim() || '未知专辑' + + const artistName = track.artist?.trim() || '未知歌手' + const albumKey = useAlbumArtistGrouping + ? `${artistName}_${albumTitle}`.toLowerCase() + : albumTitle.toLowerCase() + const id = encodeURIComponent(albumKey) + + if (!albumMap.has(id)) { + albumMap.set(id, { + id, + title: albumTitle, + artist: artistName, + tracks: [], + cover: track.cover ?? null, + trackCount: 0, + representativeTrack: track, + }) + } + + const album = albumMap.get(id)! + album.tracks.push(track) + album.trackCount = album.tracks.length + + if (!album.cover && track.cover) { + album.cover = track.cover + } + } + + return albumMap + }) + + #artistsMap = $derived.by(() => { + const artistMap = new SvelteMap< + string, + Artist & { + albumKeys: Set + } + >() + + for (const track of this.tracks) { + const artistName = track.artist?.trim() || '未知歌手' + const artistKey = artistName.toLowerCase() + const artistId = encodeURIComponent(artistKey) + + if (!artistMap.has(artistId)) { + artistMap.set(artistId, { + id: artistId, + name: artistName, + cover: track.cover ?? null, + trackCount: 0, + albumCount: 0, + tracks: [], + albumKeys: new Set(), + }) + } + + const artist = artistMap.get(artistId)! + + artist.tracks.push(track) + artist.trackCount = artist.tracks.length + + const albumTitle = track.album?.trim() || '未知专辑' + const albumKey = albumTitle.toLowerCase() + + artist.albumKeys.add(albumKey) + artist.albumCount = artist.albumKeys.size + + if (!artist.cover && track.cover) { + artist.cover = track.cover + } + } + + return new SvelteMap( + Array.from(artistMap.values()).map(({ albumKeys, ...artist }) => [ + artist.id, + artist, + ]), + ) + }) + + constructor() { + void this.#setupListener() + } + + get albums(): Album[] { + return Array.from(this.#albumsMap.values()) + } + + get artists(): Artist[] { + return Array.from(this.#artistsMap.values()) + } + + get albumCount(): number { + return this.#albumsMap.size + } + + get artistCount(): number { + return this.#artistsMap.size + } + + get trackCount(): number { + return this.tracks.length + } + + getAlbum(id: string): Album | undefined { + return this.#albumsMap.get(id) + } + + getArtist(id: string): Artist | undefined { + return this.#artistsMap.get(id) + } + + async #setupListener() { + if (this.#unlisten) return + + this.#unlisten = await listen('library:refreshed', event => { + this.tracks = event.payload + }) + } + + async load(options: { force?: boolean } = {}): Promise { + const { force = false } = options + + if (this.#refreshPromise) { + return this.#refreshPromise + } + + if (!force && this.tracks.length > 0) { + return this.tracks + } + + this.isLoading = true + this.error = null + + this.#refreshPromise = (async () => { + try { + const tracks = await invoke('load_track_library') + this.tracks = tracks + return tracks + } catch (err) { + console.error('全局加载媒体库失败:', err) + this.error = String(err) + return this.tracks + } finally { + this.isLoading = false + this.#refreshPromise = null + } + })() + + return this.#refreshPromise + } + + async scan(): Promise { + try { + this.isLoading = true + this.error = null + + const tracks = await invoke('scan_track_directories', { + dirs: settings.data.libraryDirs, + }) + + this.tracks = tracks + + return tracks + } catch (err) { + console.error('扫描媒体库失败:', err) + this.error = String(err) + return this.tracks + } finally { + this.isLoading = false + } + } + + destroy() { + if (this.#unlisten) { + this.#unlisten() + this.#unlisten = null + } + } +} + +export const musicLibrary = new MusicLibrary() \ No newline at end of file diff --git a/src/lib/state/lyrics.svelte.ts b/src/lib/state/lyrics.svelte.ts new file mode 100644 index 0000000..38935b3 --- /dev/null +++ b/src/lib/state/lyrics.svelte.ts @@ -0,0 +1,39 @@ +import type { LyricDocument, LyricLine } from '$lib/types/lyrics' + +class LyricsState { + currentLyrics = $state(null) + currentLineIndex = $state(-1) + isOpen = $state(false) + + handleLyricsLoaded(songId: number, lines: LyricLine[]) { + this.currentLyrics = { songId, lines } + this.currentLineIndex = -1 + } + + updateCurrentTime(currentTimeMs: number) { + if (!this.currentLyrics || this.currentLyrics.lines.length === 0) { + this.currentLineIndex = -1 + return + } + + let idx = -1 + for (let i = this.currentLyrics.lines.length - 1; i >= 0; i--) { + if (this.currentLyrics.lines[i].timestampMs <= currentTimeMs) { + idx = i + break + } + } + this.currentLineIndex = idx + } + + clear() { + this.currentLyrics = null + this.currentLineIndex = -1 + } + + toggle() { + this.isOpen = !this.isOpen + } +} + +export const lyrics = new LyricsState() diff --git a/src/lib/state/player.svelte.ts b/src/lib/state/player.svelte.ts new file mode 100644 index 0000000..9177537 --- /dev/null +++ b/src/lib/state/player.svelte.ts @@ -0,0 +1,283 @@ +import type { GlobalAppEvent, PlaybackStatusSnapshot, PlayMode } from "$lib/types" +import { invoke } from "@tauri-apps/api/core" +import { listen, type UnlistenFn } from "@tauri-apps/api/event" +import type { PlaybackQueue, Track } from "../types/music" +import { lyrics } from "./lyrics.svelte" +import { recentlyPlayed } from "./recent.svelte" + +class Player { + queue = $state({ + tracks: [], + currentIndex: null, + playMode: 'ListLoop', + history: [], + }) + + playing = $state(false) + currentTime = $state(0) + queueOpen = $state(false) + #muted = $state(false) + #volume = $state(80) + #previousVolume = 80 + #globalUnlisten: UnlistenFn | null = null + #isInitialized = false + #eventBuffer: GlobalAppEvent[] = [] + + constructor() { + this.#setupMediaSession() + } + + #setupMediaSession() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return + if (typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window) return + + navigator.mediaSession.setActionHandler('play', () => this.resume()) + navigator.mediaSession.setActionHandler('pause', () => this.pause()) + navigator.mediaSession.setActionHandler('previoustrack', () => this.prev()) + navigator.mediaSession.setActionHandler('nexttrack', () => this.next()) + navigator.mediaSession.setActionHandler('seekto', details => { + const seekTime = details.seekTime + if (seekTime == null) return + void this.seek(seekTime) + }) + navigator.mediaSession.setActionHandler('seekforward', () => { + if (!this.currentTrack) return + void this.seek(Math.min(this.currentTime + 10, this.#trackDurationSeconds(this.currentTrack))) + }) + navigator.mediaSession.setActionHandler('seekbackward', () => { + if (!this.currentTrack) return + void this.seek(Math.max(this.currentTime - 10, 0)) + }) + } + + #updateMediaMetadata() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator) || !this.currentTrack) return + + navigator.mediaSession.metadata = new MediaMetadata({ + title: this.currentTrack.title, + artist: this.currentTrack.artist ?? '未知歌手', + album: this.currentTrack.album ?? '未知专辑', + artwork: this.#buildArtwork(), + }) + } + + #buildArtwork() { + const cover = this.currentTrack?.cover + return cover ? [{ src: cover }] : [] + } + + #trackDurationSeconds(track: Track): number { + return track.duration / 1000 + } + + #updatePlaybackState() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return + navigator.mediaSession.playbackState = this.playing ? 'playing' : 'paused' + } + + #updatePositionState() { + const track = this.currentTrack + if (!track || typeof navigator === 'undefined' || !('mediaSession' in navigator) || !('setPositionState' in navigator.mediaSession)) return + + navigator.mediaSession.setPositionState({ + duration: this.#trackDurationSeconds(track), + playbackRate: 1, + position: this.currentTime, + }) + } + + #syncMediaSession() { + this.#updateMediaMetadata() + this.#updatePlaybackState() + this.#updatePositionState() + } + + #clearMediaSession() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return + navigator.mediaSession.metadata = null + navigator.mediaSession.playbackState = 'none' + } + + get currentTrack(): Track | undefined { + const index = this.queue.currentIndex + if (index === null || index < 0 || index >= this.queue.tracks.length) { + return undefined + } + return this.queue.tracks[index] + } + + get volume() { return this.#volume } + set volume(volume: number) { + invoke('set_volume', { volume }).catch(console.error) + } + + get muted() { return this.#muted } + set muted(value: boolean) { + if (value) { + this.#previousVolume = this.#volume + this.#muted = true + invoke('set_volume', { volume: 0 }).catch(console.error) + } else { + this.#muted = false + invoke('set_volume', { volume: this.#previousVolume }).catch(console.error) + } + } + + async replacePlaylistAndPlay(tracks: Track[], targetId: number) { + await invoke('replace_playlist_and_play', { tracks, targetId }) + } + + async insertTracksAsNext(tracks: Track[]) { + await invoke('insert_tracks_as_next', { tracks }) + } + + async insertTrackAsNext(track: Track) { + await invoke('insert_track_as_next', { track }) + } + + async removeTrack(trackId: number) { + await invoke('remove_track_from_queue', { trackId }) + } + + async playTrackInQueue(index: number) { + await invoke('play_queue_track', { index }) + } + + async cyclePlayMode() { + const modes: PlayMode[] = ['ListLoop', 'SingleLoop', 'Shuffle'] + const index = modes.indexOf(this.queue.playMode) + const mode = modes[(index + 1) % modes.length] + await invoke('set_play_mode', { mode }) + } + + async clearQueue() { + await invoke('clear_queue') + } + + async next() { + await invoke('play_next_track') + } + + async prev() { + await invoke('play_previous_track') + } + + resume = async () => { + try { + await invoke('resume_track') + } catch (err) { + console.error(err) + } + } + + pause = async () => { + try { + await invoke('pause_track') + } catch (err) { + console.error(err) + } + } + + toggle = async () => { + if (this.playing) { + await this.pause() + } else { + await this.resume() + } + } + + seek = async (time: number) => { + await invoke('set_current_time', { time }) + } + + toggleQueue() { + this.queueOpen = !this.queueOpen + } + + async loadState() { + await this.#setupPlaybackListeners() + + try { + const queue = await invoke('get_playback_queue') + this.queue = queue + + const status = await invoke('get_current_status') + this.playing = status.playing + this.currentTime = status.currentTime + this.#syncMediaSession() + } catch (err) { + console.error(err) + } finally { + this.#isInitialized = true + this.#flushEventBuffer() + } + } + + #flushEventBuffer() { + while (this.#eventBuffer.length > 0) { + const event = this.#eventBuffer.shift() + if (event) { + this.#handleGlobalEvent(event) + } + } + } + + async #setupPlaybackListeners() { + if (this.#globalUnlisten) return + + this.#globalUnlisten = await listen('global-app-event', event => { + if (!this.#isInitialized) { + this.#eventBuffer.push(event.payload) + return + } + this.#handleGlobalEvent(event.payload) + }) + } + + #handleGlobalEvent(payload: GlobalAppEvent) { + const { type, payload: data } = payload + + switch (type) { + case 'PlaybackProgress': + this.currentTime = data.currentTime + this.#updatePositionState() + break + + case 'TrackStarted': + this.currentTime = 0 + this.playing = true + this.queue = { ...this.queue, currentIndex: data.index } + this.#syncMediaSession() + recentlyPlayed.add(data.track) + break + + case 'VolumeChanged': + this.#volume = Math.round(data * 100) + this.#muted = this.#volume === 0 + break + + case 'QueueChanged': + this.queue = data + break + + case 'PlaybackStateChanged': + this.playing = data.playing + this.currentTime = data.currentTime + this.#syncMediaSession() + break + + case 'LyricsLoaded': + lyrics.handleLyricsLoaded(data.songId, data.lines) + break + } + } + + destroy() { + if (this.#globalUnlisten) { + this.#globalUnlisten() + this.#globalUnlisten = null + } + } +} + +export const player = new Player() \ No newline at end of file diff --git a/src/lib/state/plugins.svelte.ts b/src/lib/state/plugins.svelte.ts new file mode 100644 index 0000000..4f9f1a2 --- /dev/null +++ b/src/lib/state/plugins.svelte.ts @@ -0,0 +1,128 @@ +import { invoke } from '@tauri-apps/api/core' +import type { + MenuExtension, + NativeViewExtension, + PluginSetting, + ResolvedPluginManifest, + SettingValue, + SidebarExtension, + ViewContribution, +} from '$lib/types/plugin' + +class PluginState { + manifests = $state([]) + sidebarExtensions = $state([]) + nativeViewExtensions = $state([]) + /** 当前激活的原生面板(pluginId),null 表示无面板显示 */ + activeNativePanel = $state(null) + isLoading = $state(false) + error = $state(null) + + /** + * 按 slug 解析插件 view。默认渲染 manifest 的第一个 view + * (多 view 场景不在本期范围)。 + */ + getViewForSlug(slug: string): { manifest: ResolvedPluginManifest; view: ViewContribution } | null { + const manifest = this.manifests.find(m => m.route === `/plugins/view/${slug}`) + if (!manifest) return null + const view = manifest.contributes.views[0] + if (!view) return null + return { manifest, view } + } + + async loadManifests() { + try { + this.manifests = await invoke( + 'get_plugin_manifests', + ) + } catch (err) { + console.error(err) + this.error = String(err) + } + } + + async loadSidebarExtensions() { + this.isLoading = true + this.error = null + try { + this.sidebarExtensions = await invoke( + 'get_all_sidebar_extensions', + ) + } catch (err) { + console.error(err) + this.error = String(err) + } finally { + this.isLoading = false + } + } + + async loadNativeViewExtensions() { + this.isLoading = true + this.error = null + try { + this.nativeViewExtensions = await invoke( + 'get_native_view_extensions', + ) + } catch (err) { + console.error(err) + this.error = String(err) + } finally { + this.isLoading = false + } + } + + async getMenuExtensions(location: string): Promise { + try { + return await invoke('get_menu_extensions', { + location, + }) + } catch (err) { + console.error(err) + return [] + } + } + + async getPluginSettings(pluginId: string): Promise { + try { + return await invoke('get_plugin_settings', { + pluginId, + }) + } catch (err) { + console.error(err) + return [] + } + } + + async updatePluginSetting( + pluginId: string, + key: string, + value: SettingValue, + ) { + try { + await invoke('update_plugin_setting', { pluginId, key, value }) + } catch (err) { + console.error(err) + this.error = String(err) + } + } + + async enablePlugin(pluginId: string) { + try { + await invoke('enable_plugin_command', { pluginId }) + } catch (err) { + console.error(err) + this.error = String(err) + } + } + + async disablePlugin(pluginId: string) { + try { + await invoke('disable_plugin_command', { pluginId }) + } catch (err) { + console.error(err) + this.error = String(err) + } + } +} + +export const pluginState = new PluginState() diff --git a/src/lib/state/recent.svelte.ts b/src/lib/state/recent.svelte.ts new file mode 100644 index 0000000..a93fbc5 --- /dev/null +++ b/src/lib/state/recent.svelte.ts @@ -0,0 +1,81 @@ +import { invoke } from '@tauri-apps/api/core' +import type { Track } from '../types/music' + +type RecentPlayedTrack = { + track: Track + playedAt: string +} + +class RecentlyPlayed { + tracks = $state([]) + isLoading = $state(false) + error = $state(null) + + private loadPromise: Promise | null = null + + async load(): Promise { + if (this.loadPromise) { + return this.loadPromise + } + + if (this.tracks.length > 0) { + return this.tracks + } + + this.isLoading = true + this.error = null + + this.loadPromise = invoke('load_recently_played', { + limit: 100, + offset: 0, + }) + .then(records => { + const tracks = records.map(record => record.track) + this.tracks = tracks + return tracks + }) + .catch(err => { + console.error('加载最近播放失败:', err) + this.error = String(err) + return this.tracks + }) + .finally(() => { + this.isLoading = false + this.loadPromise = null + }) + + return this.loadPromise + } + + async add(track: Track): Promise { + this.tracks = [ + track, + ...this.tracks.filter(item => item.id !== track.id), + ].slice(0, 100) + + try { + await invoke('add_recently_played', { + trackId: track.id, + playedAt: new Date().toISOString(), + }) + + return this.tracks + } catch (err) { + console.error('写入最近播放失败:', err) + this.error = String(err) + return this.tracks + } + } + + async clear() { + try { + await invoke('clear_recently_played') + this.tracks = [] + } catch (err) { + console.error('清空最近播放失败:', err) + this.error = String(err) + } + } +} + +export const recentlyPlayed = new RecentlyPlayed() \ No newline at end of file diff --git a/src/lib/state/search.svelte.ts b/src/lib/state/search.svelte.ts new file mode 100644 index 0000000..1d649cf --- /dev/null +++ b/src/lib/state/search.svelte.ts @@ -0,0 +1,5 @@ +class SearchState { + query = $state('') +} + +export const globalSearch = new SearchState() \ No newline at end of file diff --git a/src/lib/state/settings.svelte.ts b/src/lib/state/settings.svelte.ts new file mode 100644 index 0000000..c79a6c3 --- /dev/null +++ b/src/lib/state/settings.svelte.ts @@ -0,0 +1,101 @@ +import type { AppSettings } from '$lib/types' +import { invoke } from '@tauri-apps/api/core' + +export const DEFAULT_SETTINGS: AppSettings = { + theme: 'auto', + volume: 80, + libraryDirs: [], + scanOnStartup: false, + reduceMotion: false, + useAlbumArtistGrouping: false, + pluginDirs: [], + pluginDevMode: false, + pluginScanOnStartup: true, + pluginLogLevel: 'warn', +} + +class SettingsState { + data = $state({ ...DEFAULT_SETTINGS }) + isLoading = $state(false) + error = $state(null) + + #saveTimer: ReturnType | null = null + #loaded = false + + async load(): Promise { + if (this.#loaded) { + return this.data + } + + this.#loaded = true + this.isLoading = true + this.error = null + + try { + this.data = await invoke('load_settings') + return this.data + } catch (error) { + console.error(error) + this.error = String(error) + return this.data + } finally { + this.isLoading = false + } + } + + updateLocalState(newSettings: AppSettings) { + this.data = newSettings + } + + update(patch: Partial) { + const nextSettings = { + ...this.data, + ...patch, + } + this.data = nextSettings + this.scheduleSave(nextSettings) + } + + async save(targetSettings: AppSettings): Promise { + this.error = null + + try { + const savedSettings = await invoke('save_settings', { + settings: targetSettings, + }) + this.data = savedSettings + } catch (error) { + console.error(error) + this.error = String(error) + } + } + + scheduleSave(targetSettings: AppSettings) { + if (this.#saveTimer) { + clearTimeout(this.#saveTimer) + } + + this.#saveTimer = setTimeout(() => { + void this.save(targetSettings) + }, 300) + } + + addLibraryDir(dir: string) { + const value = dir.trim() + + if (!value) return + if (this.data.libraryDirs.includes(value)) return + + this.update({ + libraryDirs: [...this.data.libraryDirs, value], + }) + } + + removeLibraryDir(dir: string) { + this.update({ + libraryDirs: this.data.libraryDirs.filter((item) => item !== dir), + }) + } +} + +export const settings = new SettingsState() diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts new file mode 100644 index 0000000..a1dbe26 --- /dev/null +++ b/src/lib/types/events.ts @@ -0,0 +1,13 @@ +import type { PlaybackQueue } from "./music" +import type { LyricLine } from "./lyrics" +import type { PlaybackProgressPayload, PlaybackStatePayload, PlaybackTrackStartedPayload } from "./playback" +import type { AppSettings } from "./settings" + +export type GlobalAppEvent = + | { type: 'VolumeChanged'; payload: number } + | { type: 'SettingsChanged'; payload: AppSettings } + | { type: 'TrackStarted'; payload: PlaybackTrackStartedPayload } + | { type: 'PlaybackProgress'; payload: PlaybackProgressPayload } + | { type: 'QueueChanged'; payload: PlaybackQueue } + | { type: 'PlaybackStateChanged'; payload: PlaybackStatePayload } + | { type: 'LyricsLoaded'; payload: { songId: number; lines: LyricLine[] } } \ No newline at end of file diff --git a/src/lib/types/extension.ts b/src/lib/types/extension.ts new file mode 100644 index 0000000..6873d28 --- /dev/null +++ b/src/lib/types/extension.ts @@ -0,0 +1,22 @@ +export interface ExtensionNavItem { + id: string + title: string + icon: string + href: string + order?: number +} + +export interface ExtensionCommand { + id: string + title: string + run: () => void | Promise +} + +export interface AppExtension { + id: string + name: string + description?: string + enabled?: boolean + navItems?: ExtensionNavItem[] + commands?: ExtensionCommand[] +} \ No newline at end of file diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..c28b762 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,17 @@ +export type { GlobalAppEvent } from './events' +export type { + AppExtension, + ExtensionCommand, + ExtensionNavItem +} from './extension' +export type { LyricDocument, LyricLine } from './lyrics' +export type { Album, Artist, Playlist, Track } from './music' +export type { BackendQueueTrack, PlayMode, PlaybackProgressPayload, PlaybackStatusSnapshot, PlaybackTrackInfo } from './playback' +export type { + MenuExtension, + PluginInfo, + PluginSetting, + SettingValue, + SidebarExtension, +} from './plugin' +export type { AppSettings, PluginLogLevel, ThemeMode } from './settings' diff --git a/src/lib/types/lyrics.ts b/src/lib/types/lyrics.ts new file mode 100644 index 0000000..9e32a01 --- /dev/null +++ b/src/lib/types/lyrics.ts @@ -0,0 +1,9 @@ +export interface LyricLine { + timestampMs: number + text: string +} + +export interface LyricDocument { + songId: number + lines: LyricLine[] +} diff --git a/src/lib/types/music.ts b/src/lib/types/music.ts new file mode 100644 index 0000000..6da8bb3 --- /dev/null +++ b/src/lib/types/music.ts @@ -0,0 +1,47 @@ +import type { PlayMode } from "./playback" + +export interface Track { + id: number + title: string + artist: string | null + album: string | null + duration: number + cover: string | null + fileSize: number | null + playCount: number + lastPlayedAt: string | null + createdAt: string + updatedAt: string +} + +export interface Playlist { + id: number + name: string + cover: string +} + +export interface Artist { + id: string + name: string + cover: string | null + trackCount: number + albumCount: number + tracks: Track[] +} + +export interface Album { + id: string + title: string + artist: string + cover: string | null + tracks: Track[] + trackCount: number + representativeTrack: Track +} + +export interface PlaybackQueue { + tracks: Track[] + currentIndex: number | null + playMode: PlayMode + history: number[] +} \ No newline at end of file diff --git a/src/lib/types/playback.ts b/src/lib/types/playback.ts new file mode 100644 index 0000000..8c27a8d --- /dev/null +++ b/src/lib/types/playback.ts @@ -0,0 +1,31 @@ +import type { Track } from "./music" + +export type PlayMode = 'ListLoop' | 'SingleLoop' | 'Shuffle' + +export type PlaybackTrackInfo = { + id: number | null + path: string | null +} + +export type PlaybackStatusSnapshot = { + hasMedia: boolean + playing: boolean + currentTime: number + track: PlaybackTrackInfo | null +} + +export type PlaybackProgressPayload = { + currentTime: number +} + +export type PlaybackStatePayload = { + playing: boolean + currentTime: number +} + +export type PlaybackTrackStartedPayload = { + track: Track + index: number +} + +export type BackendQueueTrack = Omit \ No newline at end of file diff --git a/src/lib/types/plugin.ts b/src/lib/types/plugin.ts new file mode 100644 index 0000000..ca0a56a --- /dev/null +++ b/src/lib/types/plugin.ts @@ -0,0 +1,111 @@ +export type PluginSource = 'builtin' | 'packaged' | 'user' + +export type PluginPermission = + | 'playerRead' + | 'playerControl' + | 'queueRead' + | 'queueWrite' + | 'libraryRead' + | 'libraryWrite' + | 'settingsRead' + | 'settingsWrite' + | 'pluginUI' + +export type SettingValue = + | { type: 'Bool'; value: boolean } + | { type: 'Integer'; value: number } + | { type: 'Float'; value: number } + | { type: 'Text'; value: string } + | { type: 'List'; value: string[] } + | { type: 'Json'; value: unknown } + +export interface PluginSetting { + key: string + title: string + value: SettingValue + defaultValue: SettingValue +} + +export interface SidebarExtension { + id: string + pluginId: string + title: string + icon: string + route: string + state: 'Enabled' | 'Disabled' +} + +export interface NativeViewExtension { + id: string + pluginId: string + title: string + token: string + icon: string | null + state: 'Enabled' | 'Disabled' +} + +export interface MenuExtension { + id: string + pluginId: string + command: string + location: string + group: string | null + state: 'Enabled' | 'Disabled' +} + +export interface ViewContribution { + id: string + title: string + entry: string + icon: string | null +} + +export interface NativeViewContribution { + id: string + title: string + token: string + icon: string | null +} + +/** Fully resolved, normalized manifest — the only plugin-identity shape + * the frontend ever consumes. Routes are already normalized to + * `/plugins/view/`; no optional fields. */ +export interface ResolvedPluginManifest { + id: string + source: PluginSource + route: string + name: string + displayName: string + version: string + author: string + description: string + entry: string + minAppVersion: string + permissions: string[] + activationEvents: string[] + contributes: { + commands: { id: string; title: string; category: string | null }[] + menus: { + command: string + title: string + location: string + group: string | null + }[] + sidebars: { id: string; title: string; icon: string }[] + views: ViewContribution[] + nativeViews: NativeViewContribution[] + } + settings: { + key: string + title: string + defaultValue: SettingValue + }[] +} + +export interface PluginInfo { + id: string + displayName: string + icon: string + route: string + state: 'Enabled' | 'Disabled' +} diff --git a/src/lib/types/settings.ts b/src/lib/types/settings.ts new file mode 100644 index 0000000..8500233 --- /dev/null +++ b/src/lib/types/settings.ts @@ -0,0 +1,16 @@ +export type ThemeMode = 'auto' | 'light' | 'dark' + +export type PluginLogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' + +export interface AppSettings { + theme: ThemeMode + volume: number + libraryDirs: string[] + scanOnStartup: boolean + reduceMotion: boolean + useAlbumArtistGrouping: boolean + pluginDirs: string[] + pluginDevMode: boolean + pluginScanOnStartup: boolean + pluginLogLevel: PluginLogLevel +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..478680d --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1 @@ +export { formatDuration, formatTime } from './time' diff --git a/src/lib/utils/library.ts b/src/lib/utils/library.ts new file mode 100644 index 0000000..d18c3e1 --- /dev/null +++ b/src/lib/utils/library.ts @@ -0,0 +1,26 @@ +import { musicLibrary } from '$lib/state/library.svelte' +import { settings } from '$lib/state/settings.svelte' +import { open } from '@tauri-apps/plugin-dialog' + +export async function scanDirectory() { + try { + const selected = await open({ + directory: true, + multiple: false, + title: '选择音乐媒体库目录', + }) + + if (!selected || typeof selected !== 'string') return + + settings.addLibraryDir(selected) + + await musicLibrary.scan() + } catch (err) { + console.error('扫描音乐媒体库目录失败:', err) + } +} + +export async function removeMusicDirectory(dir: string) { + settings.removeLibraryDir(dir) + await musicLibrary.scan() +} \ No newline at end of file diff --git a/src/lib/utils/time.ts b/src/lib/utils/time.ts new file mode 100644 index 0000000..9daf708 --- /dev/null +++ b/src/lib/utils/time.ts @@ -0,0 +1,34 @@ +export function formatSeconds( + seconds: number, + options: { + padMinutes?: boolean + showHours?: boolean + } = {} +): string { + const { padMinutes = false, showHours = false } = options + + if (!Number.isFinite(seconds) || seconds <= 0) { + return padMinutes ? '00:00' : '0:00' + } + + const total = Math.floor(seconds) + const h = Math.floor(total / 3600) + const m = Math.floor((total % 3600) / 60) + const s = total % 60 + + if (h > 0 || showHours) { + return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` + } + + const minutes = padMinutes ? String(m).padStart(2, '0') : String(m) + + return `${minutes}:${String(s).padStart(2, '0')}` +} + +export function formatTime(seconds: number): string { + return formatSeconds(seconds, { padMinutes: true }) +} + +export function formatDuration(seconds: number): string { + return formatSeconds(seconds) +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b084323..c7d63b8 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,25 +1,132 @@ - - - - + + + + + {@render children()} + + - -
    + - {@render children()} - - +{#if activeNativeView} +
    + +
    +{/if} + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index 554da60..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
    - - -
    - - diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..dca901d --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit' + +export async function load() { + redirect(307, '/library') +} \ No newline at end of file diff --git a/src/routes/album/+page.svelte b/src/routes/album/+page.svelte new file mode 100644 index 0000000..4de94be --- /dev/null +++ b/src/routes/album/+page.svelte @@ -0,0 +1,218 @@ + + + + + + 专辑 + + +
    +
    + +
    +
    + +
    + +
    + 按专辑艺术家分组 + +
    +
    +
    + + {#if musicLibrary.isLoading} +
    + +
    + {:else if musicLibrary.error} +
    + {musicLibrary.error} +
    + {:else if isMobile} +
    + {#if selectedAlbum === undefined} +
    + {#if filteredAlbums.length === 0} +
    + 🎵 + 没有找到专辑 +
    + {:else} + + {#each filteredAlbums as album (album.id)} + {@const cover = trackCovers.get(album.representativeTrack) ?? album.cover} + { selectedAlbumId = album.id }} + onkeydown={(e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && + (selectedAlbumId = album.id)} + role="button" + tabindex="0" + > + {#if cover} + + {:else} +
    + 🎵 +
    + {/if} +
    + {/each} +
    + {/if} +
    + {:else} +
    +
    + { selectedAlbumId = null }} + /> +
    +

    + {selectedAlbum.title} +

    +

    + {selectedAlbum.trackCount} 首歌曲 +

    +
    +
    +
    + +
    +
    + {/if} +
    + {:else} +
    +
    + +
    +
    + {#if selectedAlbum === undefined} +
    + 🎵 + 选择一个专辑以查看歌曲 +
    + {:else} +
    + +
    + {/if} +
    +
    + {/if} +
    diff --git a/src/routes/artists/+page.svelte b/src/routes/artists/+page.svelte new file mode 100644 index 0000000..35153dd --- /dev/null +++ b/src/routes/artists/+page.svelte @@ -0,0 +1,220 @@ + + + + + + 艺术家 + + +
    +
    + +
    +
    + +
    +
    +
    + + {#if musicLibrary.isLoading} +
    + +
    + {:else if musicLibrary.error} +
    + {musicLibrary.error} +
    + {:else if isMobile} +
    + {#if selectedArtist === undefined} +
    + {#if filteredArtists.length === 0} +
    + 👤 + 没有找到歌手 +
    + {:else} + + {#each filteredArtists as artist (artist.id)} + { + selectedArtistId = artist.id + }} + onkeydown={(e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && + (selectedArtistId = artist.id)} + role="button" + tabindex="0" + > + {#if artist.cover} + + {:else} +
    + 👤 +
    + {/if} +
    + {/each} +
    + {/if} +
    + {:else} +
    +
    + { + selectedArtistId = null + }} + /> +
    +

    + {selectedArtist.name} +

    +

    + {selectedArtist.trackCount} 首歌曲 · {selectedArtist.albumCount} + 张专辑 +

    +
    + +
    +
    + +
    +
    + {/if} +
    + {:else} +
    +
    + +
    +
    + {#if selectedArtist === undefined} +
    + 👤 + 选择一个歌手以查看歌曲 +
    + {:else} +
    + +
    + {/if} +
    +
    + {/if} +
    diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte new file mode 100644 index 0000000..3d55da1 --- /dev/null +++ b/src/routes/library/+page.svelte @@ -0,0 +1,534 @@ + + + + 歌曲 + + +
    +
    + +
    +
    + + + musicLibrary.scan()} + /> + + + + + + + + + + + { + isLibraryDialogOpen = true + }} + /> + +
    +
    +
    + +
    +
    + + 标题 + 歌手 + 专辑 + +
    +
    +
    + + {#if isBatchMode} +
    +
    + 已选择 {selectedTrackIds.length} 首歌曲 +
    + +
    + + + + + + + + + +
    +
    + {/if} +
    + + {#if musicLibrary.isLoading} +
    + +
    + {:else if musicLibrary.error} +
    + {musicLibrary.error} +
    + {:else if displayTracks.length === 0} +
    +
    🎵
    +
    没有找到歌曲
    +
    尝试刷新媒体库或修改搜索关键词
    +
    + {:else} +
    + +
    + {/if} +
    + + { + isLibraryDialogOpen = false + }} +> +
    +
    +
    + 管理媒体文件夹 +
    +
    + 配置音乐媒体库扫描目录和启动行为 +
    +
    + + + + handleSwitchChange(event, 'scanOnStartup')} + > + + +
    +
    +
    媒体库目录
    +
    + 管理应用扫描音乐文件的本地文件夹 +
    +
    + +
    + +
    + + {#if settings.data?.libraryDirs?.length > 0} +
    + {#each settings.data.libraryDirs as dir (dir)} + + + + {/each} +
    + {:else} +
    + 暂未添加媒体库目录,应用将无法扫描到任何歌曲。 +
    + {/if} +
    + +
    + +
    +
    +
    + + { + isDeleteConfirmDialogOpen = false + }} +> +
    +
    +
    + 确认删除所选歌曲 +
    +
    + 将对已选择的 {selectedTrackIds.length} 首歌曲执行删除操作。移至回收站后通常可以恢复;从磁盘中删除将永久移除文件且无法撤销。 +
    +
    + +
    + + + + + +
    +
    +
    + + { + isAddAllSnackbarOpen = false + }} +> + 成功添加至播放列表 + diff --git a/src/routes/plugins/+page.svelte b/src/routes/plugins/+page.svelte new file mode 100644 index 0000000..de05789 --- /dev/null +++ b/src/routes/plugins/+page.svelte @@ -0,0 +1,88 @@ + + + + 插件 + + +
    + + +
    + +
    + +
    + {#if pluginState.manifests.length === 0} +
    + 暂无已注册的插件。 +
    + {:else} + {#each pluginState.manifests as manifest (manifest.id)} + {@const ext = pluginState.sidebarExtensions.find(e => e.pluginId === manifest.id)} + {@const state = ext?.state ?? 'Enabled'} + + {/each} + {/if} + + {#if pluginState.error} +
    + {pluginState.error} +
    + {/if} +
    +
    diff --git a/src/routes/plugins/detail/+page.svelte b/src/routes/plugins/detail/+page.svelte new file mode 100644 index 0000000..f3d5246 --- /dev/null +++ b/src/routes/plugins/detail/+page.svelte @@ -0,0 +1,293 @@ + + + + 插件 - {sidebarEntry?.title ?? pluginId} + + +
    +
    + + + + +
    + +
    + +
    + +
    + + + + {pluginId} + + + + + togglePlugin()} + > + + + + {#if settings.length > 0} + + {#each settings as setting (setting.key)} + + {#if setting.value.type === 'Bool'} + { + const target = + e.currentTarget as HTMLInputElement + handleSettingBoolChange( + setting.key, + target.checked, + ) + }} + > + {:else if setting.value.type === 'Integer'} + { + if (e.key === 'Enter') { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Integer', + target.value, + ) + } + }} + role="textbox" + tabindex="0" + style="width: 200px;" + > + {:else if setting.value.type === 'Float'} + { + if (e.key === 'Enter') { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Float', + target.value, + ) + } + }} + role="textbox" + tabindex="0" + style="width: 200px;" + > + {:else if setting.value.type === 'Text'} + { + if (e.key === 'Enter') { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Text', + target.value, + ) + } + }} + role="textbox" + tabindex="0" + style="width: 200px;" + > + {:else if setting.value.type === 'Json'} +
    + handleJsonInput(setting.key)} + onblur={(e: Event) => { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Json', + target.value, + ) + }} + role="textbox" + tabindex="0" + > + {#if jsonErrors[setting.key]} +
    + JSON 解析失败:{jsonErrors[setting.key]} +
    + {/if} +
    + {:else if setting.value.type === 'List'} + + handleListChange(setting.key, arr)} + /> + {/if} +
    + {/each} +
    + {/if} +
    +
    diff --git a/src/routes/plugins/view/[...slug]/+page.server.ts b/src/routes/plugins/view/[...slug]/+page.server.ts new file mode 100644 index 0000000..38c25cc --- /dev/null +++ b/src/routes/plugins/view/[...slug]/+page.server.ts @@ -0,0 +1,4 @@ +// 插件 view 路由按运行时 pluginState.manifests 解析,不能预渲染。 +// 全局 +layout.ts 设置了 prerender=true,此处覆盖为 false, +// 使 /plugins/view/<任意 slug> 走 SPA fallback 而非仅限 lyrics。 +export const prerender = false diff --git a/src/routes/plugins/view/[...slug]/+page.svelte b/src/routes/plugins/view/[...slug]/+page.svelte new file mode 100644 index 0000000..392e522 --- /dev/null +++ b/src/routes/plugins/view/[...slug]/+page.svelte @@ -0,0 +1,145 @@ + + + + {manifest?.displayName ?? 'Plugin'} — Plugins + + +
    + {#if !manifest} +
    +

    + 未找到插件 "{slug}" +

    + + 返回插件列表 + +
    + {:else} +
    + + + + +
    + + {#if !resolved} + +
    +
    +
    +
    + ID + {manifest.id} +
    +
    + 来源 + {manifest.source} +
    +
    + 版本 + {manifest.version} +
    +
    + 路由 + {manifest.route} +
    +
    + 作者 + {manifest.author} +
    +
    + {manifest.description} +
    +
    +
    +
    + {:else if !hasUIPermission} +
    +

    + 插件未声明 UI 权限 +

    +
    + {:else} +
    + +
    + {/if} + {/if} +
    diff --git a/src/routes/recent/+page.svelte b/src/routes/recent/+page.svelte new file mode 100644 index 0000000..dbddb63 --- /dev/null +++ b/src/routes/recent/+page.svelte @@ -0,0 +1,58 @@ + + + + 最近播放 + + +
    + +
    + + + {#if recentlyPlayed.tracks.length > 0} + + {/if} +
    +
    + +{#if recentlyPlayed.isLoading} +
    + +
    +{:else if recentlyPlayed.error} +
    + {recentlyPlayed.error} +
    +{:else if recentlyPlayed.tracks.length === 0} +
    +
    🎧
    +
    还没有最近播放记录
    +
    播放一首歌后,它会出现在这里
    +
    +{:else} +
    + +
    +{/if} diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte new file mode 100644 index 0000000..0d27130 --- /dev/null +++ b/src/routes/settings/+page.svelte @@ -0,0 +1,258 @@ + + + + 设置 + + +
    + + +
    + +
    + +
    + + +
    + + settings.update({ + pluginLogLevel: value as PluginLogLevel, + })} + /> +
    +
    + +
    +
    +
    插件目录
    +
    + 管理应用扫描插件的本地文件夹 +
    +
    + +
    + +
    + + {#if settings.data?.pluginDirs?.length > 0} +
    + {#each settings.data.pluginDirs as dir (dir)} + + + + {/each} +
    + {:else} +
    + 暂未添加插件目录。 +
    + {/if} +
    +
    + + {#if settings.error} +
    + {settings.error} +
    + {/if} +
    +
    diff --git a/static/default_cover.png b/static/default_cover.png new file mode 100644 index 0000000..6c7c6c1 Binary files /dev/null and b/static/default_cover.png differ diff --git a/static/fonts/Material_Icons.woff2 b/static/fonts/Material_Icons.woff2 new file mode 100644 index 0000000..5492a6e Binary files /dev/null and b/static/fonts/Material_Icons.woff2 differ diff --git a/static/fonts/Material_Icons_Outlined.woff2 b/static/fonts/Material_Icons_Outlined.woff2 new file mode 100644 index 0000000..d44b948 Binary files /dev/null and b/static/fonts/Material_Icons_Outlined.woff2 differ diff --git a/static/fonts/Material_Icons_Round.woff2 b/static/fonts/Material_Icons_Round.woff2 new file mode 100644 index 0000000..e9e305f Binary files /dev/null and b/static/fonts/Material_Icons_Round.woff2 differ diff --git "a/static/music/40mP \345\210\235\351\237\263\343\203\237\343\202\257 - \346\201\213\346\204\233\350\243\201\345\210\244.flac" "b/static/music/40mP \345\210\235\351\237\263\343\203\237\343\202\257 - \346\201\213\346\204\233\350\243\201\345\210\244.flac" deleted file mode 100644 index 82ea6bc..0000000 Binary files "a/static/music/40mP \345\210\235\351\237\263\343\203\237\343\202\257 - \346\201\213\346\204\233\350\243\201\345\210\244.flac" and /dev/null differ diff --git a/static/plugin-sdk/plugin-host-sdk.js b/static/plugin-sdk/plugin-host-sdk.js new file mode 100644 index 0000000..94bd433 --- /dev/null +++ b/static/plugin-sdk/plugin-host-sdk.js @@ -0,0 +1,177 @@ +/** + * RustEchoMusic 插件宿主 SDK + * + * 注入到插件 iframe 内,封装 postMessage 协议为 Promise-based API。 + * 零依赖 IIFE,挂载到 window.rem。 + * 协议信封:{ source, type, id?, payload? } + */ +(function (window) { + 'use strict'; + + var HOST_SOURCE = 'rem-plugin-host'; + var PLUGIN_SOURCE = 'rem-plugin'; + var DEFAULT_TIMEOUT = 10000; // 请求默认超时 10s + + // 请求-响应配对表:id -> { resolve, reject, timer } + var pendingRequests = new Map(); + // 事件监听器:eventName -> Set + var listeners = new Map(); + // 握手信息(收到宿主 ready 后填充) + var readyInfo = null; + var readyResolvers = []; + + // 生成唯一请求 id,优先用 crypto.randomUUID,降级到时间戳+随机串 + function genId() { + if (window.crypto && typeof window.crypto.randomUUID === 'function') { + return window.crypto.randomUUID(); + } + return 'r_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10); + } + + // 向宿主发送消息信封 + function send(type, payload, id) { + var envelope = { source: PLUGIN_SOURCE, type: type }; + if (id !== undefined) envelope.id = id; + if (payload !== undefined) envelope.payload = payload; + window.parent.postMessage(envelope, '*'); + } + + // 发起请求并等待对应 result + function request(type, payload) { + return new Promise(function (resolve, reject) { + var id = genId(); + var timer = window.setTimeout(function () { + if (pendingRequests.has(id)) { + pendingRequests.delete(id); + reject(new Error('请求超时: ' + type + ' (' + id + ')')); + } + }, DEFAULT_TIMEOUT); + pendingRequests.set(id, { resolve: resolve, reject: reject, timer: timer }); + send(type, payload, id); + }); + } + + // 按 kind 派发事件给监听器,单个异常不影响其他 + function dispatch(kind, data) { + var set = listeners.get(kind); + if (!set) return; + set.forEach(function (cb) { + try { cb(data); } catch (e) { /* swallow */ } + }); + } + + // 处理请求响应:command 始终 { ok, data|error };其余兼容裸值与错误信封 + function handleResult(data) { + var pending = pendingRequests.get(data.id); + if (!pending) return; + window.clearTimeout(pending.timer); + pendingRequests.delete(data.id); + var payload = data.payload; + + if (data.type === 'command:result') { + if (payload && payload.ok === false) { + pending.reject(new Error(payload.error || '命令执行失败')); + } else { + pending.resolve(payload ? payload.data : payload); + } + return; + } + // settings:get / settings:set / state:get:裸值原样返回,错误信封 reject + if (payload && typeof payload === 'object' && payload.ok === false) { + pending.reject(new Error(payload.error || ('请求失败: ' + data.type))); + return; + } + pending.resolve(payload); + } + + // 处理宿主 → 插件消息 + function onMessage(event) { + var data = event.data; + if (!data || data.source !== HOST_SOURCE) return; + + switch (data.type) { + case 'ready': + readyInfo = data.payload || {}; + readyResolvers.forEach(function (r) { r.resolve(readyInfo); }); + readyResolvers = []; + dispatch('ready', readyInfo); + break; + case 'event': { + var p = data.payload || {}; + dispatch(p.kind, p.data); + break; + } + case 'command:result': + case 'settings:get:result': + case 'settings:set:result': + case 'state:get:result': + handleResult(data); + break; + default: + break; // 未知类型忽略 + } + } + + window.addEventListener('message', onMessage); + + var sdk = { + // 等待宿主握手,返回 { pluginId, capabilities } + ready: function () { + return new Promise(function (resolve) { + if (readyInfo) { + resolve(readyInfo); + } else { + readyResolvers.push({ resolve: resolve }); + } + }); + }, + + // 执行宿主命令:command 名 + args + command: function (command, args) { + return request('command', { command: command, args: args }); + }, + + settings: { + get: function (key) { + return request('settings:get', { key: key }); + }, + set: function (key, value) { + return request('settings:set', { key: key, value: value }); + } + }, + + state: { + get: function (kind) { + return request('state:get', { kind: kind }); + } + }, + + // 订阅事件:ready | trackChanged | playbackStateChanged | queueChanged | settingsChanged + on: function (eventName, callback) { + if (!listeners.has(eventName)) listeners.set(eventName, new Set()); + listeners.get(eventName).add(callback); + if (eventName !== 'ready' && listeners.get(eventName).size === 1) { + // 首次监听非 ready 事件时通知宿主 + send('subscribe', { kind: eventName }); + } else if (eventName === 'ready' && readyInfo) { + // 已握手完成则异步补发,避免错过 ready + Promise.resolve().then(function () { callback(readyInfo); }); + } + return sdk; + }, + + off: function (eventName, callback) { + var set = listeners.get(eventName); + if (set) { + set.delete(callback); + if (set.size === 0 && eventName !== 'ready') { + listeners.delete(eventName); + send('unsubscribe', { kind: eventName }); + } + } + return sdk; + } + }; + + window.rem = sdk; +})(window); diff --git a/static/svelte.svg b/static/svelte.svg deleted file mode 100644 index c5e0848..0000000 --- a/static/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/tauri.svg b/static/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/static/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/static/vite.svg b/static/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/static/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 194baa0..2b4cb12 100644 --- a/vite.config.js +++ b/vite.config.js @@ -26,8 +26,8 @@ export default defineConfig(async () => ({ } : undefined, watch: { - // 3. tell Vite to ignore watching `src-tauri` - ignored: ['**/src-tauri/**'], + // 3. tell Vite to ignore watching `src-tauri` and `target` + ignored: ['**/src-tauri/**', '**/target/**'], }, }, }))