mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
128
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9de420d57 | ||
|
|
ab7359a4c5 | ||
|
|
97b5138672 | ||
|
|
579109a478 | ||
|
|
5f7493491f | ||
|
|
755825b09a | ||
|
|
06b7c44dcc | ||
|
|
e087e571f3 | ||
|
|
fbdde8f3d8 | ||
|
|
290bd2b934 | ||
|
|
9ecbe8eaa6 | ||
|
|
549b333eb2 | ||
|
|
4b82cca3bf | ||
|
|
734fd9cdb3 | ||
|
|
2fd4495104 | ||
|
|
552bc3f739 | ||
|
|
4758cabbf9 | ||
|
|
6ec979a076 | ||
|
|
c34bd26f87 | ||
|
|
2a43792fd9 | ||
|
|
2d567b93ce | ||
|
|
26e05ee9e5 | ||
|
|
ca8c5a717f | ||
|
|
759f67a551 | ||
|
|
66628fd69d | ||
|
|
1ffcd67562 | ||
|
|
fda5c62bc6 | ||
|
|
f8f05ce91b | ||
|
|
8b2438aebb | ||
|
|
07bac8ffde | ||
|
|
d0856d46f2 | ||
|
|
e8aee15664 | ||
|
|
98cb6b6b44 | ||
|
|
f27387462e | ||
|
|
ef90f08813 | ||
|
|
9fd45ef2ce | ||
|
|
92971a3e0d | ||
|
|
357224f22a | ||
|
|
e2612d1a33 | ||
|
|
318840e403 | ||
|
|
1a56233c96 | ||
|
|
ff8c4f16a9 | ||
|
|
757ec9ab53 | ||
|
|
f38d351d32 | ||
|
|
b78dd9bf1b | ||
|
|
5072c1d298 | ||
|
|
e4cbec6b2c | ||
|
|
5ef999bf8a | ||
|
|
b0394de164 | ||
|
|
9181c155e0 | ||
|
|
2e8aaa31be | ||
|
|
bca5303ae3 | ||
|
|
75335647ee | ||
|
|
a71a361e0f | ||
|
|
6ed044a54b | ||
|
|
3fa6905006 | ||
|
|
d344cbcb7a | ||
|
|
204f4da33f | ||
|
|
c57e88ecd4 | ||
|
|
feb3c3ee31 | ||
|
|
09f2af61dc | ||
|
|
61b52ecd2c | ||
|
|
9625740906 | ||
|
|
f234b7a0ac | ||
|
|
8aa22b9bed | ||
|
|
b8d97268cb | ||
|
|
c671ca52f6 | ||
|
|
a89711841d | ||
|
|
0038ea8b15 | ||
|
|
c0e6f094d9 | ||
|
|
1c574a1f2d | ||
|
|
bca8b1a02b | ||
|
|
5148e8ec35 | ||
|
|
cc53d9a6f8 | ||
|
|
474c6560fc | ||
|
|
f42a3bfde3 | ||
|
|
540b3269df | ||
|
|
36857a167b | ||
|
|
3487523f74 | ||
|
|
bb2193b6f5 | ||
|
|
6cfe999cb3 | ||
|
|
893ab49fdf | ||
|
|
fe0c97190a | ||
|
|
99377f8436 | ||
|
|
4c3abc62a8 | ||
|
|
af7336fffb | ||
|
|
cfea3aba34 | ||
|
|
6715a9c679 | ||
|
|
c3249ee51d | ||
|
|
2e43f6a42b | ||
|
|
779edf0333 | ||
|
|
ae13d37edc | ||
|
|
1e49d7da7a | ||
|
|
8b753a52ad | ||
|
|
a53c9c3771 | ||
|
|
919b235621 | ||
|
|
18b0b2848d | ||
|
|
65a3ac4b34 | ||
|
|
295db3fb7a | ||
|
|
7abe6f8c73 | ||
|
|
c5ce5bc9b3 | ||
|
|
572b8027ff | ||
|
|
0a53211389 | ||
|
|
22b2b7746e | ||
|
|
39b3e20d0a | ||
|
|
3892b62451 | ||
|
|
5ffffec2e5 | ||
|
|
a0724cc5fb | ||
|
|
a0320f425e | ||
|
|
06262e7bb1 | ||
|
|
9ccaab5fad | ||
|
|
39745086fa | ||
|
|
a8dcfa8a5d | ||
|
|
0f6a74b87f | ||
|
|
d5b763c8e3 | ||
|
|
7a98f45c3d | ||
|
|
0ea47ad7ef | ||
|
|
cc804625ec | ||
|
|
20235b7d32 | ||
|
|
920dc8dcbb | ||
|
|
30f64c0020 | ||
|
|
cfafa25ed4 | ||
|
|
7495b99ddd | ||
|
|
800271a606 | ||
|
|
36791a33d8 | ||
|
|
20ec70b917 | ||
|
|
ab6edbee48 | ||
|
|
b17523e7fd |
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: api-module
|
||||
description: Add an API endpoint module to packages/api-client from an OpenAPI schema. Use for new backend endpoints, API client modules, or tasks that provide an OpenAPI schema.
|
||||
---
|
||||
|
||||
# Add an API Module
|
||||
|
||||
Read the applicable `AGENTS.md` files before you edit code.
|
||||
|
||||
Read [the API module standard](../../../standards/frontend/ADDING_API_MODULES.md) in full.
|
||||
|
||||
1. Identify the OpenAPI schema from the request. If more than one schema is possible, ask the user to select one.
|
||||
2. Read the schema. Identify each endpoint, HTTP method, request type, response type, and path parameter.
|
||||
3. Get the service and version from the URL prefix. For example, map `/v3/projects` to `labrinth/v3/`.
|
||||
4. Define the API types in `types.ts`. Make each type match the schema exactly.
|
||||
5. Do not change, rename, or remove API fields.
|
||||
6. Make a module class that extends `AbstractModule`. Implement each endpoint with `this.client.request()` or `this.client.upload()`.
|
||||
7. Use the request-option pattern from the standard. Do not call `$fetch`, `fetch`, or another HTTP client directly.
|
||||
8. Add the module to `MODULE_REGISTRY` so the client can instantiate it.
|
||||
9. Export new service types from the applicable barrel `index.ts`.
|
||||
10. Check the module paths, registry key, public type exports, and endpoint types.
|
||||
|
||||
Run only the checks that the user or the applicable `AGENTS.md` permits.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Add API Module"
|
||||
short_description: "Add typed API client modules from OpenAPI"
|
||||
default_prompt: "Use $api-module to add an API client module from this OpenAPI schema."
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: cross-platform-pages
|
||||
description: Convert a page to the shared Modrinth page system for the website and desktop app. Use for shared layouts, wrapped layouts, or platform dependency-injection contracts.
|
||||
---
|
||||
|
||||
# Convert a Cross-Platform Page
|
||||
|
||||
Read the applicable `AGENTS.md` files before you edit code.
|
||||
|
||||
Read these standards in full:
|
||||
|
||||
- [Cross-platform pages](../../../standards/frontend/CROSS_PLATFORM_PAGES.md)
|
||||
- [Dependency injection](../../../standards/frontend/DEPENDENCY_INJECTION.md)
|
||||
|
||||
1. Identify the target page from the request.
|
||||
2. Read the page and its route shell. Identify data sources, mutations, navigation, and platform APIs.
|
||||
3. Use a wrapped layout when both platforms use the same API source and page logic.
|
||||
4. Use a shared layout when platform data or operations have different implementations.
|
||||
|
||||
For a shared layout:
|
||||
|
||||
1. Define a provider contract for all platform operations.
|
||||
2. Put common UI and state logic in the shared layout.
|
||||
3. Put reusable search, filter, and selection logic in local composables.
|
||||
4. Implement the contract in `apps/frontend/` and `apps/app-frontend/`.
|
||||
5. Use optional contract fields only for capabilities that are not available on both platforms.
|
||||
|
||||
For a wrapped layout:
|
||||
|
||||
1. Move the page to `packages/ui/src/layouts/wrapped/` and preserve its route structure.
|
||||
2. Replace platform-only imports with common utilities or provider calls.
|
||||
3. Make each frontend route shell render the wrapped component.
|
||||
4. Match primary query options in both route shells when the layout uses `ReadyTransition` and `useReadyState`.
|
||||
5. Prefetch these queries with `ensureQueryData`, as the standard specifies.
|
||||
|
||||
Check that both route shells resolve their imports. Check that all required provider fields have implementations.
|
||||
|
||||
Run only the checks that the user or the applicable `AGENTS.md` permits.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Convert Cross-Platform Page"
|
||||
short_description: "Share pages across the web and desktop app"
|
||||
default_prompt: "Use $cross-platform-pages to convert this page for the website and desktop app."
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: figma-mcp
|
||||
description: Convert a Figma design into a Modrinth Vue page or component. Use when a request provides a Figma URL or asks to implement a Figma layout.
|
||||
---
|
||||
|
||||
# Implement a Figma Design
|
||||
|
||||
Read the applicable `AGENTS.md` files before you edit code.
|
||||
|
||||
Read `packages/ui/AGENTS.md` in full.
|
||||
|
||||
1. Load the available Figma design-to-code instructions and follow the MCP tool guidance.
|
||||
2. Call `get_design_context` first with `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`.
|
||||
3. Treat the result as reference code and adapt it to the Modrinth codebase.
|
||||
4. Map Figma color variables to the applicable `surface-*` and `text-*` tokens. Do not use aliased Figma names directly.
|
||||
5. Reuse applicable components from `packages/ui/src/components/` before creating new ones. Also refer to `standards/frontend/COMPONENT_STRUCTURE.md`
|
||||
6. Read `packages/assets/styles/variables.scss` when Figma does not supply a required token.
|
||||
7. Use exact spacing values from the design.
|
||||
8. Implement the result as a Vue SFC with Tailwind classes and the existing component library.
|
||||
|
||||
Run only the checks that the user or the applicable `AGENTS.md` permits.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Implement Figma Design"
|
||||
short_description: "Build Modrinth Vue UI from Figma designs"
|
||||
default_prompt: "Use $figma-mcp to implement this Figma design as a Modrinth Vue component."
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: i18n-pass
|
||||
description: Convert hard-coded English text in changed Vue components to the @modrinth/ui localization system. Use for an i18n pass, untranslated-string review, pull request, or component migration.
|
||||
---
|
||||
|
||||
# Do an Internationalization Pass
|
||||
|
||||
Read the applicable `AGENTS.md` files before you edit code.
|
||||
|
||||
Read [the internationalization standard](../../../standards/frontend/INTERNATIONALIZATION.md) in full.
|
||||
|
||||
1. Identify the scope from the request.
|
||||
2. For a pull request, use `gh pr diff <number>` to identify changed files.
|
||||
3. For a file path, inspect that file.
|
||||
4. When the request gives no scope, inspect the current uncommitted diff.
|
||||
5. Limit the pass to changed `.vue` files.
|
||||
6. Find user-visible text in templates and scripts.
|
||||
|
||||
Check inner text, `alt`, `placeholder`, `aria-label`, buttons, tooltips, notifications, dropdown labels, and error messages.
|
||||
|
||||
Do not change dynamic expressions, HTML tag names, CSS classes, internal identifiers, or log messages.
|
||||
|
||||
1. Define stable message IDs with `defineMessage` or `defineMessages`.
|
||||
2. Replace simple text with `formatMessage()` calls.
|
||||
3. Use `<IntlFormatted>` for text that contains links or markup.
|
||||
4. Use ICU selections and plurals when grammar depends on a value.
|
||||
5. Add a space before `}}` when an ICU placeholder ends at the Vue delimiter.
|
||||
6. Do not change component logic, layout, or reactivity.
|
||||
7. Do not edit localization JSON files. The user maintains those files.
|
||||
8. Check the changed templates again for hard-coded English text.
|
||||
|
||||
Run only the checks that the user or the applicable `AGENTS.md` permits.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Run Internationalization Pass"
|
||||
short_description: "Localize user-visible text in Vue files"
|
||||
default_prompt: "Use $i18n-pass to localize the user-visible text in these changed Vue files."
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: review-changelog
|
||||
description: Review the latest packages/blog/changelog.ts entry against the Modrinth changelog standard. Use before a pull request or when asked to review or lint a changelog entry.
|
||||
---
|
||||
|
||||
# Review a Changelog Entry
|
||||
|
||||
Read [the changelog standard](../../../standards/maintaining/CHANGELOG.md) in full before the review.
|
||||
|
||||
1. Open `packages/blog/changelog.ts`.
|
||||
2. Find the first entry in the `VERSIONS` array.
|
||||
3. If the request names `web`, `hosting`, or `app`, review the latest entry for that product.
|
||||
4. Otherwise, review the latest entry and all adjacent entries with the same date.
|
||||
|
||||
Check the entry structure:
|
||||
|
||||
- `date` contains a valid ISO 8601 timestamp.
|
||||
- `product` is `web`, `hosting`, or `app`.
|
||||
- An `app` entry has a `version` value.
|
||||
- A `web` or `hosting` entry does not have a `version` value.
|
||||
- Standard headings are `## Added`, `## Changed`, `## Fixed`, and `## Security`.
|
||||
- A featured release can use a linked heading.
|
||||
- Flag the legacy `## Improvements` heading.
|
||||
|
||||
Check each bullet:
|
||||
|
||||
- The voice and tense agree with the section.
|
||||
- The first verb agrees with the section.
|
||||
- The bullet describes user-visible behavior, not implementation.
|
||||
- The bullet identifies the applicable page, tab, modal, or feature.
|
||||
- The bullet contains one sentence, uses sentence case, and ends with a period.
|
||||
- Product and UI names use the public labels.
|
||||
- The bullet does not contain filler, vague intensifiers, apologies, or internal references.
|
||||
- The bullet is not a duplicate detail of a larger listed change.
|
||||
|
||||
Group findings by entry. For each finding, show the original bullet and a proposed replacement.
|
||||
|
||||
If the entry has no findings, state this result. Do not edit the changelog unless the user asks you to apply fixes.
|
||||
|
||||
When the user asks for fixes, preserve tab indentation and template-literal formatting.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Review Changelog"
|
||||
short_description: "Review changelog entries for style problems"
|
||||
default_prompt: "Use $review-changelog to review the latest changelog entry."
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: tanstack-query
|
||||
description: Convert Vue server-state code to TanStack Query. Use for useQuery, useMutation, cache invalidation, optimistic updates, or replacement of useAsyncData and manual ref patterns.
|
||||
---
|
||||
|
||||
# Convert Data Code to TanStack Query
|
||||
|
||||
Read the applicable `AGENTS.md` files before you edit code.
|
||||
|
||||
Read [the TanStack Query standard](../../../standards/frontend/FETCHING_DATA.md) in full.
|
||||
|
||||
1. Identify the target file from the request.
|
||||
2. Find `useAsyncData`, `useFetch`, manual API refs, and fetch calls in `onMounted`.
|
||||
3. Identify mutations that use manual loading, error, or result refs.
|
||||
|
||||
For queries:
|
||||
|
||||
1. Replace manual fetch logic with `useQuery`.
|
||||
2. Get `api-client` with `injectModrinthClient()`.
|
||||
3. Use a hierarchical query key with the resource, qualifier, and parameters.
|
||||
4. Use a computed query key for reactive parameters.
|
||||
5. Use a computed `enabled` option when the query depends on other data.
|
||||
6. Use a shared query-option factory when multiple components use the query.
|
||||
|
||||
For mutations:
|
||||
|
||||
1. Replace manual mutation state with `useMutation`.
|
||||
2. Invalidate or update related query data after success.
|
||||
3. Use an optimistic update only when the UI needs an immediate response.
|
||||
4. Cancel the applicable query and save its prior data before an optimistic update.
|
||||
5. Restore the prior data after an error. Invalidate the query after settlement.
|
||||
|
||||
Remove manual loading and error refs that TanStack Query replaces. Remove obsolete `onMounted` fetch calls.
|
||||
|
||||
Keep Nuxt SSR behavior. Match route-shell prefetch options when `ReadyTransition` and `useReadyState` depend on the query.
|
||||
|
||||
Check query keys, invalidation prefixes, reactive values, and rollback data.
|
||||
|
||||
Run only the checks that the user or the applicable `AGENTS.md` permits.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Migrate to TanStack Query"
|
||||
short_description: "Migrate Vue server state to TanStack Query"
|
||||
default_prompt: "Use $tanstack-query to migrate this Vue component to TanStack Query."
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: api-module
|
||||
description: Add a new API endpoint module to packages/api-client from an OpenAPI schema. Use when adding new backend endpoints, creating API client modules, or when an openapi.yml is provided.
|
||||
argument-hint: <path-to-openapi.yml>
|
||||
---
|
||||
|
||||
Refer to the standard: @standards/frontend/ADDING_API_MODULES.md
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read the OpenAPI schema** at `$ARGUMENTS` — identify the endpoints, request/response shapes, and path parameters.
|
||||
2. **Read the standard above** for naming conventions, type rules, and the module registration pattern.
|
||||
3. **Determine the service and version** — the URL path prefix tells you which service directory and version namespace to use (e.g. `/v3/projects` → `labrinth/v3/`).
|
||||
4. **Define types in `types.ts`** — types must match the API response 1:1. Use the OpenAPI schema as the source of truth. Do not reshape or rename fields.
|
||||
5. **Create the module class** — extend `BaseModule`, implement each endpoint as a method. Use the correct HTTP verb and request options pattern from the standard.
|
||||
6. **Register in `MODULE_REGISTRY`** — add the module entry so it's auto-instantiated on the client.
|
||||
7. **Export types** from the service's barrel `index.ts`.
|
||||
8. **Verify** — check that the module compiles and the types are accessible from `@modrinth/api-client`.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: cross-platform-pages
|
||||
description: Convert a page to the cross-platform page system so it works in both the website and the desktop app. Use when moving a page into packages/ui/src/layouts/, creating shared or wrapped layouts, or setting up DI contracts for platform abstraction.
|
||||
argument-hint: <path-to-page>
|
||||
---
|
||||
|
||||
Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standards/frontend/DEPENDENCY_INJECTION.md
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read the target page** at `$ARGUMENTS` and understand its data sources, mutations, and navigation.
|
||||
2. **Read the standards above** to understand the shared vs wrapped distinction and the DI pattern.
|
||||
3. **Decide the category:**
|
||||
- **Wrapped** (`layouts/wrapped/`) — if the page uses the same API source on both platforms (e.g. web requests, not Tauri plugins). Just move the page component into `packages/ui` and import it from both frontends.
|
||||
- **Shared** (`layouts/shared/`) — if the page has different data-fetching logic per platform (e.g. website uses `api-client`, app uses Tauri `invoke`). Requires a DI contract.
|
||||
4. **For shared layouts:**
|
||||
- Define a DI contract interface in `providers/` capturing all platform-specific operations.
|
||||
- Create the layout component that injects the context and handles all UI logic.
|
||||
- Extract reusable stateful logic (search, filtering, selection) into `composables/`.
|
||||
- Implement the contract separately in each frontend (`apps/frontend/`, `apps/app-frontend/`).
|
||||
5. **For wrapped pages:**
|
||||
- Move the page component into `packages/ui/src/layouts/wrapped/` matching the route structure.
|
||||
- Replace any platform-specific imports with shared utilities.
|
||||
- Import and render the wrapped page from both frontends as a simple component.
|
||||
- If the layout uses TanStack Query for initial route paint with `ReadyTransition` / `useReadyState`, each platform route shell must call `ensureQueryData` for those queries with matching keys and fetchers — see **Platform route shells: prefetch with `ensureQueryData`** in `standards/frontend/CROSS_PLATFORM_PAGES.md`.
|
||||
6. **Verify** the page renders correctly by checking for missing imports and that all DI contracts are satisfied.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: figma-mcp
|
||||
description: Use the Figma MCP server to translate a Figma design into a Vue page or component layout. Use when the user provides a Figma URL, asks to implement a design, or wants to draft a page layout from Figma.
|
||||
argument-hint: <figma-url>
|
||||
---
|
||||
|
||||
Refer to the standard: @standards/frontend/FIGMA_MCP_USAGE.md
|
||||
Also read @packages/ui/CLAUDE.md for color token mapping and component conventions.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Parse the Figma URL** from `$ARGUMENTS` — extract the `fileKey` and `nodeId`. Convert `-` to `:` in the node ID.
|
||||
2. **Read the standards above** for the available tools, adaptation rules, and color usage.
|
||||
3. **Call `get_design_context`** with the extracted `nodeId` and `fileKey`, using `clientLanguages: "typescript,html,css"` and `clientFrameworks: "vue"`. This is always the first tool to call.
|
||||
5. **Adapt the output to the Modrinth codebase:**
|
||||
- Map Figma color variables to `surface-*` / `text-*` tokens — never use Figma's aliased names directly.
|
||||
- Check `packages/ui/src/components/` for existing components that match elements in the design (buttons, cards, modals, inputs, etc.).
|
||||
- Check `packages/assets/styles/variables.scss` for tokens not exposed in Figma.
|
||||
- Match spacing values exactly from the design.
|
||||
6. **Use `get_screenshot`** if you need a closer visual reference of specific nodes.
|
||||
7. **Use `get_variable_defs`** to verify which design tokens are applied to ambiguous elements.
|
||||
8. **Build the component** as a Vue SFC using Tailwind classes and the project's existing component library.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: i18n-pass
|
||||
description: Perform an i18n localization pass on changed files or a pull request, converting hard-coded English strings to the @modrinth/ui i18n system. Use when internationalizing a set of changes, reviewing a PR for untranslated strings, or converting a specific component.
|
||||
argument-hint: [file-path-or-pr-number]
|
||||
---
|
||||
|
||||
Refer to the standard: @standards/frontend/INTERNATIONALIZATION.md
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Identify the scope of changes:**
|
||||
- If `$ARGUMENTS` is a PR number, run `gh pr diff $ARGUMENTS` to get the changed files.
|
||||
- If `$ARGUMENTS` is a file path, use that directly.
|
||||
- If no argument, check `git diff` for uncommitted changes.
|
||||
2. **Read the standard above** for the message definition pattern, ICU format rules, and `IntlFormatted` usage.
|
||||
3. **Filter to Vue SFCs** — only `.vue` files need i18n passes. Skip non-component files.
|
||||
4. **For each file, scan for hard-coded strings:**
|
||||
- `<template>`: inner text, `alt`, `placeholder`, `aria-label`, button labels, tooltip text.
|
||||
- `<script>`: string literals passed to user-visible UI (notification messages, dropdown labels, error messages).
|
||||
- Skip: dynamic expressions, HTML tag names, CSS classes, internal identifiers, log messages.
|
||||
5. **Define messages** with `defineMessages` — use descriptive, stable `id`s based on the component's domain (e.g. `project.settings.title`).
|
||||
6. **Replace strings in templates** with `formatMessage()` calls, or `<IntlFormatted>` for strings containing links or markup.
|
||||
7. **Handle ICU edge cases** — add a space before `}}` if an ICU placeholder ends at a Vue template delimiter boundary.
|
||||
8. **Verify** no hard-coded English strings remain in the changed templates. Do not alter logic, layout, or reactivity.
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: review-changelog
|
||||
description: Review the latest changelog entry in packages/blog/changelog.ts against the project's changelog style guide and flag bullets that need rewriting. Use when checking a freshly added changelog entry before opening a PR, or when the user asks to review/lint the latest changelog.
|
||||
argument-hint: [product?]
|
||||
---
|
||||
|
||||
Refer to the standard: @standards/maintaining/CHANGELOG.md
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Locate the latest entry:**
|
||||
- Open `packages/blog/changelog.ts`.
|
||||
- The latest entries are at the top of the `VERSIONS` array.
|
||||
- If `$ARGUMENTS` specifies a product (`web`, `hosting`, `app`), review the most recent entry for that product. Otherwise, review the most recent entry overall, plus any sibling entries sharing the same `date` (coordinated releases ship together).
|
||||
|
||||
2. **Read the standard above** in full before reviewing. The bullet rules, section/verb agreement, and "Don't" list are the source of truth.
|
||||
|
||||
3. **Check the entry shell:**
|
||||
- `date` is a valid ISO 8601 timestamp.
|
||||
- `product` is one of `web`, `hosting`, `app`.
|
||||
- `version` is present for `app` entries and omitted for `web`/`hosting`.
|
||||
- Section headings use `## Added`, `## Changed`, `## Fixed`, `## Security` (or a featured-release linked heading). Flag legacy `## Improvements`.
|
||||
|
||||
4. **Review each bullet** against the standard. For each bullet, check:
|
||||
- Voice/tense matches the section heading.
|
||||
- Opening verb agrees with its section.
|
||||
- Describes observable behavior, not implementation.
|
||||
- Specific enough to identify the surface (names the tab/page/modal).
|
||||
- One sentence, ends with a period, sentence case.
|
||||
- Uses branded names (Modrinth App, Modrinth Hosting) correctly.
|
||||
- No filler ("issue with", "issue where", "various", "some"), no vague intensifiers, no apologies, no PR/commit references (unless crediting a third-party contributor with a linked GitHub profile).
|
||||
- Not a duplicate sub-fix of a bigger change already listed.
|
||||
|
||||
5. **Report findings** as a short list grouped by entry. For each problem bullet, show the original line and a suggested rewrite. If the entry is clean, say so explicitly. Do not edit the file unless the user asks - this skill is a review pass, not a rewrite pass.
|
||||
|
||||
6. **If the user then asks to apply fixes**, edit `packages/blog/changelog.ts` directly using the suggested rewrites. Preserve tab indentation and template literal formatting.
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: tanstack-query
|
||||
description: Convert a page or component from useAsyncData/manual ref patterns to TanStack Query for server state management. Use when migrating data fetching to useQuery/useMutation, adding cache invalidation, or replacing useAsyncData with TanStack Query.
|
||||
argument-hint: <path-to-file>
|
||||
---
|
||||
|
||||
Refer to the standard: @standards/frontend/FETCHING_DATA.md
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Read the target file** at `$ARGUMENTS` and identify all data-fetching patterns: `useAsyncData`, `useFetch`, manual `ref()` + `await`, or `onMounted` fetch calls.
|
||||
2. **Read the standard above** for the query/mutation patterns, query key conventions, and optimistic update approach.
|
||||
3. **Convert queries:**
|
||||
- Replace `useAsyncData` / `useFetch` / manual fetches with `useQuery`.
|
||||
- Use the `api-client` via `injectModrinthClient()` for the `queryFn`.
|
||||
- Design query keys with the `['resource', 'version', ...params]` convention.
|
||||
- Use `computed` query keys for reactive parameters.
|
||||
- Use the `enabled` option for conditional queries that depend on other data.
|
||||
4. **Convert mutations:**
|
||||
- Replace manual `try/catch` + `ref` patterns with `useMutation`.
|
||||
- Add `onSuccess` handlers that invalidate or update related query caches.
|
||||
- Consider optimistic updates for UI-critical mutations (follow the pattern in the standard).
|
||||
5. **Clean up:**
|
||||
- Remove manual loading/error `ref()`s that are now handled by TanStack Query's return values (`isPending`, `isError`, `error`).
|
||||
- Remove manual `onMounted` fetch calls.
|
||||
- Ensure SSR compatibility — queries in Nuxt pages are automatically awaited during SSR.
|
||||
6. **Verify** the page still renders correctly and that cache invalidation triggers re-fetches where expected.
|
||||
@@ -34,13 +34,13 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract PR Number and Commit ID
|
||||
id: extract-pr-info
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const githubRef = process.env.GITHUB_REF;
|
||||
@@ -90,20 +90,26 @@ runs:
|
||||
- name: Print PR Branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }}
|
||||
run: |
|
||||
echo "PR Branch: ${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||
echo "PR Branch: $PR_BRANCH"
|
||||
|
||||
- name: Check if PR branch contains the Merge Queue target commit ID
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }}
|
||||
COMMIT_ID: ${{ steps.extract-pr-info.outputs.commitId }}
|
||||
TARGET_BRANCH: ${{ steps.extract-pr-info.outputs.targetBranchName }}
|
||||
run: |
|
||||
# Get the branch name from previous steps
|
||||
branch_name="origin/${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||
commit_id="${{ steps.extract-pr-info.outputs.commitId }}"
|
||||
branch_name="origin/$PR_BRANCH"
|
||||
commit_id="$COMMIT_ID"
|
||||
|
||||
# Check if the branch history contains the commit
|
||||
if git branch -r --contains "$commit_id" | grep -q "$branch_name"; then
|
||||
echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with ${{ steps.extract-pr-info.outputs.targetBranchName }}."
|
||||
if git branch -r --contains "$commit_id" | grep -qF "$branch_name"; then
|
||||
echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with $TARGET_BRANCH."
|
||||
else
|
||||
echo "Branch '$branch_name' does not contain commit '$commit_id'. It is outdated. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
@@ -112,8 +118,10 @@ runs:
|
||||
- name: Compare PR Branch with Current Branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
env:
|
||||
PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }}
|
||||
run: |
|
||||
if git diff --quiet "origin/${{ steps.get-pr-branch.outputs.prBranch }}"; then
|
||||
if git diff --quiet "origin/$PR_BRANCH"; then
|
||||
echo "No differences found. PR branch is identical with this merge queue branch."
|
||||
else
|
||||
echo "Differences detected. PR branch has been updated after PR was added to merge queue. Setting CAN_SKIP_CHECKS to false."
|
||||
@@ -122,9 +130,11 @@ runs:
|
||||
|
||||
- name: Compute/publish skip result
|
||||
id: passed-checks
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
env:
|
||||
SECRET: ${{ inputs.secret }}
|
||||
PR_BRANCH: ${{ steps.get-pr-branch.outputs.prBranch }}
|
||||
TARGET_BRANCH: ${{ steps.extract-pr-info.outputs.targetBranchName }}
|
||||
with:
|
||||
github-token: ${{ inputs.secret != '' && inputs.secret || github.token }}
|
||||
script: |
|
||||
@@ -144,7 +154,7 @@ runs:
|
||||
const { data: branchProtection } = await github.rest.repos.getBranchProtection({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: "${{ steps.extract-pr-info.outputs.targetBranchName }}",
|
||||
branch: process.env.TARGET_BRANCH,
|
||||
});
|
||||
const requiredCheckNames = branchProtection.required_status_checks.contexts;
|
||||
console.log(`requiredCheckNames = ${requiredCheckNames}`);
|
||||
@@ -152,7 +162,7 @@ runs:
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: "refs/heads/${{ steps.get-pr-branch.outputs.prBranch }}",
|
||||
ref: `refs/heads/${process.env.PR_BRANCH}`,
|
||||
});
|
||||
|
||||
console.log(`checks.check_runs = ${checks.check_runs.map(check => `${check.status},${check.conclusion},${check.name};`)}`);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Deploy Command
|
||||
run-name: Deploy PR #${{ github.event.client_payload.github.payload.issue.number }}
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [deploy-command]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: ${{ github.event.client_payload.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: SparkUniverse/workflows/.github/workflows/deploy-command.yaml@main
|
||||
secrets:
|
||||
ARGOCD_DEPLOY_KEY: ${{ secrets.ARGOCD_DEPLOY_KEY }}
|
||||
CMD_DISPATCH_GH_TOKEN: ${{ secrets.SLASH_CMD_GH_TOKEN }}
|
||||
with:
|
||||
application-set: labrinth
|
||||
build-workflow: labrinth-build.yml
|
||||
branch: ${{ github.event.client_payload.pull_request.head.ref }}
|
||||
issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
|
||||
head-sha: ${{ github.event.client_payload.pull_request.head.sha }}
|
||||
@@ -1,19 +1,26 @@
|
||||
name: docker-build
|
||||
name: Labrinth Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'prod'
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- .github/workflows/labrinth-build.yml
|
||||
- 'apps/labrinth/**'
|
||||
- 'packages/**'
|
||||
- '!packages/api-client/**'
|
||||
- '!packages/app-lib/**'
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- .github/workflows/labrinth-build.yml
|
||||
- 'apps/labrinth/**'
|
||||
- 'packages/**'
|
||||
- '!packages/api-client/**'
|
||||
- '!packages/app-lib/**'
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
merge_group:
|
||||
@@ -69,7 +76,8 @@ jobs:
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
docker:
|
||||
build:
|
||||
name: Build Labrinth
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-modrinth-labrinth' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
@@ -120,42 +128,30 @@ jobs:
|
||||
cp -r apps/labrinth/migrations apps/labrinth/docker-stage/migrations
|
||||
cp -r apps/labrinth/assets apps/labrinth/docker-stage/assets
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
env:
|
||||
# GitHub Packages requires annotations metadata in at least the index descriptor to show them
|
||||
# up properly in its UI it seems, but it's not clear about it, because the docs refer to the
|
||||
# image manifest only. See:
|
||||
# https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#adding-a-description-to-multi-arch-images
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
- name: Upload Docker context
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
|
||||
with:
|
||||
images: ghcr.io/modrinth/labrinth
|
||||
labels: |
|
||||
org.opencontainers.image.title=labrinth
|
||||
org.opencontainers.image.description=Modrinth API
|
||||
org.opencontainers.image.licenses=AGPL-3.0-only
|
||||
annotations: |
|
||||
org.opencontainers.image.title=labrinth
|
||||
org.opencontainers.image.description=Modrinth API
|
||||
org.opencontainers.image.licenses=AGPL-3.0-only
|
||||
name: labrinth-docker-context
|
||||
retention-days: 1
|
||||
path: apps/labrinth/docker-stage
|
||||
|
||||
- name: Login to GitHub Packages
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
docker-build:
|
||||
needs: [skip-if-clean, build]
|
||||
if: ${{ needs.skip-if-clean.outputs.internal == 'true' }}
|
||||
uses: SparkUniverse/workflows/.github/workflows/docker-build.yaml@main
|
||||
with:
|
||||
image-name: labrinth
|
||||
dockerfile-path: apps/labrinth/Dockerfile
|
||||
artifacts-name: labrinth-docker-context
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: ./apps/labrinth/docker-stage
|
||||
file: ./apps/labrinth/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
annotations: ${{ steps.docker-meta.outputs.annotations }}
|
||||
deploy:
|
||||
needs: [skip-if-clean, docker-build]
|
||||
if: ${{ needs.skip-if-clean.outputs.internal == 'true' && github.ref == 'refs/heads/prod' }}
|
||||
uses: SparkUniverse/workflows/.github/workflows/argo-update.yaml@main
|
||||
secrets:
|
||||
ARGOCD_DEPLOY_KEY: ${{ secrets.ARGOCD_DEPLOY_KEY }}
|
||||
with:
|
||||
application-set: labrinth
|
||||
branch: ${{ github.ref == 'refs/heads/prod' && 'main' || 'develop' }}
|
||||
environment-name: ${{ github.ref == 'refs/heads/prod' && 'production' || 'staging' }}
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Slash Command Dispatch
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dispatch-command:
|
||||
if: ${{ github.event.sender.type == 'User' && contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association) }}
|
||||
runs-on: namespace-profile-tiny-arm64
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@9bdcd7914ec1b75590b790b844aa3b8eee7c683a # v5.0.2
|
||||
with:
|
||||
token: ${{ secrets.SLASH_CMD_GH_TOKEN }}
|
||||
issue-type: pull-request
|
||||
commands: |
|
||||
deploy
|
||||
@@ -74,10 +74,10 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||
with:
|
||||
rustflags: ''
|
||||
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
target: ${{ contains(matrix.artifact-target-name, 'darwin') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Setup mold
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
if: contains(matrix.artifact-target-name, 'linux')
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Setup sccache
|
||||
@@ -92,7 +92,6 @@ jobs:
|
||||
run: corepack enable
|
||||
|
||||
- name: Set up caches
|
||||
if: contains(matrix.platform, 'ubuntu') || contains(matrix.platform, 'macos')
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
cache: |
|
||||
@@ -100,7 +99,6 @@ jobs:
|
||||
pnpm
|
||||
|
||||
- name: Configure sccache
|
||||
if: contains(matrix.platform, 'ubuntu') || contains(matrix.platform, 'macos')
|
||||
run: nsc cache sccache setup --cache_name default >> "$GITHUB_ENV"
|
||||
|
||||
- name: Generate tauri-dev.conf.json
|
||||
@@ -119,7 +117,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
if: contains(matrix.artifact-target-name, 'linux')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
@@ -136,7 +134,7 @@ jobs:
|
||||
repo: TomWright/dasel
|
||||
tag: v2.8.1
|
||||
extension-matching: disable
|
||||
rename-to: ${{ contains(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
rename-to: ${{ contains(matrix.artifact-target-name, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
chmod: 0755
|
||||
|
||||
- name: Set application version and environment
|
||||
@@ -159,7 +157,7 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Set up Windows code signing
|
||||
if: contains(matrix.platform, 'windows')
|
||||
if: contains(matrix.artifact-target-name, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
|
||||
@@ -170,7 +168,7 @@ jobs:
|
||||
|
||||
- name: Build macOS app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
|
||||
if: contains(matrix.platform, 'macos')
|
||||
if: contains(matrix.artifact-target-name, 'darwin')
|
||||
env:
|
||||
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -185,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Build Linux app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
if: contains(matrix.artifact-target-name, 'linux')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
@@ -197,7 +195,7 @@ jobs:
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
|
||||
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
|
||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||
if: contains(matrix.platform, 'windows')
|
||||
if: contains(matrix.artifact-target-name, 'windows')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
+9
-2
@@ -59,14 +59,21 @@ apps/frontend/src/generated
|
||||
.turbo
|
||||
target
|
||||
generated
|
||||
!apps/app-frontend/src/generated/
|
||||
!apps/app-frontend/src/generated/app-events/
|
||||
!apps/app-frontend/src/generated/app-events/*.ts
|
||||
!apps/app-frontend/src/generated/app-events/README.md
|
||||
!apps/app-frontend/src/generated/app-events/postcard/
|
||||
!apps/app-frontend/src/generated/app-events/postcard/index.d.ts
|
||||
!apps/app-frontend/src/generated/app-events/postcard/index.js
|
||||
!apps/app-frontend/src/generated/app-events/postcard/package.json
|
||||
.env
|
||||
|
||||
# app testing dir
|
||||
app-playground-data/*
|
||||
|
||||
.astro
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
.claude/
|
||||
.letta
|
||||
|
||||
# labrinth demo fixtures
|
||||
|
||||
Generated
+11
@@ -17,6 +17,17 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-maxmind/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-util/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/muralpay/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/async-minecraft-ping/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/async-minecraft-ping/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/async-minecraft-ping/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/labrinth-derive/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-content-management/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-content-management/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/neverbounce/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/serde-binhum/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/sqlx-tracing/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/sqlx-tracing/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/xredis/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
merge_queue:
|
||||
mode: parallel
|
||||
scopes:
|
||||
source:
|
||||
files:
|
||||
frontend:
|
||||
include:
|
||||
- apps/frontend/**/*
|
||||
- packages/ui/**/*
|
||||
- packages/utils/**/*
|
||||
- packages/assets/**/*
|
||||
- "**/wrangler.jsonc"
|
||||
- "**/pnpm-*.yaml"
|
||||
backend:
|
||||
include:
|
||||
- apps/labrinth/**
|
||||
- Cargo.toml
|
||||
app:
|
||||
include:
|
||||
- apps/app/**
|
||||
- apps/app-frontend/**
|
||||
- packages/app-lib/**
|
||||
- packages/app-macros/**
|
||||
@@ -0,0 +1,93 @@
|
||||
# Modrinth Monorepo
|
||||
|
||||
This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. When entering a project, either to edit or analyse, you should read its AGENTS.md.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`)
|
||||
- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3
|
||||
- **Backend:** Rust (Labrinth API), Postgres, Clickhouse
|
||||
- **Indentation:** Use TAB everywhere, never spaces
|
||||
|
||||
### Apps (`apps/`)
|
||||
|
||||
| App | Description |
|
||||
| ----------------- | ------------------------------ |
|
||||
| `frontend` | Main Modrinth website (Nuxt 3) |
|
||||
| `app-frontend` | Desktop/app frontend (Vue 3) |
|
||||
| `app` | Desktop/app shell (Tauri) |
|
||||
| `app-playground` | Testing playground for app |
|
||||
| `labrinth` | Backend API service |
|
||||
| `daedalus_client` | Daedalus client implementation |
|
||||
| `docs` | Documentation site (Astro) |
|
||||
|
||||
### Packages (`packages/`)
|
||||
|
||||
| Package | Description |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| `ui` | Shared Vue component library (`@modrinth/ui`) |
|
||||
| `assets` | Styling and auto-generated icons (`@modrinth/assets`) |
|
||||
| `api-client` | API client for Nuxt, Tauri, and Node/browser |
|
||||
| `app-lib` | Shared app library |
|
||||
| `blog` | Blog system and changelog data |
|
||||
| `utils` | Shared utility functions (mostly deprecated) |
|
||||
| `moderation` | Moderation utilities |
|
||||
| `daedalus` | Daedalus protocol |
|
||||
| `tooling-config` | ESLint, Prettier, TypeScript configs |
|
||||
| `ariadne` | Analytics library |
|
||||
| `modrinth-log` | Logging utilities |
|
||||
| `modrinth-maxmind` | MaxMind GeoIP |
|
||||
| `modrinth-util` | General utilities |
|
||||
| `muralpay` | Payment processing |
|
||||
| `path-util` | Path utilities |
|
||||
| `sqlx-tracing` | SQLx query tracing |
|
||||
|
||||
## Pre-PR Commands
|
||||
|
||||
Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request.
|
||||
|
||||
- **Website:** `pnpm prepr:frontend:web`
|
||||
- **App frontend:** `pnpm prepr:frontend:app`
|
||||
- **Frontend libs:** `pnpm prepr:frontend:lib`
|
||||
- **All frontend (app+web):** `pnpm prepr`
|
||||
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
|
||||
|
||||
The website and app `prepr` commands
|
||||
|
||||
## Dev Commands
|
||||
|
||||
- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first)
|
||||
- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first)
|
||||
- **Storybook (packages/ui):** `pnpm storybook`
|
||||
|
||||
## Project-Specific Instructions
|
||||
|
||||
Each project may have its own file with detailed instructions:
|
||||
|
||||
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
|
||||
- [`apps/frontend/AGENTS.md`](apps/frontend/AGENTS.md) - Frontend Website
|
||||
|
||||
## Code Guidelines
|
||||
|
||||
### Comments
|
||||
- DO NOT use "heading" comments like: `=== Helper methods ===`.
|
||||
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!
|
||||
|
||||
## Bash Guidelines
|
||||
|
||||
### Output handling
|
||||
- DO NOT pipe output through `head`, `tail`, `less`, or `more`
|
||||
- NEVER use `| head -n X` or `| tail -n X` to truncate output
|
||||
- IMPORTANT: Run commands directly without pipes when possible
|
||||
- IMPORTANT: If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`)
|
||||
- ALWAYS read the full output — never pipe through filters
|
||||
|
||||
### General
|
||||
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to
|
||||
- For Frontend, when doing lint checks, only use the `prepr` commands, do not use `typecheck` or `tsc` etc.
|
||||
- Types in `@modrinth/utils` are considered highly outdated, if a component needs them, check if you can switch said component to use types from `packages/api-client`
|
||||
- When provided problems, do not say "I didn't introduce these problems" (shifting the blame/effort) - just fix them.
|
||||
|
||||
## Standards
|
||||
|
||||
Standards available at the @standards/ folder.
|
||||
@@ -1,110 +0,0 @@
|
||||
# Modrinth Monorepo
|
||||
|
||||
This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. When entering a project, either to edit or analyse, you should read it's CLAUDE.md.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`)
|
||||
- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3
|
||||
- **Backend:** Rust (Labrinth API), Postgres, Clickhouse
|
||||
- **Indentation:** Use TAB everywhere, never spaces
|
||||
|
||||
### Apps (`apps/`)
|
||||
|
||||
| App | Description |
|
||||
| ----------------- | ------------------------------ |
|
||||
| `frontend` | Main Modrinth website (Nuxt 3) |
|
||||
| `app-frontend` | Desktop/app frontend (Vue 3) |
|
||||
| `app` | Desktop/app shell (Tauri) |
|
||||
| `app-playground` | Testing playground for app |
|
||||
| `labrinth` | Backend API service |
|
||||
| `daedalus_client` | Daedalus client implementation |
|
||||
| `docs` | Documentation site (Astro) |
|
||||
|
||||
### Packages (`packages/`)
|
||||
|
||||
| Package | Description |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| `ui` | Shared Vue component library (`@modrinth/ui`) |
|
||||
| `assets` | Styling and auto-generated icons (`@modrinth/assets`) |
|
||||
| `api-client` | API client for Nuxt, Tauri, and Node/browser |
|
||||
| `app-lib` | Shared app library |
|
||||
| `blog` | Blog system and changelog data |
|
||||
| `utils` | Shared utility functions (mostly deprecated) |
|
||||
| `moderation` | Moderation utilities |
|
||||
| `daedalus` | Daedalus protocol |
|
||||
| `tooling-config` | ESLint, Prettier, TypeScript configs |
|
||||
| `ariadne` | Analytics library |
|
||||
| `modrinth-log` | Logging utilities |
|
||||
| `modrinth-maxmind` | MaxMind GeoIP |
|
||||
| `modrinth-util` | General utilities |
|
||||
| `muralpay` | Payment processing |
|
||||
| `path-util` | Path utilities |
|
||||
| `sqlx-tracing` | SQLx query tracing |
|
||||
|
||||
## Pre-PR Commands
|
||||
|
||||
Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request.
|
||||
|
||||
- **Website:** `pnpm prepr:frontend:web`
|
||||
- **App frontend:** `pnpm prepr:frontend:app`
|
||||
- **Frontend libs:** `pnpm prepr:frontend:lib`
|
||||
- **All frontend (app+web):** `pnpm prepr`
|
||||
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
|
||||
|
||||
The website and app `prepr` commands
|
||||
|
||||
## Dev Commands
|
||||
|
||||
- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first)
|
||||
- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first)
|
||||
- **Storybook (packages/ui):** `pnpm storybook`
|
||||
|
||||
## Project-Specific Instructions
|
||||
|
||||
Each project may have its own file with detailed instructions:
|
||||
|
||||
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
|
||||
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
|
||||
|
||||
## Code Guidelines
|
||||
|
||||
### Comments
|
||||
- DO NOT use "heading" comments like: `=== Helper methods ===`.
|
||||
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!
|
||||
|
||||
## Bash Guidelines
|
||||
|
||||
### Output handling
|
||||
- DO NOT pipe output through `head`, `tail`, `less`, or `more`
|
||||
- NEVER use `| head -n X` or `| tail -n X` to truncate output
|
||||
- IMPORTANT: Run commands directly without pipes when possible
|
||||
- IMPORTANT: If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`)
|
||||
- ALWAYS read the full output — never pipe through filters
|
||||
|
||||
### General
|
||||
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to
|
||||
- For Frontend, when doing lint checks, only use the `prepr` commands, do not use `typecheck` or `tsc` etc.
|
||||
- Types in `@modrinth/utils` are considered highly outdated, if a component needs them, check if you can switch said component to use types from `packages/api-client`
|
||||
- When provided problems, do not say "I didn't introduce these problems" (shifting the blame/effort) - just fix them.
|
||||
|
||||
## Edit Tool - Whitespace Handling (CLAUDE ONLY)
|
||||
|
||||
The Read tool uses `→` to mark where line numbers end and file content begins.
|
||||
|
||||
**Rule:** Copy the EXACT whitespace that appears after the `→` marker.
|
||||
- Whatever appears between `→` and the code text is what's actually in the file
|
||||
- That whitespace must be used EXACTLY in Edit tool's old_string
|
||||
- Don't count arrows, don't interpret - just copy what's after the `→`
|
||||
|
||||
**Example:**
|
||||
14→ private byte tag;
|
||||
For Edit, use: ` private byte tag;` (copy everything after →, including the two tabs)
|
||||
|
||||
**If Edit fails:** Stop and explain the problem. Do not attempt sed/awk/bash workarounds.
|
||||
|
||||
**IMPORTANT**: Trust the Read tool output. Copy what's after `→` into Edit immediately. DO NOT verify with sed/od/grep first - that's wasting time and the instructions already tell you to stop if Edit fails, not to pre-verify.
|
||||
|
||||
## Standards
|
||||
|
||||
Standards available at the @standards/ folder.
|
||||
Generated
+198
-84
@@ -2116,7 +2116,7 @@ checksum = "ff6669899e23cb87b43daf7996f0ea3b9c07d0fb933d745bb7b815b052515ae3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"serde_derive_internals 0.29.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -2333,18 +2333,18 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -4000,6 +4000,28 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "genco"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ab846431e5d637791b3279e7242fe2b21e11c3d8b4cf6a99f645c5f16ba7c0"
|
||||
dependencies = [
|
||||
"genco-macros",
|
||||
"relative-path",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "genco-macros"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42a1fe5a699c7f1d36ea6e04ed680a5c787cabff4b610ae3b8954ea3bcefec1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.9"
|
||||
@@ -5217,15 +5239,6 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "iso8601"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46"
|
||||
dependencies = [
|
||||
"nom 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@@ -5365,6 +5378,16 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonptr"
|
||||
version = "0.6.3"
|
||||
@@ -5385,19 +5408,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"js-sys",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.7.0"
|
||||
@@ -5491,7 +5501,6 @@ dependencies = [
|
||||
"json-patch 4.1.0",
|
||||
"labrinth",
|
||||
"lettre",
|
||||
"meilisearch-sdk",
|
||||
"modrinth-content-management",
|
||||
"modrinth-util",
|
||||
"muralpay",
|
||||
@@ -5515,6 +5524,7 @@ dependencies = [
|
||||
"scalar_api_reference",
|
||||
"sentry",
|
||||
"serde",
|
||||
"serde-binhum",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha1 0.10.6",
|
||||
@@ -5954,49 +5964,6 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||
|
||||
[[package]]
|
||||
name = "meilisearch-index-setting-macro"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36e5cad3754ed329feb201c24f26c0694442bef50a90dbcff0ba6d12b5ca133"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meilisearch-sdk"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f620c9ed9b790cf292b66c8dab16f0e4aafb377583ec0e589da7c3ee81ff2038"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"either",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"iso8601",
|
||||
"jsonwebtoken",
|
||||
"log",
|
||||
"meilisearch-index-setting-macro",
|
||||
"pin-project-lite",
|
||||
"reqwest 0.12.24",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"tokio",
|
||||
"uuid 1.23.3",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"yaup",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
@@ -7573,6 +7540,45 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard-bindgen"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e709af61573ae2563a00e760ff7db47f5c70b9d96c6dcb28993526fb8479f68"
|
||||
dependencies = [
|
||||
"postcard-bindgen-core",
|
||||
"postcard-bindgen-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard-bindgen-core"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f19cebcb145fdccb24b63ad410785aa7c2ff9364551646ca7a927422d2d0386"
|
||||
dependencies = [
|
||||
"convert_case 0.11.0",
|
||||
"genco",
|
||||
"tree-ds",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard-bindgen-derive"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0372f55dd7c15d73f7b0f7ca507ac3db5653f346b2b44a36537e2ad39f55441b"
|
||||
dependencies = [
|
||||
"convert_case 0.11.0",
|
||||
"genco",
|
||||
"postcard-bindgen-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"regex-macro",
|
||||
"serde",
|
||||
"serde_derive_internals 0.30.0",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
@@ -8455,12 +8461,27 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-macro"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d306632607af6ec61c0b117971d57a96381b6317cf18ae419b5558048fe016e"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.2"
|
||||
@@ -9126,7 +9147,7 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"serde_derive_internals 0.29.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -9369,6 +9390,16 @@ dependencies = [
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sequential_gen"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d630b1418311e028ceb45e1e567845501a9d9c770a258b80d2edc025ffad17c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -9379,6 +9410,16 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-binhum"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-untagged"
|
||||
version = "0.1.9"
|
||||
@@ -9464,6 +9505,17 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_ini"
|
||||
version = "0.2.0"
|
||||
@@ -9904,6 +9956,9 @@ name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spinning_top"
|
||||
@@ -10331,6 +10386,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -10944,6 +11010,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "testcontainers"
|
||||
version = "0.25.2"
|
||||
@@ -11023,6 +11098,7 @@ dependencies = [
|
||||
"image",
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"json5",
|
||||
"modrinth-content-management",
|
||||
"notify",
|
||||
"notify-debouncer-mini",
|
||||
@@ -11032,6 +11108,8 @@ dependencies = [
|
||||
"path-util",
|
||||
"phf 0.13.1",
|
||||
"png 0.18.0",
|
||||
"postcard",
|
||||
"postcard-bindgen",
|
||||
"quartz_nbt",
|
||||
"quick-xml 0.38.3",
|
||||
"rand 0.8.5",
|
||||
@@ -11039,6 +11117,7 @@ dependencies = [
|
||||
"reqwest 0.12.24",
|
||||
"rgb",
|
||||
"serde",
|
||||
"serde-binhum",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@@ -11052,9 +11131,11 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml 0.9.8",
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid 1.23.3",
|
||||
@@ -11731,12 +11812,49 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-ds"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "710c388a2bb52d1a7186ae4aa2e92c6fae0606426bf4bdfb4106fa1e8e5c602e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"sequential_gen",
|
||||
"serde",
|
||||
"spin 0.10.0",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs-macros",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.27.0"
|
||||
@@ -11774,6 +11892,12 @@ version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
@@ -13443,12 +13567,13 @@ dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"eyre",
|
||||
"futures",
|
||||
"lz4_flex",
|
||||
"postcard",
|
||||
"prometheus",
|
||||
"redis",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -13487,17 +13612,6 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaup"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0144f1a16a199846cb21024da74edd930b43443463292f536b7110b4855b5c6"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
|
||||
+6
-1
@@ -15,6 +15,7 @@ members = [
|
||||
"packages/modrinth-util",
|
||||
"packages/neverbounce",
|
||||
"packages/path-util",
|
||||
"packages/serde-binhum",
|
||||
"packages/xredis",
|
||||
]
|
||||
|
||||
@@ -112,6 +113,7 @@ indicatif = "0.18.0"
|
||||
itertools = "0.14.0"
|
||||
jemalloc_pprof = "0.8.1"
|
||||
json-patch = { version = "4.1.0", default-features = false }
|
||||
json5 = "1.3.1"
|
||||
lettre = { version = "0.11.19", default-features = false, features = [
|
||||
"aws-lc-rs",
|
||||
"builder",
|
||||
@@ -130,7 +132,6 @@ lz4_flex = { version = "0.11.5", default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||
modrinth-content-management = { path = "packages/modrinth-content-management" }
|
||||
modrinth-log = { path = "packages/modrinth-log" }
|
||||
modrinth-util = { path = "packages/modrinth-util" }
|
||||
@@ -148,6 +149,7 @@ path-util = { path = "packages/path-util" }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
png = "0.18.0"
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
|
||||
postcard-bindgen = "0.8.0"
|
||||
proc-macro2 = { version = "1.0" }
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
@@ -183,6 +185,7 @@ sentry = { version = "0.45.0", default-features = false, features = [
|
||||
"rustls",
|
||||
] }
|
||||
serde = "1.0.228"
|
||||
serde-binhum = { path = "packages/serde-binhum" }
|
||||
serde_bytes = "0.11.19"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
@@ -223,12 +226,14 @@ tikv-jemallocator = "0.6.0"
|
||||
tokio = "1.47.1"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-util = "0.7.16"
|
||||
toml = "0.9.8"
|
||||
totp-rs = "5.7.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-actix-web = { version = "0.7.19", default-features = false }
|
||||
tracing-ecs = "0.5.0"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-subscriber = "0.3.20"
|
||||
ts-rs = "12.0.1"
|
||||
typed-path = "0.12.0"
|
||||
url = "2.5.7"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
## Modrinth Monorepo
|
||||
|
||||
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains  lines of code and has  contributors!
|
||||
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains  lines of code and has  contributors!
|
||||
|
||||
If you're not a developer and you've stumbled upon this repository, you can access the web interface on the [Modrinth website](https://modrinth.com) and download the latest release of the app [here](https://modrinth.com/app).
|
||||
|
||||
|
||||
@@ -24,3 +24,7 @@ gam = "gam"
|
||||
consts = "consts"
|
||||
# short for "Copy"
|
||||
Cpy = "Cpy"
|
||||
|
||||
[default.extend-identifiers]
|
||||
# Constant from the `zip` crate
|
||||
ZIP64_BYTES_THR = "ZIP64_BYTES_THR"
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
*.gltf
|
||||
src/locales/
|
||||
src/assets/**/*.svg
|
||||
|
||||
# Generated app-event bindings
|
||||
src/generated/app-events/*.ts
|
||||
src/generated/app-events/postcard/**
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
|
||||
export default config.append([
|
||||
{
|
||||
ignores: ['src/generated/app-events/*.ts', 'src/generated/app-events/postcard/**'],
|
||||
},
|
||||
])
|
||||
|
||||
+164
-70
@@ -10,9 +10,10 @@ import {
|
||||
} from '@modrinth/api-client'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CompassIcon,
|
||||
HomeIcon,
|
||||
LeftArrowIcon,
|
||||
LibraryIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
@@ -28,24 +29,26 @@ import {
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
ButtonLink,
|
||||
commonMessages,
|
||||
ContentInstallModal,
|
||||
ContentUpdaterModal,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
I18nDebugPanel,
|
||||
IconButton,
|
||||
IntlFormatted,
|
||||
LoadingBar,
|
||||
NewsArticleCard,
|
||||
NotificationPanel,
|
||||
OverflowMenu,
|
||||
PopupNotificationPanel,
|
||||
provideModalBehavior,
|
||||
provideModrinthClient,
|
||||
provideNotificationManager,
|
||||
providePageContext,
|
||||
providePopupNotificationManager,
|
||||
TeleportOverflowMenu,
|
||||
TextLogo,
|
||||
useDebugLogger,
|
||||
useFormatBytes,
|
||||
useHostingIntercom,
|
||||
@@ -63,12 +66,12 @@ import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import AppActionBar from '@/components/ui/AppActionBar.vue'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue'
|
||||
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
|
||||
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
|
||||
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
|
||||
@@ -87,19 +90,19 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import SurveyPopup from '@/components/ui/SurveyPopup.vue'
|
||||
import WindowControls from '@/components/ui/WindowControls.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { config } from '@/config'
|
||||
import {
|
||||
ads_consent_listener,
|
||||
hide_ads_window,
|
||||
init_ads_window,
|
||||
perform_ads_consent_action,
|
||||
release_ads_window_hold,
|
||||
should_show_ads_consent_popup,
|
||||
show_ads_window,
|
||||
take_ads_window_hold,
|
||||
} from '@/helpers/ads.js'
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user, get_version } from '@/helpers/cache.js'
|
||||
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
|
||||
import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance'
|
||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
|
||||
@@ -119,6 +122,7 @@ import {
|
||||
} from '@/helpers/utils.js'
|
||||
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
|
||||
import i18n from '@/i18n.config'
|
||||
import { instanceKeys } from '@/pages/instance/query-options'
|
||||
import {
|
||||
appUpdateState,
|
||||
downloadAvailableAppUpdate,
|
||||
@@ -129,6 +133,7 @@ import {
|
||||
openAppUpdateChangelog,
|
||||
setAppUpdateActions,
|
||||
} from '@/providers/app-update.ts'
|
||||
import { createBreadcrumbManager, provideBreadcrumbManager } from '@/providers/breadcrumbs'
|
||||
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
|
||||
import {
|
||||
provideAppUpdateDownloadProgress,
|
||||
@@ -136,6 +141,7 @@ import {
|
||||
} from '@/providers/download-progress.ts'
|
||||
import { createServerInstall, provideServerInstall } from '@/providers/server-install'
|
||||
import { setupProviders } from '@/providers/setup'
|
||||
import { setupAppEventsProvider } from '@/providers/setup/app-events'
|
||||
import { setupAuthProvider } from '@/providers/setup/auth'
|
||||
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
|
||||
import { useError } from '@/store/error.js'
|
||||
@@ -151,6 +157,39 @@ import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider()
|
||||
const breadcrumbManager = createBreadcrumbManager()
|
||||
provideBreadcrumbManager(breadcrumbManager)
|
||||
const canNavigateBack = ref(false)
|
||||
const canNavigateForward = ref(false)
|
||||
|
||||
function updateHistoryNavigationState() {
|
||||
const historyState = window.history.state
|
||||
canNavigateBack.value = historyState?.back != null
|
||||
canNavigateForward.value = historyState?.forward != null
|
||||
}
|
||||
|
||||
let fullscreenAdsWindowHold = false
|
||||
|
||||
async function handleFullscreenChange() {
|
||||
const fullscreen = document.fullscreenElement !== null
|
||||
if (fullscreen === fullscreenAdsWindowHold) return
|
||||
|
||||
fullscreenAdsWindowHold = fullscreen
|
||||
try {
|
||||
if (fullscreen) {
|
||||
await take_ads_window_hold()
|
||||
} else {
|
||||
await release_ads_window_hold()
|
||||
}
|
||||
} catch (error) {
|
||||
fullscreenAdsWindowHold = !fullscreen
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
updateHistoryNavigationState()
|
||||
|
||||
const APP_LEFT_NAV_WIDTH = '4rem'
|
||||
const APP_SIDEBAR_WIDTH = 300
|
||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||
@@ -158,14 +197,26 @@ const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
|
||||
const credentials = ref()
|
||||
let credentialsRefreshId = 0
|
||||
const sidebarToggled = ref(true)
|
||||
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
|
||||
sidebarToggled.value = !themeStore.toggleSidebar
|
||||
})
|
||||
watch(
|
||||
() => themeStore.toggleSidebar,
|
||||
(toggleSidebar) => {
|
||||
sidebarToggled.value = !toggleSidebar
|
||||
},
|
||||
)
|
||||
const forceSidebar = computed(
|
||||
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
||||
() =>
|
||||
route.path.startsWith('/browse') ||
|
||||
route.path.startsWith('/project') ||
|
||||
route.path.startsWith('/user'),
|
||||
)
|
||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
|
||||
const hostingUpdateRequired = computed(
|
||||
() =>
|
||||
hostingRouteActive.value &&
|
||||
!!appUpdateState.availableUpdate.value &&
|
||||
appUpdateState.updatesEnabled.value,
|
||||
)
|
||||
const prideFundraiserEnabled = computed(
|
||||
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
|
||||
)
|
||||
@@ -176,7 +227,9 @@ const hostingIntercomIdentityKey = computed(() => {
|
||||
return `${userId}:${serverId ?? 'hosting'}`
|
||||
})
|
||||
const hostingIntercom = useHostingIntercom({
|
||||
enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session),
|
||||
enabled: computed(
|
||||
() => hostingRouteActive.value && !hostingUpdateRequired.value && !!credentials.value?.session,
|
||||
),
|
||||
appId: 'ykeritl9',
|
||||
fetchToken: fetchIntercomToken,
|
||||
identityKey: hostingIntercomIdentityKey,
|
||||
@@ -191,11 +244,22 @@ const notificationManager = new AppNotificationManager()
|
||||
provideNotificationManager(notificationManager)
|
||||
const { handleError, addNotification } = notificationManager
|
||||
|
||||
useAppEvent(
|
||||
'warning',
|
||||
(event) =>
|
||||
addNotification({
|
||||
title: 'Warning',
|
||||
text: event.message,
|
||||
type: 'warning',
|
||||
}),
|
||||
appEvents,
|
||||
)
|
||||
|
||||
const popupNotificationManager = new AppPopupNotificationManager()
|
||||
providePopupNotificationManager(popupNotificationManager)
|
||||
const { addPopupNotification } = popupNotificationManager
|
||||
let adsConsentPopupId = null
|
||||
let unlistenAdsConsent
|
||||
useAppEvent('ads_consent_required', handleAdsConsentRequired, appEvents)
|
||||
|
||||
const appVersion = getVersion()
|
||||
const tauriApiClient = new TauriModrinthClient({
|
||||
@@ -227,7 +291,7 @@ const { data: authenticatedModrinthUser } = useQuery({
|
||||
retry: false,
|
||||
})
|
||||
useQuery({
|
||||
queryKey: computed(() => ['shared-instance-eligibility', credentials.value?.user?.id]),
|
||||
queryKey: computed(() => instanceKeys.sharedEligibility(credentials.value?.user?.id)),
|
||||
queryFn: can_current_user_use_shared_instances,
|
||||
enabled: () => !!credentials.value?.session && !!credentials.value?.user?.id,
|
||||
retry: false,
|
||||
@@ -264,8 +328,8 @@ providePageContext({
|
||||
})
|
||||
provideModalBehavior({
|
||||
noblur: computed(() => !themeStore.advancedRendering),
|
||||
onShow: () => hide_ads_window(),
|
||||
onHide: () => show_ads_window(),
|
||||
onShow: () => take_ads_window_hold(),
|
||||
onHide: () => release_ads_window_hold(),
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -280,7 +344,7 @@ const {
|
||||
setModpackAlreadyInstalledModal,
|
||||
handleModpackDuplicateCreateAnyway,
|
||||
handleModpackDuplicateGoToInstance,
|
||||
} = setupProviders(notificationManager, popupNotificationManager)
|
||||
} = setupProviders(tauriApiClient, notificationManager, popupNotificationManager)
|
||||
|
||||
const news = ref([])
|
||||
const displayedServerInviteNotifications = new Set()
|
||||
@@ -332,7 +396,6 @@ const authUnreachable = computed(() => {
|
||||
onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
try {
|
||||
unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired)
|
||||
handleAdsConsentRequired(await should_show_ads_consent_popup())
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
@@ -340,6 +403,7 @@ onMounted(async () => {
|
||||
|
||||
document.querySelector('body').addEventListener('click', handleClick)
|
||||
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||
|
||||
checkUpdates()
|
||||
})
|
||||
@@ -347,10 +411,13 @@ onMounted(async () => {
|
||||
onUnmounted(async () => {
|
||||
document.querySelector('body').removeEventListener('click', handleClick)
|
||||
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
|
||||
unsubscribeSidebarToggle()
|
||||
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||
clearDelayedUpdatePopup()
|
||||
|
||||
await unlistenAdsConsent?.()
|
||||
if (fullscreenAdsWindowHold) {
|
||||
fullscreenAdsWindowHold = false
|
||||
await release_ads_window_hold().catch(handleError)
|
||||
}
|
||||
await unlistenUpdateDownload?.()
|
||||
})
|
||||
|
||||
@@ -559,14 +626,6 @@ async function setupApp() {
|
||||
document.getElementsByTagName('html')[0].classList.add('windows')
|
||||
}
|
||||
|
||||
await warning_listener((e) =>
|
||||
addNotification({
|
||||
title: 'Warning',
|
||||
text: e.message,
|
||||
type: 'warning',
|
||||
}),
|
||||
)
|
||||
|
||||
fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
@@ -615,7 +674,7 @@ async function setupApp() {
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
initialize_state()
|
||||
initialize_state(appEventChannel)
|
||||
.then(() => {
|
||||
setupApp().catch((err) => {
|
||||
stateFailed.value = true
|
||||
@@ -655,6 +714,7 @@ router.beforeEach(() => {
|
||||
routerToken = loading.begin()
|
||||
})
|
||||
router.afterEach((to, from, failure) => {
|
||||
updateHistoryNavigationState()
|
||||
trackEvent('PageView', {
|
||||
path: to.path,
|
||||
fromPath: from.path,
|
||||
@@ -744,7 +804,7 @@ const errorModal = ref()
|
||||
const minecraftAuthErrorModal = ref()
|
||||
const minecraftRequiredModal = ref()
|
||||
|
||||
const contentInstall = createContentInstall({ router, handleError })
|
||||
const contentInstall = createContentInstall({ router, handleError, appEvents })
|
||||
provideContentInstall(contentInstall)
|
||||
const {
|
||||
instances: contentInstallInstances,
|
||||
@@ -777,7 +837,12 @@ const {
|
||||
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
|
||||
} = contentInstall
|
||||
|
||||
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
|
||||
const serverInstall = createServerInstall({
|
||||
router,
|
||||
handleError,
|
||||
popupNotificationManager,
|
||||
appEvents,
|
||||
})
|
||||
provideServerInstall(serverInstall)
|
||||
const {
|
||||
setInstallToPlayModal: setServerInstallToPlayModal,
|
||||
@@ -953,8 +1018,8 @@ onMounted(() => {
|
||||
const accounts = ref(null)
|
||||
provide('accountsCard', accounts)
|
||||
|
||||
command_listener(handleCommand)
|
||||
notification_listener(handleLiveNotification)
|
||||
useAppEvent('command', handleCommand, appEvents)
|
||||
useAppEvent('notification', handleLiveNotification, appEvents)
|
||||
|
||||
async function markLiveNotificationRead(notification) {
|
||||
try {
|
||||
@@ -1380,6 +1445,7 @@ async function downloadUpdate(versionToDownload) {
|
||||
handleError(e)
|
||||
})
|
||||
unlistenUpdateDownload = await subscribeToDownloadProgress(
|
||||
appEvents,
|
||||
appUpdateDownload,
|
||||
versionToDownload.version,
|
||||
)
|
||||
@@ -1529,8 +1595,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<NavButton
|
||||
v-tooltip.right="formatMessage(commonMessages.discoverContentLabel)"
|
||||
to="/browse/modpack"
|
||||
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
|
||||
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
|
||||
:is-primary="() => route.path.startsWith('/browse') && !route.query.i && !route.query.sid"
|
||||
:is-subpage="
|
||||
(route) => route.path.startsWith('/project') && !route.query.i && !route.query.sid
|
||||
"
|
||||
>
|
||||
<CompassIcon />
|
||||
</NavButton>
|
||||
@@ -1554,7 +1622,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
v-tooltip.right="formatMessage(messages.modrinthHosting)"
|
||||
to="/hosting/manage"
|
||||
:is-primary="(r) => r.path === '/hosting/manage' || r.path === '/hosting/manage/'"
|
||||
:is-subpage="(r) => r.path.startsWith('/hosting/manage/') && r.path !== '/hosting/manage/'"
|
||||
:is-subpage="
|
||||
(r) =>
|
||||
(r.path.startsWith('/hosting/manage/') && r.path !== '/hosting/manage/') ||
|
||||
((r.path.startsWith('/browse') || r.path.startsWith('/project')) && r.query.sid)
|
||||
"
|
||||
>
|
||||
<ServerStackIcon />
|
||||
</NavButton>
|
||||
@@ -1575,22 +1647,29 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
>
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
<OverflowMenu
|
||||
<TeleportOverflowMenu
|
||||
v-if="credentials?.user"
|
||||
v-tooltip.right="formatMessage(messages.modrinthAccount)"
|
||||
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast border-0 cursor-pointer"
|
||||
type="quiet"
|
||||
size="xl"
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'view-profile',
|
||||
label: formatMessage(messages.signedInAs, {
|
||||
username: credentials.user.username,
|
||||
}),
|
||||
action: () => router.push(`/user/${encodeURIComponent(credentials.user.username)}`),
|
||||
},
|
||||
{
|
||||
id: 'sign-out',
|
||||
label: formatMessage(commonMessages.signOutButton),
|
||||
tone: 'red',
|
||||
action: () => logOut(),
|
||||
color: 'danger',
|
||||
},
|
||||
]"
|
||||
placement="right-end"
|
||||
:distance="4"
|
||||
>
|
||||
<Avatar :src="credentials?.user?.avatar_url" alt="" size="32px" circle />
|
||||
<template #view-profile>
|
||||
@@ -1613,7 +1692,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<LogOutIcon />
|
||||
{{ formatMessage(commonMessages.signOutButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</TeleportOverflowMenu>
|
||||
<NavButton
|
||||
v-else
|
||||
v-tooltip.right="formatMessage(messages.signInToModrinthAccount)"
|
||||
@@ -1623,38 +1702,47 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</NavButton>
|
||||
</div>
|
||||
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
||||
<div data-tauri-drag-region class="flex min-w-0 flex-1 overflow-hidden p-3">
|
||||
<ModrinthAppLogo class="h-full w-auto shrink-0 text-contrast pointer-events-none" />
|
||||
<div data-tauri-drag-region class="flex shrink-0 items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
<div data-tauri-drag-region class="flex min-w-0 flex-1 items-center overflow-hidden p-2">
|
||||
<TextLogo class="h-7 w-auto shrink-0 text-contrast pointer-events-none" />
|
||||
<div data-tauri-drag-region class="ml-2 flex shrink-0 items-center gap-2">
|
||||
<IconButton
|
||||
type="outlined"
|
||||
label="Go back"
|
||||
class="!h-7 !min-w-7 !w-7 !border !border-surface-4 !p-0 !opacity-100"
|
||||
:disabled="!canNavigateBack"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
<ChevronLeftIcon
|
||||
class="!size-4 !text-primary"
|
||||
:class="{ 'opacity-20': !canNavigateBack }"
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="outlined"
|
||||
label="Go forward"
|
||||
class="!h-7 !min-w-7 !w-7 !border !border-surface-4 !p-0 !opacity-100"
|
||||
:disabled="!canNavigateForward"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
<ChevronRightIcon
|
||||
class="!size-4 !text-primary"
|
||||
:class="{ 'opacity-20': !canNavigateForward }"
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Breadcrumbs class="pt-[2px]" />
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<section data-tauri-drag-region class="flex shrink-0 ml-auto items-center">
|
||||
<ButtonStyled
|
||||
<IconButton
|
||||
v-if="!forceSidebar && themeStore.toggleSidebar"
|
||||
:type="sidebarToggled ? 'standard' : 'transparent'"
|
||||
circular
|
||||
:type="sidebarToggled ? 'base' : 'quiet'"
|
||||
label="Next image"
|
||||
class="mr-3 transition-transform"
|
||||
:class="{ 'rotate-180': !sidebarToggled }"
|
||||
@click="sidebarToggled = !sidebarToggled"
|
||||
>
|
||||
<button
|
||||
class="mr-3 transition-transform"
|
||||
:class="{ 'rotate-180': !sidebarToggled }"
|
||||
@click="sidebarToggled = !sidebarToggled"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<RightArrowIcon />
|
||||
</IconButton>
|
||||
<div class="flex mr-3">
|
||||
<Suspense>
|
||||
<AppActionBar />
|
||||
@@ -1716,7 +1804,8 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
>
|
||||
{{ formatMessage(messages.authUnreachableBody) }}
|
||||
</Admonition>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<HostingUpdateRequired v-if="hostingUpdateRequired" />
|
||||
<RouterView v-else v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
|
||||
<component :is="Component"></component>
|
||||
@@ -1763,12 +1852,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
:key="`news-${index}`"
|
||||
:article="item"
|
||||
/>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<a href="https://modrinth.com/news" target="_blank" class="my-4">
|
||||
<NewspaperIcon />
|
||||
{{ formatMessage(messages.viewAllNews) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonLink
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
href="https://modrinth.com/news"
|
||||
target="_blank"
|
||||
class="my-4"
|
||||
>
|
||||
<NewspaperIcon />
|
||||
{{ formatMessage(messages.viewAllNews) }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
class="flex flex-col gap-3 bg-button-bg border border-solid border-surface-5 rounded-xl p-3 mt-2"
|
||||
>
|
||||
<span>{{ formatMessage(messages.notSignedIn) }}</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button color="primary" :disabled="loginDisabled" @click="login()">
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
{{ formatMessage(messages.signInToMinecraft) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" :disabled="loginDisabled" @click="login()">
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
{{ formatMessage(messages.signInToMinecraft) }}
|
||||
</Button>
|
||||
</div>
|
||||
<Accordion
|
||||
v-else
|
||||
@@ -60,24 +58,28 @@
|
||||
{{ account.profile.name }}
|
||||
</p>
|
||||
</button>
|
||||
<ButtonStyled circular color="red" color-fill="none" hover-color-fill="background">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.removeAccount)"
|
||||
class="mr-2"
|
||||
@click="logout(account.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.removeAccount)"
|
||||
type="quiet"
|
||||
color="red"
|
||||
:label="formatMessage(messages.removeAccount)"
|
||||
class="mr-2 !bg-button-bg !text-primary ![box-shadow:var(--shadow-button)] hover:!bg-red focus-visible:!bg-red hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
|
||||
@click="logout(account.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2 px-2 pt-2">
|
||||
<ButtonStyled v-if="accounts.length > 0" class="w-full">
|
||||
<button :disabled="loginDisabled" @click="login()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addAccount) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
v-if="accounts.length > 0"
|
||||
class="w-full !bg-button-bg !text-primary ![box-shadow:var(--shadow-button)]"
|
||||
:disabled="loginDisabled"
|
||||
@click="login()"
|
||||
>
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addAccount) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -95,14 +97,16 @@ import {
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import {
|
||||
get_default_user,
|
||||
@@ -111,7 +115,6 @@ import {
|
||||
set_default_user,
|
||||
users,
|
||||
} from '@/helpers/auth'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import type { Skin } from '@/helpers/skins'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
@@ -248,16 +251,12 @@ async function logout(id: string) {
|
||||
trackEvent('AccountLogOut')
|
||||
}
|
||||
|
||||
const unlisten = await process_listener(async (e) => {
|
||||
useAppEvent('process', async (e) => {
|
||||
if (e.event === 'launched') {
|
||||
await refreshValues()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
notSignedIn: {
|
||||
id: 'minecraft-account.not-signed-in',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, TeleportOverflowMenu } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -36,27 +36,26 @@ const handleSearchContent = async () => {
|
||||
|
||||
<template>
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled>
|
||||
<button @click="handleSearchContent">
|
||||
<PlusIcon />
|
||||
Install content
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'from_file',
|
||||
action: handleAddContentFromFile,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<Button @click="handleSearchContent">
|
||||
<PlusIcon />
|
||||
Install content
|
||||
</Button>
|
||||
<TeleportOverflowMenu
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'from_file',
|
||||
label: 'Add from file',
|
||||
action: handleAddContentFromFile,
|
||||
},
|
||||
]"
|
||||
class="!w-auto !px-2.5 !rounded-xl"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ButtonStyled
|
||||
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
|
||||
color="brand"
|
||||
type="transparent"
|
||||
circular
|
||||
>
|
||||
<button v-tooltip="formatMessage(messages.viewActiveDownloads)" @click="openDownloadToast()">
|
||||
<div v-if="downloadState.total > 0 || hasActiveLoadingBars" class="relative">
|
||||
<IconButton
|
||||
v-tooltip="downloadToggleLabel"
|
||||
:color="downloadState.hidden > 0 ? 'brand' : undefined"
|
||||
type="quiet"
|
||||
:label="downloadToggleLabel"
|
||||
@click="toggleDownloadNotifications"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div v-if="offline" class="flex items-center gap-1">
|
||||
<UnplugIcon class="text-secondary" />
|
||||
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
|
||||
@@ -36,17 +37,23 @@
|
||||
@show="showInstances = true"
|
||||
@hide="showInstances = false"
|
||||
>
|
||||
<ButtonStyled type="transparent" circular size="small">
|
||||
<button
|
||||
v-tooltip="
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
>
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showInstances }" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
class="!size-6"
|
||||
type="quiet"
|
||||
size="xs"
|
||||
:label="
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
>
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showInstances }" />
|
||||
</IconButton>
|
||||
<template #popper>
|
||||
<div class="flex w-[20rem] max-h-[24rem] flex-col gap-2 overflow-auto">
|
||||
<div
|
||||
@@ -125,8 +132,8 @@ import {
|
||||
TerminalSquareIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { IconButton } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
@@ -141,8 +148,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import AppUpdateButton from '@/components/ui/app-update-button/index.vue'
|
||||
import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { loading_listener, process_listener } from '@/helpers/events'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
import type { LoadingBar } from '@/helpers/state'
|
||||
@@ -212,8 +219,35 @@ const messages = defineMessages({
|
||||
id: 'app.action-bar.view-active-downloads',
|
||||
defaultMessage: 'View active downloads',
|
||||
},
|
||||
hideDownloads: {
|
||||
id: 'app.action-bar.hide-downloads',
|
||||
defaultMessage: 'Hide active downloads',
|
||||
},
|
||||
showDownloads: {
|
||||
id: 'app.action-bar.show-downloads',
|
||||
defaultMessage: 'Show active downloads',
|
||||
},
|
||||
})
|
||||
|
||||
const downloadState = computed(() => popupNotificationManager.getDownloadState())
|
||||
const downloadToggleLabel = computed(() =>
|
||||
formatMessage(
|
||||
downloadState.value.hidden > 0
|
||||
? messages.showDownloads
|
||||
: downloadState.value.total > 0
|
||||
? messages.hideDownloads
|
||||
: messages.viewActiveDownloads,
|
||||
),
|
||||
)
|
||||
|
||||
function toggleDownloadNotifications(): void {
|
||||
if (downloadState.value.total > 0) {
|
||||
popupNotificationManager.toggleDownloadNotifications()
|
||||
} else if (hasActiveLoadingBars.value) {
|
||||
openDownloadToast()
|
||||
}
|
||||
}
|
||||
|
||||
const currentProcesses = ref<RunningProcess[]>([])
|
||||
const selectedProcess = ref<RunningProcess | undefined>()
|
||||
|
||||
@@ -260,7 +294,7 @@ onMounted(() => {
|
||||
window.addEventListener('online', handleOnline)
|
||||
})
|
||||
|
||||
const unlistenProcess = await process_listener(async () => {
|
||||
useAppEvent('process', async () => {
|
||||
await refresh()
|
||||
})
|
||||
|
||||
@@ -290,6 +324,7 @@ function goToTerminal(instanceId?: string) {
|
||||
const currentLoadingBars = ref<LoadingBar[]>([])
|
||||
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
|
||||
const notificationId = ref<string | number | null>(null)
|
||||
const terminalNotificationIds = new Map<string, string | number>()
|
||||
const dismissed = ref(false)
|
||||
|
||||
function getLoadingBarKey(loadingBar: LoadingBar): string {
|
||||
@@ -335,6 +370,44 @@ function removeNotification(): void {
|
||||
notificationId.value = null
|
||||
}
|
||||
|
||||
function syncTerminalNotifications(): void {
|
||||
const terminalNotifications = installJobNotifications.terminalNotifications.value
|
||||
const currentJobIds = new Set(terminalNotifications.map((notification) => notification.id))
|
||||
|
||||
for (const terminal of terminalNotifications) {
|
||||
const popupId = terminalNotificationIds.get(terminal.id)
|
||||
let notification = popupId
|
||||
? popupNotificationManager.getNotifications().find((candidate) => candidate.id === popupId)
|
||||
: undefined
|
||||
|
||||
if (!notification) {
|
||||
notification = popupNotificationManager.addPopupNotification({
|
||||
title: terminal.title,
|
||||
text: terminal.text,
|
||||
type: terminal.type,
|
||||
buttons: terminal.buttons,
|
||||
onDismiss: terminal.onDismiss,
|
||||
autoCloseMs: null,
|
||||
})
|
||||
terminalNotificationIds.set(terminal.id, notification.id)
|
||||
continue
|
||||
}
|
||||
|
||||
notification.title = terminal.title
|
||||
notification.text = terminal.text
|
||||
notification.type = terminal.type
|
||||
notification.buttons = terminal.buttons
|
||||
notification.onDismiss = terminal.onDismiss
|
||||
}
|
||||
|
||||
for (const [jobId, popupId] of terminalNotificationIds) {
|
||||
if (!currentJobIds.has(jobId)) {
|
||||
popupNotificationManager.removeNotification(popupId)
|
||||
terminalNotificationIds.delete(jobId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildDownloadItems(): PopupNotificationProgressItem[] {
|
||||
return [
|
||||
...installJobNotifications.progressItems.value,
|
||||
@@ -345,19 +418,20 @@ function buildDownloadItems(): PopupNotificationProgressItem[] {
|
||||
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
|
||||
progress: getLoadingProgress(bar),
|
||||
waiting: !bar.total || bar.total <= 0,
|
||||
progressType: 'percentage',
|
||||
progressType: bar.bar_type?.type === 'pack_import' ? 'bytes' : 'percentage',
|
||||
progressCurrent: bar.current,
|
||||
progressTotal: bar.total,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
|
||||
const hasActiveLoadingBars = computed(
|
||||
() => currentLoadingBars.value.length > 0 || installJobNotifications.active.value,
|
||||
)
|
||||
|
||||
function updateNotification(resummon = false): void {
|
||||
syncTerminalNotifications()
|
||||
|
||||
if (resummon) {
|
||||
dismissed.value = false
|
||||
}
|
||||
@@ -386,7 +460,6 @@ function updateNotification(resummon = false): void {
|
||||
: formatMessage(messages.downloads)
|
||||
notif.text = undefined
|
||||
notif.progressItems = progressItems
|
||||
notif.buttons = installJobNotifications.buttons.value
|
||||
notif.progress = undefined
|
||||
notif.waiting = undefined
|
||||
} else {
|
||||
@@ -397,7 +470,6 @@ function updateNotification(resummon = false): void {
|
||||
type: 'download',
|
||||
autoCloseMs: null,
|
||||
progressItems,
|
||||
buttons: installJobNotifications.buttons.value,
|
||||
})
|
||||
notificationId.value = notif.id
|
||||
}
|
||||
@@ -482,7 +554,7 @@ const installJobNotifications = await useInstallJobNotifications({
|
||||
|
||||
await refreshLoadingBars()
|
||||
|
||||
const unlistenLoading = await loading_listener(async () => {
|
||||
useAppEvent('loading', async () => {
|
||||
await refreshLoadingBars()
|
||||
})
|
||||
|
||||
@@ -496,11 +568,11 @@ function selectProcess(process: RunningProcess) {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeNotification()
|
||||
terminalNotificationIds.forEach((id) => popupNotificationManager.removeNotification(id))
|
||||
terminalNotificationIds.clear()
|
||||
dismissed.value = false
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
unlistenProcess()
|
||||
unlistenLoading()
|
||||
installJobNotifications.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
ref="outerRef"
|
||||
data-tauri-drag-region
|
||||
class="min-w-0 overflow-hidden pl-3"
|
||||
class="min-w-0 overflow-hidden pl-4"
|
||||
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
|
||||
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
|
||||
@mouseenter="onMouseEnter"
|
||||
@@ -11,30 +11,48 @@
|
||||
<div
|
||||
ref="innerRef"
|
||||
data-tauri-drag-region
|
||||
class="flex w-fit items-center gap-1"
|
||||
class="flex w-fit items-center gap-2 pr-4"
|
||||
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
||||
@animationiteration="onAnimationIteration"
|
||||
>
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
||||
query: breadcrumb.query,
|
||||
}"
|
||||
class="shrink-0 whitespace-nowrap text-primary"
|
||||
<template v-for="(breadcrumb, index) in breadcrumbs" :key="breadcrumb.slot">
|
||||
<component
|
||||
:is="index < breadcrumbs.length - 1 && breadcrumb.to ? RouterLink : 'span'"
|
||||
v-bind="index < breadcrumbs.length - 1 && breadcrumb.to ? { to: breadcrumb.to } : {}"
|
||||
:data-tauri-drag-region="index === breadcrumbs.length - 1 ? '' : undefined"
|
||||
class="flex shrink-0 items-center gap-1.5 whitespace-nowrap text-base font-medium leading-6"
|
||||
:class="
|
||||
index === breadcrumbs.length - 1
|
||||
? 'cursor-default select-none text-contrast'
|
||||
: 'text-primary hover:text-contrast'
|
||||
"
|
||||
:aria-current="index === breadcrumbs.length - 1 ? 'page' : undefined"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</router-link>
|
||||
<span
|
||||
v-else
|
||||
<Avatar
|
||||
v-if="breadcrumb.visual?.type === 'image'"
|
||||
:src="breadcrumb.visual.src"
|
||||
:alt="breadcrumb.visual.alt ?? breadcrumb.label"
|
||||
:circle="breadcrumb.visual.circle"
|
||||
:tint-by="breadcrumb.visual.tintBy ?? breadcrumb.id"
|
||||
size="20px"
|
||||
no-shadow
|
||||
raised
|
||||
class="inline-block shrink-0 align-middle"
|
||||
:class="{ '!rounded-md': !breadcrumb.visual.circle }"
|
||||
/>
|
||||
<component
|
||||
:is="breadcrumb.visual.component"
|
||||
v-else-if="breadcrumb.visual?.type === 'icon'"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
</component>
|
||||
<ChevronRightIcon
|
||||
v-if="index < breadcrumbs.length - 1"
|
||||
data-tauri-drag-region
|
||||
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</span>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
|
||||
class="size-5 shrink-0 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,34 +60,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { injectBreadcrumbManager } from '@/providers/breadcrumbs'
|
||||
|
||||
interface Breadcrumb {
|
||||
name: string
|
||||
link?: string
|
||||
query?: Record<string, string>
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbData = useBreadcrumbs()
|
||||
|
||||
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||
const additionalContext =
|
||||
route.meta.useContext === true
|
||||
? breadcrumbData.context
|
||||
: route.meta.useRootContext === true
|
||||
? breadcrumbData.rootContext
|
||||
: null
|
||||
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
|
||||
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
|
||||
})
|
||||
|
||||
function resolveLabel(name: string): string {
|
||||
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
|
||||
}
|
||||
const { entries: breadcrumbs } = injectBreadcrumbManager()
|
||||
|
||||
// Overflow detection
|
||||
const outerRef = ref<HTMLDivElement | null>(null)
|
||||
@@ -83,9 +80,13 @@ let stopping = false
|
||||
|
||||
function checkOverflow() {
|
||||
if (!outerRef.value || !innerRef.value) return
|
||||
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
|
||||
const outerStyles = window.getComputedStyle(outerRef.value)
|
||||
const horizontalPadding =
|
||||
Number.parseFloat(outerStyles.paddingLeft) + Number.parseFloat(outerStyles.paddingRight)
|
||||
const availableWidth = outerRef.value.clientWidth - horizontalPadding
|
||||
const overflow = innerRef.value.scrollWidth - availableWidth
|
||||
isOverflowing.value = overflow > 0
|
||||
overflowAmount.value = overflow + 12
|
||||
overflowAmount.value = overflow
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
Collapsible,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ChatIcon } from '@/assets/icons'
|
||||
@@ -273,12 +279,10 @@ async function copyToClipboard(text) {
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="closable">
|
||||
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
||||
</ButtonStyled>
|
||||
<ButtonLink :href="supportLink" @click="errorModal.hide()"
|
||||
><ChatIcon /> Get support</ButtonLink
|
||||
>
|
||||
<Button v-if="closable" @click="errorModal.hide()"><XIcon /> Close</Button>
|
||||
</div>
|
||||
<template v-if="hasDebugInfo">
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -307,16 +311,15 @@ async function copyToClipboard(text) {
|
||||
>
|
||||
{{ debugInfo }}
|
||||
</div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="'Copy debug info'"
|
||||
:label="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
<script setup>
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { FolderOpenIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileTreeSelect,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { readDir, stat } from '@tauri-apps/plugin-fs'
|
||||
import { ref } from 'vue'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
import { PackageIcon } from '@/assets/icons'
|
||||
import {
|
||||
export_instance_mrpack,
|
||||
get_full_path,
|
||||
get_pack_export_candidates,
|
||||
} from '@/helpers/instance'
|
||||
import { export_instance_mrpack, get_pack_export_candidates } from '@/helpers/instance'
|
||||
import { highlightInFolder } from '@/helpers/utils'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -44,6 +42,14 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Enter modpack description...',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
exportComplete: {
|
||||
id: 'app.export-modal.export-complete',
|
||||
defaultMessage: 'Export complete',
|
||||
},
|
||||
exportCompleteDescription: {
|
||||
id: 'app.export-modal.export-complete-description',
|
||||
defaultMessage: '{name} was exported successfully.',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -65,30 +71,24 @@ const exportModal = ref(null)
|
||||
const nameInput = ref(props.instance.name)
|
||||
const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const selectedFilePaths = ref([])
|
||||
const files = shallowRef([])
|
||||
const includedFilePaths = ref([])
|
||||
const excludedFilePaths = ref([])
|
||||
const fileTreeKey = ref(0)
|
||||
const filesLoadId = ref(0)
|
||||
const instanceRoot = ref('')
|
||||
const loadedDirectories = ref(new Set())
|
||||
const directoryEntries = new Map()
|
||||
const currentDirectory = ref('')
|
||||
|
||||
async function initFiles() {
|
||||
const loadId = ++filesLoadId.value
|
||||
const [filePaths, root] = await Promise.all([
|
||||
get_pack_export_candidates(props.instance.id),
|
||||
get_full_path(props.instance.id),
|
||||
])
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
instanceRoot.value = root
|
||||
const exportCandidates = await Promise.all(
|
||||
filePaths.map((path) => buildExportCandidateItem(root, path)),
|
||||
)
|
||||
const exportCandidates = await get_pack_export_candidates(props.instance.id)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
files.value = exportCandidates
|
||||
selectedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path))
|
||||
directoryEntries.set('', exportCandidates)
|
||||
currentDirectory.value = ''
|
||||
includedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && file.defaultSelected)
|
||||
.map((file) => file.path)
|
||||
}
|
||||
|
||||
@@ -104,15 +104,35 @@ const exportPack = async () => {
|
||||
})
|
||||
|
||||
if (outputPath) {
|
||||
export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
selectedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
).catch((err) => handleError(err))
|
||||
exportModal.value.hide()
|
||||
|
||||
try {
|
||||
await export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
includedFilePaths.value,
|
||||
excludedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
)
|
||||
|
||||
const fileName = outputPath.split(/[\\/]/).pop() ?? outputPath
|
||||
popupNotificationManager.addPopupNotification({
|
||||
title: formatMessage(messages.exportComplete),
|
||||
text: formatMessage(messages.exportCompleteDescription, { name: fileName }),
|
||||
type: 'success',
|
||||
buttons: [
|
||||
{
|
||||
label: formatMessage(commonMessages.openInFolderButton),
|
||||
icon: FolderOpenIcon,
|
||||
action: () => highlightInFolder(outputPath).catch(handleError),
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,115 +141,45 @@ function resetExportState() {
|
||||
exportDescription.value = ''
|
||||
versionInput.value = '1.0.0'
|
||||
files.value = []
|
||||
selectedFilePaths.value = []
|
||||
includedFilePaths.value = []
|
||||
excludedFilePaths.value = []
|
||||
fileTreeKey.value += 1
|
||||
instanceRoot.value = ''
|
||||
loadedDirectories.value = new Set()
|
||||
directoryEntries.clear()
|
||||
currentDirectory.value = ''
|
||||
}
|
||||
|
||||
async function loadExportDirectory(path) {
|
||||
if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return
|
||||
const normalizedPath = normalizeExportPath(path)
|
||||
currentDirectory.value = normalizedPath
|
||||
|
||||
const cachedEntries = directoryEntries.get(normalizedPath)
|
||||
if (cachedEntries) {
|
||||
files.value = cachedEntries
|
||||
return
|
||||
}
|
||||
|
||||
const loadId = filesLoadId.value
|
||||
loadedDirectories.value.add(path)
|
||||
files.value = []
|
||||
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot.value}/${path}`)
|
||||
const childItems = await Promise.all(
|
||||
entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)),
|
||||
const childItems = await get_pack_export_candidates(
|
||||
props.instance.id,
|
||||
normalizedPath || undefined,
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
appendExportItems(childItems)
|
||||
} catch {
|
||||
loadedDirectories.value.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportCandidateItem(instanceRoot, path) {
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot}/${path}`)
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
count: entries.length,
|
||||
directoryEntries.set(normalizedPath, childItems)
|
||||
if (currentDirectory.value === normalizedPath) {
|
||||
files.value = childItems
|
||||
}
|
||||
} catch {
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportDirectoryChildItem(instanceRoot, parentPath, entry) {
|
||||
const path = `${parentPath}/${entry.name}`
|
||||
if (entry.isDirectory) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
|
||||
async function buildExportFileItem(instanceRoot, path) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'file',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
size: metadata.size,
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
function appendExportItems(items) {
|
||||
const nextFiles = new Map(files.value.map((file) => [normalizeExportPath(file.path), file]))
|
||||
for (const item of items) {
|
||||
nextFiles.set(normalizeExportPath(item.path), item)
|
||||
}
|
||||
files.value = [...nextFiles.values()]
|
||||
}
|
||||
|
||||
async function getExportCandidateMetadata(instanceRoot, path) {
|
||||
try {
|
||||
const metadata = await stat(`${instanceRoot}/${path}`)
|
||||
return {
|
||||
size: metadata.size,
|
||||
modified: metadata.mtime ? Math.floor(metadata.mtime.getTime() / 1000) : undefined,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
if (currentDirectory.value === normalizedPath) files.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExportPath(path) {
|
||||
return path.replaceAll('\\', '/').split('/').filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
function isDefaultSelectedExportCandidate(path) {
|
||||
return (
|
||||
path.startsWith('mods') ||
|
||||
path.startsWith('datapacks') ||
|
||||
path.startsWith('resourcepacks') ||
|
||||
path.startsWith('shaderpacks') ||
|
||||
path.startsWith('config')
|
||||
)
|
||||
}
|
||||
|
||||
function isExportCandidateDisabled(path) {
|
||||
return (
|
||||
path === 'profile.json' ||
|
||||
path.startsWith('modrinth_logs') ||
|
||||
path.startsWith('.fabric') ||
|
||||
path.startsWith('__MACOSX')
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -278,26 +228,24 @@ function isExportCandidateDisabled(path) {
|
||||
</div>
|
||||
<FileTreeSelect
|
||||
:key="fileTreeKey"
|
||||
v-model="selectedFilePaths"
|
||||
v-model="includedFilePaths"
|
||||
v-model:excluded-paths="excludedFilePaths"
|
||||
class="min-w-0"
|
||||
:items="files"
|
||||
lazy
|
||||
@navigate="loadExportDirectory"
|
||||
/>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, ExcitedRinthbot, RefreshCwIcon, ServerStackIcon } from '@modrinth/assets'
|
||||
import { Button, commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {
|
||||
appUpdateState,
|
||||
downloadAvailableAppUpdate,
|
||||
installAvailableAppUpdate,
|
||||
} from '@/providers/app-update'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'app.hosting.update-required.title',
|
||||
defaultMessage: 'Modrinth App update required',
|
||||
},
|
||||
description: {
|
||||
id: 'app.hosting.update-required.description',
|
||||
defaultMessage: 'You need to update to use Modrinth Hosting through the Modrinth App',
|
||||
},
|
||||
downloadToUpdate: {
|
||||
id: 'app.hosting.update-required.download',
|
||||
defaultMessage: 'Download to update',
|
||||
},
|
||||
downloadingUpdate: {
|
||||
id: 'app.action-bar.downloading-update',
|
||||
defaultMessage: 'Downloading update',
|
||||
},
|
||||
reloadToUpdate: {
|
||||
id: 'app.action-bar.reload-to-update',
|
||||
defaultMessage: 'Reload to update',
|
||||
},
|
||||
excitedRinthbotAlt: {
|
||||
id: 'app.hosting.update-required.rinthbot-alt',
|
||||
defaultMessage: 'Excited Modrinth Bot',
|
||||
},
|
||||
})
|
||||
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'servers',
|
||||
label: () => formatMessage(commonMessages.serversLabel),
|
||||
to: '/hosting/manage/',
|
||||
visual: { type: 'icon', component: ServerStackIcon },
|
||||
})
|
||||
|
||||
const { downloading, downloadPercent, downloadProgress, finishedDownloading } = appUpdateState
|
||||
|
||||
const isUpdateDownloading = computed(
|
||||
() =>
|
||||
downloading.value ||
|
||||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
|
||||
)
|
||||
|
||||
async function handleUpdateClick() {
|
||||
if (isUpdateDownloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
await installAvailableAppUpdate()
|
||||
} else {
|
||||
await downloadAvailableAppUpdate()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="box-border flex min-h-full items-center justify-center p-4">
|
||||
<div class="relative mx-auto w-full max-w-xl pt-28">
|
||||
<img
|
||||
:src="ExcitedRinthbot"
|
||||
:alt="formatMessage(messages.excitedRinthbotAlt)"
|
||||
class="absolute right-8 top-0 h-28 w-auto md:right-20"
|
||||
/>
|
||||
<div class="relative flex flex-col gap-5 rounded-lg bg-bg-raised p-7 shadow-lg">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-px w-full bg-gradient-to-r from-transparent via-green-500 to-transparent opacity-40"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 2rem,
|
||||
var(--color-green) calc(100% - 13rem),
|
||||
var(--color-green) calc(100% - 5rem),
|
||||
transparent calc(100% - 2rem)
|
||||
);
|
||||
"
|
||||
></div>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
<h1 class="m-0 text-3xl font-extrabold">
|
||||
{{ formatMessage(messages.title) }}
|
||||
</h1>
|
||||
<p class="m-0 text-lg">
|
||||
{{ formatMessage(messages.description) }}
|
||||
</p>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">
|
||||
{{ downloadPercent }}%
|
||||
</span>
|
||||
</span>
|
||||
<span v-else-if="finishedDownloading">
|
||||
{{ formatMessage(messages.reloadToUpdate) }}
|
||||
</span>
|
||||
<span v-else>{{ formatMessage(messages.downloadToUpdate) }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
StopCircleIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||
import { Avatar, IconButton, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
@@ -136,7 +136,7 @@ defineExpose({
|
||||
|
||||
const currentEvent = ref(null)
|
||||
|
||||
const unlisten = await process_listener((e) => {
|
||||
useAppEvent('process', (e) => {
|
||||
if (e.instance_id === props.instance.id) {
|
||||
currentEvent.value = e.event
|
||||
if (e.event === 'finished') {
|
||||
@@ -148,7 +148,6 @@ const unlisten = await process_listener((e) => {
|
||||
onMounted(() => {
|
||||
checkProcess()
|
||||
})
|
||||
onUnmounted(() => unlisten())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -168,30 +167,37 @@ onUnmounted(() => unlisten())
|
||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||
<button v-tooltip="'Instance is loading...'" disabled>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="!instance.quarantined"
|
||||
:color="first ? 'brand' : 'standard'"
|
||||
circular
|
||||
<IconButton
|
||||
v-if="playing"
|
||||
v-tooltip="'Stop'"
|
||||
type="colored"
|
||||
color="red"
|
||||
:label="'Stop'"
|
||||
@mouseenter="checkProcess"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<!-- Translate for optical centering -->
|
||||
<PlayIcon class="translate-x-[1px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<StopCircleIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="modLoading"
|
||||
v-tooltip="'Instance is loading...'"
|
||||
:label="'Instance is loading...'"
|
||||
disabled
|
||||
>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="!instance.quarantined"
|
||||
v-tooltip="'Play'"
|
||||
:type="first ? 'colored' : 'base'"
|
||||
:color="first ? 'brand' : undefined"
|
||||
label="Play"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<!-- Translate for optical centering -->
|
||||
<PlayIcon class="translate-x-[1px]" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon />
|
||||
@@ -219,47 +225,51 @@ onUnmounted(() => unlisten())
|
||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||
<button
|
||||
v-tooltip="'Stop'"
|
||||
:class="{ 'scale-100 opacity-100': playing }"
|
||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-if="playing"
|
||||
v-tooltip="'Stop'"
|
||||
type="colored"
|
||||
color="red"
|
||||
size="xl"
|
||||
:label="'Stop'"
|
||||
:class="{ 'scale-100 opacity-100': playing }"
|
||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</IconButton>
|
||||
<SpinnerIcon
|
||||
v-else-if="modLoading || installing"
|
||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||
class="animate-spin w-8 h-8"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<ButtonStyled
|
||||
<IconButton
|
||||
v-else-if="!installed && !instance.quarantined"
|
||||
size="large"
|
||||
v-tooltip="'Repair'"
|
||||
type="colored"
|
||||
color="brand"
|
||||
circular
|
||||
size="xl"
|
||||
:label="'Repair'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Repair'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="!instance.quarantined" size="large" color="brand" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-[2px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="!instance.quarantined"
|
||||
v-tooltip="'Play'"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
:label="'Play'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-[2px]" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, FormattedTag } from '@modrinth/ui'
|
||||
import { Avatar, ButtonLink, FormattedTag } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -48,9 +48,7 @@ const instanceLink = computed(() => {
|
||||
</span>
|
||||
</span>
|
||||
</router-link>
|
||||
<ButtonStyled>
|
||||
<router-link :to="instanceLink"> <LeftArrowIcon /> Back to instance </router-link>
|
||||
</ButtonStyled>
|
||||
<ButtonLink :to="instanceLink"> <LeftArrowIcon /> Back to instance </ButtonLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -14,18 +14,14 @@
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex items-center justify-end">
|
||||
<ButtonStyled v-if="currentSelected.path === row.path">
|
||||
<button class="!shadow-none" disabled>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.selected) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button class="!shadow-none" @click="setJavaInstall(row)">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.select) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button v-if="currentSelected.path === row.path" disabled>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.selected) }}
|
||||
</Button>
|
||||
<Button v-else @click="setJavaInstall(row)">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.select) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty-state>
|
||||
@@ -35,28 +31,21 @@
|
||||
</template>
|
||||
</Table>
|
||||
<div class="flex justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!shadow-none !border-surface-4 !border"
|
||||
@click="$refs.detectJavaModal.hide()"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="outlined"
|
||||
class="!border-surface-4 !border"
|
||||
@click="$refs.detectJavaModal.hide()"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
Table,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { Button, defineMessages, injectNotificationManager, Table, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@@ -17,64 +17,73 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<ButtonStyled
|
||||
<Button
|
||||
type="quiet"
|
||||
:color="
|
||||
!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
: undefined
|
||||
"
|
||||
color-fill="text"
|
||||
:aria-label="formatMessage(messages.testJavaInstallation)"
|
||||
class="!text-[var(--legacy-button-color,var(--color-base))] [&>svg]:!text-[var(--legacy-button-color,var(--color-primary))]"
|
||||
:disabled="testingJava || props.disabled"
|
||||
:style="{
|
||||
'--legacy-button-color':
|
||||
(!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') &&
|
||||
(!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') !== 'standard'
|
||||
? `var(--color-${
|
||||
!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
})`
|
||||
: undefined,
|
||||
}"
|
||||
@click="runTest(props.modelValue?.path)"
|
||||
@mouseenter="!props.disabled && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<button
|
||||
:aria-label="formatMessage(messages.testJavaInstallation)"
|
||||
class="!shadow-none"
|
||||
:disabled="testingJava || props.disabled"
|
||||
@click="runTest(props.modelValue?.path)"
|
||||
@mouseenter="!props.disabled && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="testingJavaSuccess === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon v-else-if="testingJavaSuccess === true && !hoveringTest" class="h-4 w-4" />
|
||||
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<span class="installation-buttons">
|
||||
<ButtonStyled v-if="props.version">
|
||||
<button
|
||||
v-tooltip="
|
||||
testingJavaSuccess === true ? formatMessage(messages.alreadyInstalled) : undefined
|
||||
"
|
||||
class="!shadow-none"
|
||||
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
|
||||
@click="reinstallJava"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{
|
||||
installingJava
|
||||
? formatMessage(messages.installing)
|
||||
: formatMessage(messages.installRecommended)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
{{ formatMessage(messages.detect) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
{{ formatMessage(messages.browse) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
v-if="props.version"
|
||||
v-tooltip="
|
||||
testingJavaSuccess === true ? formatMessage(messages.alreadyInstalled) : undefined
|
||||
"
|
||||
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
|
||||
@click="reinstallJava"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{
|
||||
installingJava
|
||||
? formatMessage(messages.installing)
|
||||
: formatMessage(messages.installRecommended)
|
||||
}}
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
{{ formatMessage(messages.detect) }}
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
{{ formatMessage(messages.browse) }}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -90,7 +99,7 @@ import {
|
||||
XCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { CheckIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
import { Badge, IconButton } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
@@ -74,18 +74,16 @@ const onHide = () => {
|
||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<ButtonStyled
|
||||
circular
|
||||
:color="version.id === installedVersion ? 'standard' : 'brand'"
|
||||
<IconButton
|
||||
:type="version.id === installedVersion ? 'base' : 'colored'"
|
||||
:color="version.id === installedVersion ? undefined : 'brand'"
|
||||
label="Switch version"
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<button
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
<div class="version-link">
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<script setup>
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { Avatar, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { instanceKeys } from '@/pages/instance/query-options'
|
||||
|
||||
const ITEM_SIZE = 52
|
||||
const APPROX_USED_VERTICAL_SPACE = 513 // doesn't need to be exact lol just close enough so there's a little gap and no overflow
|
||||
const STORAGE_KEY = 'modrinth-quick-instance-count'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -120,6 +123,10 @@ const onDividerPointerUp = (event) => {
|
||||
const getInstances = async () => {
|
||||
const instances = await list().catch(handleError)
|
||||
|
||||
for (const instance of instances) {
|
||||
queryClient.setQueryData(instanceKeys.detail(instance.id), instance)
|
||||
}
|
||||
|
||||
allInstances.value = instances.sort((a, b) => {
|
||||
const dateACreated = dayjs(a.created)
|
||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||
@@ -141,7 +148,7 @@ const getInstances = async () => {
|
||||
await getInstances()
|
||||
updateMaxAuto()
|
||||
|
||||
const unlistenInstance = await instance_listener(async (event) => {
|
||||
useAppEvent('instance', async (event) => {
|
||||
if (event.event !== 'synced') {
|
||||
await getInstances()
|
||||
}
|
||||
@@ -155,7 +162,6 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateMaxAuto)
|
||||
document.body.classList.remove('quick-instance-dragging')
|
||||
clearOverdragFlash()
|
||||
unlistenInstance()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -82,7 +82,7 @@ import { injectLoadingState } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { loading_listener } from '@/helpers/events.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
|
||||
const doneLoading = ref(false)
|
||||
const loadingProgress = ref(0)
|
||||
@@ -132,13 +132,10 @@ function fakeLoadingIncrease() {
|
||||
}
|
||||
}
|
||||
|
||||
loading_listener(async (e) => {
|
||||
useAppEvent('loading', (e) => {
|
||||
if (e.event.type === 'directory_move') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
message.value = 'Updating app directory...'
|
||||
} else if (e.event.type === 'checking_for_updates') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
message.value = 'Checking for updates...'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { NotepadTextIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { Button, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { $fetch } from 'ofetch'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { release_ads_window_hold, take_ads_window_hold } from '@/helpers/ads.js'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { get as getCreds } from '@/helpers/mr_auth.ts'
|
||||
|
||||
let adsWindowHold = false
|
||||
|
||||
type Survey = {
|
||||
id: string
|
||||
tally_id: string
|
||||
@@ -92,30 +94,44 @@ async function openSurvey() {
|
||||
onOpen: () => console.info('Opened user survey'),
|
||||
onClose: () => {
|
||||
console.info('Closed user survey')
|
||||
show_ads_window()
|
||||
if (adsWindowHold) {
|
||||
adsWindowHold = false
|
||||
release_ads_window_hold()
|
||||
}
|
||||
},
|
||||
onSubmit: () => console.info('Active user survey submitted'),
|
||||
}
|
||||
|
||||
try {
|
||||
hide_ads_window()
|
||||
await take_ads_window_hold()
|
||||
adsWindowHold = true
|
||||
if (tallyWindow.Tally?.openPopup) {
|
||||
console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
|
||||
dismissSurvey()
|
||||
tallyWindow.Tally.openPopup(formId, popupOptions)
|
||||
} else {
|
||||
console.warn('Tally script not yet loaded')
|
||||
show_ads_window()
|
||||
adsWindowHold = false
|
||||
await release_ads_window_hold()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error opening Tally popup:', e)
|
||||
show_ads_window()
|
||||
if (adsWindowHold) {
|
||||
adsWindowHold = false
|
||||
await release_ads_window_hold()
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`Found user survey to show with tally_id: ${formId}`)
|
||||
tallyWindow.Tally?.openPopup(formId, popupOptions)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (adsWindowHold) {
|
||||
adsWindowHold = false
|
||||
release_ads_window_hold()
|
||||
}
|
||||
})
|
||||
|
||||
function dismissSurvey() {
|
||||
if (!availableSurvey.value) return
|
||||
localStorage.setItem(`survey-${availableSurvey.value.id}-display`, String(new Date()))
|
||||
@@ -193,18 +209,14 @@ onMounted(async () => {
|
||||
{{ formatMessage(messages.surveyFooter) }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openSurvey">
|
||||
<NotepadTextIcon />
|
||||
{{ formatMessage(messages.takeSurvey) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="dismissSurvey">
|
||||
<XIcon />
|
||||
{{ formatMessage(messages.surveyNoThanks) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="openSurvey">
|
||||
<NotepadTextIcon />
|
||||
{{ formatMessage(messages.takeSurvey) }}
|
||||
</Button>
|
||||
<Button @click="dismissSurvey">
|
||||
<XIcon />
|
||||
{{ formatMessage(messages.surveyNoThanks) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -69,9 +69,7 @@ async function install() {
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="install">Install</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="install">Install</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,34 +4,37 @@
|
||||
class="flex items-center gap-2 mr-1.5"
|
||||
data-tauri-drag-region-exclude
|
||||
>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button class="relative expanded-button" @click="() => getCurrentWindow().minimize()">
|
||||
<MinimizeIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button class="relative expanded-button" @click="() => getCurrentWindow().toggleMaximize()">
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
color="red"
|
||||
color-fill="none"
|
||||
hover-color-fill="background"
|
||||
circular
|
||||
<IconButton
|
||||
type="quiet"
|
||||
label="Minimize window"
|
||||
class="relative expanded-button"
|
||||
@click="() => getCurrentWindow().minimize()"
|
||||
>
|
||||
<button class="relative expanded-button close-button" @click="handleClose">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<MinimizeIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="quiet"
|
||||
label="Toggle maximize window"
|
||||
class="relative expanded-button"
|
||||
@click="() => getCurrentWindow().toggleMaximize()"
|
||||
>
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="quiet"
|
||||
label="Close window"
|
||||
class="relative expanded-button close-button"
|
||||
@click="handleClose"
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MaximizeIcon, MinimizeIcon, RestoreIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { IconButton } from '@modrinth/ui'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<template>
|
||||
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
|
||||
<button
|
||||
v-if="showUpdatePill"
|
||||
type="button"
|
||||
class="!h-[34px] text-sm !transition-[opacity,transform,background-color,color,filter] !duration-200 ease-out"
|
||||
:class="{
|
||||
'opacity-0 scale-[0.96]': finishedDownloading && !animateReadyPill,
|
||||
'opacity-100 scale-100': finishedDownloading && animateReadyPill,
|
||||
}"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
v-if="showUpdatePill"
|
||||
type="outlined"
|
||||
native-type="button"
|
||||
class="!h-[34px] text-sm !transition-[opacity,transform,background-color,color,filter] !duration-200 ease-out !text-brand [&>svg]:!text-brand !shadow-[inset_0_0_0_1px_var(--color-brand)] hover:!bg-brand focus-visible:!bg-brand hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
|
||||
:class="{
|
||||
'opacity-0 scale-[0.96]': finishedDownloading && !animateReadyPill,
|
||||
'opacity-100 scale-100': finishedDownloading && animateReadyPill,
|
||||
}"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, RefreshCwIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { Button, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { MailIcon, SearchIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
@@ -17,17 +18,40 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { useFriends } from '@/composables/use-friends'
|
||||
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||
import type { ModrinthCredentials } from '@/helpers/mr_auth'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const themeStore = useTheming()
|
||||
|
||||
const props = defineProps<{
|
||||
credentials: ModrinthCredentials | null
|
||||
signIn: () => void
|
||||
}>()
|
||||
|
||||
type FriendsSectionCollapsedFlag =
|
||||
| 'friends_active_collapsed'
|
||||
| 'friends_online_collapsed'
|
||||
| 'friends_offline_collapsed'
|
||||
| 'friends_pending_collapsed'
|
||||
|
||||
function isFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag) {
|
||||
return themeStore.getFeatureFlag(flag)
|
||||
}
|
||||
|
||||
function setFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag, collapsed: boolean) {
|
||||
themeStore.featureFlags[flag] = collapsed
|
||||
getSettings()
|
||||
.then((settings) => {
|
||||
settings.feature_flags[flag] = collapsed
|
||||
return setSettings(settings)
|
||||
})
|
||||
.catch(handleError)
|
||||
}
|
||||
|
||||
const userCredentials = computed(() => props.credentials)
|
||||
const {
|
||||
friends: userFriends,
|
||||
@@ -197,26 +221,20 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<template v-if="friend.id === userCredentials?.user_id">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="addFriend(friend)">
|
||||
<UserPlusIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Ignore
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="addFriend(friend)">
|
||||
<UserPlusIcon />
|
||||
Accept
|
||||
</Button>
|
||||
<Button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Ignore
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,26 +258,28 @@ const messages = defineMessages({
|
||||
wrapper-class="flex-1"
|
||||
@keyup.enter="addFriendFromModal"
|
||||
/>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||
<SendIcon />
|
||||
{{ formatMessage(messages.sendFriendRequest) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="username.length === 0"
|
||||
@click="addFriendFromModal"
|
||||
>
|
||||
<SendIcon />
|
||||
{{ formatMessage(messages.sendFriendRequest) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 -ml-1">
|
||||
<template v-if="sortedFriends.length > 0">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.addFriend)"
|
||||
:aria-label="formatMessage(messages.addFriend)"
|
||||
@click="addFriendModal.show"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.addFriend)"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.addFriend)"
|
||||
@click="addFriendModal.show"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
</IconButton>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
@@ -274,23 +294,23 @@ const messages = defineMessages({
|
||||
<h3 v-else class="w-full text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
</h3>
|
||||
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
class="relative"
|
||||
:aria-label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
@click="friendInvitesModal.show"
|
||||
<IconButton
|
||||
v-if="incomingRequests.length > 0"
|
||||
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
class="relative"
|
||||
@click="friendInvitesModal.show"
|
||||
>
|
||||
<MailIcon />
|
||||
<span
|
||||
v-if="incomingRequests.length > 0"
|
||||
aria-hidden="true"
|
||||
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
|
||||
>
|
||||
<MailIcon />
|
||||
<span
|
||||
v-if="incomingRequests.length > 0"
|
||||
aria-hidden="true"
|
||||
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
|
||||
>
|
||||
{{ incomingRequests.length }}
|
||||
</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
{{ incomingRequests.length }}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 v-if="loading" class="text-base text-primary font-medium m-0">
|
||||
@@ -331,33 +351,42 @@ const messages = defineMessages({
|
||||
<FriendsSection
|
||||
v-if="activeFriends.length > 0"
|
||||
:is-searching="!!search"
|
||||
open-by-default
|
||||
:open-by-default="!isFriendsSectionCollapsed('friends_active_collapsed')"
|
||||
:friends="activeFriends"
|
||||
:heading="formatMessage(messages.active)"
|
||||
:remove-friend="removeFriend"
|
||||
@on-open="setFriendsSectionCollapsed('friends_active_collapsed', false)"
|
||||
@on-close="setFriendsSectionCollapsed('friends_active_collapsed', true)"
|
||||
/>
|
||||
<FriendsSection
|
||||
v-if="onlineFriends.length > 0"
|
||||
:is-searching="!!search"
|
||||
open-by-default
|
||||
:open-by-default="!isFriendsSectionCollapsed('friends_online_collapsed')"
|
||||
:friends="onlineFriends"
|
||||
:heading="formatMessage(messages.online)"
|
||||
:remove-friend="removeFriend"
|
||||
@on-open="setFriendsSectionCollapsed('friends_online_collapsed', false)"
|
||||
@on-close="setFriendsSectionCollapsed('friends_online_collapsed', true)"
|
||||
/>
|
||||
<FriendsSection
|
||||
v-if="offlineFriends.length > 0"
|
||||
:is-searching="!!search"
|
||||
:open-by-default="activeFriends.length + onlineFriends.length < 3"
|
||||
:open-by-default="!isFriendsSectionCollapsed('friends_offline_collapsed')"
|
||||
:friends="offlineFriends"
|
||||
:heading="formatMessage(messages.offline)"
|
||||
:remove-friend="removeFriend"
|
||||
@on-open="setFriendsSectionCollapsed('friends_offline_collapsed', false)"
|
||||
@on-close="setFriendsSectionCollapsed('friends_offline_collapsed', true)"
|
||||
/>
|
||||
<FriendsSection
|
||||
v-if="pendingFriends.length > 0"
|
||||
:is-searching="!!search"
|
||||
:open-by-default="!isFriendsSectionCollapsed('friends_pending_collapsed')"
|
||||
:friends="pendingFriends"
|
||||
:heading="formatMessage(messages.pending)"
|
||||
:remove-friend="removeFriend"
|
||||
@on-open="setFriendsSectionCollapsed('friends_pending_collapsed', false)"
|
||||
@on-close="setFriendsSectionCollapsed('friends_pending_collapsed', true)"
|
||||
/>
|
||||
<p v-if="filteredFriends.length === 0 && search" class="text-sm text-secondary my-1 mx-4">
|
||||
{{ formatMessage(messages.noFriendsMatch, { query: search }) }}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
OverflowMenu,
|
||||
IconButton,
|
||||
TeleportOverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useTemplateRef } from 'vue'
|
||||
@@ -31,6 +31,11 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
onOpen: []
|
||||
onClose: []
|
||||
}>()
|
||||
|
||||
function createContextMenuOptions(friend: FriendWithUserData) {
|
||||
if (friend.accepted) {
|
||||
return [
|
||||
@@ -112,6 +117,8 @@ const messages = defineMessages({
|
||||
? ''
|
||||
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
|
||||
"
|
||||
@on-open="emit('onOpen')"
|
||||
@on-close="emit('onClose')"
|
||||
>
|
||||
<template #title>
|
||||
<h3 class="text-base text-primary font-medium m-0">
|
||||
@@ -159,29 +166,35 @@ const messages = defineMessages({
|
||||
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<ButtonStyled v-if="friend.accepted" circular type="transparent">
|
||||
<OverflowMenu
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:options="[
|
||||
{
|
||||
id: 'remove-friend',
|
||||
action: () => removeFriend(friend),
|
||||
color: 'red',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #remove-friend>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.removeFriend) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else type="transparent" circular>
|
||||
<button v-tooltip="formatMessage(messages.cancelRequest)" @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<TeleportOverflowMenu
|
||||
v-if="friend.accepted"
|
||||
type="quiet"
|
||||
label="More options"
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:options="[
|
||||
{
|
||||
id: 'remove-friend',
|
||||
label: formatMessage(messages.removeFriend),
|
||||
action: () => removeFriend(friend),
|
||||
tone: 'red',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #remove-friend>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.removeFriend) }}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
<IconButton
|
||||
v-else
|
||||
v-tooltip="formatMessage(messages.cancelRequest)"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.cancelRequest)"
|
||||
@click="removeFriend(friend)"
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import { Admonition, Avatar, Button, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -15,6 +9,7 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { add_server_to_instance, get_instance_worlds } from '@/helpers/worlds.ts'
|
||||
import { instanceKeys } from '@/pages/instance/query-options'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -67,7 +62,7 @@ async function addServer(instance) {
|
||||
try {
|
||||
await add_server_to_instance(instance.id, serverName.value, serverAddress.value, 'prompt')
|
||||
instance.added = true
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.id] })
|
||||
await queryClient.invalidateQueries({ queryKey: instanceKeys.worlds(instance.id) })
|
||||
|
||||
trackEvent('AddServerToInstance', {
|
||||
server_name: serverName.value,
|
||||
@@ -109,19 +104,15 @@ async function addServer(instance) {
|
||||
/>
|
||||
{{ instance.name }}
|
||||
</router-link>
|
||||
<ButtonStyled>
|
||||
<button :disabled="instance.added || instance.adding" @click="addServer(instance)">
|
||||
<PlusIcon v-if="!instance.added && !instance.adding" />
|
||||
<CheckIcon v-else-if="instance.added" />
|
||||
{{ instance.adding ? 'Adding...' : instance.added ? 'Added' : 'Add' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button :disabled="instance.added || instance.adding" @click="addServer(instance)">
|
||||
<PlusIcon v-if="!instance.added && !instance.adding" />
|
||||
<CheckIcon v-else-if="instance.added" />
|
||||
{{ instance.adding ? 'Adding...' : instance.added ? 'Added' : 'Add' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="modal.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="modal.hide()">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<template v-if="loadingServerPing">
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
<div
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
</template>
|
||||
|
||||
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
||||
|
||||
<div v-if="minecraftServer?.region || ping" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
||||
|
||||
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
|
||||
Linked to
|
||||
<Avatar
|
||||
:src="linkedProjectV3.icon_url"
|
||||
:alt="linkedProjectV3.name"
|
||||
:tint-by="instanceId"
|
||||
size="24px"
|
||||
/>
|
||||
<router-link
|
||||
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
||||
class="hover:underline text-primary truncate"
|
||||
>
|
||||
{{ linkedProjectV3.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
Avatar,
|
||||
ServerOnlinePlayers,
|
||||
ServerPing,
|
||||
ServerRecentPlays,
|
||||
ServerRegion,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
defineProps<{
|
||||
loadingServerPing?: boolean
|
||||
playersOnline?: number
|
||||
statusOnline?: boolean
|
||||
recentPlays?: number
|
||||
ping?: number
|
||||
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
|
||||
linkedProjectV3?: Labrinth.Projects.v3.Project
|
||||
instanceId?: string
|
||||
}>()
|
||||
</script>
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<Admonition
|
||||
type="info"
|
||||
inline-actions
|
||||
:header="formatMessage(messages.sharedInstanceUpdateAvailableHeader)"
|
||||
>
|
||||
{{ formatMessage(messages.sharedInstanceUpdateAvailableBody, { name: instanceName }) }}
|
||||
<template #actions>
|
||||
<ButtonStyled color="blue">
|
||||
<button class="!h-10" @click="emit('review', $event)">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.sharedInstanceReviewUpdateButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon } from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, useVIntl } from '@modrinth/ui'
|
||||
|
||||
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
|
||||
|
||||
defineProps<{
|
||||
instanceName: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
review: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
</script>
|
||||
+23
-29
@@ -7,10 +7,9 @@ import {
|
||||
MessagesSquareIcon,
|
||||
WrenchIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, Collapsible, NewModal } from '@modrinth/ui'
|
||||
import { Admonition, Button, ButtonLink, Collapsible, IconButton, NewModal } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
@@ -29,19 +28,13 @@ function show(errorVal: { message?: string }) {
|
||||
matchedError.value = findMinecraftAuthError(rawError.value)
|
||||
|
||||
debugCollapsed.value = true
|
||||
hide_ads_window()
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
onModalHide()
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
function onModalHide() {
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
@@ -74,7 +67,7 @@ async function copyToClipboard(text: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'" @hide="onModalHide">
|
||||
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'">
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition
|
||||
type="warning"
|
||||
@@ -137,16 +130,18 @@ async function copyToClipboard(text: string) {
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<a href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||
<MessagesSquareIcon /> Contact support
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="loadingSignIn" class="!w-full" @click="signInAgain">
|
||||
<LogInIcon /> Sign in again
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonLink href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||
<MessagesSquareIcon /> Contact support
|
||||
</ButtonLink>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="loadingSignIn"
|
||||
class="!w-full"
|
||||
@click="signInAgain"
|
||||
>
|
||||
<LogInIcon /> Sign in again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -176,16 +171,15 @@ async function copyToClipboard(text: string) {
|
||||
>
|
||||
{{ debugInfo }}
|
||||
</div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="'Copy debug info'"
|
||||
:label="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
+23
-34
@@ -20,39 +20,28 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6 px-6 pb-6">
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled>
|
||||
<a class="w-full !shadow-none" href="https://support.modrinth.com" @click="modal?.hide()">
|
||||
<MessagesSquareIcon />
|
||||
{{ formatMessage(messages.getSupport) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button class="w-full !shadow-none" :disabled="loadingSignIn" @click="signIn">
|
||||
<SpinnerIcon v-if="loadingSignIn" class="animate-spin" />
|
||||
<svg
|
||||
v-else
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect x="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect
|
||||
x="10.75"
|
||||
y="10.75"
|
||||
width="9.25"
|
||||
height="9.25"
|
||||
fill="black"
|
||||
fill-opacity="0.9"
|
||||
/>
|
||||
</svg>
|
||||
{{ formatMessage(messages.signIn) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<ButtonLink href="https://support.modrinth.com" @click="modal?.hide()">
|
||||
<MessagesSquareIcon />
|
||||
{{ formatMessage(messages.getSupport) }}
|
||||
</ButtonLink>
|
||||
<Button type="colored" color="brand" :disabled="loadingSignIn" @click="signIn">
|
||||
<SpinnerIcon v-if="loadingSignIn" class="animate-spin" />
|
||||
<svg
|
||||
v-else
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect x="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect x="10.75" y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
</svg>
|
||||
{{ formatMessage(messages.signIn) }}
|
||||
</Button>
|
||||
</div>
|
||||
<p class="m-0 text-center text-sm text-secondary">
|
||||
{{ formatMessage(messages.dontHaveAccount) }}
|
||||
@@ -69,7 +58,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MessagesSquareIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { Button, ButtonLink, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { inject, type Ref, ref } from 'vue'
|
||||
|
||||
import steveImage from '@/assets/steve-look-up-left.webp'
|
||||
|
||||
@@ -6,18 +6,14 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.deleteButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="red" @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.deleteButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -27,7 +23,7 @@
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.installToPlay)"
|
||||
:closable="true"
|
||||
:on-hide="show_ads_window"
|
||||
max-width="544px"
|
||||
width="544px"
|
||||
>
|
||||
@@ -18,12 +17,10 @@
|
||||
{{ formatMessage(messages.sharedInstance) }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="openViewContents">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" @click="openViewContents">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 rounded-2xl bg-surface-2 p-3">
|
||||
@@ -113,65 +110,56 @@
|
||||
</p>
|
||||
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<ButtonStyled type="transparent" color="red">
|
||||
<button @click="handleReport">
|
||||
<ReportIcon />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="red" @click="handleReport">
|
||||
<ReportIcon />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</Button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="hasExternalFiles">
|
||||
<ButtonStyled type="transparent" color="orange">
|
||||
<button @click="handleAccept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleDecline">
|
||||
<BanIcon />
|
||||
{{ formatMessage(messages.dontInstall) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="orange" @click="handleAccept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="handleDecline">
|
||||
<BanIcon />
|
||||
{{ formatMessage(messages.dontInstall) }}
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<button @click="handleDecline">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleAccept">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="handleDecline">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="handleAccept">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="project?.name ?? ''"
|
||||
:modpack-icon-url="project?.icon_url ?? undefined"
|
||||
<ManagedContentModal
|
||||
ref="managedContentModal"
|
||||
:header="formatMessage(messages.modpackContent)"
|
||||
:source-name="project?.name ?? ''"
|
||||
:source-icon-url="project?.icon_url ?? undefined"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { BanIcon, DownloadIcon, EyeIcon, ReportIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
type ContentItem,
|
||||
defineMessages,
|
||||
formatLoader,
|
||||
ModpackContentModal,
|
||||
ManagedContentModal,
|
||||
NewModal,
|
||||
Table,
|
||||
type TableColumn,
|
||||
@@ -181,7 +169,6 @@ import {
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
||||
import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
|
||||
@@ -281,10 +268,10 @@ function handleReport() {
|
||||
}
|
||||
}
|
||||
|
||||
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||||
const managedContentModal = ref<InstanceType<typeof ManagedContentModal>>()
|
||||
|
||||
async function openViewContents() {
|
||||
modpackContentModal.value?.showLoading()
|
||||
managedContentModal.value?.showLoading()
|
||||
try {
|
||||
// Ensure version data is available — the useQuery may not have resolved yet
|
||||
const versionId = modpackVersionId.value
|
||||
@@ -342,10 +329,10 @@ async function openViewContents() {
|
||||
}
|
||||
},
|
||||
)
|
||||
modpackContentModal.value?.show(contentItems)
|
||||
managedContentModal.value?.show(contentItems)
|
||||
} catch (err) {
|
||||
console.error('Failed to load modpack contents:', err)
|
||||
modpackContentModal.value?.show([])
|
||||
managedContentModal.value?.show([])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +351,6 @@ async function show(
|
||||
|
||||
if (modpackVersionIdVal) await fetchData(modpackVersionIdVal)
|
||||
|
||||
hide_ads_window()
|
||||
modal.value?.show(e)
|
||||
await nextTick()
|
||||
forceCheckTableScroll()
|
||||
@@ -375,6 +361,10 @@ function hide() {
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
modpackContent: {
|
||||
id: 'app.modal.install-to-play.managed-content.modpack-header',
|
||||
defaultMessage: 'Modpack content',
|
||||
},
|
||||
installToPlay: {
|
||||
id: 'app.modal.install-to-play.header',
|
||||
defaultMessage: 'Install to play',
|
||||
@@ -415,8 +405,7 @@ const messages = defineMessages({
|
||||
},
|
||||
reviewedFiles: {
|
||||
id: 'app.modal.install-to-play.reviewed-files',
|
||||
defaultMessage:
|
||||
'A file is only reviewed if it’s published to Modrinth, regardless of its file format (including .mrpack).',
|
||||
defaultMessage: "Files that aren't published to Modrinth aren't reviewed.",
|
||||
},
|
||||
installAnyway: {
|
||||
id: 'app.modal.install-to-play.install-anyway',
|
||||
|
||||
@@ -10,24 +10,18 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="handleCreateAnyway">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.create) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</Button>
|
||||
<Button type="colored" color="orange" @click="handleCreateAnyway">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.create) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -36,7 +30,7 @@
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
|
||||
@@ -19,18 +19,20 @@
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<ButtonStyled>
|
||||
<button class="w-full !shadow-none" type="button" @click="authenticate('sign-up')">
|
||||
<UserPlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createAccountButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button class="w-full" type="button" @click="authenticate('sign-in')">
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button class="w-full" native-type="button" @click="authenticate('sign-up')">
|
||||
<UserPlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createAccountButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
class="w-full"
|
||||
native-type="button"
|
||||
@click="authenticate('sign-in')"
|
||||
>
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
|
||||
@@ -69,23 +71,19 @@
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="w-full" type="button" @click="modal?.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="w-full !shadow-none"
|
||||
type="button"
|
||||
:disabled="reopeningBrowser"
|
||||
@click="reopenBrowser"
|
||||
>
|
||||
<RefreshCwIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.openBrowserAgainButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" class="w-full" native-type="button" @click="modal?.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
native-type="button"
|
||||
:disabled="reopeningBrowser"
|
||||
@click="reopenBrowser"
|
||||
>
|
||||
<RefreshCwIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.openBrowserAgainButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
|
||||
@@ -108,7 +106,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LogInIcon, RefreshCwIcon, SpinnerIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, IntlFormatted, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import {
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -247,10 +252,6 @@ const messages = defineMessages({
|
||||
id: 'modal.modrinth-account-required.waiting-for-browser',
|
||||
defaultMessage: 'Waiting for browser confirmation...',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'modal.modrinth-account-required.cancel-button',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
openBrowserAgainButton: {
|
||||
id: 'modal.modrinth-account-required.open-browser-again-button',
|
||||
defaultMessage: 'Open browser again',
|
||||
|
||||
@@ -34,6 +34,7 @@ import { get_project_many, get_version, get_version_many } from '@/helpers/cache
|
||||
import { wait_for_install_job } from '@/helpers/install'
|
||||
import { update_managed_modrinth_version } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
|
||||
type Dependency = Labrinth.Versions.v3.Dependency
|
||||
@@ -74,6 +75,7 @@ type ProjectInfo = {
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const appEvents = injectAppEvents()
|
||||
const { startInstallingServer, stopInstallingServer } = injectServerInstall()
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
@@ -253,7 +255,7 @@ async function handleUpdate() {
|
||||
try {
|
||||
if (modpackVersionId.value && instance.value) {
|
||||
const job = await update_managed_modrinth_version(instance.value.id, modpackVersionId.value)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await wait_for_install_job(appEvents, job.job_id)
|
||||
await onUpdateComplete.value()
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Settings2Icon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
@@ -78,12 +78,10 @@ watch(
|
||||
{{ formatMessage(messages.adsConsentTitle) }}
|
||||
</h2>
|
||||
<div class="mt-2 flex flex-col gap-2.5 items-start">
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" @click="manageAdsPreferences">
|
||||
<Settings2Icon aria-hidden="true" />
|
||||
{{ formatMessage(messages.adsConsentManage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="manageAdsPreferences">
|
||||
<Settings2Icon aria-hidden="true" />
|
||||
{{ formatMessage(messages.adsConsentManage) }}
|
||||
</Button>
|
||||
<div>
|
||||
{{ formatMessage(messages.adsConsentIntro) }}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, Toggle } from '@modrinth/ui'
|
||||
import { Button, Toggle } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
@@ -33,14 +33,13 @@ watch(
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
@click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="quiet"
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
@click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
|
||||
@@ -116,6 +116,35 @@ const messages = defineMessages({
|
||||
id: 'app.settings.default-instance-options.post-exit-hook.description',
|
||||
defaultMessage: 'Runs after the game closes.',
|
||||
},
|
||||
hookVariablesDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.description',
|
||||
defaultMessage:
|
||||
'Hooks run in the working directory of the instance, with the following variables:',
|
||||
},
|
||||
instanceNameDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.inst-name.description',
|
||||
defaultMessage: '$INST_NAME: The name of the instance',
|
||||
},
|
||||
instanceIdDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.inst-id.description',
|
||||
defaultMessage: "$INST_ID: The name of the instance's folder",
|
||||
},
|
||||
instanceDirDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.inst-dir.description',
|
||||
defaultMessage: "$INST_DIR: The absolute path to the instance's folder",
|
||||
},
|
||||
instanceMcDirDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.inst-mc-dir.description',
|
||||
defaultMessage: '$INST_MC_DIR: An alias for $INST_DIR',
|
||||
},
|
||||
instanceJavaDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.inst-java.description',
|
||||
defaultMessage: '$INST_JAVA: The absolute path to the java binary',
|
||||
},
|
||||
instanceJavaArgsDescription: {
|
||||
id: 'instance.settings.tabs.hooks.variables.inst-java-args.description',
|
||||
defaultMessage: '$INST_JAVA_ARGS: The JVM Arguments provided to the game',
|
||||
},
|
||||
})
|
||||
|
||||
const fetchSettings = await get()
|
||||
@@ -328,6 +357,18 @@ watch(
|
||||
{{ formatMessage(messages.postExitHookDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.hookVariablesDescription) }}
|
||||
<ul>
|
||||
<li>{{ formatMessage(messages.instanceNameDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceIdDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceDirDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceMcDirDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceJavaDescription) }}</li>
|
||||
<li>{{ formatMessage(messages.instanceJavaArgsDescription) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+14
-15
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
Slider,
|
||||
StyledInput,
|
||||
@@ -183,16 +184,14 @@ async function findLauncherDir() {
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.browseAppDirectory)"
|
||||
:aria-label="formatMessage(messages.browseAppDirectory)"
|
||||
class="ml-1.5"
|
||||
@click="findLauncherDir"
|
||||
>
|
||||
<FolderSearchIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.browseAppDirectory)"
|
||||
:label="formatMessage(messages.browseAppDirectory)"
|
||||
class="ml-1.5"
|
||||
@click="findLauncherDir"
|
||||
>
|
||||
<FolderSearchIcon aria-hidden="true" />
|
||||
</IconButton>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
@@ -235,10 +234,10 @@ async function findLauncherDir() {
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.appCacheTitle) }}
|
||||
</h2>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
|
||||
<Button id="purge-cache" class="w-fit" @click="handlePurgeCacheClick">
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.purgeCache) }}
|
||||
</button>
|
||||
</Button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.appCacheDescription) }}
|
||||
</p>
|
||||
@@ -280,10 +279,10 @@ async function findLauncherDir() {
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.appDatabaseBackupsTitle) }}
|
||||
</h2>
|
||||
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
|
||||
<Button id="open-db-backups-folder" class="w-fit" @click="openDbBackupsFolder">
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.openBackupsFolder) }}
|
||||
</button>
|
||||
</Button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.appDatabaseBackupsDescription) }}
|
||||
</p>
|
||||
|
||||
+13
-19
@@ -10,24 +10,18 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="handleInstallAnyway">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</Button>
|
||||
<Button type="colored" color="orange" @click="handleInstallAnyway">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -36,7 +30,7 @@
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
|
||||
+40
-4
@@ -26,12 +26,14 @@
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex min-w-0 flex-col gap-3 pt-4">
|
||||
<div class="max-h-[292px] overflow-y-auto rounded-[20px]">
|
||||
<div ref="configFileTreeContainer" class="max-h-[292px] overflow-y-auto rounded-[20px]">
|
||||
<FileTreeSelect
|
||||
v-model="selectedConfigPaths"
|
||||
v-model="includedConfigPaths"
|
||||
v-model:excluded-paths="excludedConfigPaths"
|
||||
:items="configFileItems"
|
||||
:show-size="false"
|
||||
:show-modified="false"
|
||||
@navigate="scrollConfigFileTreeToTop"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,13 +77,23 @@ const emit = defineEmits<{
|
||||
const { formatMessage } = useVIntl()
|
||||
const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useSharedInstanceErrors()
|
||||
const publishReviewModal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||
const configFileTreeContainer = ref<HTMLElement>()
|
||||
const publishDiffs = ref<ContentDiffItem[]>([])
|
||||
const configFilePaths = ref<string[]>([])
|
||||
const selectedConfigPaths = ref<string[]>([])
|
||||
const includedConfigPaths = ref<string[]>([])
|
||||
const excludedConfigPaths = ref<string[]>([])
|
||||
const state = ref<SharedInstancePublishState>('idle')
|
||||
const configFileItems = computed<FileTreeSelectItem[]>(() =>
|
||||
configFilePaths.value.map((path) => ({ path, type: 'file' })),
|
||||
)
|
||||
const selectedConfigPaths = computed(() => {
|
||||
const includedPaths = new Set(includedConfigPaths.value)
|
||||
const excludedPaths = new Set(excludedConfigPaths.value)
|
||||
|
||||
return configFilePaths.value.filter((path) =>
|
||||
isConfigPathSelected(path, includedPaths, excludedPaths),
|
||||
)
|
||||
})
|
||||
|
||||
async function show(e?: MouseEvent) {
|
||||
if (state.value !== 'idle') return
|
||||
@@ -106,7 +118,8 @@ async function show(e?: MouseEvent) {
|
||||
disabled: diff.disabled,
|
||||
}))
|
||||
configFilePaths.value = preview.configFiles
|
||||
selectedConfigPaths.value = []
|
||||
includedConfigPaths.value = []
|
||||
excludedConfigPaths.value = []
|
||||
if (!publishReviewModal.value) return
|
||||
|
||||
publishReviewModal.value.show(e)
|
||||
@@ -132,6 +145,29 @@ async function publishChanges() {
|
||||
}
|
||||
}
|
||||
|
||||
function isConfigPathSelected(
|
||||
path: string,
|
||||
includedPaths: Set<string>,
|
||||
excludedPaths: Set<string>,
|
||||
) {
|
||||
let selected = false
|
||||
let prefix = ''
|
||||
|
||||
for (const segment of path.split('/').filter(Boolean)) {
|
||||
prefix = prefix ? `${prefix}/${segment}` : segment
|
||||
if (includedPaths.has(prefix)) selected = true
|
||||
if (excludedPaths.has(prefix)) selected = false
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
function scrollConfigFileTreeToTop() {
|
||||
if (configFileTreeContainer.value) {
|
||||
configFileTreeContainer.value.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
function finishReview() {
|
||||
if (state.value === 'reviewing') {
|
||||
setState('idle')
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from '@/helpers/install'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
@@ -54,6 +55,7 @@ const instance = ref<GameInstance | null>(null)
|
||||
const preview = ref<SharedInstanceUpdatePreview | null>(null)
|
||||
const onComplete = ref<UpdateCompleteCallback>(() => {})
|
||||
const { formatMessage } = useVIntl()
|
||||
const appEvents = injectAppEvents()
|
||||
const { notifySharedInstanceError } = useSharedInstanceErrors()
|
||||
const diffs = computed<ContentDiffItem[]>(
|
||||
() =>
|
||||
@@ -78,7 +80,7 @@ async function update() {
|
||||
try {
|
||||
if (instance.value) {
|
||||
const job = await install_update_shared_instance(instance.value.id)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await wait_for_install_job(appEvents, job.job_id)
|
||||
await onComplete.value()
|
||||
successful = true
|
||||
}
|
||||
|
||||
+34
-63
@@ -193,84 +193,71 @@
|
||||
{{ formatMessage(messages.reviewedFiles) }}
|
||||
</p>
|
||||
<div v-if="!reportMode" class="flex w-full items-center justify-between gap-2">
|
||||
<ButtonStyled color="red" type="transparent">
|
||||
<button @click="reportMode = true">
|
||||
<ReportIcon />{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="red" @click="reportMode = true">
|
||||
<ReportIcon />{{ formatMessage(commonMessages.reportButton) }}
|
||||
</Button>
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="hasExternalFiles">
|
||||
<ButtonStyled type="transparent" color="orange">
|
||||
<button @click="accept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleCancel">
|
||||
<BanIcon />{{ formatMessage(messages.dontInstall) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="orange" @click="accept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="handleCancel">
|
||||
<BanIcon />{{ formatMessage(messages.dontInstall) }}
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="accept">
|
||||
<DownloadIcon />{{ formatMessage(messages.installButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" class="!border" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="accept">
|
||||
<DownloadIcon />{{ formatMessage(messages.installButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="reportMode" #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border" :disabled="submitLoading" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!canSubmitReport" @click="submitReport">
|
||||
<SpinnerIcon v-if="submitLoading" class="animate-spin" />
|
||||
<SendIcon v-else />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" class="!border" :disabled="submitLoading" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="!canSubmitReport" @click="submitReport">
|
||||
<SpinnerIcon v-if="submitLoading" class="animate-spin" />
|
||||
<SendIcon v-else />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
<ModpackContentModal
|
||||
<ManagedContentModal
|
||||
ref="contentModal"
|
||||
:header="formatMessage(messages.sharedInstanceContent)"
|
||||
:modpack-name="preview?.name ?? ''"
|
||||
:modpack-icon-url="preview?.iconUrl ?? undefined"
|
||||
:source-name="preview?.name ?? ''"
|
||||
:source-icon-url="preview?.iconUrl ?? undefined"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { BanIcon, DownloadIcon, ReportIcon, SendIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import {
|
||||
Admonition,
|
||||
AutoLink,
|
||||
Avatar,
|
||||
blockedUsersQueryKey,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
formatReportType,
|
||||
injectAuth,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
ManagedContentModal,
|
||||
MarkdownEditor,
|
||||
ModpackContentModal,
|
||||
NewModal,
|
||||
Table,
|
||||
type TableColumn,
|
||||
@@ -282,7 +269,6 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import { config } from '@/config'
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
import { create_report } from '@/helpers/reports'
|
||||
@@ -303,7 +289,7 @@ type SharedInstanceCreator = {
|
||||
}
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||||
const contentModal = ref<InstanceType<typeof ManagedContentModal>>()
|
||||
const externalFileTable = ref<HTMLElement | null>(null)
|
||||
const preview = ref<SharedInstanceInstallPreview | null>(null)
|
||||
const creator = ref<SharedInstanceCreator | null>(null)
|
||||
@@ -343,9 +329,9 @@ const externalFileRows = computed<ExternalFileRow[]>(() =>
|
||||
.sort((left, right) => left.name.localeCompare(right.name)),
|
||||
)
|
||||
const reportReasonOptions = computed<ComboboxOption<ReportReason>[]>(() => [
|
||||
{ value: 'malicious', label: formatMessage(messages.maliciousReason) },
|
||||
{ value: 'inappropriate', label: formatMessage(messages.inappropriateReason) },
|
||||
{ value: 'spam', label: formatMessage(messages.spamReason) },
|
||||
{ value: 'malicious', label: formatReportType(formatMessage, 'malicious') },
|
||||
{ value: 'inappropriate', label: formatReportType(formatMessage, 'inappropriate') },
|
||||
{ value: 'spam', label: formatReportType(formatMessage, 'spam') },
|
||||
])
|
||||
const canSubmitReport = computed(
|
||||
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
|
||||
@@ -462,7 +448,6 @@ function handleCancel() {
|
||||
function handleHide() {
|
||||
resetReportState()
|
||||
creator.value = null
|
||||
show_ads_window()
|
||||
}
|
||||
function resetReportState() {
|
||||
reportMode.value = false
|
||||
@@ -502,7 +487,6 @@ function showReport(
|
||||
}
|
||||
function showPreview(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
|
||||
preview.value = previewValue
|
||||
hide_ads_window()
|
||||
modal.value?.show(event)
|
||||
void nextTick(() => forceCheckTableScroll())
|
||||
}
|
||||
@@ -557,18 +541,6 @@ const messages = defineMessages({
|
||||
id: 'app.modal.install-to-play.report-reason',
|
||||
defaultMessage: 'Which rule does this instance violate?',
|
||||
},
|
||||
maliciousReason: {
|
||||
id: 'app.modal.install-to-play.report-reason.malicious',
|
||||
defaultMessage: 'Malicious',
|
||||
},
|
||||
inappropriateReason: {
|
||||
id: 'app.modal.install-to-play.report-reason.inappropriate',
|
||||
defaultMessage: 'Inappropriate',
|
||||
},
|
||||
spamReason: {
|
||||
id: 'app.modal.install-to-play.report-reason.spam',
|
||||
defaultMessage: 'Spam',
|
||||
},
|
||||
additionalContext: {
|
||||
id: 'app.modal.install-to-play.additional-context',
|
||||
defaultMessage: 'Additional context',
|
||||
@@ -612,8 +584,7 @@ const messages = defineMessages({
|
||||
},
|
||||
reviewedFiles: {
|
||||
id: 'app.modal.install-to-play.reviewed-files',
|
||||
defaultMessage:
|
||||
'A file is only reviewed if it’s published to Modrinth, regardless of its file format (including .mrpack).',
|
||||
defaultMessage: "Files that aren't published to Modrinth aren't reviewed.",
|
||||
},
|
||||
installAnyway: {
|
||||
id: 'app.modal.install-to-play.install-anyway',
|
||||
|
||||
+5
-7
@@ -4,12 +4,10 @@
|
||||
<span class="font-semibold text-contrast">{{
|
||||
heading ?? formatMessage(messages.sharedInstance)
|
||||
}}</span>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="emit('viewContents')">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" @click="emit('viewContents')">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 rounded-2xl bg-surface-2 p-3">
|
||||
<Avatar
|
||||
@@ -34,7 +32,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, defineMessages, formatLoader, useVIntl } from '@modrinth/ui'
|
||||
import { Avatar, Button, defineMessages, formatLoader, useVIntl } from '@modrinth/ui'
|
||||
import { computed, toRefs } from 'vue'
|
||||
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
|
||||
@@ -22,11 +22,9 @@
|
||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||
<section v-if="mode === 'edit' && canEditTextureAndModel">
|
||||
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.textureSection) }}</h2>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" @click="openTextureFileBrowser">
|
||||
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="openTextureFileBrowser">
|
||||
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
|
||||
</Button>
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
@@ -112,19 +110,21 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button :disabled="isSaving" @click="hide">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" :disabled="isSaving" @click="hide">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="saveTooltip"
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="disableSave || isSaving"
|
||||
@click="save"
|
||||
>
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -133,7 +133,7 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, SaveIcon, SpinnerIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
commonMessages,
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
SkinButton,
|
||||
SkinLikeTextButton,
|
||||
useScrollViewport,
|
||||
@@ -463,25 +464,26 @@ defineExpose({ getAddSkinButtonElement })
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template v-if="!readOnly" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
:aria-label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</Button>
|
||||
<IconButton
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
type="colored"
|
||||
color="red"
|
||||
:label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
@@ -503,25 +505,26 @@ defineExpose({ getAddSkinButtonElement })
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template v-if="!readOnly" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
:aria-label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</Button>
|
||||
<IconButton
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
type="colored"
|
||||
color="red"
|
||||
:label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
@@ -545,15 +548,15 @@ defineExpose({ getAddSkinButtonElement })
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TeleportOverflowMenu,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
@@ -21,12 +21,12 @@ import {
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project } from '@/helpers/cache'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
@@ -108,7 +108,7 @@ const stop = async (event: MouseEvent) => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const unlistenProcesses = await process_listener(async () => {
|
||||
useAppEvent('process', async () => {
|
||||
await checkProcess()
|
||||
})
|
||||
|
||||
@@ -121,10 +121,6 @@ const checkProcess = async () => {
|
||||
onMounted(() => {
|
||||
checkProcess()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<SmartClickable>
|
||||
@@ -185,54 +181,53 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||
<ButtonStyled v-if="playing && !loading" color="red">
|
||||
<button @click="stop">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
instance.quarantined
|
||||
? 'This instance has been locked'
|
||||
: playing
|
||||
? 'Instance is already open'
|
||||
: null
|
||||
"
|
||||
:disabled="instance.quarantined || playing || loading"
|
||||
@click="play"
|
||||
>
|
||||
<SpinnerIcon v-if="loading" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instance.id,
|
||||
action: () => router.push(encodeURI(`/instance/${instance.id}`)),
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => showInstanceInFolder(instance.id),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
View instance
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<Button v-if="playing && !loading" type="colored" color="red" @click="stop">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
v-tooltip="
|
||||
instance.quarantined
|
||||
? 'This instance has been locked'
|
||||
: playing
|
||||
? 'Instance is already open'
|
||||
: null
|
||||
"
|
||||
:disabled="instance.quarantined || playing || loading"
|
||||
@click="play"
|
||||
>
|
||||
<SpinnerIcon v-if="loading" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</Button>
|
||||
<TeleportOverflowMenu
|
||||
type="quiet"
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'open-instance',
|
||||
label: 'View instance',
|
||||
shown: !!instance.id,
|
||||
action: () => router.push(encodeURI(`/instance/${instance.id}`)),
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(commonMessages.openFolderButton),
|
||||
action: () => showInstanceInFolder(instance.id),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
View instance
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
|
||||
@@ -5,12 +5,12 @@ import { GAME_MODES, injectNotificationManager } from '@modrinth/ui'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { instance_listener, process_listener } from '@/helpers/events'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_all } from '@/helpers/process'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
@@ -215,11 +215,11 @@ async function stopInstance(path: string) {
|
||||
const currentInstance = ref<string>()
|
||||
const currentWorld = ref<string>()
|
||||
|
||||
const unlistenProcesses = await process_listener(async () => {
|
||||
useAppEvent('process', async () => {
|
||||
await checkProcesses()
|
||||
})
|
||||
|
||||
const unlistenInstances = await instance_listener(async () => {
|
||||
useAppEvent('instance', async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
@@ -251,11 +251,6 @@ onMounted(() => {
|
||||
checkProcesses()
|
||||
linuxPopulateCount.value = 0
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenInstances()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -21,13 +21,13 @@ import {
|
||||
import type { MessageDescriptor } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TagItem,
|
||||
TeleportOverflowMenu,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
useRelativeTime,
|
||||
@@ -412,177 +412,184 @@ const messages = defineMessages({
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||
<ButtonStyled
|
||||
<Button
|
||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||
type="colored"
|
||||
color="red"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<button @click="emit('stop')">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
quarantined
|
||||
? 'This instance has been locked'
|
||||
: world.type === 'server'
|
||||
? !supportsServerQuickPlay
|
||||
? formatMessage(messages.noServerQuickPlay)
|
||||
: playingOtherWorld
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: !serverStatus
|
||||
? formatMessage(messages.noContact)
|
||||
: serverIncompatible
|
||||
? formatMessage(messages.incompatibleServer)
|
||||
: null
|
||||
: !supportsWorldQuickPlay
|
||||
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||
: playingOtherWorld || locked
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: null
|
||||
"
|
||||
:disabled="
|
||||
quarantined ||
|
||||
playingOtherWorld ||
|
||||
startingInstance ||
|
||||
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||
"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
shown: !!instanceId,
|
||||
disabled: playingInstance || quarantined,
|
||||
action: () => emit('play-instance'),
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
v-tooltip="
|
||||
quarantined
|
||||
? 'This instance has been locked'
|
||||
: world.type === 'server'
|
||||
? !supportsServerQuickPlay
|
||||
? formatMessage(messages.noServerQuickPlay)
|
||||
: playingOtherWorld
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: !serverStatus
|
||||
? formatMessage(messages.noContact)
|
||||
: serverIncompatible
|
||||
? formatMessage(messages.incompatibleServer)
|
||||
: null
|
||||
: !supportsWorldQuickPlay
|
||||
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||
: playingOtherWorld || locked
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: null
|
||||
"
|
||||
:disabled="
|
||||
quarantined ||
|
||||
playingOtherWorld ||
|
||||
startingInstance ||
|
||||
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||
"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</Button>
|
||||
<TeleportOverflowMenu
|
||||
type="quiet"
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
label: formatMessage(messages.playInstance),
|
||||
shown: !!instanceId,
|
||||
disabled: playingInstance || quarantined,
|
||||
action: () => emit('play-instance'),
|
||||
},
|
||||
{
|
||||
id: 'open-instance',
|
||||
label: formatMessage(messages.viewInstance),
|
||||
shown: !!instanceId,
|
||||
action: () => router.push(`/instance/${encodeURIComponent(instanceId)}`),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
label: formatMessage(commonMessages.refreshButton),
|
||||
shown: world.type === 'server',
|
||||
action: () => emit('refresh'),
|
||||
},
|
||||
{
|
||||
id: 'copy-address',
|
||||
label: formatMessage(messages.copyAddress),
|
||||
shown: world.type === 'server',
|
||||
action: () => copyToClipboard((world as ServerWorld).address),
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
label: formatMessage(commonMessages.editButton),
|
||||
action: () => emit('edit'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(commonMessages.openFolderButton),
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
shown: !!instanceId,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
label: formatMessage(messages.dontShowOnHome),
|
||||
shown: !!instanceId,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instanceId,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instanceId,
|
||||
action: () => router.push(`/instance/${encodeURIComponent(instanceId)}`),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
shown: world.type === 'server',
|
||||
action: () => emit('refresh'),
|
||||
},
|
||||
{
|
||||
id: 'copy-address',
|
||||
shown: world.type === 'server',
|
||||
action: () => copyToClipboard((world as ServerWorld).address),
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
action: () => emit('edit'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!instanceId,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
shown: !!instanceId,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instanceId,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
shown: !!shortcutInstanceId && !quarantined,
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !instanceId,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
action: () => emit('delete'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.viewInstance) }}
|
||||
</template>
|
||||
<template #edit>
|
||||
<EditIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
<template #copy-address>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.copyAddress) }}
|
||||
</template>
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #create-shortcut>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createShortcut) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
formatMessage(
|
||||
world.type === 'server'
|
||||
? commonMessages.removeButton
|
||||
: commonMessages.deleteLabel,
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
label: formatMessage(messages.createShortcut),
|
||||
shown: !!shortcutInstanceId && !quarantined,
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
shown: !instanceId,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: formatMessage(
|
||||
world.type === 'server' ? commonMessages.removeButton : commonMessages.deleteLabel,
|
||||
),
|
||||
tone: 'red',
|
||||
action: () => emit('delete'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.viewInstance) }}
|
||||
</template>
|
||||
<template #edit>
|
||||
<EditIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
<template #copy-address>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.copyAddress) }}
|
||||
</template>
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #create-shortcut>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createShortcut) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
formatMessage(
|
||||
world.type === 'server' ? commonMessages.removeButton : commonMessages.deleteLabel,
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
@@ -96,24 +96,18 @@ defineExpose({ show, hide })
|
||||
/>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="!address" @click="addServer(false)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="addServer(true)">
|
||||
<PlayIcon />
|
||||
{{ formatMessage(messages.addAndPlay) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button :disabled="!address" @click="addServer(false)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="!address" @click="addServer(true)">
|
||||
<PlayIcon />
|
||||
{{ formatMessage(messages.addAndPlay) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
@@ -106,18 +106,19 @@ defineExpose({ show, hide })
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="!isServer && !isSingleplayer" @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(actionMessage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="red"
|
||||
:disabled="!isServer && !isSingleplayer"
|
||||
@click="confirm"
|
||||
>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(actionMessage) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessage,
|
||||
injectNotificationManager,
|
||||
@@ -105,18 +105,14 @@ const titleMessage = defineMessage({
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="saveServer">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="!address" @click="saveServer">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
@@ -113,24 +113,18 @@ const messages = defineMessages({
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="saveWorld">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
||||
<UndoIcon />
|
||||
{{ formatMessage(messages.resetIcon) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="saveWorld">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</Button>
|
||||
<Button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
||||
<UndoIcon />
|
||||
{{ formatMessage(messages.resetIcon) }}
|
||||
</Button>
|
||||
<Button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { install_job_listener } from '@/helpers/events'
|
||||
import {
|
||||
install_job_dismiss,
|
||||
install_job_list,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
type InstallProgress,
|
||||
} from '@/helpers/install'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -234,6 +234,7 @@ export async function useInstallJobNotifications(opts: {
|
||||
handleError: (err: unknown) => void
|
||||
onChange: () => void
|
||||
}) {
|
||||
const appEvents = injectAppEvents()
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const jobs = ref<InstallJobSnapshot[]>([])
|
||||
@@ -404,8 +405,6 @@ export async function useInstallJobNotifications(opts: {
|
||||
}
|
||||
|
||||
function getProgress(job: InstallJobSnapshot): number {
|
||||
if (job.status === 'succeeded') return 1
|
||||
if (job.status === 'failed' || job.status === 'interrupted') return 0
|
||||
const progress = getEffectiveProgress(job)
|
||||
if (!progress || progress.total <= 0) return 0
|
||||
return Math.max(0, Math.min(1, progress.current / progress.total))
|
||||
@@ -526,8 +525,12 @@ export async function useInstallJobNotifications(opts: {
|
||||
)
|
||||
}
|
||||
|
||||
const activeJobs = computed(() =>
|
||||
jobs.value.filter((job) => job.status === 'queued' || job.status === 'running'),
|
||||
)
|
||||
|
||||
const progressItems = computed<PopupNotificationProgressItem[]>(() =>
|
||||
jobs.value.map((job) => {
|
||||
activeJobs.value.map((job) => {
|
||||
const progress = getEffectiveProgress(job)
|
||||
|
||||
return {
|
||||
@@ -536,20 +539,26 @@ export async function useInstallJobNotifications(opts: {
|
||||
text: getText(job),
|
||||
iconUrl: iconUrls.value[job.job_id] ?? null,
|
||||
progress: getProgress(job),
|
||||
waiting: !job.progress && ['queued', 'running'].includes(job.status),
|
||||
showProgress: !isTerminalJob(job),
|
||||
wrapText: isTerminalJob(job),
|
||||
progressType: isTerminalJob(job) ? undefined : getProgressType(job),
|
||||
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
|
||||
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
|
||||
waiting: !job.progress && job.status === 'running',
|
||||
showProgress: job.status === 'running',
|
||||
progressType: getProgressType(job),
|
||||
progressCurrent: progress?.current,
|
||||
progressTotal: progress?.total,
|
||||
buttons: getButtons(job),
|
||||
dismissible: isTerminalJob(job),
|
||||
onDismiss: getDismissHandler(job),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const buttons = computed<PopupNotificationButton[] | undefined>(() => undefined)
|
||||
const terminalNotifications = computed(() =>
|
||||
jobs.value.filter(isTerminalJob).map((job) => ({
|
||||
id: job.job_id,
|
||||
title: getTitle(job),
|
||||
text: getText(job),
|
||||
type: job.status === 'failed' ? ('error' as const) : ('warning' as const),
|
||||
buttons: getButtons(job),
|
||||
onDismiss: getDismissHandler(job),
|
||||
})),
|
||||
)
|
||||
|
||||
async function refreshMetadata(notify = true) {
|
||||
const request = ++metadataRequest
|
||||
@@ -627,14 +636,14 @@ export async function useInstallJobNotifications(opts: {
|
||||
void refreshMetadata()
|
||||
}
|
||||
|
||||
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
|
||||
const unlisten = appEvents.on('install_job', applyJobUpdate)
|
||||
await refresh(false)
|
||||
|
||||
return {
|
||||
active: computed(() => jobs.value.length > 0),
|
||||
active: computed(() => activeJobs.value.length > 0),
|
||||
title: computed(() => formatMessage(messages.installs)),
|
||||
progressItems,
|
||||
buttons,
|
||||
terminalNotifications,
|
||||
refresh,
|
||||
dispose: () => {
|
||||
for (const timeout of copiedResetTimeouts.values()) {
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
fetchCachedServerStatus,
|
||||
getFreshCachedServerStatus,
|
||||
} from '@/composables/instances/use-server-status-query'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { kill, list as listInstances } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { add_server_to_instance, getServerAddress } from '@/helpers/worlds'
|
||||
import { instanceKeys } from '@/pages/instance/query-options'
|
||||
|
||||
interface BrowseServerInstance {
|
||||
id: string
|
||||
@@ -38,7 +39,7 @@ interface ContextMenuOptionClick {
|
||||
}
|
||||
|
||||
export interface UseAppServerBrowseOptions {
|
||||
instance: Ref<BrowseServerInstance | null>
|
||||
instance: Readonly<Ref<BrowseServerInstance | null>>
|
||||
isFromWorlds: ComputedRef<boolean>
|
||||
allInstalledIds: ComputedRef<Set<string>>
|
||||
newlyInstalled: Ref<string[]>
|
||||
@@ -77,7 +78,6 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
||||
const contextMenuRef = ref<ContextMenuHandle | null>(null)
|
||||
let serverPingsActive = true
|
||||
let unlistenProcesses: (() => void) | null = null
|
||||
|
||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('checkServerRunningStates', { hitCount: hits.length })
|
||||
@@ -131,7 +131,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
project.minecraft_java_server?.content?.kind,
|
||||
)
|
||||
options.newlyInstalled.value.push(project.project_id)
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instanceId] })
|
||||
await queryClient.invalidateQueries({ queryKey: instanceKeys.worlds(instanceId) })
|
||||
} catch (error) {
|
||||
options.handleError(error)
|
||||
}
|
||||
@@ -279,7 +279,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
process_listener((event: { event: string; instance_id: string }) => {
|
||||
useAppEvent('process', (event) => {
|
||||
debugLog('process event', event)
|
||||
if (event.event === 'finished') {
|
||||
const projectId = Object.entries(runningServerProjects.value).find(
|
||||
@@ -291,14 +291,9 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
unlistenProcesses = unlisten
|
||||
})
|
||||
.catch(options.handleError)
|
||||
|
||||
onUnmounted(() => {
|
||||
serverPingsActive = false
|
||||
unlistenProcesses?.()
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { onScopeDispose } from 'vue'
|
||||
|
||||
import {
|
||||
type AppEventHandler,
|
||||
type AppEvents,
|
||||
type AppEventType,
|
||||
injectAppEvents,
|
||||
} from '@/providers/app-events'
|
||||
|
||||
export function useAppEvent<Type extends AppEventType>(
|
||||
type: Type,
|
||||
handler: AppEventHandler<Type>,
|
||||
events: AppEvents = injectAppEvents(),
|
||||
) {
|
||||
const unsubscribe = events.on(type, handler)
|
||||
onScopeDispose(unsubscribe)
|
||||
return unsubscribe
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
|
||||
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import { friend_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
acceptCachedFriend,
|
||||
add_friend,
|
||||
@@ -127,13 +127,9 @@ export function useFriends(options: {
|
||||
)
|
||||
}
|
||||
|
||||
let unlisten: (() => void) | undefined
|
||||
void friend_listener(() => {
|
||||
useAppEvent('friend', () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
}).then((listener) => {
|
||||
unlisten = listener
|
||||
})
|
||||
onUnmounted(() => unlisten?.())
|
||||
|
||||
return {
|
||||
query,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CommandPayload } from "./CommandPayload";
|
||||
import type { FriendPayload } from "./FriendPayload";
|
||||
import type { InstallJobSnapshot } from "./InstallJobSnapshot";
|
||||
import type { InstanceBulkUpdateProgressPayload } from "./InstanceBulkUpdateProgressPayload";
|
||||
import type { InstancePayload } from "./InstancePayload";
|
||||
import type { LoadingPayload } from "./LoadingPayload";
|
||||
import type { LogPayload } from "./LogPayload";
|
||||
import type { ProcessPayload } from "./ProcessPayload";
|
||||
import type { WarningPayload } from "./WarningPayload";
|
||||
|
||||
export type AppEvent = { "type": "loading", "payload": LoadingPayload } | { "type": "process", "payload": ProcessPayload } | { "type": "instance", "payload": InstancePayload } | { "type": "instance_bulk_update_progress", "payload": InstanceBulkUpdateProgressPayload } | { "type": "install_job", "payload": InstallJobSnapshot } | { "type": "command", "payload": CommandPayload } | { "type": "warning", "payload": WarningPayload } | { "type": "friend", "payload": FriendPayload } | { "type": "notification", "payload": unknown } | { "type": "log", "payload": LogPayload } | { "type": "ads_consent_required", "payload": boolean };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CommandPayload = { "event": "InstallMod", id: string, } | { "event": "InstallVersion", id: string, } | { "event": "InstallModpack", id: string, } | { "event": "InstallServer", id: string, } | { "event": "LaunchInstance", id: string, server: string | null, singleplayer_world: string | null, } | { "event": "InstallSharedInstanceInvite", invite_id: string, } | { "event": "RunMRPack", path: string, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FriendStatusPayload } from "./FriendStatusPayload";
|
||||
|
||||
export type FriendPayload = { "event": "friend_request", from: string, } | { "event": "user_offline", id: string, } | { "event": "status_update", user_status: FriendStatusPayload, } | { "event": "status_sync" };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FriendStatusPayload = { user_id: string, profile_name: string | null, last_update: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ImportLauncherType = "MultiMC" | "PrismLauncher" | "ATLauncher" | "GDLauncher" | "Curseforge" | "Unknown";
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallApiErrorDetails = { error: string, status?: number, method?: string, url?: string, route?: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallErrorContext = { operation: string, source_path?: string, target_path?: string, file_path?: string, entry_path?: string, urls: Array<string>, expected_hash?: string, expected_size?: number, project_id?: string, version_id?: string, minecraft_version?: string, loader?: string, java_version?: number, os?: string, arch?: string, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InstallApiErrorDetails } from "./InstallApiErrorDetails";
|
||||
import type { InstallErrorContext } from "./InstallErrorContext";
|
||||
import type { InstallPhaseId } from "./InstallPhaseId";
|
||||
import type { SharedInstanceUnavailableReason } from "./SharedInstanceUnavailableReason";
|
||||
|
||||
export type InstallErrorView = { code: string, phase?: InstallPhaseId, message: string, reason?: SharedInstanceUnavailableReason, api?: InstallApiErrorDetails, context?: InstallErrorContext, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJavaStep = "resolving" | "fetching_metadata" | "downloading" | "extracting" | "validating";
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJobDisplay = { title: string, icon: string | null, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJobKind = "create_instance" | "create_modpack_instance" | "create_shared_instance" | "import_instance" | "duplicate_instance" | "install_existing_instance" | "install_pack_to_existing_instance" | "update_shared_instance";
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InstallErrorView } from "./InstallErrorView";
|
||||
import type { InstallJobDisplay } from "./InstallJobDisplay";
|
||||
import type { InstallJobKind } from "./InstallJobKind";
|
||||
import type { InstallJobStatus } from "./InstallJobStatus";
|
||||
import type { InstallPhaseDetails } from "./InstallPhaseDetails";
|
||||
import type { InstallPhaseId } from "./InstallPhaseId";
|
||||
import type { InstallProgress } from "./InstallProgress";
|
||||
import type { InstallTarget } from "./InstallTarget";
|
||||
|
||||
export type InstallJobSnapshot = { job_id: string, instance_id: string | null, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | null, details: InstallPhaseDetails, display: InstallJobDisplay | null, error: InstallErrorView | null, rollback_error: InstallErrorView | null, created: string, modified: string, finished: string | null, };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user