mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f245cacd | ||
|
|
641ad9942a | ||
|
|
58f89d9940 | ||
|
|
30477f3316 | ||
|
|
a7196deb98 | ||
|
|
9f28a9ba5f | ||
|
|
b5b7e9bc42 | ||
|
|
0af562ef6f | ||
|
|
e9200c7483 | ||
|
|
fe9ae94a5d | ||
|
|
871a161327 | ||
|
|
797ec2ec3c | ||
|
|
237963bb35 | ||
|
|
ba667921d1 | ||
|
|
56dba8ccc2 | ||
|
|
82d0184a85 | ||
|
|
5f9522b5cf | ||
|
|
75d2109be8 | ||
|
|
7e611c7678 | ||
|
|
0bd341371a | ||
|
|
32a43f2a22 | ||
|
|
2909aa49ca | ||
|
|
816830eea3 | ||
|
|
087752312b | ||
|
|
bcd7133251 | ||
|
|
a85b8eed3f | ||
|
|
52e055b20f | ||
|
|
5181370293 | ||
|
|
5d54cfa0dc | ||
|
|
5eb64cea3e | ||
|
|
6670e3184e | ||
|
|
12928edbcb | ||
|
|
c94034f4e2 | ||
|
|
8eb12d5d24 | ||
|
|
6a92ab0c2c | ||
|
|
d74211e827 | ||
|
|
49628a0ebc | ||
|
|
5b3177d27a | ||
|
|
a6043fc51f | ||
|
|
f08d977aff | ||
|
|
bee0c14b31 | ||
|
|
f39a602be3 | ||
|
|
3e07267e10 | ||
|
|
79fcf41316 | ||
|
|
10b1d7002a | ||
|
|
8c2611805b | ||
|
|
c204e00ebc | ||
|
|
906ead762d | ||
|
|
e568efa2a1 | ||
|
|
c8816418ea | ||
|
|
12540a5fd7 | ||
|
|
fd7794c807 | ||
|
|
5cf9d423ad | ||
|
|
ac00e2bd5d | ||
|
|
43818e4f90 | ||
|
|
7ef2492d03 | ||
|
|
37d371e46c | ||
|
|
26e1d645b8 | ||
|
|
04d00c0ba0 | ||
|
|
7f59feeb98 | ||
|
|
4c8abfb78d | ||
|
|
5ead1499cd | ||
|
|
ddb45f6aac | ||
|
|
cc77fd1b87 | ||
|
|
b92fc10e54 | ||
|
|
849943ed4e | ||
|
|
18c95e2f61 | ||
|
|
7c7efd5efe | ||
|
|
4c5e1074fd | ||
|
|
36d3aab0de | ||
|
|
88c0915066 | ||
|
|
f0dff8ebd5 | ||
|
|
8d82c228cf | ||
|
|
4b07ed720e | ||
|
|
ea42c644f3 | ||
|
|
28154689f4 | ||
|
|
3c77cc1502 | ||
|
|
b6b19e543c | ||
|
|
023c789a31 |
@@ -125,8 +125,7 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: ./apps/frontend
|
||||
run: pnpm build
|
||||
run: pnpm web:build
|
||||
env:
|
||||
CF_PAGES_BRANCH: ${{ inputs.head_ref || github.ref_name }}
|
||||
CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Frontend Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- prod
|
||||
paths:
|
||||
- 'apps/frontend/**/*'
|
||||
- 'packages/ui/**/*'
|
||||
- 'packages/utils/**/*'
|
||||
- 'packages/assets/**/*'
|
||||
- 'packages/moderation/**/*'
|
||||
- 'packages/api-client/**/*'
|
||||
- 'packages/blog/**/*'
|
||||
- '**/pnpm-*.yaml'
|
||||
- .github/workflows/frontend-docker.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build frontend
|
||||
runs-on: namespace-profile-modrinth-frontend
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Configure environment
|
||||
id: meta
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [ "$REF" = "refs/heads/main" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "env=production" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Set up caches
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
cache: |
|
||||
pnpm
|
||||
|
||||
- name: Inject build variables
|
||||
working-directory: ./apps/frontend
|
||||
run: |
|
||||
if [ "${{ steps.meta.outputs.env }}" == "staging" ]; then
|
||||
echo "Injecting staging variables from wrangler.jsonc..."
|
||||
jq -r '.env.staging.vars | to_entries[] | "\(.key)=\(.value)"' wrangler.jsonc >> $GITHUB_ENV
|
||||
else
|
||||
echo "Injecting production variables from wrangler.jsonc..."
|
||||
jq -r '.vars | to_entries[] | "\(.key)=\(.value)"' wrangler.jsonc >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./apps/frontend
|
||||
run: pnpm install
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm web:build
|
||||
env:
|
||||
NITRO_PRESET: node-server
|
||||
BUILD_ENV: ${{ steps.meta.outputs.env }}
|
||||
CF_PAGES_BRANCH: ${{ github.ref_name }}
|
||||
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
|
||||
|
||||
- name: Stage Docker context
|
||||
run: |
|
||||
mkdir -p apps/frontend/docker-stage
|
||||
cp -r apps/frontend/.output apps/frontend/docker-stage/.output
|
||||
|
||||
- name: Upload Docker context
|
||||
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
|
||||
with:
|
||||
name: frontend-docker-context
|
||||
retention-days: 1
|
||||
path: apps/frontend/docker-stage
|
||||
|
||||
docker-build:
|
||||
needs: [build]
|
||||
uses: SparkUniverse/workflows/.github/workflows/docker-build.yaml@main
|
||||
with:
|
||||
image-name: frontend
|
||||
dockerfile-path: apps/frontend/Dockerfile
|
||||
artifacts-name: frontend-docker-context
|
||||
@@ -70,10 +70,9 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Storybook
|
||||
working-directory: ./packages/ui
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
run: pnpm run build-storybook
|
||||
run: pnpm build-storybook
|
||||
|
||||
- name: Configure short SHA
|
||||
id: meta
|
||||
|
||||
@@ -8,6 +8,10 @@ on:
|
||||
concurrency:
|
||||
group: i18n-management
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: 'Pull translations from Crowdin'
|
||||
@@ -38,23 +42,17 @@ jobs:
|
||||
flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)"
|
||||
fi
|
||||
|
||||
if [ "$CROWDIN_GH_TOKEN_DEFINED" != true ]; then
|
||||
flight_failure "CROWDIN_GH_TOKEN secret is not defined (required to make pull requests)"
|
||||
fi
|
||||
|
||||
if [ "$PREFLIGHT_CHECK_RESULT" = false ]; then
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }}
|
||||
CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }}
|
||||
CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
@@ -67,12 +65,6 @@ jobs:
|
||||
- name: Install script dependencies
|
||||
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Configure Git author
|
||||
id: git-author
|
||||
uses: MarcoIeni/git-config@59144859caf016f8b817a2ac9b051578729173c4 # v0.1.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
|
||||
# # Because --all flag of Crowdin CLI is currently broken we need to create a fake source file
|
||||
# # so that the CLI won't omit translations for it. See https://github.com/crowdin/crowdin-cli/issues/724
|
||||
# - name: Write fake sources
|
||||
@@ -116,7 +108,4 @@ jobs:
|
||||
body-path: .github/templates/crowdin-pr.md
|
||||
commit-message: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})'
|
||||
branch: crowdin-pull/${{ steps.branch-name.outputs.branch_name }}
|
||||
author: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>'
|
||||
committer: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>'
|
||||
labels: sync
|
||||
token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
|
||||
@@ -55,6 +55,7 @@ Thumbs.db
|
||||
|
||||
# frontend generated files
|
||||
apps/frontend/src/generated
|
||||
/packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.ts
|
||||
|
||||
.turbo
|
||||
target
|
||||
|
||||
Generated
+81
-16
@@ -437,6 +437,23 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "antlr4rust"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093d520274bfff7278d776f7ea12981a0a0a6f96db90964658e0f38fc6e9a6a6"
|
||||
dependencies = [
|
||||
"better_any",
|
||||
"bit-set 0.8.0",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"murmur3",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"typed-arena",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
@@ -1421,10 +1438,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"pastey",
|
||||
"pastey 0.1.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "better_any"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@@ -1451,7 +1474,16 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
"bit-vec 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1460,6 +1492,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
@@ -1889,6 +1927,23 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cel"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814"
|
||||
dependencies = [
|
||||
"antlr4rust",
|
||||
"base64 0.22.1",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"pastey 0.2.3",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "censor"
|
||||
version = "0.3.0"
|
||||
@@ -2676,10 +2731,10 @@ dependencies = [
|
||||
"futures",
|
||||
"indexmap 2.11.4",
|
||||
"itertools 0.14.0",
|
||||
"quick-xml 0.38.3",
|
||||
"reqwest 0.12.24",
|
||||
"rust-s3",
|
||||
"serde",
|
||||
"serde-xml-rs",
|
||||
"serde_json",
|
||||
"sha1_smol",
|
||||
"thiserror 2.0.17",
|
||||
@@ -3530,7 +3585,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-set 0.5.3",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
@@ -5483,6 +5538,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
"cel",
|
||||
"censor",
|
||||
"chrono",
|
||||
"chumsky",
|
||||
@@ -6170,6 +6226,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb585ade2549a017db2e35978b77c319214fa4b37cede841e27954dd6e8f3ca8"
|
||||
|
||||
[[package]]
|
||||
name = "murmur3"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a198f9589efc03f544388dfc4a19fe8af4323662b62f598b8dcfdac62c14771c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mutually_exclusive_features"
|
||||
version = "0.1.0"
|
||||
@@ -7192,6 +7257,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||
|
||||
[[package]]
|
||||
name = "path-util"
|
||||
version = "0.0.0"
|
||||
@@ -9433,18 +9504,6 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-xml-rs"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_bytes"
|
||||
version = "0.11.19"
|
||||
@@ -11875,6 +11934,12 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-arena"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.12.0"
|
||||
|
||||
+1
-1
@@ -57,6 +57,7 @@ bitflags = "2.9.4"
|
||||
bon = "3.9.3"
|
||||
bytemuck = "1.24.0"
|
||||
bytes = "1.10.1"
|
||||
cel = { version = "0.14.0", default-features = false, features = ["json", "regex"] }
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.42"
|
||||
@@ -192,7 +193,6 @@ serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.145"
|
||||
serde_with = "3.15.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"intl-messageformat": "^10.7.7",
|
||||
"ofetch": "^1.3.4",
|
||||
"overlayscrollbars": "^2.15.1",
|
||||
"pinia": "^3.0.0",
|
||||
"posthog-js": "^1.158.2",
|
||||
"three": "^0.172.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
|
||||
+101
-23
@@ -94,6 +94,9 @@ 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 { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { useError } from '@/composables/use-error.js'
|
||||
import { useTheme } from '@/composables/use-theme.ts'
|
||||
import { config } from '@/config'
|
||||
import {
|
||||
hide_ads_window,
|
||||
@@ -147,8 +150,7 @@ 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'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { setupAppUserPreferencesProvider } from '@/providers/setup/user-preferences.ts'
|
||||
import { appMessages } from '@/utils/app-messages'
|
||||
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
@@ -157,7 +159,8 @@ import { AppNotificationManager } from './providers/app-notifications'
|
||||
import { AppPopupNotificationManager } from './providers/app-popup-notifications'
|
||||
import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const appTheme = useTheme()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider()
|
||||
@@ -201,7 +204,7 @@ const credentials = ref()
|
||||
let credentialsRefreshId = 0
|
||||
const sidebarToggled = ref(true)
|
||||
watch(
|
||||
() => themeStore.toggleSidebar,
|
||||
() => appSettings.toggleSidebar,
|
||||
(toggleSidebar) => {
|
||||
sidebarToggled.value = !toggleSidebar
|
||||
},
|
||||
@@ -221,7 +224,7 @@ const hostingUpdateRequired = computed(
|
||||
appUpdateState.updatesEnabled.value,
|
||||
)
|
||||
const prideFundraiserEnabled = computed(
|
||||
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
|
||||
() => appSettings.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
|
||||
)
|
||||
const hostingIntercomIdentityKey = computed(() => {
|
||||
const rawServerId = route.params.id
|
||||
@@ -324,13 +327,13 @@ providePageContext({
|
||||
intercomBubble: hostingIntercom.intercomBubble,
|
||||
featureFlags: {
|
||||
serverRamAsBytesAlwaysOn: computed(() =>
|
||||
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||
appSettings.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||
),
|
||||
},
|
||||
openExternalUrl: (url) => void openUrl(url),
|
||||
})
|
||||
provideModalBehavior({
|
||||
noblur: computed(() => !themeStore.advancedRendering),
|
||||
noblur: computed(() => !appTheme.advancedRendering),
|
||||
onShow: () => take_ads_window_hold(),
|
||||
onHide: () => release_ads_window_hold(),
|
||||
})
|
||||
@@ -634,10 +637,11 @@ async function setupApp() {
|
||||
theme,
|
||||
locale,
|
||||
telemetry,
|
||||
collapsed_navigation,
|
||||
hide_nametag_skins_page,
|
||||
advanced_rendering,
|
||||
toggle_sidebar,
|
||||
sync_theme_across_devices,
|
||||
sync_behavior_across_devices,
|
||||
developer_mode,
|
||||
feature_flags,
|
||||
pending_update_toast_for_version,
|
||||
@@ -655,13 +659,14 @@ async function setupApp() {
|
||||
nativeDecorations.value = native_decorations
|
||||
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
|
||||
|
||||
themeStore.setThemeState(theme)
|
||||
themeStore.collapsedNavigation = collapsed_navigation
|
||||
themeStore.advancedRendering = advanced_rendering
|
||||
themeStore.hideNametagSkinsPage = hide_nametag_skins_page
|
||||
themeStore.toggleSidebar = toggle_sidebar
|
||||
themeStore.devMode = developer_mode
|
||||
themeStore.featureFlags = feature_flags
|
||||
appTheme.preferred = theme
|
||||
appTheme.advancedRendering = advanced_rendering
|
||||
appTheme.syncAcrossDevices = sync_theme_across_devices
|
||||
appSettings.syncBehaviorAcrossDevices = sync_behavior_across_devices
|
||||
appSettings.hideNametagSkinsPage = hide_nametag_skins_page
|
||||
appSettings.toggleSidebar = toggle_sidebar
|
||||
appSettings.devMode = developer_mode
|
||||
Object.assign(appSettings.featureFlags, feature_flags)
|
||||
stateInitialized.value = true
|
||||
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
@@ -959,7 +964,7 @@ watch(incompatibilityWarningModal, (modal) => {
|
||||
}
|
||||
})
|
||||
|
||||
setupAuthProvider(credentials, async (_redirectPath, flow, options) => {
|
||||
const authProvider = setupAuthProvider(credentials, async (_redirectPath, flow, options) => {
|
||||
if (options?.showModal === false) {
|
||||
await signIn(flow)
|
||||
} else {
|
||||
@@ -967,6 +972,80 @@ setupAuthProvider(credentials, async (_redirectPath, flow, options) => {
|
||||
}
|
||||
})
|
||||
|
||||
const userPreferences = setupAppUserPreferencesProvider(authProvider, notificationManager)
|
||||
let userPreferencesSync = Promise.resolve()
|
||||
|
||||
watch(
|
||||
[userPreferences.preferences, stateInitialized],
|
||||
([preferences, initialized]) => {
|
||||
if (!preferences || !initialized) return
|
||||
|
||||
userPreferencesSync = userPreferencesSync
|
||||
.then(async () => {
|
||||
const settings = await getSettings()
|
||||
const selectedTheme = preferences.appearance.auto ? 'system' : preferences.appearance.theme
|
||||
const locale = preferences.localization.locale
|
||||
const behavior = preferences.behavior
|
||||
let settingsChanged = false
|
||||
|
||||
if (appTheme.syncAcrossDevices && appTheme.preferred !== selectedTheme) {
|
||||
appTheme.preferred = selectedTheme
|
||||
}
|
||||
if (i18n.global.locale.value !== locale) {
|
||||
i18n.global.locale.value = locale
|
||||
}
|
||||
|
||||
if (appTheme.syncAcrossDevices && settings.theme !== selectedTheme) {
|
||||
settings.theme = selectedTheme
|
||||
settingsChanged = true
|
||||
}
|
||||
if (settings.locale !== locale) {
|
||||
settings.locale = locale
|
||||
settingsChanged = true
|
||||
}
|
||||
|
||||
if (behavior && appSettings.syncBehaviorAcrossDevices) {
|
||||
const behaviorFeatureFlags = {
|
||||
worlds_in_home: behavior.show_jump_in,
|
||||
compact_instance_cards: behavior.compact_instance_cards,
|
||||
show_instance_play_time: behavior.show_play_time,
|
||||
skip_unknown_pack_warning: !behavior.warn_on_unknown_modpacks,
|
||||
skip_non_essential_warnings: behavior.skip_non_essential_warnings,
|
||||
}
|
||||
|
||||
appSettings.toggleSidebar = behavior.hide_right_sidebar
|
||||
appSettings.hideNametagSkinsPage = behavior.hide_nametag
|
||||
Object.assign(appSettings.featureFlags, behaviorFeatureFlags)
|
||||
|
||||
if (settings.hide_on_process_start !== behavior.minimize_app) {
|
||||
settings.hide_on_process_start = behavior.minimize_app
|
||||
settingsChanged = true
|
||||
}
|
||||
if (settings.toggle_sidebar !== behavior.hide_right_sidebar) {
|
||||
settings.toggle_sidebar = behavior.hide_right_sidebar
|
||||
settingsChanged = true
|
||||
}
|
||||
if (settings.hide_nametag_skins_page !== behavior.hide_nametag) {
|
||||
settings.hide_nametag_skins_page = behavior.hide_nametag
|
||||
settingsChanged = true
|
||||
}
|
||||
for (const [flag, value] of Object.entries(behaviorFeatureFlags)) {
|
||||
if (settings.feature_flags[flag] !== value) {
|
||||
settings.feature_flags[flag] = value
|
||||
settingsChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
await setSettings(settings)
|
||||
}
|
||||
})
|
||||
.catch(handleError)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function validateSession(sessionToken) {
|
||||
try {
|
||||
const response = await tauriFetch(`${config.labrinthBaseUrl}/v2/user`, {
|
||||
@@ -1523,9 +1602,8 @@ async function downloadUpdate(versionToDownload) {
|
||||
.then(() => {
|
||||
downloading.value = false
|
||||
finishedDownloading.value = true
|
||||
unlistenUpdateDownload?.().then(() => {
|
||||
unlistenUpdateDownload = null
|
||||
})
|
||||
unlistenUpdateDownload?.()
|
||||
unlistenUpdateDownload = null
|
||||
console.log('Finished downloading!')
|
||||
markAppUpdateActionable(versionToDownload.version, 'downloaded')
|
||||
scheduleDelayedUpdatePopup()
|
||||
@@ -1642,7 +1720,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div
|
||||
v-if="stateInitialized"
|
||||
class="app-grid-layout relative"
|
||||
:class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }"
|
||||
:class="{ 'disable-advanced-rendering': !appTheme.advancedRendering }"
|
||||
>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
@@ -1828,7 +1906,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</div>
|
||||
<section data-tauri-drag-region class="flex shrink-0 ml-auto items-center">
|
||||
<IconButton
|
||||
v-if="!forceSidebar && themeStore.toggleSidebar"
|
||||
v-if="!forceSidebar && appSettings.toggleSidebar"
|
||||
:type="sidebarToggled ? 'base' : 'quiet'"
|
||||
:label="formatMessage(messages.nextImage)"
|
||||
class="mr-3 transition-transform"
|
||||
@@ -1851,7 +1929,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
class="app-contents"
|
||||
:class="{
|
||||
'sidebar-enabled': sidebarVisible,
|
||||
'disable-advanced-rendering': !themeStore.advancedRendering,
|
||||
'disable-advanced-rendering': !appTheme.advancedRendering,
|
||||
}"
|
||||
>
|
||||
<div class="app-viewport flex-grow router-view">
|
||||
@@ -1867,7 +1945,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<LoadingBar position="absolute" />
|
||||
</div>
|
||||
<div
|
||||
v-if="themeStore.featureFlags.page_path"
|
||||
v-if="appSettings.featureFlags.page_path"
|
||||
class="absolute bottom-0 left-0 m-2 bg-tooltip-bg text-tooltip-text font-semibold rounded-full px-2 py-1 text-xs z-50"
|
||||
>
|
||||
{{ route.fullPath }}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -20,6 +20,7 @@
|
||||
<div class="flex gap-2 w-full min-w-0">
|
||||
<Avatar
|
||||
size="36px"
|
||||
disable-conditional-icon-padding
|
||||
:src="
|
||||
selectedAccount
|
||||
? avatarUrl
|
||||
@@ -46,7 +47,11 @@
|
||||
class="w-5 h-5 text-brand shrink-0"
|
||||
/>
|
||||
<RadioButtonIcon v-else class="w-5 h-5 text-secondary shrink-0" />
|
||||
<Avatar :src="getAccountAvatarUrl(account)" size="24px" />
|
||||
<Avatar
|
||||
:src="getAccountAvatarUrl(account)"
|
||||
size="24px"
|
||||
disable-conditional-icon-padding
|
||||
/>
|
||||
<p
|
||||
class="m-0 truncate min-w-0"
|
||||
:class="
|
||||
@@ -107,6 +112,7 @@ import type { Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import {
|
||||
get_default_user,
|
||||
@@ -118,7 +124,6 @@ import {
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import type { Skin } from '@/helpers/skins'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="shown"
|
||||
ref="contextMenu"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
left: left,
|
||||
top: top,
|
||||
}"
|
||||
>
|
||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||
<hr v-if="option.type === 'divider'" class="divider" />
|
||||
<div
|
||||
v-else-if="!(isInstanceLink(item) && option.name === `add_content`)"
|
||||
class="item clickable"
|
||||
:class="[option.color ?? 'base']"
|
||||
>
|
||||
<slot :name="option.name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
||||
|
||||
const item = ref(null)
|
||||
const contextMenu = ref(null)
|
||||
const options = ref([])
|
||||
const left = ref('0px')
|
||||
const top = ref('0px')
|
||||
const shown = ref(false)
|
||||
const contextMenuId = Symbol()
|
||||
const contextMenuOpenEvent = 'modrinth-context-menu-open'
|
||||
|
||||
const hideContextMenu = () => {
|
||||
shown.value = false
|
||||
emit('menu-closed')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showMenu: (event, passedItem, passedOptions) => {
|
||||
window.dispatchEvent(new CustomEvent(contextMenuOpenEvent, { detail: contextMenuId }))
|
||||
|
||||
item.value = passedItem
|
||||
options.value = passedOptions
|
||||
|
||||
// show to get dimensions
|
||||
shown.value = true
|
||||
|
||||
// then, adjust position if overflowing
|
||||
nextTick(() => {
|
||||
const menuWidth = contextMenu.value?.clientWidth || 200
|
||||
const menuHeight = contextMenu.value?.clientHeight || 100
|
||||
const minFromEdge = 10
|
||||
|
||||
if (event.pageX + menuWidth + minFromEdge >= window.innerWidth) {
|
||||
left.value = Math.max(minFromEdge, event.pageX - menuWidth - minFromEdge) + 'px'
|
||||
} else {
|
||||
left.value = event.pageX + minFromEdge + 'px'
|
||||
}
|
||||
|
||||
if (event.pageY + menuHeight + minFromEdge >= window.innerHeight) {
|
||||
top.value = Math.max(minFromEdge, event.pageY - menuHeight - minFromEdge) + 'px'
|
||||
} else {
|
||||
top.value = event.pageY + minFromEdge + 'px'
|
||||
}
|
||||
})
|
||||
},
|
||||
hideMenu: hideContextMenu,
|
||||
})
|
||||
|
||||
const isInstanceLink = (item) => {
|
||||
if (item.instance != undefined && item.instance.link) {
|
||||
return true
|
||||
} else if (item != undefined && item.link) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const optionClicked = (option) => {
|
||||
emit('option-clicked', {
|
||||
item: item.value,
|
||||
option: option,
|
||||
})
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
const onEscKeyRelease = (event) => {
|
||||
if (event.keyCode === 27) {
|
||||
hideContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextMenuOpen = (event) => {
|
||||
if (shown.value && event.detail !== contextMenuId) {
|
||||
hideContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
contextMenu.value &&
|
||||
contextMenu.value.$el !== event.target &&
|
||||
!elements.includes(contextMenu.value.$el)
|
||||
) {
|
||||
hideContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleClickOutside)
|
||||
window.addEventListener(contextMenuOpenEvent, handleContextMenuOpen)
|
||||
document.body.addEventListener('keyup', onEscKeyRelease)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', handleClickOutside)
|
||||
window.removeEventListener(contextMenuOpenEvent, handleContextMenuOpen)
|
||||
document.removeEventListener('keyup', onEscKeyRelease)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
border: 1px solid var(--color-divider);
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
z-index: 1000000;
|
||||
overflow: hidden;
|
||||
padding: var(--gap-sm);
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
padding: var(--gap-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
:deep(svg) {
|
||||
color: var(--color-base);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
:deep(svg) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.base {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.contrast {
|
||||
background-color: var(--color-orange);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: 1px solid var(--color-divider);
|
||||
margin: var(--gap-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -20,11 +20,11 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { ChatIcon } from '@/assets/icons'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { install_existing_instance } from '@/helpers/install'
|
||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
|
||||
import { getInstanceIconUrl, kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import { showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getInstanceIconUrl, 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 APPROX_USED_VERTICAL_SPACE = 475 // 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()
|
||||
|
||||
@@ -39,17 +39,17 @@ import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
|
||||
const nativeDecorations = ref(true)
|
||||
const isMaximized = ref(false)
|
||||
const os = ref('')
|
||||
|
||||
const alwaysShowAppControls = computed(() => themeStore.getFeatureFlag('always_show_app_controls'))
|
||||
const alwaysShowAppControls = computed(() => appSettings.getFeatureFlag('always_show_app_controls'))
|
||||
|
||||
const showControls = computed(
|
||||
() =>
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<Teleport to="#teleports">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="shown && !isMobileActiveSubmenu"
|
||||
ref="contextMenu"
|
||||
class="context-menu"
|
||||
:style="menuStyle"
|
||||
role="menu"
|
||||
@keydown="handleMenuKeydown"
|
||||
@mousemove="(event) => handleMenuMouseMove(event, 'menu')"
|
||||
>
|
||||
<template v-for="(option, index) in options" :key="index">
|
||||
<hr v-if="isDivider(option)" class="divider" />
|
||||
<button
|
||||
v-else-if="isOptionVisible(option)"
|
||||
:ref="(element) => setOptionButtonRef(index, element)"
|
||||
type="button"
|
||||
class="item clickable"
|
||||
:class="[
|
||||
option.color ?? 'base',
|
||||
{
|
||||
active: index === activeOptionIndex,
|
||||
'safe-triangle-hover': index === pendingOptionIndex,
|
||||
},
|
||||
]"
|
||||
role="menuitem"
|
||||
:aria-haspopup="hasChildren(option) ? 'menu' : undefined"
|
||||
:aria-expanded="hasChildren(option) ? index === activeOptionIndex : undefined"
|
||||
@click.stop="handleOptionClick(option, index)"
|
||||
@focus="handleOptionFocus(option, index)"
|
||||
@mouseenter="handleOptionMouseEnter(option, index)"
|
||||
>
|
||||
<span class="item-content"><slot :name="option.name" /></span>
|
||||
<ChevronRightIcon v-if="hasChildren(option)" class="submenu-chevron" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
|
||||
<Teleport to="#teleports">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="shown && activeOption && hasSubmenuPosition"
|
||||
ref="submenu"
|
||||
class="context-menu submenu"
|
||||
:style="submenuStyle"
|
||||
role="menu"
|
||||
@keydown="handleSubmenuKeydown"
|
||||
@mouseenter="handleSubmenuMouseEnter"
|
||||
@mouseleave="isCursorInsideSubmenu = false"
|
||||
@mousemove="(event) => handleMenuMouseMove(event, 'submenu')"
|
||||
>
|
||||
<button
|
||||
v-if="isMobileSubmenuLayout"
|
||||
type="button"
|
||||
class="item clickable base mobile-back"
|
||||
@click.stop="returnToMenu"
|
||||
>
|
||||
<ChevronLeftIcon class="submenu-chevron" />
|
||||
<span class="item-content"><slot :name="activeOption.name" /></span>
|
||||
</button>
|
||||
<template v-for="(option, index) in activeOption.children" :key="index">
|
||||
<hr v-if="isDivider(option)" class="divider" />
|
||||
<button
|
||||
v-else-if="isOptionVisible(option)"
|
||||
:ref="(element) => setSubmenuButtonRef(index, element)"
|
||||
type="button"
|
||||
class="item clickable"
|
||||
:class="option.color ?? 'base'"
|
||||
role="menuitem"
|
||||
@click.stop="optionClicked(option.name)"
|
||||
>
|
||||
<span class="item-content"><slot :name="option.name" /></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
import type { ContextMenuEmit } from './types'
|
||||
import { useContextMenu } from './use-context-menu'
|
||||
import { hasChildren, isDivider } from './utils'
|
||||
|
||||
const emit = defineEmits<ContextMenuEmit>()
|
||||
|
||||
const {
|
||||
shown,
|
||||
contextMenu,
|
||||
submenu,
|
||||
options,
|
||||
menuStyle,
|
||||
submenuStyle,
|
||||
activeOption,
|
||||
activeOptionIndex,
|
||||
pendingOptionIndex,
|
||||
hasSubmenuPosition,
|
||||
isMobileSubmenuLayout,
|
||||
isMobileActiveSubmenu,
|
||||
isCursorInsideSubmenu,
|
||||
isOptionVisible,
|
||||
setOptionButtonRef,
|
||||
setSubmenuButtonRef,
|
||||
showMenu,
|
||||
hideMenu,
|
||||
handleOptionClick,
|
||||
optionClicked,
|
||||
handleOptionFocus,
|
||||
handleOptionMouseEnter,
|
||||
returnToMenu,
|
||||
handleSubmenuMouseEnter,
|
||||
handleMenuMouseMove,
|
||||
handleMenuKeydown,
|
||||
handleSubmenuKeydown,
|
||||
} = useContextMenu(emit)
|
||||
|
||||
defineExpose({ showMenu, hideMenu })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
border: 1px solid var(--color-divider);
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
z-index: 1000000;
|
||||
overflow: hidden;
|
||||
padding: var(--gap-sm);
|
||||
min-width: 12rem;
|
||||
max-width: calc(100vw - 1.25rem);
|
||||
|
||||
&.submenu {
|
||||
z-index: 1000001;
|
||||
}
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
gap: var(--gap-sm);
|
||||
justify-content: space-between;
|
||||
padding: var(--gap-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
.item-content {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.submenu-chevron {
|
||||
color: var(--color-text-secondary);
|
||||
flex-shrink: 0;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-right: -0.25rem;
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
color: var(--color-base);
|
||||
}
|
||||
|
||||
&:hover:not(.safe-triangle-hover),
|
||||
&:active,
|
||||
&:focus-visible,
|
||||
&.active {
|
||||
:deep(svg) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.base {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.contrast {
|
||||
background-color: var(--color-orange);
|
||||
color: var(--color-accent-contrast);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-back {
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
border-radius: 0;
|
||||
margin: calc(var(--gap-sm) * -1) calc(var(--gap-sm) * -1) var(--gap-sm);
|
||||
width: calc(100% + var(--gap-sm) * 2);
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: 1px solid var(--color-divider);
|
||||
margin: var(--gap-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
|
||||
export type ContextMenuDivider = {
|
||||
type: string
|
||||
}
|
||||
|
||||
export type ContextMenuAction = {
|
||||
name: string
|
||||
color?: string
|
||||
children?: ContextMenuOption[]
|
||||
}
|
||||
|
||||
export type ContextMenuParentAction = ContextMenuAction & {
|
||||
children: ContextMenuOption[]
|
||||
}
|
||||
|
||||
export type ContextMenuOption = ContextMenuDivider | ContextMenuAction
|
||||
|
||||
export type ContextMenuSelection = {
|
||||
item: unknown
|
||||
option: string
|
||||
}
|
||||
|
||||
export type ContextMenuEmit = {
|
||||
(event: 'menu-closed'): void
|
||||
(event: 'option-clicked', selection: ContextMenuSelection): void
|
||||
}
|
||||
|
||||
export type Point = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type ViewportRect = {
|
||||
width: number
|
||||
height: number
|
||||
offsetTop: number
|
||||
offsetLeft: number
|
||||
}
|
||||
|
||||
export type ButtonRefElement = Element | ComponentPublicInstance | null
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { CSSProperties, Ref } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { ContextMenuParentAction, Point, ViewportRect } from './types'
|
||||
|
||||
const MENU_GAP = 8
|
||||
const VIEWPORT_MARGIN = 10
|
||||
|
||||
type ContextMenuPositionOptions = {
|
||||
shown: Ref<boolean>
|
||||
activeOption: Ref<ContextMenuParentAction | null>
|
||||
activeOptionIndex: Ref<number | null>
|
||||
isMobileSubmenuLayout: Ref<boolean>
|
||||
contextMenu: Ref<HTMLElement | null>
|
||||
submenu: Ref<HTMLElement | null>
|
||||
optionButtonRefs: Map<number, HTMLElement>
|
||||
}
|
||||
|
||||
export function useContextMenuPosition({
|
||||
shown,
|
||||
activeOption,
|
||||
activeOptionIndex,
|
||||
isMobileSubmenuLayout,
|
||||
contextMenu,
|
||||
submenu,
|
||||
optionButtonRefs,
|
||||
}: ContextMenuPositionOptions) {
|
||||
const menuStyle = ref<CSSProperties>({ left: '0px', top: '0px' })
|
||||
const menuAnchor = ref<Point>({ x: 0, y: 0 })
|
||||
const submenuPosition = ref<Point>({ x: 0, y: 0 })
|
||||
const hasSubmenuPosition = ref(false)
|
||||
let positionRafId: number | null = null
|
||||
|
||||
const submenuStyle = computed<CSSProperties>(() => {
|
||||
if (isMobileSubmenuLayout.value) return menuStyle.value
|
||||
|
||||
return {
|
||||
left: `${submenuPosition.value.x}px`,
|
||||
top: `${submenuPosition.value.y}px`,
|
||||
}
|
||||
})
|
||||
|
||||
function updateMenuPosition(x: number, y: number) {
|
||||
if (!contextMenu.value) return
|
||||
|
||||
const viewport = getViewportRect()
|
||||
const menuRect = contextMenu.value.getBoundingClientRect()
|
||||
const viewportLeft = viewport.offsetLeft
|
||||
const viewportTop = viewport.offsetTop
|
||||
const viewportRight = viewport.offsetLeft + viewport.width
|
||||
const viewportBottom = viewport.offsetTop + viewport.height
|
||||
const anchorX = x + viewport.offsetLeft
|
||||
const anchorY = y + viewport.offsetTop
|
||||
const left = Math.min(
|
||||
Math.max(viewportLeft + VIEWPORT_MARGIN, anchorX + MENU_GAP),
|
||||
Math.max(viewportLeft + VIEWPORT_MARGIN, viewportRight - menuRect.width - VIEWPORT_MARGIN),
|
||||
)
|
||||
const top = Math.min(
|
||||
Math.max(viewportTop + VIEWPORT_MARGIN, anchorY + MENU_GAP),
|
||||
Math.max(viewportTop + VIEWPORT_MARGIN, viewportBottom - menuRect.height - VIEWPORT_MARGIN),
|
||||
)
|
||||
|
||||
menuStyle.value = { left: `${left}px`, top: `${top}px` }
|
||||
if (activeOption.value) scheduleSubmenuPositionUpdate()
|
||||
}
|
||||
|
||||
function updateSubmenuPosition() {
|
||||
if (!activeOption.value || activeOptionIndex.value === null) return false
|
||||
|
||||
if (isMobileSubmenuLayout.value) {
|
||||
hasSubmenuPosition.value = true
|
||||
return true
|
||||
}
|
||||
|
||||
const optionButton = optionButtonRefs.get(activeOptionIndex.value)
|
||||
if (!optionButton || !contextMenu.value) return false
|
||||
|
||||
const viewport = getViewportRect()
|
||||
const buttonRect = optionButton.getBoundingClientRect()
|
||||
const menuRect = contextMenu.value.getBoundingClientRect()
|
||||
const submenuRect = submenu.value?.getBoundingClientRect()
|
||||
const submenuWidth = submenuRect?.width ?? menuRect.width
|
||||
const submenuHeight = submenuRect?.height ?? 100
|
||||
const direction = getSubmenuOpenDirection(menuRect, submenuWidth, viewport)
|
||||
const preferredLeft =
|
||||
direction === 'right'
|
||||
? buttonRect.right + MENU_GAP
|
||||
: buttonRect.left - submenuWidth - MENU_GAP
|
||||
const minLeft = viewport.offsetLeft + VIEWPORT_MARGIN
|
||||
const maxLeft = Math.max(
|
||||
minLeft,
|
||||
viewport.offsetLeft + viewport.width - submenuWidth - VIEWPORT_MARGIN,
|
||||
)
|
||||
const minTop = viewport.offsetTop + VIEWPORT_MARGIN
|
||||
const maxTop = Math.max(
|
||||
minTop,
|
||||
viewport.offsetTop + viewport.height - submenuHeight - VIEWPORT_MARGIN,
|
||||
)
|
||||
|
||||
submenuPosition.value = {
|
||||
x: Math.min(Math.max(minLeft, preferredLeft), maxLeft),
|
||||
y: Math.min(Math.max(minTop, buttonRect.top), maxTop),
|
||||
}
|
||||
hasSubmenuPosition.value = true
|
||||
return true
|
||||
}
|
||||
|
||||
function scheduleSubmenuPositionUpdate(retries = 8) {
|
||||
nextTick(() => {
|
||||
if (!shown.value || !activeOption.value) return
|
||||
|
||||
const hasRenderedSubmenu = submenu.value !== null
|
||||
if (updateSubmenuPosition()) {
|
||||
if (!hasRenderedSubmenu) nextTick(updateSubmenuPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (retries > 0) setTimeout(() => scheduleSubmenuPositionUpdate(retries - 1), 0)
|
||||
})
|
||||
}
|
||||
|
||||
function schedulePositionUpdate() {
|
||||
if (!shown.value || positionRafId !== null) return
|
||||
|
||||
positionRafId = window.requestAnimationFrame(() => {
|
||||
positionRafId = null
|
||||
updateMenuPosition(menuAnchor.value.x, menuAnchor.value.y)
|
||||
})
|
||||
}
|
||||
|
||||
function cancelPositionUpdate() {
|
||||
if (positionRafId !== null) window.cancelAnimationFrame(positionRafId)
|
||||
}
|
||||
|
||||
return {
|
||||
menuStyle,
|
||||
menuAnchor,
|
||||
submenuStyle,
|
||||
hasSubmenuPosition,
|
||||
updateMenuPosition,
|
||||
scheduleSubmenuPositionUpdate,
|
||||
schedulePositionUpdate,
|
||||
cancelPositionUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
function getViewportRect(): ViewportRect {
|
||||
const visualViewport = window.visualViewport
|
||||
return {
|
||||
width: visualViewport?.width ?? window.innerWidth,
|
||||
height: visualViewport?.height ?? window.innerHeight,
|
||||
offsetTop: visualViewport?.offsetTop ?? 0,
|
||||
offsetLeft: visualViewport?.offsetLeft ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
function getSubmenuOpenDirection(
|
||||
menuRect: DOMRect,
|
||||
submenuWidth: number,
|
||||
viewport: ViewportRect,
|
||||
): 'left' | 'right' {
|
||||
const viewportLeft = viewport.offsetLeft
|
||||
const viewportRight = viewport.offsetLeft + viewport.width
|
||||
const rightSpace = viewportRight - menuRect.right - MENU_GAP - VIEWPORT_MARGIN
|
||||
const leftSpace = menuRect.left - viewportLeft - MENU_GAP - VIEWPORT_MARGIN
|
||||
|
||||
if (rightSpace >= submenuWidth) return 'right'
|
||||
if (leftSpace >= submenuWidth) return 'left'
|
||||
return rightSpace >= leftSpace ? 'right' : 'left'
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
ButtonRefElement,
|
||||
ContextMenuAction,
|
||||
ContextMenuEmit,
|
||||
ContextMenuOption,
|
||||
ContextMenuParentAction,
|
||||
Point,
|
||||
} from './types'
|
||||
import { useContextMenuPosition } from './use-context-menu-position'
|
||||
import {
|
||||
focusRelativeButton,
|
||||
getFocusableButtons,
|
||||
hasChildren,
|
||||
isAction,
|
||||
isInstanceLink,
|
||||
isPointInTriangle,
|
||||
} from './utils'
|
||||
|
||||
const MOBILE_SUBMENU_LAYOUT_QUERY = '(pointer: coarse), (max-width: 800px)'
|
||||
const CONTEXT_MENU_OPEN_EVENT = 'modrinth-context-menu-open'
|
||||
|
||||
export function useContextMenu(emit: ContextMenuEmit) {
|
||||
const item = ref<unknown>(null)
|
||||
const contextMenu = ref<HTMLElement | null>(null)
|
||||
const submenu = ref<HTMLElement | null>(null)
|
||||
const options = ref<ContextMenuOption[]>([])
|
||||
const shown = ref(false)
|
||||
const activeOptionIndex = ref<number | null>(null)
|
||||
const pendingOptionIndex = ref<number | null>(null)
|
||||
const isCursorInsideSubmenu = ref(false)
|
||||
const isMobileSubmenuLayout = ref(false)
|
||||
const lastMousePosition = ref<Point | null>(null)
|
||||
const contextMenuId = Symbol()
|
||||
const optionButtonRefs = new Map<number, HTMLElement>()
|
||||
const submenuButtonRefs = new Map<number, HTMLElement>()
|
||||
let previousMousePosition: Point | null = null
|
||||
let pendingOptionTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
let mobileSubmenuMediaQuery: MediaQueryList | null = null
|
||||
|
||||
const activeOption = computed<ContextMenuParentAction | null>(() => {
|
||||
if (activeOptionIndex.value === null) return null
|
||||
const option = options.value[activeOptionIndex.value]
|
||||
return option && isAction(option) && hasChildren(option) ? option : null
|
||||
})
|
||||
|
||||
const {
|
||||
menuStyle,
|
||||
menuAnchor,
|
||||
submenuStyle,
|
||||
hasSubmenuPosition,
|
||||
updateMenuPosition,
|
||||
scheduleSubmenuPositionUpdate,
|
||||
schedulePositionUpdate,
|
||||
cancelPositionUpdate,
|
||||
} = useContextMenuPosition({
|
||||
shown,
|
||||
activeOption,
|
||||
activeOptionIndex,
|
||||
isMobileSubmenuLayout,
|
||||
contextMenu,
|
||||
submenu,
|
||||
optionButtonRefs,
|
||||
})
|
||||
|
||||
const isMobileActiveSubmenu = computed(
|
||||
() => isMobileSubmenuLayout.value && activeOption.value !== null && hasSubmenuPosition.value,
|
||||
)
|
||||
|
||||
function isOptionVisible(option: ContextMenuOption): option is ContextMenuAction {
|
||||
return isAction(option) && !(isInstanceLink(item.value) && option.name === 'add_content')
|
||||
}
|
||||
|
||||
function setOptionButtonRef(index: number, element: ButtonRefElement) {
|
||||
setButtonRef(optionButtonRefs, index, element)
|
||||
}
|
||||
|
||||
function setSubmenuButtonRef(index: number, element: ButtonRefElement) {
|
||||
setButtonRef(submenuButtonRefs, index, element)
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
if (!shown.value) return
|
||||
|
||||
shown.value = false
|
||||
deactivateSubmenu()
|
||||
emit('menu-closed')
|
||||
}
|
||||
|
||||
function showMenu(event: MouseEvent, passedItem: unknown, passedOptions: ContextMenuOption[]) {
|
||||
window.dispatchEvent(new CustomEvent(CONTEXT_MENU_OPEN_EVENT, { detail: contextMenuId }))
|
||||
|
||||
item.value = passedItem
|
||||
options.value = passedOptions
|
||||
menuAnchor.value = { x: event.clientX, y: event.clientY }
|
||||
shown.value = true
|
||||
deactivateSubmenu()
|
||||
syncMobileSubmenuLayout()
|
||||
nextTick(() => updateMenuPosition(event.clientX, event.clientY))
|
||||
}
|
||||
|
||||
function handleOptionClick(option: ContextMenuAction, index: number) {
|
||||
if (hasChildren(option)) {
|
||||
activateSubmenu(index)
|
||||
return
|
||||
}
|
||||
|
||||
optionClicked(option.name)
|
||||
}
|
||||
|
||||
function optionClicked(option: string) {
|
||||
emit('option-clicked', { item: item.value, option })
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
function handleOptionFocus(option: ContextMenuAction, index: number) {
|
||||
if (hasChildren(option) && !isMobileSubmenuLayout.value) {
|
||||
activateSubmenu(index)
|
||||
} else if (!hasChildren(option)) {
|
||||
deactivateSubmenu()
|
||||
}
|
||||
}
|
||||
|
||||
function handleOptionMouseEnter(option: ContextMenuAction, index: number) {
|
||||
if (isMobileSubmenuLayout.value) return
|
||||
|
||||
if (activeOptionIndex.value === null) {
|
||||
if (hasChildren(option)) activateSubmenu(index)
|
||||
return
|
||||
}
|
||||
|
||||
if (activeOptionIndex.value === index) return
|
||||
|
||||
if (!isCursorAimingAtSubmenu(lastMousePosition.value, previousMousePosition)) {
|
||||
commitHoveredOption(index)
|
||||
return
|
||||
}
|
||||
|
||||
pendingOptionIndex.value = index
|
||||
clearPendingOptionTimeout()
|
||||
pendingOptionTimeout = setTimeout(() => {
|
||||
if (pendingOptionIndex.value !== index) return
|
||||
if (isCursorInsideSubmenu.value) {
|
||||
pendingOptionIndex.value = null
|
||||
return
|
||||
}
|
||||
|
||||
commitHoveredOption(index)
|
||||
}, 180)
|
||||
}
|
||||
|
||||
function commitHoveredOption(index: number) {
|
||||
const option = options.value[index]
|
||||
if (option && hasChildren(option)) {
|
||||
activateSubmenu(index)
|
||||
} else {
|
||||
deactivateSubmenu()
|
||||
}
|
||||
}
|
||||
|
||||
function activateSubmenu(index: number) {
|
||||
clearPendingOptionTimeout()
|
||||
pendingOptionIndex.value = null
|
||||
activeOptionIndex.value = index
|
||||
hasSubmenuPosition.value = false
|
||||
scheduleSubmenuPositionUpdate()
|
||||
}
|
||||
|
||||
function deactivateSubmenu() {
|
||||
clearPendingOptionTimeout()
|
||||
activeOptionIndex.value = null
|
||||
pendingOptionIndex.value = null
|
||||
hasSubmenuPosition.value = false
|
||||
isCursorInsideSubmenu.value = false
|
||||
lastMousePosition.value = null
|
||||
previousMousePosition = null
|
||||
}
|
||||
|
||||
function returnToMenu() {
|
||||
const previousIndex = activeOptionIndex.value
|
||||
deactivateSubmenu()
|
||||
nextTick(() => {
|
||||
if (previousIndex !== null) optionButtonRefs.get(previousIndex)?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function handleSubmenuMouseEnter() {
|
||||
isCursorInsideSubmenu.value = true
|
||||
clearPendingOptionTimeout()
|
||||
pendingOptionIndex.value = null
|
||||
}
|
||||
|
||||
function handleMenuMouseMove(event: MouseEvent, source: 'menu' | 'submenu') {
|
||||
previousMousePosition = lastMousePosition.value
|
||||
lastMousePosition.value = { x: event.clientX, y: event.clientY }
|
||||
|
||||
if (
|
||||
source === 'menu' &&
|
||||
pendingOptionIndex.value !== null &&
|
||||
!isCursorAimingAtSubmenu(lastMousePosition.value, previousMousePosition)
|
||||
) {
|
||||
commitHoveredOption(pendingOptionIndex.value)
|
||||
}
|
||||
}
|
||||
|
||||
function isCursorAimingAtSubmenu(cursor: Point | null, origin: Point | null) {
|
||||
const submenuRect = submenu.value?.getBoundingClientRect()
|
||||
if (!submenuRect || !cursor || !origin) return false
|
||||
|
||||
const submenuTargetX =
|
||||
origin.x <= submenuRect.left
|
||||
? submenuRect.left
|
||||
: origin.x >= submenuRect.right
|
||||
? submenuRect.right
|
||||
: cursor.x <= submenuRect.left
|
||||
? submenuRect.left
|
||||
: submenuRect.right
|
||||
const upperTarget = { x: submenuTargetX, y: submenuRect.top - 20 }
|
||||
const lowerTarget = { x: submenuTargetX, y: submenuRect.bottom + 20 }
|
||||
|
||||
return isPointInTriangle(cursor, origin, upperTarget, lowerTarget)
|
||||
}
|
||||
|
||||
function handleMenuKeydown(event: KeyboardEvent) {
|
||||
const buttons = getFocusableButtons(optionButtonRefs)
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
focusRelativeButton(buttons, event.key === 'ArrowDown' ? 1 : -1)
|
||||
} else if (event.key === 'Home' || event.key === 'End') {
|
||||
event.preventDefault()
|
||||
buttons[event.key === 'Home' ? 0 : buttons.length - 1]?.focus()
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
const focusedIndex = [...optionButtonRefs.entries()].find(
|
||||
([, button]) => button === document.activeElement,
|
||||
)?.[0]
|
||||
const focusedOption = focusedIndex === undefined ? undefined : options.value[focusedIndex]
|
||||
if (focusedIndex !== undefined && focusedOption && hasChildren(focusedOption)) {
|
||||
event.preventDefault()
|
||||
activateSubmenu(focusedIndex)
|
||||
nextTick(() => getFocusableButtons(submenuButtonRefs)[0]?.focus())
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmenuKeydown(event: KeyboardEvent) {
|
||||
const buttons = getFocusableButtons(submenuButtonRefs)
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
focusRelativeButton(buttons, event.key === 'ArrowDown' ? 1 : -1)
|
||||
} else if (event.key === 'Home' || event.key === 'End') {
|
||||
event.preventDefault()
|
||||
buttons[event.key === 'Home' ? 0 : buttons.length - 1]?.focus()
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
returnToMenu()
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function syncMobileSubmenuLayout(event?: MediaQueryListEvent) {
|
||||
isMobileSubmenuLayout.value = event?.matches ?? mobileSubmenuMediaQuery?.matches ?? false
|
||||
}
|
||||
|
||||
function handleDocumentKeydown(event: KeyboardEvent) {
|
||||
if (shown.value && event.key === 'Escape') hideMenu()
|
||||
}
|
||||
|
||||
function handleContextMenuOpen(event: Event) {
|
||||
if (shown.value && event instanceof CustomEvent && event.detail !== contextMenuId) hideMenu()
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) return
|
||||
if (!contextMenu.value?.contains(target) && !submenu.value?.contains(target)) hideMenu()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mobileSubmenuMediaQuery = window.matchMedia(MOBILE_SUBMENU_LAYOUT_QUERY)
|
||||
syncMobileSubmenuLayout()
|
||||
mobileSubmenuMediaQuery.addEventListener('change', syncMobileSubmenuLayout)
|
||||
window.addEventListener('click', handleClickOutside)
|
||||
window.addEventListener('resize', schedulePositionUpdate)
|
||||
window.addEventListener('scroll', schedulePositionUpdate, true)
|
||||
window.visualViewport?.addEventListener('scroll', schedulePositionUpdate)
|
||||
window.visualViewport?.addEventListener('resize', schedulePositionUpdate)
|
||||
window.addEventListener(CONTEXT_MENU_OPEN_EVENT, handleContextMenuOpen)
|
||||
document.addEventListener('keydown', handleDocumentKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearPendingOptionTimeout()
|
||||
cancelPositionUpdate()
|
||||
mobileSubmenuMediaQuery?.removeEventListener('change', syncMobileSubmenuLayout)
|
||||
window.removeEventListener('click', handleClickOutside)
|
||||
window.removeEventListener('resize', schedulePositionUpdate)
|
||||
window.removeEventListener('scroll', schedulePositionUpdate, true)
|
||||
window.visualViewport?.removeEventListener('scroll', schedulePositionUpdate)
|
||||
window.visualViewport?.removeEventListener('resize', schedulePositionUpdate)
|
||||
window.removeEventListener(CONTEXT_MENU_OPEN_EVENT, handleContextMenuOpen)
|
||||
document.removeEventListener('keydown', handleDocumentKeydown)
|
||||
})
|
||||
|
||||
function clearPendingOptionTimeout() {
|
||||
if (!pendingOptionTimeout) return
|
||||
clearTimeout(pendingOptionTimeout)
|
||||
pendingOptionTimeout = null
|
||||
}
|
||||
|
||||
return {
|
||||
shown,
|
||||
contextMenu,
|
||||
submenu,
|
||||
options,
|
||||
menuStyle,
|
||||
submenuStyle,
|
||||
activeOption,
|
||||
activeOptionIndex,
|
||||
pendingOptionIndex,
|
||||
hasSubmenuPosition,
|
||||
isMobileSubmenuLayout,
|
||||
isMobileActiveSubmenu,
|
||||
isCursorInsideSubmenu,
|
||||
isOptionVisible,
|
||||
setOptionButtonRef,
|
||||
setSubmenuButtonRef,
|
||||
showMenu,
|
||||
hideMenu,
|
||||
handleOptionClick,
|
||||
optionClicked,
|
||||
handleOptionFocus,
|
||||
handleOptionMouseEnter,
|
||||
returnToMenu,
|
||||
handleSubmenuMouseEnter,
|
||||
handleMenuMouseMove,
|
||||
handleMenuKeydown,
|
||||
handleSubmenuKeydown,
|
||||
}
|
||||
}
|
||||
|
||||
function setButtonRef(
|
||||
buttonRefs: Map<number, HTMLElement>,
|
||||
index: number,
|
||||
element: ButtonRefElement,
|
||||
) {
|
||||
if (element instanceof HTMLElement) {
|
||||
buttonRefs.set(index, element)
|
||||
} else {
|
||||
buttonRefs.delete(index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
ContextMenuAction,
|
||||
ContextMenuDivider,
|
||||
ContextMenuOption,
|
||||
ContextMenuParentAction,
|
||||
Point,
|
||||
} from './types'
|
||||
|
||||
export function isAction(option: ContextMenuOption): option is ContextMenuAction {
|
||||
return 'name' in option
|
||||
}
|
||||
|
||||
export function isDivider(option: ContextMenuOption): option is ContextMenuDivider {
|
||||
return 'type' in option && option.type === 'divider'
|
||||
}
|
||||
|
||||
export function hasChildren(option: ContextMenuOption): option is ContextMenuParentAction {
|
||||
return isAction(option) && Boolean(option.children?.length)
|
||||
}
|
||||
|
||||
export function isInstanceLink(value: unknown) {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
|
||||
if ('instance' in value) {
|
||||
const instance = value.instance
|
||||
return Boolean(instance && typeof instance === 'object' && 'link' in instance && instance.link)
|
||||
}
|
||||
|
||||
return 'link' in value && Boolean(value.link)
|
||||
}
|
||||
|
||||
export function getFocusableButtons(buttonRefs: Map<number, HTMLElement>) {
|
||||
return [...buttonRefs.entries()]
|
||||
.sort(([left], [right]) => left - right)
|
||||
.map(([, button]) => button)
|
||||
.filter((button) => button.offsetParent !== null)
|
||||
}
|
||||
|
||||
export function focusRelativeButton(buttons: HTMLElement[], direction: 1 | -1) {
|
||||
if (!buttons.length) return
|
||||
|
||||
const currentIndex = buttons.indexOf(document.activeElement as HTMLElement)
|
||||
const nextIndex =
|
||||
currentIndex === -1 ? (direction === 1 ? 0 : buttons.length - 1) : currentIndex + direction
|
||||
buttons[(nextIndex + buttons.length) % buttons.length]?.focus()
|
||||
}
|
||||
|
||||
export function isPointInTriangle(point: Point, a: Point, b: Point, c: Point) {
|
||||
const area = triangleArea(a, b, c)
|
||||
const area1 = triangleArea(point, b, c)
|
||||
const area2 = triangleArea(a, point, c)
|
||||
const area3 = triangleArea(a, b, point)
|
||||
|
||||
return Math.abs(area - (area1 + area2 + area3)) < 0.5
|
||||
}
|
||||
|
||||
function triangleArea(a: Point, b: Point, c: Point) {
|
||||
return Math.abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2)
|
||||
}
|
||||
@@ -15,17 +15,17 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
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 appSettings = useAppSettings()
|
||||
|
||||
const props = defineProps<{
|
||||
credentials: ModrinthCredentials | null
|
||||
@@ -39,11 +39,11 @@ type FriendsSectionCollapsedFlag =
|
||||
| 'friends_pending_collapsed'
|
||||
|
||||
function isFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag) {
|
||||
return themeStore.getFeatureFlag(flag)
|
||||
return appSettings.getFeatureFlag(flag)
|
||||
}
|
||||
|
||||
function setFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag, collapsed: boolean) {
|
||||
themeStore.featureFlags[flag] = collapsed
|
||||
appSettings.featureFlags[flag] = collapsed
|
||||
getSettings()
|
||||
.then((settings) => {
|
||||
settings.feature_flags[flag] = collapsed
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
import { UnknownFileWarningModal } from '@modrinth/ui'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { type FeatureFlag, useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
@@ -34,7 +33,7 @@ function show(
|
||||
fileName.value = selectedFileName
|
||||
externalFilesInModpack.value = selectedExternalFiles
|
||||
|
||||
if (themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) {
|
||||
if (appSettings.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) {
|
||||
void createInstance()
|
||||
return
|
||||
}
|
||||
@@ -50,7 +49,7 @@ function reset() {
|
||||
|
||||
async function proceed(dontShowAgain: boolean) {
|
||||
if (dontShowAgain) {
|
||||
themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = true
|
||||
appSettings.featureFlags[skipUnknownPackWarningFeatureFlag] = true
|
||||
const settings = await getSettings()
|
||||
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = true
|
||||
await setSettings(settings)
|
||||
|
||||
+51
-2
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@modrinth/ui'
|
||||
import { defineMessages, type MessageDescriptor } from '@modrinth/ui'
|
||||
|
||||
import backpack from '@/assets/instance-icons/backpack.png'
|
||||
import beacon from '@/assets/instance-icons/beacon.png'
|
||||
@@ -18,6 +18,8 @@ import enchantingTable from '@/assets/instance-icons/enchanting-table.png'
|
||||
import enderChest from '@/assets/instance-icons/ender-chest.png'
|
||||
import enderDragon from '@/assets/instance-icons/ender-dragon.png'
|
||||
import engine from '@/assets/instance-icons/engine.png'
|
||||
import fabric from '@/assets/instance-icons/fabric.png'
|
||||
import forge from '@/assets/instance-icons/forge.png'
|
||||
import furnace from '@/assets/instance-icons/furnace.png'
|
||||
import gizmo from '@/assets/instance-icons/gizmo.png'
|
||||
import globe from '@/assets/instance-icons/globe.png'
|
||||
@@ -25,11 +27,13 @@ import grassBlock from '@/assets/instance-icons/grass-block.png'
|
||||
import lantern from '@/assets/instance-icons/lantern.png'
|
||||
import moobloom from '@/assets/instance-icons/moobloom.png'
|
||||
import mrPack from '@/assets/instance-icons/mr-pack.png'
|
||||
import neoForge from '@/assets/instance-icons/neoforge.png'
|
||||
import orb from '@/assets/instance-icons/orb.png'
|
||||
import oxygenDistributor from '@/assets/instance-icons/oxygen-distributor.png'
|
||||
import pancakes from '@/assets/instance-icons/pancakes.png'
|
||||
import pickaxe from '@/assets/instance-icons/pickaxe.png'
|
||||
import pokeBall from '@/assets/instance-icons/poke-ball.png'
|
||||
import quilt from '@/assets/instance-icons/quilt.png'
|
||||
import redstoneBlock from '@/assets/instance-icons/redstone-block.png'
|
||||
import sculkSensor from '@/assets/instance-icons/sculk-sensor.png'
|
||||
import skeleton from '@/assets/instance-icons/skeleton.png'
|
||||
@@ -197,8 +201,20 @@ const names = defineMessages({
|
||||
defaultMessage: 'Modrinth Wrench',
|
||||
},
|
||||
zombie: { id: 'instance.icon-editor.symbol.zombie', defaultMessage: 'Zombie' },
|
||||
fabric: { id: 'instance.icon-editor.symbol.fabric', defaultMessage: 'Fabric' },
|
||||
forge: { id: 'instance.icon-editor.symbol.forge', defaultMessage: 'Forge' },
|
||||
neoForge: { id: 'instance.icon-editor.symbol.neoforge', defaultMessage: 'NeoForge' },
|
||||
quilt: { id: 'instance.icon-editor.symbol.quilt', defaultMessage: 'Quilt' },
|
||||
})
|
||||
|
||||
export interface SymbolOption {
|
||||
id: string
|
||||
name: MessageDescriptor
|
||||
asset: string
|
||||
category: 'loader' | 'modded' | 'vanilla'
|
||||
excludeFromRandomization?: boolean
|
||||
}
|
||||
|
||||
export const backgroundOptions = [
|
||||
{
|
||||
id: 'rose',
|
||||
@@ -339,6 +355,39 @@ export const backgroundOptions = [
|
||||
] as const
|
||||
|
||||
export const symbolOptions = [
|
||||
/////////////////////////
|
||||
// loaders
|
||||
/////////////////////////
|
||||
|
||||
{
|
||||
id: 'fabric',
|
||||
name: names.fabric,
|
||||
asset: fabric,
|
||||
category: 'loader',
|
||||
excludeFromRandomization: true,
|
||||
},
|
||||
{
|
||||
id: 'forge',
|
||||
name: names.forge,
|
||||
asset: forge,
|
||||
category: 'loader',
|
||||
excludeFromRandomization: true,
|
||||
},
|
||||
{
|
||||
id: 'neoforge',
|
||||
name: names.neoForge,
|
||||
asset: neoForge,
|
||||
category: 'loader',
|
||||
excludeFromRandomization: true,
|
||||
},
|
||||
{
|
||||
id: 'quilt',
|
||||
name: names.quilt,
|
||||
asset: quilt,
|
||||
category: 'loader',
|
||||
excludeFromRandomization: true,
|
||||
},
|
||||
|
||||
// Cobblemon: Poké Ball
|
||||
{ id: 'poke_ball', name: names.pokeBall, asset: pokeBall, category: 'modded' },
|
||||
|
||||
@@ -438,7 +487,7 @@ export const symbolOptions = [
|
||||
{ id: 'lantern', name: names.lantern, asset: lantern, category: 'vanilla' },
|
||||
{ id: 'tnt', name: names.tnt, asset: tnt, category: 'vanilla' },
|
||||
{ id: 'command_block', name: names.commandBlock, asset: commandBlock, category: 'vanilla' },
|
||||
] as const
|
||||
] as const satisfies readonly SymbolOption[]
|
||||
|
||||
export type BackgroundId = (typeof backgroundOptions)[number]['id']
|
||||
export type SymbolId = (typeof symbolOptions)[number]['id']
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, InfoIcon, RefreshCwIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
DEFAULT_SYMBOL_ID,
|
||||
RANDOM_CONFIG_BLACKLIST,
|
||||
type SymbolId,
|
||||
type SymbolOption,
|
||||
symbolOptions,
|
||||
} from './editor-catalog'
|
||||
|
||||
@@ -201,7 +203,8 @@ function surpriseMe() {
|
||||
const configurations = backgroundOptions.flatMap((background) =>
|
||||
symbolOptions
|
||||
.filter(
|
||||
(symbol) =>
|
||||
(symbol: SymbolOption) =>
|
||||
!symbol.excludeFromRandomization &&
|
||||
background.id !== selectedBackground.value &&
|
||||
symbol.id !== selectedSymbol.value &&
|
||||
!RANDOM_CONFIG_BLACKLIST.some(
|
||||
@@ -342,37 +345,37 @@ const messages = defineMessages({
|
||||
class="flex w-[244px] shrink-0 flex-col gap-4 overflow-y-auto border-0 border-r border-solid border-surface-5 p-6"
|
||||
>
|
||||
<div
|
||||
class="flex w-full flex-col items-center gap-3 rounded-[20px] border border-solid border-surface-4 bg-surface-2 p-4"
|
||||
class="flex w-full flex-col items-center gap-3.5 rounded-[20px] border border-solid border-surface-4 bg-surface-2 p-5"
|
||||
>
|
||||
<div
|
||||
class="icon-outline relative size-[132px] overflow-hidden rounded-[20px]"
|
||||
<Avatar
|
||||
:src="selectedSymbolOption.asset"
|
||||
size="132px"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
no-shadow
|
||||
/>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div
|
||||
class="icon-outline relative size-12 overflow-hidden rounded-2xl"
|
||||
<Avatar
|
||||
:src="selectedSymbolOption.asset"
|
||||
size="40px"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
class="icon-outline relative size-8 overflow-hidden rounded-[10px]"
|
||||
no-shadow
|
||||
/>
|
||||
<Avatar
|
||||
:src="selectedSymbolOption.asset"
|
||||
size="30px"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
class="icon-outline relative size-4 overflow-hidden rounded-[5px]"
|
||||
no-shadow
|
||||
/>
|
||||
<Avatar
|
||||
:src="selectedSymbolOption.asset"
|
||||
size="20px"
|
||||
:style="backgroundStyle(selectedBackgroundOption.background)"
|
||||
>
|
||||
<img :src="selectedSymbolOption.asset" alt="" class="size-full object-cover" />
|
||||
</div>
|
||||
no-shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button class="w-full !shadow-none" @click="surpriseMe">
|
||||
<Button class="w-full" @click="surpriseMe">
|
||||
<RefreshCwIcon />
|
||||
{{ formatMessage(messages.surpriseMe) }}
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
<span class="text-2xl font-semibold text-contrast">
|
||||
{{
|
||||
formatMessage(messages.title, {
|
||||
groupName: groupInstancesModalGroup?.name ?? '',
|
||||
groupName:
|
||||
groupInstancesModalGroup?.id === 'group:none'
|
||||
? formatMessage(messages.ungrouped)
|
||||
: (groupInstancesModalGroup?.name ?? ''),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
@@ -53,6 +56,10 @@
|
||||
</div>
|
||||
<Button
|
||||
:type="selectedGroupInstanceIds.has(instance.id) ? 'outlined' : 'base'"
|
||||
:disabled="
|
||||
groupInstancesModalGroup?.id === 'group:none' &&
|
||||
selectedGroupInstanceIds.has(instance.id)
|
||||
"
|
||||
@click="toggleGroupInstance(instance.id)"
|
||||
>
|
||||
<CheckIcon v-if="selectedGroupInstanceIds.has(instance.id)" />
|
||||
@@ -100,6 +107,10 @@ import { getInstanceIconUrl } from '@/helpers/instance'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const messages = defineMessages({
|
||||
ungrouped: {
|
||||
id: 'app.library.group.ungrouped',
|
||||
defaultMessage: 'Ungrouped',
|
||||
},
|
||||
title: {
|
||||
id: 'app.library.group.instances-modal.title',
|
||||
defaultMessage: 'Add instances to "{groupName}"',
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ClipboardCopyIcon,
|
||||
EditIcon,
|
||||
EyeIcon,
|
||||
FolderOpenIcon,
|
||||
MinusIcon,
|
||||
PaletteIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
StarIcon,
|
||||
StopCircleIcon,
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, nextTick, onDeactivated, onUnmounted, ref, toRef, watch } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import IconEditorModal from '@/components/ui/instance_settings/icon-editor-modal/index.vue'
|
||||
import GroupInstancesModal from '@/components/ui/library/group-instances-modal.vue'
|
||||
import InstanceGroup from '@/components/ui/library/instance-group/index.vue'
|
||||
import InstanceGroupDnd from '@/components/ui/library/instance-group/instance-group-dnd.vue'
|
||||
@@ -55,6 +59,30 @@ const messages = defineMessages({
|
||||
id: 'app.library.instance.action.view-instance',
|
||||
defaultMessage: 'View instance',
|
||||
},
|
||||
editIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon',
|
||||
defaultMessage: 'Edit icon',
|
||||
},
|
||||
selectIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.select',
|
||||
defaultMessage: 'Select icon',
|
||||
},
|
||||
replaceIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.replace',
|
||||
defaultMessage: 'Replace icon',
|
||||
},
|
||||
createIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.create',
|
||||
defaultMessage: 'Create an icon',
|
||||
},
|
||||
editCreatedIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.edit-created',
|
||||
defaultMessage: 'Edit icon',
|
||||
},
|
||||
removeIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.remove',
|
||||
defaultMessage: 'Remove icon',
|
||||
},
|
||||
duplicateInstance: {
|
||||
id: 'app.library.instance.action.duplicate',
|
||||
defaultMessage: 'Duplicate instance',
|
||||
@@ -78,10 +106,13 @@ const {
|
||||
reorderGroups,
|
||||
instanceOptions,
|
||||
confirmDeleteModal,
|
||||
iconEditorModal,
|
||||
currentIconEditorInstance,
|
||||
currentDeleteInstances,
|
||||
clearLibraryInstanceSelection,
|
||||
deleteInstance,
|
||||
handleInstanceOption,
|
||||
handleInstanceIconSaved,
|
||||
selectedLibraryInstances,
|
||||
setSelectedLibraryInstances,
|
||||
toggleLibraryInstanceSelection,
|
||||
@@ -100,40 +131,35 @@ const visibleInstanceGroups = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
const visibleCustomGroups = computed(() =>
|
||||
const visibleReorderableGroups = computed(() =>
|
||||
displayState.value.group === 'Group'
|
||||
? visibleInstanceGroups.value.filter(
|
||||
(group) => group.id !== FAVORITES_GROUP_ID && group.id !== 'group:none',
|
||||
)
|
||||
? visibleInstanceGroups.value.filter((group) => group.id !== FAVORITES_GROUP_ID)
|
||||
: [],
|
||||
)
|
||||
const visibleFavoritesGroup = computed(() =>
|
||||
visibleInstanceGroups.value.find((group) => group.id === FAVORITES_GROUP_ID),
|
||||
)
|
||||
const visibleUngroupedGroup = computed(() =>
|
||||
visibleInstanceGroups.value.find((group) => group.id === 'group:none'),
|
||||
)
|
||||
const draggableCustomGroups = ref<InstanceGroupType[]>([])
|
||||
const draggableGroups = ref<InstanceGroupType[]>([])
|
||||
const libraryGroupsContainer = ref<HTMLElement>()
|
||||
const isDraggingGroup = ref(false)
|
||||
const GROUP_REORDERING_CLASS = 'instance-group-reordering'
|
||||
const canDragReorderGroups = computed(
|
||||
() => !reorderingGroups.value && draggableCustomGroups.value.length > 1,
|
||||
() => !reorderingGroups.value && draggableGroups.value.length > 1,
|
||||
)
|
||||
|
||||
watch(
|
||||
visibleCustomGroups,
|
||||
visibleReorderableGroups,
|
||||
(groups) => {
|
||||
if (!isDraggingGroup.value) {
|
||||
const previousGroupTops = getCustomGroupTops()
|
||||
draggableCustomGroups.value = [...groups]
|
||||
void nextTick(() => animateCustomGroupReorder(previousGroupTops))
|
||||
const previousGroupTops = getReorderableGroupTops()
|
||||
draggableGroups.value = [...groups]
|
||||
void nextTick(() => animateGroupReorder(previousGroupTops))
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function getCustomGroupTops() {
|
||||
function getReorderableGroupTops() {
|
||||
const groupTops = new Map<string, number>()
|
||||
const groupElements = libraryGroupsContainer.value?.querySelectorAll<HTMLElement>(
|
||||
'[data-instance-group-reorder-id]',
|
||||
@@ -149,7 +175,7 @@ function getCustomGroupTops() {
|
||||
return groupTops
|
||||
}
|
||||
|
||||
function animateCustomGroupReorder(previousGroupTops: Map<string, number>) {
|
||||
function animateGroupReorder(previousGroupTops: Map<string, number>) {
|
||||
if (
|
||||
previousGroupTops.size === 0 ||
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
@@ -185,10 +211,10 @@ function onGroupDragEnd() {
|
||||
isDraggingGroup.value = false
|
||||
document.documentElement.classList.remove(GROUP_REORDERING_CLASS)
|
||||
|
||||
const currentGroupIds = visibleCustomGroups.value.map((group) => group.id)
|
||||
const orderedGroupIds = draggableCustomGroups.value.map((group) => group.id)
|
||||
const currentGroupIds = visibleReorderableGroups.value.map((group) => group.id)
|
||||
const orderedGroupIds = draggableGroups.value.map((group) => group.id)
|
||||
if (orderedGroupIds.every((groupId, index) => groupId === currentGroupIds[index])) {
|
||||
draggableCustomGroups.value = [...visibleCustomGroups.value]
|
||||
draggableGroups.value = [...visibleReorderableGroups.value]
|
||||
return
|
||||
}
|
||||
|
||||
@@ -259,6 +285,10 @@ function setConfirmDeleteModal(component: unknown) {
|
||||
confirmDeleteModal.value = component as InstanceType<typeof ConfirmDeleteInstanceModal> | null
|
||||
}
|
||||
|
||||
function setIconEditorModal(component: unknown) {
|
||||
iconEditorModal.value = component as InstanceType<typeof IconEditorModal> | null
|
||||
}
|
||||
|
||||
watch(selectedLibraryInstances, (selectedInstances) => {
|
||||
if (selectedInstances.size === 0) {
|
||||
anchorInstance.value = null
|
||||
@@ -313,7 +343,7 @@ watch(selectedLibraryInstances, (selectedInstances) => {
|
||||
</div>
|
||||
|
||||
<Draggable
|
||||
:list="draggableCustomGroups"
|
||||
:list="draggableGroups"
|
||||
class="flex flex-col"
|
||||
item-key="id"
|
||||
:disabled="!canDragReorderGroups"
|
||||
@@ -341,6 +371,7 @@ watch(selectedLibraryInstances, (selectedInstances) => {
|
||||
>
|
||||
<InstanceGroup
|
||||
:can-drag-reorder="canDragReorderGroups"
|
||||
:hide-header="visibleInstanceGroups.length === 1"
|
||||
:instance-group="instanceGroup"
|
||||
:selection-anchor-instance-id="
|
||||
anchorInstance?.groupId === instanceGroup.id ? anchorInstance?.instanceId : null
|
||||
@@ -353,20 +384,6 @@ watch(selectedLibraryInstances, (selectedInstances) => {
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
|
||||
<div v-if="visibleUngroupedGroup" class="min-w-0">
|
||||
<InstanceGroup
|
||||
:hide-header="visibleInstanceGroups.length === 1"
|
||||
:instance-group="visibleUngroupedGroup"
|
||||
:selection-anchor-instance-id="
|
||||
anchorInstance?.groupId === 'group:none' ? anchorInstance.instanceId : null
|
||||
"
|
||||
@toggle-selection="
|
||||
(instanceId: string, shiftKey: boolean) =>
|
||||
handleToggleInstance('group:none', instanceId, shiftKey)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TransitionGroup
|
||||
@@ -407,6 +424,12 @@ watch(selectedLibraryInstances, (selectedInstances) => {
|
||||
:instances="currentDeleteInstances"
|
||||
@delete="deleteInstance"
|
||||
/>
|
||||
<IconEditorModal
|
||||
:ref="setIconEditorModal"
|
||||
:instance-id="currentIconEditorInstance?.id"
|
||||
:config="currentIconEditorInstance?.icon_config"
|
||||
@saved="handleInstanceIconSaved"
|
||||
/>
|
||||
<ContextMenu :ref="setInstanceOptions" @option-clicked="handleInstanceOption">
|
||||
<template #play> <PlayIcon /> {{ formatMessage(messages.play) }} </template>
|
||||
<template #stop> <StopCircleIcon /> {{ formatMessage(messages.stop) }} </template>
|
||||
@@ -419,6 +442,14 @@ watch(selectedLibraryInstances, (selectedInstances) => {
|
||||
</template>
|
||||
<template #add_content> <PlusIcon /> {{ formatMessage(messages.addContent) }} </template>
|
||||
<template #edit> <EyeIcon /> {{ formatMessage(messages.viewInstance) }} </template>
|
||||
<template #edit_icon> <EditIcon /> {{ formatMessage(messages.editIcon) }} </template>
|
||||
<template #select_icon> <UploadIcon /> {{ formatMessage(messages.selectIcon) }} </template>
|
||||
<template #replace_icon> <UploadIcon /> {{ formatMessage(messages.replaceIcon) }} </template>
|
||||
<template #create_icon> <PaletteIcon /> {{ formatMessage(messages.createIcon) }} </template>
|
||||
<template #edit_created_icon>
|
||||
<PaletteIcon /> {{ formatMessage(messages.editCreatedIcon) }}
|
||||
</template>
|
||||
<template #remove_icon> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||
<template #duplicate>
|
||||
<ClipboardCopyIcon /> {{ formatMessage(messages.duplicateInstance) }}
|
||||
</template>
|
||||
|
||||
+21
-10
@@ -2,16 +2,25 @@
|
||||
import { ArrowDownIcon, ArrowUpIcon, EditIcon, SquarePlusIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { defineMessages, IconButton, useVIntl } from '@modrinth/ui'
|
||||
|
||||
defineProps<{
|
||||
deleting?: boolean
|
||||
canMoveDown: boolean
|
||||
canMoveUp: boolean
|
||||
onAddToGroup: () => void
|
||||
onDeleteGroup: () => void
|
||||
onEditGroupName: () => void
|
||||
onMoveDown: () => void
|
||||
onMoveUp: () => void
|
||||
}>()
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
deleting?: boolean
|
||||
canMoveDown: boolean
|
||||
canMoveUp: boolean
|
||||
onAddToGroup: () => void
|
||||
onDeleteGroup: () => void
|
||||
onEditGroupName: () => void
|
||||
onMoveDown: () => void
|
||||
onMoveUp: () => void
|
||||
showDelete?: boolean
|
||||
showEdit?: boolean
|
||||
}>(),
|
||||
{
|
||||
deleting: false,
|
||||
showDelete: true,
|
||||
showEdit: true,
|
||||
},
|
||||
)
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -64,6 +73,7 @@ const messages = defineMessages({
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="showEdit"
|
||||
v-tooltip="formatMessage(messages.editGroupName)"
|
||||
:label="formatMessage(messages.editGroupName)"
|
||||
type="quiet"
|
||||
@@ -82,6 +92,7 @@ const messages = defineMessages({
|
||||
<SquarePlusIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-if="showDelete"
|
||||
v-tooltip="formatMessage(messages.deleteGroup)"
|
||||
:label="formatMessage(messages.deleteGroup)"
|
||||
type="quiet"
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { computed, inject, nextTick, onActivated, onDeactivated, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import GroupActionButtons from '@/components/ui/library/instance-group/group-action-buttons.vue'
|
||||
import InstanceCard from '@/components/ui/library/instance-group/instance-card.vue'
|
||||
import type {
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
InstanceGroup as InstanceGroupType,
|
||||
} from '@/components/ui/library/use-library'
|
||||
import { useLibrary } from '@/components/ui/library/use-library'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { FAVORITES_GROUP_ID, MAX_INSTANCE_GROUP_NAME_LENGTH } from '@/helpers/instance-groups'
|
||||
|
||||
const INSTANCE_GRID_OBSERVER_ACTIVATION_DELAY = 500
|
||||
@@ -48,6 +49,8 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const appSettings = useAppSettings()
|
||||
const compactMode = computed(() => appSettings.getFeatureFlag('compact_instance_cards'))
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const {
|
||||
isSectionCollapsed,
|
||||
@@ -84,6 +87,9 @@ const isFavorites = computed(() => props.instanceGroup.id === FAVORITES_GROUP_ID
|
||||
const isCustomGroup = computed(
|
||||
() => displayState.value.group === 'Group' && !isUngrouped.value && !isFavorites.value,
|
||||
)
|
||||
const isReorderableGroup = computed(
|
||||
() => displayState.value.group === 'Group' && !isFavorites.value,
|
||||
)
|
||||
const groupContextMenuOpen = ref(false)
|
||||
const isGroupToggleBlocked = computed(
|
||||
() => isSearching.value || groupContextMenuOpen.value || Boolean(groupNameInput.value?.isEditing),
|
||||
@@ -383,7 +389,7 @@ onMounted(startInstanceGridResizeObserver)
|
||||
v-if="!hideHeader"
|
||||
class="group/header h-10 flex w-full items-center gap-2 border-0 border-b border-solid border-b-surface-5"
|
||||
:class="{
|
||||
'instance-group-reorder-handle': isCustomGroup && canDragReorder,
|
||||
'instance-group-reorder-handle': isReorderableGroup && canDragReorder,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -442,7 +448,7 @@ onMounted(startInstanceGridResizeObserver)
|
||||
</div>
|
||||
<div class="min-w-0 flex-1" />
|
||||
<GroupActionButtons
|
||||
v-if="isCustomGroup"
|
||||
v-if="isCustomGroup || isUngrouped"
|
||||
:can-move-down="canMoveGroupDown(instanceGroup.id)"
|
||||
:can-move-up="canMoveGroupUp(instanceGroup.id)"
|
||||
:deleting="deletingGroup"
|
||||
@@ -451,6 +457,8 @@ onMounted(startInstanceGridResizeObserver)
|
||||
:on-edit-group-name="() => groupNameInput?.startEditing()"
|
||||
:on-move-down="() => moveGroup(instanceGroup.id, 1)"
|
||||
:on-move-up="() => moveGroup(instanceGroup.id, -1)"
|
||||
:show-delete="!isUngrouped"
|
||||
:show-edit="!isUngrouped"
|
||||
/>
|
||||
</div>
|
||||
<Accordion
|
||||
@@ -470,7 +478,12 @@ onMounted(startInstanceGridResizeObserver)
|
||||
<div ref="instanceGridContent">
|
||||
<TransitionGroup
|
||||
tag="section"
|
||||
class="grid min-h-[45px] w-full grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))] max-xl:grid-cols-[repeat(auto-fill,minmax(min(8rem,100%),1fr))] gap-3 overflow-y-auto scroll-smooth"
|
||||
class="grid min-h-[45px] w-full gap-3 overflow-y-auto scroll-smooth"
|
||||
:class="
|
||||
compactMode
|
||||
? 'grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))]'
|
||||
: 'grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))] max-xl:grid-cols-[repeat(auto-fill,minmax(min(8rem,100%),1fr))]'
|
||||
"
|
||||
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
||||
enter-active-class="transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none"
|
||||
enter-from-class="opacity-0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Avatar, truncatedTooltip } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { getInstanceIconUrl } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
@@ -16,6 +17,8 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const iconSrc = computed(() => getInstanceIconUrl(props.instance.icon_path))
|
||||
const appSettings = useAppSettings()
|
||||
const compactMode = computed(() => appSettings.getFeatureFlag('compact_instance_cards'))
|
||||
|
||||
const nameRef = ref<HTMLElement | null>(null)
|
||||
const versionRef = ref<HTMLElement | null>(null)
|
||||
@@ -23,30 +26,40 @@ const versionRef = ref<HTMLElement | null>(null)
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex w-full min-w-0 select-none flex-col items-start justify-end gap-3 overflow-clip rounded-[20px] border border-solid bg-surface-3 p-3 text-left transition-all"
|
||||
class="relative flex w-full min-w-0 select-none overflow-clip border border-solid bg-surface-3 text-left transition-all"
|
||||
:class="{
|
||||
'flex-row items-center justify-start gap-2.5 rounded-xl p-2.5': compactMode,
|
||||
'flex-col items-start justify-end gap-3 rounded-[20px] p-3': !compactMode,
|
||||
'[border-color:color-mix(in_srgb,var(--color-text-primary)_40%,transparent)] brightness-110':
|
||||
selected,
|
||||
'border-surface-4': !selected,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative flex aspect-square min-w-full shrink-0 items-center overflow-clip rounded-2xl"
|
||||
class="relative flex shrink-0 items-center overflow-clip"
|
||||
:class="compactMode ? 'size-10 rounded-lg' : 'aspect-square min-w-full rounded-2xl'"
|
||||
>
|
||||
<Avatar
|
||||
class="pointer-events-none !rounded-2xl outline-none"
|
||||
class="pointer-events-none outline-none"
|
||||
:class="compactMode ? '!rounded-lg' : '!rounded-2xl'"
|
||||
size="100%"
|
||||
:src="iconSrc"
|
||||
:tint-by="instance.id"
|
||||
alt=""
|
||||
no-shadow
|
||||
/>
|
||||
<slot name="loading" />
|
||||
<div class="absolute bottom-1.5 right-1.5 z-[1] flex size-12 items-center justify-center">
|
||||
<slot name="leading" />
|
||||
<slot name="loading" :compact="compactMode" />
|
||||
<div
|
||||
class="absolute z-[1] flex items-center justify-center"
|
||||
:class="compactMode ? 'inset-0' : 'bottom-1.5 right-1.5 size-12'"
|
||||
>
|
||||
<slot name="leading" :compact="compactMode" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 w-full flex-col items-start justify-center gap-1 px-0.5">
|
||||
<div
|
||||
class="flex min-w-0 w-full flex-col items-start justify-center gap-1 px-0.5"
|
||||
:class="{ 'pr-10': compactMode }"
|
||||
>
|
||||
<p
|
||||
ref="nameRef"
|
||||
v-tooltip="truncatedTooltip(nameRef, instance.name)"
|
||||
@@ -62,6 +75,6 @@ const versionRef = ref<HTMLElement | null>(null)
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</p>
|
||||
</div>
|
||||
<slot name="overlay" />
|
||||
<slot name="overlay" :compact="compactMode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useRouter } from 'vue-router'
|
||||
import InstanceCardView from '@/components/ui/library/instance-group/instance-card-view.vue'
|
||||
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
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'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
type ProcessEvent = 'installing' | 'launched' | 'finished'
|
||||
|
||||
@@ -285,7 +285,7 @@ onMounted(() => {
|
||||
@mouseenter="checkProcess"
|
||||
@pointerdown="handlePointerDown"
|
||||
>
|
||||
<template #loading>
|
||||
<template #loading="{ compact }">
|
||||
<div
|
||||
v-if="loadingIndicatorVisible"
|
||||
class="pointer-events-none absolute inset-0 z-[1] flex items-center justify-center"
|
||||
@@ -293,13 +293,17 @@ onMounted(() => {
|
||||
<div class="absolute inset-0 bg-surface-1 opacity-30" />
|
||||
<SpinnerIcon
|
||||
v-tooltip="formatMessage(modLoading ? messages.loading : messages.installing)"
|
||||
class="relative size-[30%] animate-spin text-contrast"
|
||||
class="relative animate-spin text-contrast"
|
||||
:class="compact ? 'size-5' : 'size-[30%]'"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #leading>
|
||||
<div class="relative flex size-12 shrink-0 items-center justify-center">
|
||||
<template #leading="{ compact }">
|
||||
<div
|
||||
class="relative flex shrink-0 items-center justify-center"
|
||||
:class="compact ? 'size-10' : 'size-12'"
|
||||
>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<IconButton
|
||||
v-if="playing"
|
||||
@@ -307,7 +311,7 @@ onMounted(() => {
|
||||
:label="formatMessage(messages.stop)"
|
||||
type="colored"
|
||||
color="red"
|
||||
size="lg"
|
||||
:size="compact ? 'md' : 'lg'"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
@@ -325,7 +329,7 @@ onMounted(() => {
|
||||
:label="formatMessage(messages.repair)"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="lg"
|
||||
:size="compact ? 'md' : 'lg'"
|
||||
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
@@ -342,7 +346,7 @@ onMounted(() => {
|
||||
:label="formatMessage(messages.play)"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="lg"
|
||||
:size="compact ? 'md' : 'lg'"
|
||||
class="origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
@@ -352,18 +356,21 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #overlay>
|
||||
<template #overlay="{ compact }">
|
||||
<button
|
||||
type="button"
|
||||
class="selection-button group/selection absolute right-2 top-1.5 z-[2] flex size-[50px] cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
|
||||
class="selection-button group/selection absolute z-[2] flex size-[50px] cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
|
||||
:class="compact ? '-right-1 -top-1' : 'right-2 top-1.5'"
|
||||
:aria-label="formatMessage(selected ? messages.deselect : messages.select)"
|
||||
:aria-pressed="selected"
|
||||
@click.stop="toggleSelection"
|
||||
>
|
||||
<span
|
||||
v-tooltip="formatMessage(selected ? messages.deselect : messages.select)"
|
||||
class="relative flex size-[24px] items-center justify-center rounded-full opacity-0 transition-opacity duration-200 ease-out group-hover/card:opacity-100 group-hover/selection:brightness-125"
|
||||
class="relative flex items-center justify-center rounded-full opacity-0 transition-opacity duration-200 ease-out group-hover/card:opacity-100 group-hover/selection:brightness-125"
|
||||
:class="{
|
||||
'size-[20px]': compact,
|
||||
'size-[24px]': !compact,
|
||||
'border-0 !opacity-100': selected,
|
||||
'border-2 border-solid border-primary bg-transparent': !selected,
|
||||
'[outline:3px_solid_var(--color-purple)] outline-offset-1':
|
||||
|
||||
@@ -15,6 +15,8 @@ const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
name: { id: 'app.library.sort.name', defaultMessage: 'Name' },
|
||||
loaderSort: { id: 'app.library.sort.loader', defaultMessage: 'Loader' },
|
||||
gameVersionSort: { id: 'app.library.sort.game-version', defaultMessage: 'Game version' },
|
||||
lastPlayed: { id: 'app.library.sort.last-played', defaultMessage: 'Last played' },
|
||||
hoursPlayed: { id: 'app.library.sort.hours-played', defaultMessage: 'Hours played' },
|
||||
dateCreated: { id: 'app.library.sort.date-created', defaultMessage: 'Date created' },
|
||||
@@ -34,6 +36,8 @@ const sortLabels = {
|
||||
'Hours played': messages.hoursPlayed,
|
||||
'Date created': messages.dateCreated,
|
||||
'Date modified': messages.dateModified,
|
||||
Loader: messages.loaderSort,
|
||||
'Game version': messages.gameVersionSort,
|
||||
}
|
||||
|
||||
const groupLabels = {
|
||||
@@ -60,6 +64,7 @@ const groupOptions: ComboboxOption<LibraryGroupBy>[] = libraryGroupOptions.map((
|
||||
class="w-max"
|
||||
:options="sortOptions"
|
||||
:show-icon-in-selected="false"
|
||||
:max-height="320"
|
||||
dropdown-min-width="160px"
|
||||
>
|
||||
<template #prefix>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
import { useEventListener, useStorage } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
type InjectionKey,
|
||||
nextTick,
|
||||
provide,
|
||||
type Ref,
|
||||
ref,
|
||||
@@ -13,10 +15,11 @@ import {
|
||||
watchEffect,
|
||||
} from 'vue'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_v3_many } from '@/helpers/cache.js'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import { install_duplicate_instance } from '@/helpers/install'
|
||||
import { edit, remove } from '@/helpers/instance'
|
||||
import { edit, edit_icon, remove } from '@/helpers/instance'
|
||||
import {
|
||||
create_group as createInstanceGroup,
|
||||
delete_group as deleteInstanceGroup,
|
||||
@@ -28,7 +31,7 @@ import {
|
||||
set_group_memberships as setInstanceGroupMemberships,
|
||||
set_group_order as setInstanceGroupOrder,
|
||||
} from '@/helpers/instance-groups'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import type { GameInstance, InstanceIconConfig } from '@/helpers/types'
|
||||
|
||||
export const librarySortOptions = [
|
||||
'Name',
|
||||
@@ -36,6 +39,8 @@ export const librarySortOptions = [
|
||||
'Hours played',
|
||||
'Date created',
|
||||
'Date modified',
|
||||
'Loader',
|
||||
'Game version',
|
||||
] as const
|
||||
|
||||
export const libraryGroupOptions = [
|
||||
@@ -100,6 +105,10 @@ type ConfirmDeleteModal = {
|
||||
show: () => void
|
||||
}
|
||||
|
||||
type IconEditorModal = {
|
||||
show: () => void
|
||||
}
|
||||
|
||||
type ContextMenuSelection = {
|
||||
option: string
|
||||
item: InstanceCard
|
||||
@@ -153,17 +162,25 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
)
|
||||
const currentContextGroupId = ref<string | null>(null)
|
||||
const confirmDeleteModal = ref<ConfirmDeleteModal | null>(null)
|
||||
const iconEditorModal = ref<IconEditorModal | null>(null)
|
||||
const currentIconEditorInstanceId = ref<string | null>(null)
|
||||
const currentIconEditorInstance = computed(
|
||||
() =>
|
||||
instances.value.find((instance) => instance.id === currentIconEditorInstanceId.value) ?? null,
|
||||
)
|
||||
|
||||
const displayState = useStorage<{
|
||||
group: LibraryGroupBy
|
||||
sortBy: LibrarySort
|
||||
collapsedGroups: string[]
|
||||
ungroupedGroupPosition: number
|
||||
}>(
|
||||
'Instances-grid-display-state',
|
||||
{
|
||||
group: 'Group',
|
||||
sortBy: 'Last played',
|
||||
collapsedGroups: [],
|
||||
ungroupedGroupPosition: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
localStorage,
|
||||
{ mergeDefaults: true },
|
||||
@@ -209,10 +226,15 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
})
|
||||
const groupInstancesModalGroup = computed(
|
||||
() =>
|
||||
libraryGroups.value.find((group) => group.id === groupInstancesModalGroupId.value) ?? null,
|
||||
)
|
||||
const groupInstancesModalGroup = computed(() => {
|
||||
if (groupInstancesModalGroupId.value === 'group:none') {
|
||||
return { id: 'group:none', name: 'None' }
|
||||
}
|
||||
|
||||
return (
|
||||
libraryGroups.value.find((group) => group.id === groupInstancesModalGroupId.value) ?? null
|
||||
)
|
||||
})
|
||||
const groupInstances = computed(() => {
|
||||
const query = groupInstancesSearch.value.trim().toLowerCase()
|
||||
|
||||
@@ -227,8 +249,23 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
const customLibraryGroups = computed(() =>
|
||||
libraryGroups.value.filter((group) => group.id !== FAVORITES_GROUP_ID),
|
||||
)
|
||||
const customGroupOrder = computed(
|
||||
() => new Map(customLibraryGroups.value.map((group, index) => [group.id, index])),
|
||||
const orderedLibraryGroupIds = computed(() => {
|
||||
const groupIds = customLibraryGroups.value.map((group) => group.id)
|
||||
const storedUngroupedGroupPosition = displayState.value.ungroupedGroupPosition
|
||||
const ungroupedGroupPosition = Math.min(
|
||||
Math.max(
|
||||
Number.isFinite(storedUngroupedGroupPosition)
|
||||
? Math.trunc(storedUngroupedGroupPosition)
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
0,
|
||||
),
|
||||
groupIds.length,
|
||||
)
|
||||
groupIds.splice(ungroupedGroupPosition, 0, 'group:none')
|
||||
return groupIds
|
||||
})
|
||||
const libraryGroupOrder = computed(
|
||||
() => new Map(orderedLibraryGroupIds.value.map((groupId, index) => [groupId, index])),
|
||||
)
|
||||
|
||||
const refreshGroups = async () => {
|
||||
@@ -328,6 +365,18 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
case 'Name':
|
||||
visibleInstances.sort((a, b) => a.name.localeCompare(b.name))
|
||||
break
|
||||
case 'Loader':
|
||||
visibleInstances.sort((a, b) =>
|
||||
formatLoader(formatMessage, a.loader).localeCompare(
|
||||
formatLoader(formatMessage, b.loader),
|
||||
),
|
||||
)
|
||||
break
|
||||
case 'Game version':
|
||||
visibleInstances.sort((a, b) =>
|
||||
a.game_version.localeCompare(b.game_version, undefined, { numeric: true }),
|
||||
)
|
||||
break
|
||||
case 'Last played':
|
||||
visibleInstances.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
|
||||
break
|
||||
@@ -436,11 +485,9 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
if (a.id === b.id) return 0
|
||||
if (a.id === FAVORITES_GROUP_ID) return -1
|
||||
if (b.id === FAVORITES_GROUP_ID) return 1
|
||||
if (a.id === 'group:none') return 1
|
||||
if (b.id === 'group:none') return -1
|
||||
|
||||
const aOrder = customGroupOrder.value.get(a.id) ?? Number.MAX_SAFE_INTEGER
|
||||
const bOrder = customGroupOrder.value.get(b.id) ?? Number.MAX_SAFE_INTEGER
|
||||
const aOrder = libraryGroupOrder.value.get(a.id) ?? Number.MAX_SAFE_INTEGER
|
||||
const bOrder = libraryGroupOrder.value.get(b.id) ?? Number.MAX_SAFE_INTEGER
|
||||
return aOrder - bOrder || a.key.localeCompare(b.key) || a.id.localeCompare(b.id)
|
||||
})
|
||||
}
|
||||
@@ -701,13 +748,17 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
|
||||
const openGroupInstancesModal = (groupId: string) => {
|
||||
const group = libraryGroups.value.find((candidate) => candidate.id === groupId)
|
||||
if (!group) return
|
||||
if (!group && groupId !== 'group:none') return
|
||||
|
||||
groupInstancesModalGroupId.value = groupId
|
||||
groupInstancesSearch.value = ''
|
||||
selectedGroupInstanceIds.value = new Set(
|
||||
instances.value
|
||||
.filter((instance) => instance.group_ids.includes(groupId))
|
||||
.filter((instance) =>
|
||||
groupId === 'group:none'
|
||||
? instance.group_ids.length === 0
|
||||
: instance.group_ids.includes(groupId),
|
||||
)
|
||||
.map((instance) => instance.id),
|
||||
)
|
||||
isGroupInstancesModalOpen.value = true
|
||||
@@ -722,6 +773,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
const selectedIds = new Set(selectedGroupInstanceIds.value)
|
||||
|
||||
if (selectedIds.has(instanceId)) {
|
||||
if (groupInstancesModalGroupId.value === 'group:none') return
|
||||
selectedIds.delete(instanceId)
|
||||
} else {
|
||||
selectedIds.add(instanceId)
|
||||
@@ -734,15 +786,20 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
const groupId = groupInstancesModalGroupId.value
|
||||
if (!groupId || savingGroupInstances.value) return false
|
||||
|
||||
const changedInstances = instances.value.filter(
|
||||
(instance) =>
|
||||
instance.group_ids.includes(groupId) !== selectedGroupInstanceIds.value.has(instance.id),
|
||||
)
|
||||
const isUngrouped = groupId === 'group:none'
|
||||
const changedInstances = instances.value.filter((instance) => {
|
||||
const isSelected = selectedGroupInstanceIds.value.has(instance.id)
|
||||
return isUngrouped
|
||||
? isSelected && instance.group_ids.length > 0
|
||||
: instance.group_ids.includes(groupId) !== isSelected
|
||||
})
|
||||
const operations = changedInstances.map((instance) => {
|
||||
const shouldIncludeGroup = selectedGroupInstanceIds.value.has(instance.id)
|
||||
const nextGroupIds = shouldIncludeGroup
|
||||
? [...instance.group_ids, groupId]
|
||||
: instance.group_ids.filter((instanceGroupId) => instanceGroupId !== groupId)
|
||||
const nextGroupIds = isUngrouped
|
||||
? []
|
||||
: shouldIncludeGroup
|
||||
? [...instance.group_ids, groupId]
|
||||
: instance.group_ids.filter((instanceGroupId) => instanceGroupId !== groupId)
|
||||
|
||||
return {
|
||||
instance,
|
||||
@@ -964,14 +1021,16 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
|
||||
const canMoveGroupUp = (groupId: string) =>
|
||||
!reorderingGroups.value &&
|
||||
customLibraryGroups.value.findIndex((group) => group.id === groupId) > 0
|
||||
orderedLibraryGroupIds.value.findIndex((orderedGroupId) => orderedGroupId === groupId) > 0
|
||||
|
||||
const canMoveGroupDown = (groupId: string) => {
|
||||
const groupIndex = customLibraryGroups.value.findIndex((group) => group.id === groupId)
|
||||
const groupIndex = orderedLibraryGroupIds.value.findIndex(
|
||||
(orderedGroupId) => orderedGroupId === groupId,
|
||||
)
|
||||
return (
|
||||
!reorderingGroups.value &&
|
||||
groupIndex >= 0 &&
|
||||
groupIndex < customLibraryGroups.value.length - 1
|
||||
groupIndex < orderedLibraryGroupIds.value.length - 1
|
||||
)
|
||||
}
|
||||
|
||||
@@ -979,35 +1038,48 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
if (reorderingGroups.value) return false
|
||||
|
||||
const previousGroups = libraryGroups.value
|
||||
const previousUngroupedGroupPosition = displayState.value.ungroupedGroupPosition
|
||||
const customGroupsById = new Map(customLibraryGroups.value.map((group) => [group.id, group]))
|
||||
const reorderableGroupIds = new Set([...customGroupsById.keys(), 'group:none'])
|
||||
const orderedGroupIdSet = new Set(orderedGroupIds)
|
||||
|
||||
if (
|
||||
orderedGroupIdSet.size !== orderedGroupIds.length ||
|
||||
orderedGroupIds.some((groupId) => !customGroupsById.has(groupId))
|
||||
orderedGroupIds.some((groupId) => !reorderableGroupIds.has(groupId))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const orderedGroups = orderedGroupIds.map((groupId) => customGroupsById.get(groupId)!)
|
||||
let orderedGroupIndex = 0
|
||||
const reorderedCustomGroups = customLibraryGroups.value.map((group) =>
|
||||
orderedGroupIdSet.has(group.id) ? orderedGroups[orderedGroupIndex++] : group,
|
||||
const reorderedGroupIds = orderedLibraryGroupIds.value.map((groupId) =>
|
||||
orderedGroupIdSet.has(groupId) ? orderedGroupIds[orderedGroupIndex++] : groupId,
|
||||
)
|
||||
|
||||
if (reorderedCustomGroups.every((group, index) => group === customLibraryGroups.value[index])) {
|
||||
if (
|
||||
reorderedGroupIds.every((groupId, index) => groupId === orderedLibraryGroupIds.value[index])
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const reorderedCustomGroups = reorderedGroupIds
|
||||
.filter((groupId) => groupId !== 'group:none')
|
||||
.map((groupId) => customGroupsById.get(groupId)!)
|
||||
const customGroupOrderChanged = reorderedCustomGroups.some(
|
||||
(group, index) => group !== customLibraryGroups.value[index],
|
||||
)
|
||||
const favoriteGroups = previousGroups.filter((group) => group.id === FAVORITES_GROUP_ID)
|
||||
libraryGroups.value = [...favoriteGroups, ...reorderedCustomGroups]
|
||||
displayState.value.ungroupedGroupPosition = reorderedGroupIds.indexOf('group:none')
|
||||
reorderingGroups.value = true
|
||||
|
||||
try {
|
||||
await setInstanceGroupOrder(reorderedCustomGroups.map((group) => group.id))
|
||||
if (customGroupOrderChanged) {
|
||||
await setInstanceGroupOrder(reorderedCustomGroups.map((group) => group.id))
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
libraryGroups.value = previousGroups
|
||||
displayState.value.ungroupedGroupPosition = previousUngroupedGroupPosition
|
||||
handleError(toError(error))
|
||||
await refreshGroups()
|
||||
return false
|
||||
@@ -1017,7 +1089,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
}
|
||||
|
||||
const moveGroup = async (groupId: string, direction: -1 | 1) => {
|
||||
const orderedGroupIds = customLibraryGroups.value.map((group) => group.id)
|
||||
const orderedGroupIds = [...orderedLibraryGroupIds.value]
|
||||
const groupIndex = orderedGroupIds.indexOf(groupId)
|
||||
const targetIndex = groupIndex + direction
|
||||
|
||||
@@ -1043,6 +1115,47 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
await install_duplicate_instance(instanceId).catch((error) => handleError(toError(error)))
|
||||
}
|
||||
|
||||
const selectInstanceIcon = async (item: InstanceCard) => {
|
||||
const iconPath = await openDialog({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'Image',
|
||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (!iconPath) return
|
||||
|
||||
try {
|
||||
await edit_icon(item.instance.id, iconPath)
|
||||
trackEvent('InstanceSetIcon')
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
}
|
||||
}
|
||||
|
||||
const removeInstanceIcon = async (item: InstanceCard) => {
|
||||
try {
|
||||
await edit_icon(item.instance.id, null)
|
||||
trackEvent('InstanceRemoveIcon')
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
}
|
||||
}
|
||||
|
||||
const openInstanceIconEditor = async (item: InstanceCard) => {
|
||||
currentIconEditorInstanceId.value = item.instance.id
|
||||
await nextTick()
|
||||
iconEditorModal.value?.show()
|
||||
trackEvent(item.instance.icon_config ? 'InstanceEditCreatedIcon' : 'InstanceCreateIcon')
|
||||
}
|
||||
|
||||
const handleInstanceIconSaved = (_iconPath: string, _config: InstanceIconConfig) => {
|
||||
trackEvent('InstanceSaveCreatedIcon')
|
||||
}
|
||||
|
||||
const handleInstanceContextMenu = (
|
||||
event: MouseEvent,
|
||||
item: InstanceCard,
|
||||
@@ -1069,6 +1182,14 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
{ name: 'duplicate' },
|
||||
{ name: 'open' },
|
||||
{ name: 'copy' },
|
||||
{
|
||||
name: 'edit_icon',
|
||||
children: [
|
||||
{ name: item.instance.icon_path ? 'replace_icon' : 'select_icon' },
|
||||
{ name: item.instance.icon_config ? 'edit_created_icon' : 'create_icon' },
|
||||
...(item.instance.icon_path ? [{ name: 'remove_icon' }] : []),
|
||||
],
|
||||
},
|
||||
...(currentContextGroupId.value
|
||||
? [{ name: 'remove_from_group' }, { type: 'divider' }]
|
||||
: [{ type: 'divider' }]),
|
||||
@@ -1113,6 +1234,17 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
case 'edit':
|
||||
await item.seeInstance()
|
||||
break
|
||||
case 'select_icon':
|
||||
case 'replace_icon':
|
||||
await selectInstanceIcon(item)
|
||||
break
|
||||
case 'create_icon':
|
||||
case 'edit_created_icon':
|
||||
await openInstanceIconEditor(item)
|
||||
break
|
||||
case 'remove_icon':
|
||||
await removeInstanceIcon(item)
|
||||
break
|
||||
case 'duplicate':
|
||||
if (item.instance.install_stage === 'installed') {
|
||||
await duplicateInstance(item.instance.id)
|
||||
@@ -1175,6 +1307,8 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
canCreateGroup,
|
||||
instanceOptions,
|
||||
confirmDeleteModal,
|
||||
iconEditorModal,
|
||||
currentIconEditorInstance,
|
||||
currentDeleteInstances,
|
||||
isSectionCollapsed,
|
||||
setSectionCollapsed,
|
||||
@@ -1206,6 +1340,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
deleteInstance,
|
||||
handleInstanceContextMenu,
|
||||
handleInstanceOption,
|
||||
handleInstanceIconSaved,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@ import {
|
||||
import { Admonition, Button, ButtonLink, Collapsible, IconButton, NewModal } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
import { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors'
|
||||
|
||||
|
||||
+1
-1
@@ -63,9 +63,9 @@ import { inject, type Ref, ref } from 'vue'
|
||||
|
||||
import steveImage from '@/assets/steve-look-up-left.webp'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { login as loginFlow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const accountsCard = inject('accountsCard') as Ref<InstanceType<typeof AccountsCard> | null>
|
||||
|
||||
@@ -36,16 +36,16 @@ import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.
|
||||
import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultInstanceSettings.vue'
|
||||
import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
|
||||
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import {
|
||||
appSettingsModalContextKey,
|
||||
type UnsavedChangesController,
|
||||
} from '@/providers/app-settings-modal'
|
||||
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -157,7 +157,9 @@ const tabs = [
|
||||
},
|
||||
]
|
||||
|
||||
const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode))
|
||||
const availableTabs = computed(() =>
|
||||
tabs.filter((tab) => !tab.developerOnly || appSettings.devMode),
|
||||
)
|
||||
|
||||
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
|
||||
@@ -170,10 +172,19 @@ const modifiedUnsavedChangesState = computed(
|
||||
() => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState,
|
||||
)
|
||||
const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false)
|
||||
const hasUnsavedChanges = computed(() => unsavedChangesController.value?.hasChanges() ?? false)
|
||||
const hasUnsavedChanges = computed(
|
||||
() =>
|
||||
(unsavedChangesController.value?.hasChanges() ?? false) ||
|
||||
(unsavedChangesController.value?.isSaving() ?? false),
|
||||
)
|
||||
|
||||
function canLeaveCurrentTab(): boolean {
|
||||
if (!unsavedChangesController.value?.hasChanges()) return true
|
||||
if (
|
||||
!unsavedChangesController.value?.hasChanges() &&
|
||||
!unsavedChangesController.value?.isSaving()
|
||||
) {
|
||||
return true
|
||||
}
|
||||
unsavedChangesPopup.value?.nudge()
|
||||
return false
|
||||
}
|
||||
@@ -233,8 +244,8 @@ function devModeCount() {
|
||||
if (devModeCounter.value > 5) {
|
||||
const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined
|
||||
|
||||
themeStore.devMode = !themeStore.devMode
|
||||
settings.value.developer_mode = !!themeStore.devMode
|
||||
appSettings.devMode = !appSettings.devMode
|
||||
settings.value.developer_mode = !!appSettings.devMode
|
||||
devModeCounter.value = 0
|
||||
|
||||
if (modal.value) {
|
||||
@@ -298,7 +309,7 @@ const messages = defineMessages({
|
||||
<ProgressBar :progress="progress" />
|
||||
</template>
|
||||
</div>
|
||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||
<p v-if="appSettings.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||
{{ formatMessage(developerModeEnabled) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -306,8 +317,8 @@ const messages = defineMessages({
|
||||
:aria-label="formatMessage(messages.developerModeButtonLabel)"
|
||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||
:class="{
|
||||
'text-brand': themeStore.devMode,
|
||||
'text-secondary': !themeStore.devMode,
|
||||
'text-brand': appSettings.devMode,
|
||||
'text-secondary': !appSettings.devMode,
|
||||
}"
|
||||
@click="devModeCount"
|
||||
>
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@ defineExpose({ show, hide })
|
||||
:disable-close="applying"
|
||||
class="!overflow-hidden !rounded-[20px]"
|
||||
>
|
||||
<div class="grid h-[384px] w-[768px] max-w-full grid-cols-2">
|
||||
<div class="grid w-[768px] max-w-full grid-cols-2">
|
||||
<section class="flex min-w-0 flex-col gap-6 bg-surface-3 p-8">
|
||||
<div
|
||||
class="flex h-8 w-fit items-center rounded-full border border-solid border-brand bg-brand-highlight px-2.5 text-sm font-base leading-5 text-brand"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<AccountSocialSettings
|
||||
ref="socialSettings"
|
||||
:get-blocked-users="get_blocked_users"
|
||||
:get-users="getUsers"
|
||||
:unblock-user="unblock_user"
|
||||
@@ -9,10 +10,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { AccountSocialSettings, injectModrinthClient } from '@modrinth/ui'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { get_blocked_users, unblock_user } from '@/helpers/users'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const socialSettings = ref<InstanceType<typeof AccountSocialSettings> | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => socialSettings.value?.hasChanges ?? false,
|
||||
getOriginal: () => socialSettings.value?.originalState ?? {},
|
||||
getModified: () => socialSettings.value?.modifiedState ?? {},
|
||||
isSaving: () => socialSettings.value?.saving ?? false,
|
||||
reset: () => socialSettings.value?.reset(),
|
||||
save: () => socialSettings.value?.save(),
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
|
||||
function getUsers(userIds: string[]): Promise<Labrinth.Users.v2.User[]> {
|
||||
return client.labrinth.users_v2.getMultiple(userIds)
|
||||
|
||||
@@ -1,108 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
AppearanceSettingsLayout,
|
||||
injectAuth,
|
||||
injectUserPreferences,
|
||||
provideAppearanceSettings,
|
||||
useSavable,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { type ColorTheme, useTheme } from '@/composables/use-theme.ts'
|
||||
import { type AppSettings, get, set } from '@/helpers/settings.ts'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { ColorTheme } from '@/store/theme.ts'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
colorThemeTitle: {
|
||||
id: 'app.appearance-settings.color-theme.title',
|
||||
defaultMessage: 'Color theme',
|
||||
},
|
||||
colorThemeDescription: {
|
||||
id: 'app.appearance-settings.color-theme.description',
|
||||
defaultMessage: 'Choose the color theme used by Modrinth App.',
|
||||
},
|
||||
advancedRenderingTitle: {
|
||||
id: 'app.appearance-settings.advanced-rendering.title',
|
||||
defaultMessage: 'Advanced rendering',
|
||||
},
|
||||
advancedRenderingDescription: {
|
||||
id: 'app.appearance-settings.advanced-rendering.description',
|
||||
defaultMessage:
|
||||
'Enable visual effects such as background blur. This may reduce performance without hardware acceleration.',
|
||||
},
|
||||
nativeDecorationsTitle: {
|
||||
id: 'app.appearance-settings.native-decorations.title',
|
||||
defaultMessage: 'System window frame',
|
||||
},
|
||||
nativeDecorationsDescription: {
|
||||
id: 'app.appearance-settings.native-decorations.description',
|
||||
defaultMessage:
|
||||
"Use your operating system's title bar and window controls. Requires an app restart.",
|
||||
},
|
||||
})
|
||||
|
||||
const os = ref(await getOS())
|
||||
const theme = useTheme()
|
||||
const auth = injectAuth()
|
||||
const { updatePreferences } = injectUserPreferences()
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const os = await getOS()
|
||||
const settings = ref(await get())
|
||||
const themeOptions = computed(() =>
|
||||
themeStore
|
||||
.getThemeOptions()
|
||||
.filter((theme) => theme !== 'retro' || themeStore.devMode || settings.value.theme === 'retro'),
|
||||
|
||||
type AppearanceSettingsState = {
|
||||
theme: ColorTheme
|
||||
syncAcrossDevices: boolean
|
||||
advancedRendering: boolean
|
||||
nativeDecorations: boolean
|
||||
}
|
||||
|
||||
function getAppearanceSettingsState(settings: AppSettings): AppearanceSettingsState {
|
||||
return {
|
||||
theme: settings.theme,
|
||||
syncAcrossDevices: settings.sync_theme_across_devices,
|
||||
advancedRendering: settings.advanced_rendering,
|
||||
nativeDecorations: settings.native_decorations,
|
||||
}
|
||||
}
|
||||
|
||||
const { saved, current, changes, saving, hasChanges, reset, save } = useSavable(
|
||||
() => getAppearanceSettingsState(settings.value),
|
||||
async (appearanceChanges) => {
|
||||
const value = current.value
|
||||
if (
|
||||
value.syncAcrossDevices &&
|
||||
auth.user.value &&
|
||||
(appearanceChanges.theme !== undefined || appearanceChanges.syncAcrossDevices !== undefined)
|
||||
) {
|
||||
await updatePreferences({
|
||||
appearance: value.theme === 'system' ? { auto: true } : { auto: false, theme: value.theme },
|
||||
})
|
||||
}
|
||||
|
||||
const nextSettings: AppSettings = {
|
||||
...settings.value,
|
||||
theme: value.theme,
|
||||
sync_theme_across_devices: value.syncAcrossDevices,
|
||||
advanced_rendering: value.advancedRendering,
|
||||
native_decorations: value.nativeDecorations,
|
||||
}
|
||||
|
||||
await set(nextSettings)
|
||||
settings.value = nextSettings
|
||||
theme.preferred = value.theme
|
||||
theme.syncAcrossDevices = value.syncAcrossDevices
|
||||
theme.advancedRendering = value.advancedRendering
|
||||
},
|
||||
)
|
||||
|
||||
const themeOptions = computed(() =>
|
||||
theme.options.filter(
|
||||
(option) =>
|
||||
option !== 'retro' || settings.value.developer_mode || current.value.theme === 'retro',
|
||||
),
|
||||
)
|
||||
|
||||
function setTheme(value: ColorTheme): void {
|
||||
current.value.theme = value
|
||||
}
|
||||
|
||||
function setSyncAcrossDevices(enabled: boolean): void {
|
||||
current.value.syncAcrossDevices = enabled
|
||||
}
|
||||
|
||||
function setAdvancedRendering(enabled: boolean): void {
|
||||
current.value.advancedRendering = enabled
|
||||
}
|
||||
|
||||
function setNativeDecorations(enabled: boolean): void {
|
||||
current.value.nativeDecorations = enabled
|
||||
}
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
[() => current.value.theme, () => saved.value.theme],
|
||||
([selectedTheme, savedTheme]) => {
|
||||
theme.preview = selectedTheme === savedTheme ? null : selectedTheme
|
||||
},
|
||||
{ deep: true },
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.colorThemeTitle) }}
|
||||
</h2>
|
||||
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
|
||||
async function saveAppearanceSettings(): Promise<void> {
|
||||
try {
|
||||
await save()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme: ColorTheme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeOptions"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => hasChanges.value,
|
||||
getOriginal: () => saved.value,
|
||||
getModified: () => changes.value,
|
||||
isSaving: () => saving.value,
|
||||
reset,
|
||||
save: saveAppearanceSettings,
|
||||
})
|
||||
})
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.advancedRenderingTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.advancedRenderingDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
onBeforeUnmount(() => {
|
||||
theme.preview = null
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.advancedRendering"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.advancedRendering = !!e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
provideAppearanceSettings({
|
||||
deferPersistence: true,
|
||||
theme: {
|
||||
current: computed(() => current.value.theme),
|
||||
options: themeOptions,
|
||||
system: computed(() => theme.native),
|
||||
set: setTheme,
|
||||
syncAcrossDevices: {
|
||||
value: computed(() => current.value.syncAcrossDevices),
|
||||
set: setSyncAcrossDevices,
|
||||
},
|
||||
syncDisabled: computed(() => !auth.user.value),
|
||||
},
|
||||
advancedRendering: {
|
||||
value: computed(() => current.value.advancedRendering),
|
||||
set: setAdvancedRendering,
|
||||
},
|
||||
nativeDecorations:
|
||||
os !== 'MacOS'
|
||||
? {
|
||||
value: computed(() => current.value.nativeDecorations),
|
||||
set: setNativeDecorations,
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
: undefined,
|
||||
updatePreferences,
|
||||
})
|
||||
</script>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.nativeDecorationsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||
</div>
|
||||
<template>
|
||||
<AppearanceSettingsLayout />
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessages, Toggle, useVIntl } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
defineMessages,
|
||||
injectAuth,
|
||||
injectUserPreferences,
|
||||
Toggle,
|
||||
useSavable,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { FeatureFlag } from '@/store/theme.ts'
|
||||
import {
|
||||
DEFAULT_FEATURE_FLAGS,
|
||||
type FeatureFlag,
|
||||
useAppSettings,
|
||||
} from '@/composables/use-app-settings.ts'
|
||||
import { type AppSettings, get, set } from '@/helpers/settings.ts'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const { formatMessage } = useVIntl()
|
||||
const auth = injectAuth()
|
||||
const { updatePreferences } = injectUserPreferences()
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
|
||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const compactInstanceCardsFlag: FeatureFlag = 'compact_instance_cards'
|
||||
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||
|
||||
const messages = defineMessages({
|
||||
syncAcrossDevicesTitle: {
|
||||
id: 'app.behavior-settings.sync-across-devices.title',
|
||||
defaultMessage: 'Sync behavior across devices',
|
||||
},
|
||||
syncAcrossDevicesDescription: {
|
||||
id: 'app.behavior-settings.sync-across-devices.description',
|
||||
defaultMessage:
|
||||
"Use these behavior settings everywhere you're signed in. Turn this off to keep separate settings on this device.",
|
||||
},
|
||||
syncAcrossDevicesSignedOutTooltip: {
|
||||
id: 'app.behavior-settings.sync-across-devices.signed-out-tooltip',
|
||||
defaultMessage: 'Sign into a Modrinth account to sync settings.',
|
||||
},
|
||||
startupAndNavigationTitle: {
|
||||
id: 'app.behavior-settings.startup-and-navigation.title',
|
||||
defaultMessage: 'Startup and navigation',
|
||||
@@ -60,6 +88,14 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'Show recently played worlds or instances in the "Jump in" section on the Home page.',
|
||||
},
|
||||
compactModeTitle: {
|
||||
id: 'app.appearance-settings.compact-mode.title',
|
||||
defaultMessage: 'Compact mode',
|
||||
},
|
||||
compactModeDescription: {
|
||||
id: 'app.appearance-settings.compact-mode.description',
|
||||
defaultMessage: 'Display library instances in a compact row layout.',
|
||||
},
|
||||
showPlayTimeTitle: {
|
||||
id: 'app.appearance-settings.show-play-time.title',
|
||||
defaultMessage: 'Show play time',
|
||||
@@ -96,18 +132,143 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const settings = ref(await get())
|
||||
type BehaviorSettingsState = {
|
||||
syncBehaviorAcrossDevices: boolean
|
||||
minimizeApp: boolean
|
||||
hideRightSidebar: boolean
|
||||
showJumpIn: boolean
|
||||
compactInstanceCards: boolean
|
||||
showPlayTime: boolean
|
||||
hideNametag: boolean
|
||||
warnOnUnknownModpacks: boolean
|
||||
skipNonEssentialWarnings: boolean
|
||||
}
|
||||
|
||||
watch(
|
||||
settings,
|
||||
const persistedSettings = ref(await get())
|
||||
|
||||
function getBehaviorSettingsState(settings: AppSettings): BehaviorSettingsState {
|
||||
return {
|
||||
syncBehaviorAcrossDevices: settings.sync_behavior_across_devices,
|
||||
minimizeApp: settings.hide_on_process_start,
|
||||
hideRightSidebar: settings.toggle_sidebar,
|
||||
showJumpIn: settings.feature_flags[worldsInHomeFlag] ?? DEFAULT_FEATURE_FLAGS[worldsInHomeFlag],
|
||||
compactInstanceCards:
|
||||
settings.feature_flags[compactInstanceCardsFlag] ??
|
||||
DEFAULT_FEATURE_FLAGS[compactInstanceCardsFlag],
|
||||
showPlayTime:
|
||||
settings.feature_flags[showPlayTimeFlag] ?? DEFAULT_FEATURE_FLAGS[showPlayTimeFlag],
|
||||
hideNametag: settings.hide_nametag_skins_page,
|
||||
warnOnUnknownModpacks: !(
|
||||
settings.feature_flags[skipUnknownPackWarningFlag] ??
|
||||
DEFAULT_FEATURE_FLAGS[skipUnknownPackWarningFlag]
|
||||
),
|
||||
skipNonEssentialWarnings:
|
||||
settings.feature_flags[skipNonEssentialWarningsFlag] ??
|
||||
DEFAULT_FEATURE_FLAGS[skipNonEssentialWarningsFlag],
|
||||
}
|
||||
}
|
||||
|
||||
const { saved, current, changes, saving, hasChanges, reset, save } = useSavable(
|
||||
() => getBehaviorSettingsState(persistedSettings.value),
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
const value = current.value
|
||||
|
||||
if (value.syncBehaviorAcrossDevices && auth.user.value) {
|
||||
await updatePreferences({
|
||||
behavior: {
|
||||
minimize_app: value.minimizeApp,
|
||||
hide_right_sidebar: value.hideRightSidebar,
|
||||
show_jump_in: value.showJumpIn,
|
||||
compact_instance_cards: value.compactInstanceCards,
|
||||
show_play_time: value.showPlayTime,
|
||||
hide_nametag: value.hideNametag,
|
||||
warn_on_unknown_modpacks: value.warnOnUnknownModpacks,
|
||||
skip_non_essential_warnings: value.skipNonEssentialWarnings,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const nextSettings: AppSettings = {
|
||||
...persistedSettings.value,
|
||||
sync_behavior_across_devices: value.syncBehaviorAcrossDevices,
|
||||
hide_on_process_start: value.minimizeApp,
|
||||
toggle_sidebar: value.hideRightSidebar,
|
||||
hide_nametag_skins_page: value.hideNametag,
|
||||
feature_flags: {
|
||||
...persistedSettings.value.feature_flags,
|
||||
[worldsInHomeFlag]: value.showJumpIn,
|
||||
[compactInstanceCardsFlag]: value.compactInstanceCards,
|
||||
[showPlayTimeFlag]: value.showPlayTime,
|
||||
[skipUnknownPackWarningFlag]: !value.warnOnUnknownModpacks,
|
||||
[skipNonEssentialWarningsFlag]: value.skipNonEssentialWarnings,
|
||||
},
|
||||
}
|
||||
|
||||
await set(nextSettings)
|
||||
persistedSettings.value = nextSettings
|
||||
appSettings.setBehaviorSyncAcrossDevices(value.syncBehaviorAcrossDevices)
|
||||
appSettings.toggleSidebar = value.hideRightSidebar
|
||||
appSettings.hideNametagSkinsPage = value.hideNametag
|
||||
appSettings.featureFlags[worldsInHomeFlag] = value.showJumpIn
|
||||
appSettings.featureFlags[compactInstanceCardsFlag] = value.compactInstanceCards
|
||||
appSettings.featureFlags[showPlayTimeFlag] = value.showPlayTime
|
||||
appSettings.featureFlags[skipUnknownPackWarningFlag] = !value.warnOnUnknownModpacks
|
||||
appSettings.featureFlags[skipNonEssentialWarningsFlag] = value.skipNonEssentialWarnings
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
async function saveBehaviorSettings(): Promise<void> {
|
||||
try {
|
||||
await save()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => hasChanges.value,
|
||||
getOriginal: () => saved.value,
|
||||
getModified: () => changes.value,
|
||||
isSaving: () => saving.value,
|
||||
reset,
|
||||
save: saveBehaviorSettings,
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<section>
|
||||
<section class="border-0 border-b border-solid border-divider pb-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 id="sync-behavior-across-devices-label" class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.syncAcrossDevicesTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1 text-secondary">
|
||||
{{ formatMessage(messages.syncAcrossDevicesDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
v-tooltip="
|
||||
!auth.user.value ? formatMessage(messages.syncAcrossDevicesSignedOutTooltip) : undefined
|
||||
"
|
||||
class="inline-flex shrink-0"
|
||||
>
|
||||
<Toggle
|
||||
id="sync-behavior-across-devices"
|
||||
:model-value="Boolean(auth.user.value) && current.syncBehaviorAcrossDevices"
|
||||
:disabled="!auth.user.value"
|
||||
aria-labelledby="sync-behavior-across-devices-label"
|
||||
@update:model-value="current.syncBehaviorAcrossDevices = $event"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-6">
|
||||
<h2 class="m-0 text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.startupAndNavigationTitle) }}
|
||||
</h2>
|
||||
@@ -121,7 +282,7 @@ watch(
|
||||
{{ formatMessage(messages.minimizeLauncherDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
||||
<Toggle id="minimize-launcher" v-model="current.minimizeApp" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
@@ -131,16 +292,7 @@ watch(
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="toggle-sidebar"
|
||||
:model-value="settings.toggle_sidebar"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.toggle_sidebar = !!e
|
||||
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Toggle id="toggle-sidebar" v-model="current.hideRightSidebar" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -159,17 +311,17 @@ watch(
|
||||
{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="jump-back-into-worlds"
|
||||
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
|
||||
themeStore.featureFlags[worldsInHomeFlag] = newValue
|
||||
settings.feature_flags[worldsInHomeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Toggle id="jump-back-into-worlds" v-model="current.showJumpIn" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.compactModeTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.compactModeDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="compact-mode" v-model="current.compactInstanceCards" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
@@ -179,17 +331,7 @@ watch(
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="show-play-time"
|
||||
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
|
||||
themeStore.featureFlags[showPlayTimeFlag] = newValue
|
||||
settings.feature_flags[showPlayTimeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Toggle id="show-play-time" v-model="current.showPlayTime" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
@@ -199,16 +341,7 @@ watch(
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="hide-nametag-skins-page"
|
||||
:model-value="themeStore.hideNametagSkinsPage"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.hideNametagSkinsPage = !!e
|
||||
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Toggle id="hide-nametag-skins-page" v-model="current.hideNametag" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -229,15 +362,7 @@ watch(
|
||||
</div>
|
||||
<Toggle
|
||||
id="warn-before-installing-unknown-modpacks"
|
||||
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
const warnBeforeUnknownPackInstall = !!e
|
||||
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
|
||||
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
}
|
||||
"
|
||||
v-model="current.warnOnUnknownModpacks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -250,17 +375,7 @@ watch(
|
||||
{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="skip-non-essential-warnings"
|
||||
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
|
||||
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
|
||||
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Toggle id="skip-non-essential-warnings" v-model="current.skipNonEssentialWarnings" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
import { Button, Toggle } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
DEFAULT_FEATURE_FLAGS,
|
||||
type FeatureFlag,
|
||||
useAppSettings,
|
||||
} from '@/composables/use-app-settings.ts'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
|
||||
const settings = ref(await getSettings())
|
||||
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||
const options = ref(Object.keys(DEFAULT_FEATURE_FLAGS) as FeatureFlag[])
|
||||
|
||||
function setFeatureFlag(key: string, value: boolean) {
|
||||
themeStore.featureFlags[key] = value
|
||||
function setFeatureFlag(key: FeatureFlag, value: boolean) {
|
||||
appSettings.featureFlags[key] = value
|
||||
settings.value.feature_flags[key] = value
|
||||
}
|
||||
|
||||
@@ -35,15 +38,15 @@ watch(
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
type="quiet"
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
:disabled="appSettings.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)"
|
||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||
:model-value="appSettings.getFeatureFlag(option)"
|
||||
@update:model-value="() => setFeatureFlag(option, !appSettings.getFeatureFlag(option))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,35 @@
|
||||
<template>
|
||||
<SharedLanguageSettings ref="languageSettings" product="app" :persist-locale="persistLocale" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Admonition,
|
||||
AutoLink,
|
||||
commonSettingsMessages,
|
||||
IntlFormatted,
|
||||
LanguageSelector,
|
||||
languageSelectorMessages,
|
||||
LOCALES,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { LanguageSettings as SharedLanguageSettings } from '@modrinth/ui'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import i18n from '@/i18n.config'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const languageSettings = ref<InstanceType<typeof SharedLanguageSettings> | null>(null)
|
||||
|
||||
const platform = computed(() => formatMessage(languageSelectorMessages.platformApp))
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => languageSettings.value?.hasChanges ?? false,
|
||||
getOriginal: () => languageSettings.value?.originalState ?? {},
|
||||
getModified: () => languageSettings.value?.modifiedState ?? {},
|
||||
isSaving: () => languageSettings.value?.saving ?? false,
|
||||
reset: () => languageSettings.value?.reset(),
|
||||
save: () => languageSettings.value?.save(),
|
||||
})
|
||||
})
|
||||
|
||||
const settings = ref(await get())
|
||||
onBeforeUnmount(() => {
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const $isChanging = ref(false)
|
||||
|
||||
async function onLocaleChange(newLocale: string) {
|
||||
if (settings.value.locale === newLocale) return
|
||||
|
||||
$isChanging.value = true
|
||||
try {
|
||||
i18n.global.locale.value = newLocale
|
||||
settings.value.locale = newLocale
|
||||
} finally {
|
||||
$isChanging.value = false
|
||||
}
|
||||
async function persistLocale(locale: string): Promise<void> {
|
||||
const settings = await get()
|
||||
if (settings.locale === locale) return
|
||||
await set({ ...settings, locale })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(commonSettingsMessages.language) }}
|
||||
</h2>
|
||||
|
||||
<Admonition type="warning" class="mt-2 mb-4">
|
||||
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
|
||||
</Admonition>
|
||||
|
||||
<p class="m-0 mb-4">
|
||||
<IntlFormatted
|
||||
:message-id="languageSelectorMessages.languagesDescription"
|
||||
:values="{ platform }"
|
||||
>
|
||||
<template #~crowdin-link="{ children }">
|
||||
<AutoLink to="https://translate.modrinth.com">
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<LanguageSelector
|
||||
:current-locale="settings.locale"
|
||||
:locales="LOCALES"
|
||||
:on-locale-change="onLocaleChange"
|
||||
:is-changing="$isChanging"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+6
-6
@@ -14,14 +14,14 @@ import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const settings = ref(await get())
|
||||
const purgeCacheConfirmModal = ref(null)
|
||||
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
|
||||
@@ -145,7 +145,7 @@ async function purgeCache() {
|
||||
}
|
||||
|
||||
function handlePurgeCacheClick() {
|
||||
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
if (appSettings.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
void purgeCache()
|
||||
return
|
||||
}
|
||||
@@ -210,11 +210,11 @@ async function findLauncherDir() {
|
||||
</div>
|
||||
<Toggle
|
||||
id="always-show-copy-details"
|
||||
:model-value="themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)"
|
||||
:model-value="appSettings.getFeatureFlag(alwaysShowCopyDetailsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
|
||||
themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
|
||||
const newValue = !appSettings.getFeatureFlag(alwaysShowCopyDetailsFlag)
|
||||
appSettings.featureFlags[alwaysShowCopyDetailsFlag] = newValue
|
||||
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
|
||||
}
|
||||
"
|
||||
|
||||
+3
-3
@@ -9,6 +9,7 @@ import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { type Ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import {
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
} from '@/helpers/install'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
|
||||
import type { AppNotification, SharedInstanceInvite } from './shared-instance-invite-types'
|
||||
@@ -58,7 +58,7 @@ export function useSharedInstanceInviteHandler(
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const displayedNotifications = new Set<string | number>()
|
||||
const displayedNotificationKeys = new Set<string>()
|
||||
const popupNotificationIds = new Set<string | number>()
|
||||
@@ -121,7 +121,7 @@ export function useSharedInstanceInviteHandler(
|
||||
(instance) => instance.shared_instance?.id === sharedInstanceId,
|
||||
)
|
||||
|
||||
if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
if (!existingInstance || appSettings.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
showInstall(preview, install, creator)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { getInstanceIconUrl, kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showInstanceInFolder } from '@/helpers/utils'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { LoaderCircleIcon } from '@modrinth/assets'
|
||||
import type { GameVersion } from '@modrinth/ui'
|
||||
import { defineMessages, GAME_MODES, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import {
|
||||
Accordion,
|
||||
defineMessages,
|
||||
GAME_MODES,
|
||||
type GameVersion,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, 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 { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_all } from '@/helpers/process'
|
||||
@@ -29,20 +37,19 @@ import {
|
||||
start_join_singleplayer_world,
|
||||
type WorldWithInstance,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const messages = defineMessages({
|
||||
jumpIn: { id: 'app.home.jump-back-in.title', defaultMessage: 'Jump in' },
|
||||
resize: { id: 'app.home.jump-back-in.resize', defaultMessage: 'Drag to resize' },
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
recentInstances: GameInstance[]
|
||||
}>()
|
||||
|
||||
const theme = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
|
||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||
const loading = ref(true)
|
||||
@@ -54,6 +61,100 @@ const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []
|
||||
const MAX_JUMP_BACK_IN = 5
|
||||
const MAX_NEW_INSTANCES = 3
|
||||
const MAX_LINUX_POPULATES = 3
|
||||
const ITEM_DRAG_DISTANCE = 92
|
||||
const STORAGE_KEY = 'modrinth-jump-back-in-count'
|
||||
|
||||
const storedVisibleCount = Number.parseInt(localStorage.getItem(STORAGE_KEY) ?? '', 10)
|
||||
const visibleItemLimit = ref(
|
||||
Number.isFinite(storedVisibleCount)
|
||||
? Math.max(1, Math.min(storedVisibleCount, MAX_JUMP_BACK_IN))
|
||||
: MAX_JUMP_BACK_IN,
|
||||
)
|
||||
const maxVisibleItems = computed(() => Math.min(jumpBackInItems.value.length, MAX_JUMP_BACK_IN))
|
||||
const visibleItemCount = computed(() => Math.min(visibleItemLimit.value, maxVisibleItems.value))
|
||||
const visibleJumpBackInItems = computed(() =>
|
||||
jumpBackInItems.value.slice(0, visibleItemCount.value),
|
||||
)
|
||||
const canResize = computed(() => maxVisibleItems.value > 1)
|
||||
const resizing = ref(false)
|
||||
const showOverdrag = ref(false)
|
||||
|
||||
function setVisibleItemLimit(count: number) {
|
||||
const clamped = Math.max(1, Math.min(count, maxVisibleItems.value))
|
||||
if (clamped >= maxVisibleItems.value) {
|
||||
visibleItemLimit.value = MAX_JUMP_BACK_IN
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} else {
|
||||
visibleItemLimit.value = clamped
|
||||
localStorage.setItem(STORAGE_KEY, String(clamped))
|
||||
}
|
||||
}
|
||||
|
||||
function adjustVisibleItemLimit(delta: number) {
|
||||
const target = visibleItemCount.value + delta
|
||||
if (target < 1 || target > maxVisibleItems.value) {
|
||||
flashOverdrag()
|
||||
}
|
||||
setVisibleItemLimit(target)
|
||||
}
|
||||
|
||||
let dragStartY = 0
|
||||
let dragStartCount = 0
|
||||
let wasOverdragging = false
|
||||
let overdragTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function clearOverdragFlash() {
|
||||
showOverdrag.value = false
|
||||
if (overdragTimeout) {
|
||||
clearTimeout(overdragTimeout)
|
||||
overdragTimeout = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function flashOverdrag() {
|
||||
showOverdrag.value = true
|
||||
if (overdragTimeout) clearTimeout(overdragTimeout)
|
||||
overdragTimeout = setTimeout(() => {
|
||||
showOverdrag.value = false
|
||||
overdragTimeout = undefined
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function onResizePointerDown(event: PointerEvent) {
|
||||
if (!canResize.value) return
|
||||
event.preventDefault()
|
||||
resizing.value = true
|
||||
wasOverdragging = false
|
||||
clearOverdragFlash()
|
||||
dragStartY = event.clientY
|
||||
dragStartCount = visibleItemCount.value
|
||||
document.body.classList.add('recent-worlds-resizing')
|
||||
const handle = event.currentTarget as HTMLElement
|
||||
handle.setPointerCapture(event.pointerId)
|
||||
}
|
||||
|
||||
function onResizePointerMove(event: PointerEvent) {
|
||||
if (!resizing.value) return
|
||||
const target = dragStartCount + Math.round((event.clientY - dragStartY) / ITEM_DRAG_DISTANCE)
|
||||
const isOverdragging = target < 1 || target > maxVisibleItems.value
|
||||
if (isOverdragging && !wasOverdragging) {
|
||||
flashOverdrag()
|
||||
}
|
||||
wasOverdragging = isOverdragging
|
||||
setVisibleItemLimit(target)
|
||||
}
|
||||
|
||||
function endResize(event?: PointerEvent) {
|
||||
if (!resizing.value) return
|
||||
resizing.value = false
|
||||
wasOverdragging = false
|
||||
clearOverdragFlash()
|
||||
document.body.classList.remove('recent-worlds-resizing')
|
||||
const target = event?.currentTarget as HTMLElement | undefined
|
||||
if (event && target?.hasPointerCapture(event.pointerId)) {
|
||||
target.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
}
|
||||
|
||||
// Track populate calls on Linux to prevent server ping spam
|
||||
const isLinux = platform() === 'linux'
|
||||
@@ -76,7 +177,7 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||
|
||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||
|
||||
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
||||
const showWorlds = computed(() => appSettings.getFeatureFlag('worlds_in_home'))
|
||||
|
||||
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
@@ -276,100 +377,179 @@ onMounted(() => {
|
||||
checkProcesses()
|
||||
linuxPopulateCount.value = 0
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.classList.remove('recent-worlds-resizing')
|
||||
clearOverdragFlash()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="flex flex-col gap-2">
|
||||
<span
|
||||
class="flex mt-1 mb-3 leading-none items-center gap-1 text-2xl font-semibold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.jumpIn) }}
|
||||
</span>
|
||||
<div class="text-center py-4">
|
||||
<Accordion
|
||||
v-if="loading || jumpBackInItems.length > 0"
|
||||
open-by-default
|
||||
button-class="group mt-1 mb-3 flex w-fit cursor-pointer items-center border-0 bg-transparent p-0 text-left"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-1 text-2xl font-semibold leading-none text-contrast mr-1">
|
||||
{{ formatMessage(messages.jumpIn) }}
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="loading" class="text-center py-4">
|
||||
<LoaderCircleIcon class="mx-auto size-8 animate-spin text-contrast" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<span
|
||||
class="flex mt-1 mb-3 leading-none items-center gap-1 text-2xl font-semibold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.jumpIn) }}
|
||||
</span>
|
||||
<div class="grid-when-huge flex flex-col w-full gap-3">
|
||||
<template
|
||||
v-for="item in jumpBackInItems"
|
||||
:key="`${item.instance.id}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
>
|
||||
<WorldItem
|
||||
v-if="item.type === 'world'"
|
||||
:world="item.world"
|
||||
:playing-instance="runningInstances.includes(item.instance.id)"
|
||||
:playing-world="
|
||||
currentInstance === item.instance.id && currentWorld === getWorldIdentifier(item.world)
|
||||
"
|
||||
:refreshing="
|
||||
item.world.type === 'server'
|
||||
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
||||
: undefined
|
||||
"
|
||||
:supports-server-quick-play="
|
||||
item.world.type === 'server' &&
|
||||
hasServerQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||
"
|
||||
:supports-world-quick-play="
|
||||
item.world.type === 'singleplayer' &&
|
||||
hasWorldQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||
"
|
||||
:quarantined="item.instance.quarantined"
|
||||
:server-status="
|
||||
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
||||
"
|
||||
:rendered-motd="
|
||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||
"
|
||||
:current-protocol="protocolVersions[item.instance.id]"
|
||||
:game-mode="
|
||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||
"
|
||||
:instance-id="item.instance.id"
|
||||
:instance-name="item.instance.name"
|
||||
:instance-icon="item.instance.icon_path"
|
||||
@refresh="
|
||||
() =>
|
||||
<div v-else class="grid-when-huge relative flex w-full flex-col gap-3">
|
||||
<TransitionGroup name="jump-back-in-item">
|
||||
<div
|
||||
v-for="item in visibleJumpBackInItems"
|
||||
:key="`${item.instance.id}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
class="jump-back-in-item min-w-0"
|
||||
>
|
||||
<WorldItem
|
||||
v-if="item.type === 'world'"
|
||||
:world="item.world"
|
||||
:playing-instance="runningInstances.includes(item.instance.id)"
|
||||
:playing-world="
|
||||
currentInstance === item.instance.id &&
|
||||
currentWorld === getWorldIdentifier(item.world)
|
||||
"
|
||||
:refreshing="
|
||||
item.world.type === 'server'
|
||||
? refreshServer(item.world.address, item.instance.id)
|
||||
: {}
|
||||
"
|
||||
@update="() => populateJumpBackIn()"
|
||||
@play="
|
||||
() => {
|
||||
currentInstance = item.instance.id
|
||||
currentWorld = getWorldIdentifier(item.world)
|
||||
joinWorld(item.world, item.instance)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
() => {
|
||||
currentInstance = item.instance.id
|
||||
playInstance(item.instance)
|
||||
}
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.id)"
|
||||
/>
|
||||
<InstanceItem
|
||||
v-else
|
||||
:instance="item.instance"
|
||||
:last_played="item.sort_time"
|
||||
:newly-added="item.newly_added"
|
||||
@play="() => markInstancePlayed(item)"
|
||||
/>
|
||||
</template>
|
||||
? serverData[item.world.address].refreshing &&
|
||||
!serverData[item.world.address].status
|
||||
: undefined
|
||||
"
|
||||
:supports-server-quick-play="
|
||||
item.world.type === 'server' &&
|
||||
hasServerQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||
"
|
||||
:supports-world-quick-play="
|
||||
item.world.type === 'singleplayer' &&
|
||||
hasWorldQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||
"
|
||||
:quarantined="item.instance.quarantined"
|
||||
:server-status="
|
||||
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
||||
"
|
||||
:rendered-motd="
|
||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||
"
|
||||
:current-protocol="protocolVersions[item.instance.id]"
|
||||
:game-mode="
|
||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||
"
|
||||
:instance-id="item.instance.id"
|
||||
:instance-name="item.instance.name"
|
||||
:instance-icon="item.instance.icon_path"
|
||||
@refresh="
|
||||
() =>
|
||||
item.world.type === 'server'
|
||||
? refreshServer(item.world.address, item.instance.id)
|
||||
: {}
|
||||
"
|
||||
@update="() => populateJumpBackIn()"
|
||||
@play="
|
||||
() => {
|
||||
currentInstance = item.instance.id
|
||||
currentWorld = getWorldIdentifier(item.world)
|
||||
joinWorld(item.world, item.instance)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
() => {
|
||||
currentInstance = item.instance.id
|
||||
playInstance(item.instance)
|
||||
}
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.id)"
|
||||
/>
|
||||
<InstanceItem
|
||||
v-else
|
||||
:instance="item.instance"
|
||||
:last_played="item.sort_time"
|
||||
:newly-added="item.newly_added"
|
||||
@play="() => markInstancePlayed(item)"
|
||||
/>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-if="canResize"
|
||||
v-tooltip="resizing ? null : formatMessage(messages.resize)"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
aria-orientation="horizontal"
|
||||
:aria-label="formatMessage(messages.resize)"
|
||||
:aria-valuemin="1"
|
||||
:aria-valuemax="maxVisibleItems"
|
||||
:aria-valuenow="visibleItemCount"
|
||||
class="group/resize relative inset-x-0 bottom-0 flex h-6 -mt-3 w-1/3 mx-auto cursor-ns-resize touch-none select-none items-center justify-center"
|
||||
@pointerdown="onResizePointerDown"
|
||||
@pointermove="onResizePointerMove"
|
||||
@pointerup="endResize"
|
||||
@pointercancel="endResize"
|
||||
@keydown.up.prevent="adjustVisibleItemLimit(-1)"
|
||||
@keydown.down.prevent="adjustVisibleItemLimit(1)"
|
||||
@keydown.home.prevent="setVisibleItemLimit(1)"
|
||||
@keydown.end.prevent="setVisibleItemLimit(maxVisibleItems)"
|
||||
>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span
|
||||
class="h-px w-6 transition-colors"
|
||||
:class="showOverdrag ? 'bg-red' : 'bg-secondary'"
|
||||
/>
|
||||
<span
|
||||
class="h-px w-6 transition-colors"
|
||||
:class="showOverdrag ? 'bg-red' : 'bg-secondary'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.grid-when-huge {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
||||
}
|
||||
|
||||
.jump-back-in-item {
|
||||
height: 5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.jump-back-in-item-enter-active,
|
||||
.jump-back-in-item-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s ease,
|
||||
height 0.25s ease;
|
||||
}
|
||||
|
||||
.jump-back-in-item-enter-from,
|
||||
.jump-back-in-item-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.jump-back-in-item-enter-active,
|
||||
.jump-back-in-item-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.jump-back-in-item-enter-from,
|
||||
.jump-back-in-item-leave-to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
height: 5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
body.recent-worlds-resizing,
|
||||
body.recent-worlds-resizing * {
|
||||
cursor: ns-resize !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MoreVerticalIcon,
|
||||
NoSignalIcon,
|
||||
PlayIcon,
|
||||
SignalIcon,
|
||||
SkullIcon,
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
@@ -25,15 +26,17 @@ import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
ServerOnlinePlayers,
|
||||
SmartClickable,
|
||||
TagItem,
|
||||
TeleportOverflowMenu,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { getPingLevel } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -53,6 +56,7 @@ import { LockIcon } from '../../../../../../packages/assets/generated-icons'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatNumber = useFormatNumber()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
@@ -121,6 +125,9 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const playingOtherWorld = computed(() => props.playingInstance && !props.playingWorld)
|
||||
const hasPlayersTooltip = computed(
|
||||
() => !!props.serverStatus?.players?.sample && props.serverStatus.players.sample.length > 0,
|
||||
)
|
||||
const serverIncompatible = computed(
|
||||
() =>
|
||||
!!props.serverStatus &&
|
||||
@@ -238,6 +245,10 @@ const messages = defineMessages({
|
||||
id: 'app.world.world-item.incompatible-version',
|
||||
defaultMessage: 'Incompatible version {version}',
|
||||
},
|
||||
playersOnline: {
|
||||
id: 'app.world.world-item.players-online',
|
||||
defaultMessage: '{count} online',
|
||||
},
|
||||
offline: {
|
||||
id: 'app.world.world-item.offline',
|
||||
defaultMessage: 'Offline',
|
||||
@@ -315,13 +326,34 @@ const messages = defineMessages({
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<ServerOnlinePlayers
|
||||
:online="serverStatus.players?.online ?? 0"
|
||||
status-online
|
||||
hide-label
|
||||
<template v-else>
|
||||
<SignalIcon
|
||||
v-tooltip="`${serverStatus.ping}ms`"
|
||||
aria-hidden="true"
|
||||
:style="`--_signal-${getPingLevel(serverStatus.ping ?? 0)}: var(--color-green)`"
|
||||
stroke-width="3px"
|
||||
class="shrink-0 smart-clickable:allow-pointer-events"
|
||||
/>
|
||||
</div>
|
||||
<Tooltip :disabled="!hasPlayersTooltip">
|
||||
<span
|
||||
class="smart-clickable:allow-pointer-events"
|
||||
:class="{ 'cursor-help': hasPlayersTooltip }"
|
||||
>
|
||||
{{
|
||||
formatMessage(messages.playersOnline, {
|
||||
count: formatNumber(serverStatus.players?.online ?? 0),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<template #popper>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span v-for="player in serverStatus.players?.sample" :key="player.id">
|
||||
{{ player.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import {
|
||||
install_job_dismiss,
|
||||
install_job_list,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
} 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({
|
||||
installs: {
|
||||
@@ -236,7 +236,7 @@ export async function useInstallJobNotifications(opts: {
|
||||
}) {
|
||||
const appEvents = injectAppEvents()
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const jobs = ref<InstallJobSnapshot[]>([])
|
||||
const iconUrls = ref<Record<string, string | null>>({})
|
||||
const instanceNames = ref<Record<string, string>>({})
|
||||
@@ -421,7 +421,7 @@ export async function useInstallJobNotifications(opts: {
|
||||
}
|
||||
|
||||
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
|
||||
return isTerminalJob(job) || themeStore.getFeatureFlag('always_show_copy_details')
|
||||
return isTerminalJob(job) || appSettings.getFeatureFlag('always_show_copy_details')
|
||||
}
|
||||
|
||||
function isCopied(job: InstallJobSnapshot): boolean {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = {
|
||||
project_background: false,
|
||||
page_path: false,
|
||||
worlds_in_home: true,
|
||||
server_project_qa: false,
|
||||
show_version_environment_column: false,
|
||||
server_ram_as_bytes_always_on: false,
|
||||
always_show_app_controls: false,
|
||||
skip_non_essential_warnings: false,
|
||||
skip_unknown_pack_warning: false,
|
||||
pride_fundraiser: true,
|
||||
i18n_debug: false,
|
||||
show_instance_play_time: true,
|
||||
compact_instance_cards: false,
|
||||
advanced_filters_collapsed: true,
|
||||
always_show_copy_details: false,
|
||||
hide_installed_modpacks: false,
|
||||
friends_active_collapsed: false,
|
||||
friends_online_collapsed: false,
|
||||
friends_offline_collapsed: true,
|
||||
friends_pending_collapsed: true,
|
||||
dismissed_photosensitivity_filter_warning: false,
|
||||
}
|
||||
|
||||
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
|
||||
type FeatureFlags = Record<FeatureFlag, boolean>
|
||||
|
||||
const syncBehaviorAcrossDevices = ref(false)
|
||||
const featureFlags = reactive<FeatureFlags>({ ...DEFAULT_FEATURE_FLAGS })
|
||||
|
||||
function setBehaviorSyncAcrossDevices(enabled: boolean): void {
|
||||
syncBehaviorAcrossDevices.value = enabled
|
||||
}
|
||||
|
||||
function getFeatureFlag(key: FeatureFlag): boolean {
|
||||
return featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
|
||||
}
|
||||
|
||||
const appSettings = reactive({
|
||||
syncBehaviorAcrossDevices,
|
||||
hideNametagSkinsPage: false,
|
||||
toggleSidebar: false,
|
||||
devMode: false,
|
||||
featureFlags,
|
||||
setBehaviorSyncAcrossDevices,
|
||||
getFeatureFlag,
|
||||
})
|
||||
|
||||
export function useAppSettings() {
|
||||
return appSettings
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import { findMinecraftAuthError } from '@/components/ui/minecraft-auth-error-modal/minecraft-auth-errors'
|
||||
|
||||
const errorState = reactive({
|
||||
errorModal: null,
|
||||
minecraftAuthErrorModal: null,
|
||||
minecraftRequiredModal: null,
|
||||
setErrorModal(ref) {
|
||||
this.errorModal = ref
|
||||
},
|
||||
setMinecraftAuthErrorModal(ref) {
|
||||
this.minecraftAuthErrorModal = ref
|
||||
},
|
||||
setMinecraftRequiredModal(ref) {
|
||||
this.minecraftRequiredModal = ref
|
||||
},
|
||||
showError(error, context, closable = true, source = null) {
|
||||
const errorMessage = error.message?.toLowerCase()
|
||||
if (
|
||||
(errorMessage?.includes('user is not logged in') ||
|
||||
errorMessage?.includes('cannot play instance since minecraft is required')) &&
|
||||
this.minecraftRequiredModal
|
||||
) {
|
||||
this.minecraftRequiredModal.show()
|
||||
return
|
||||
}
|
||||
if (
|
||||
error.message &&
|
||||
(error.message.includes('Minecraft authentication error:') ||
|
||||
findMinecraftAuthError(error.message)) &&
|
||||
this.minecraftAuthErrorModal
|
||||
) {
|
||||
this.minecraftAuthErrorModal.show(error)
|
||||
return
|
||||
}
|
||||
this.errorModal.show(error, context, closable, source)
|
||||
},
|
||||
})
|
||||
|
||||
export function useError() {
|
||||
return errorState
|
||||
}
|
||||
|
||||
export const handleSevereError = (error, context) => {
|
||||
useError().showError(error, context)
|
||||
console.error(error)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
|
||||
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'retro', 'system'] as const
|
||||
|
||||
export type ColorTheme = (typeof THEME_OPTIONS)[number]
|
||||
type Theme = Exclude<ColorTheme, 'system'>
|
||||
|
||||
const preferred = ref<ColorTheme>('dark')
|
||||
const preview = ref<ColorTheme | null>(null)
|
||||
const advancedRendering = ref(true)
|
||||
const syncAcrossDevices = ref(false)
|
||||
const nativeThemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const native = ref<Theme>(nativeThemeQuery.matches ? 'dark' : 'light')
|
||||
const active = computed<Theme>(() => {
|
||||
const selectedTheme = preview.value ?? preferred.value
|
||||
return selectedTheme === 'system' ? native.value : selectedTheme
|
||||
})
|
||||
|
||||
nativeThemeQuery.addEventListener('change', (event) => {
|
||||
native.value = event.matches ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
watch(
|
||||
active,
|
||||
(theme) => {
|
||||
const html = document.documentElement
|
||||
for (const option of THEME_OPTIONS) {
|
||||
html.classList.remove(`${option}-mode`)
|
||||
}
|
||||
html.classList.add(`${theme}-mode`)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const theme = reactive({
|
||||
preferred,
|
||||
preview,
|
||||
active,
|
||||
native,
|
||||
syncAcrossDevices,
|
||||
advancedRendering,
|
||||
options: THEME_OPTIONS,
|
||||
})
|
||||
|
||||
export function useTheme() {
|
||||
return theme
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import type { FeatureFlag } from '@/composables/use-app-settings.ts'
|
||||
import type { ColorTheme } from '@/composables/use-theme.ts'
|
||||
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
@@ -43,6 +44,8 @@ export type AppSettings = {
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
toggle_sidebar: boolean
|
||||
sync_theme_across_devices: boolean
|
||||
sync_behavior_across_devices: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
|
||||
+2
@@ -213,6 +213,8 @@ type AppSettings = {
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
worlds_in_home: boolean
|
||||
sync_theme_across_devices: boolean
|
||||
sync_behavior_across_devices: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { UserPreferencesContext } from '@modrinth/ui'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
type UserPreferences = NonNullable<UserPreferencesContext['preferences']['value']>
|
||||
type PartialUserPreferences = Parameters<UserPreferencesContext['updatePreferences']>[0]
|
||||
|
||||
export async function get_user_preferences(userId: string): Promise<UserPreferences> {
|
||||
return await invoke<UserPreferences>('plugin:users|get_user_preferences', { userId })
|
||||
}
|
||||
|
||||
export async function patch_user_preferences(
|
||||
userId: string,
|
||||
preferences: PartialUserPreferences,
|
||||
): Promise<UserPreferences> {
|
||||
return await invoke<UserPreferences>('plugin:users|patch_user_preferences', {
|
||||
userId,
|
||||
preferences,
|
||||
})
|
||||
}
|
||||
@@ -25,8 +25,8 @@ export async function get_user_profile(userId: string): Promise<Labrinth.Users.v
|
||||
return await invoke<Labrinth.Users.v3.User>('plugin:users|get_user_profile', { userId })
|
||||
}
|
||||
|
||||
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return await invoke<Labrinth.Projects.v2.Project[]>('plugin:users|get_user_projects', {
|
||||
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v3.Project[]> {
|
||||
return await invoke<Labrinth.Projects.v3.Project[]>('plugin:users|get_user_projects', {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -134,12 +134,6 @@
|
||||
"app.ads-consent.reject": {
|
||||
"message": "رفض الكل"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "العرض المتقدم"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "لون السمة"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "صفحة الرئيسية"
|
||||
},
|
||||
@@ -161,12 +155,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "تصغير نافذة المشغل"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "استخدام إطار نافذة النظام (يتطلب إعادة تشغيل التطبيق)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "الزخارف التقليدية"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
|
||||
},
|
||||
|
||||
@@ -74,12 +74,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Vaše soukromí a způsob, jakým reklamy podporují Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Pokročilé vykreslování"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Barevný motiv"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Domů"
|
||||
},
|
||||
@@ -101,12 +95,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimalizovat launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Systémové dekorace"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
|
||||
},
|
||||
|
||||
@@ -137,12 +137,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Dit privatliv og hvordan reklamer hjælper Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Avanceret rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farvetema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Hjem"
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Deine Privatsphäre und wie Werbung Modrinth unterstützt"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Visuelle Effekte wie Hintergrundunschärfe aktivieren. Ohne Hardwarebeschleunigung kann dies die Leistung beeinträchtigen."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Erweitertes Rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wähle das Farbschema der Modrinth App aus."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farbschema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Start"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "App minimieren"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Nutzt die Titelleiste und Fenstersteuerung deines Betriebssystems. Erfordert einen App-Neustart."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "System-Fensterrahmen"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Zeigt, wie lange du jede einzelne Instanz gespielt hast."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Deine Privatsphäre und wie Werbung Modrinth unterstützt"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Visuelle Effekte wie Hintergrundunschärfe aktivieren. Ohne Hardwarebeschleunigung kann dies die Leistung beeinträchtigen."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Erweitertes Rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wähle das Farbschema der Modrinth App aus."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farbschema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Start"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "App minimieren"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Nutzt die Titelleiste und Fenstersteuerung deines Betriebssystems. Erfordert einen App-Neustart."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "System-Fensterrahmen"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Zeigt, wie lange du jede einzelne Instanz gespielt hast."
|
||||
},
|
||||
|
||||
@@ -149,17 +149,11 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Your privacy and how ads support Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Enable visual effects such as background blur. This may reduce performance without hardware acceleration."
|
||||
"app.appearance-settings.compact-mode.description": {
|
||||
"message": "Display library instances in a compact row layout."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Advanced rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Choose the color theme used by Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Color theme"
|
||||
"app.appearance-settings.compact-mode.title": {
|
||||
"message": "Compact mode"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Home"
|
||||
@@ -185,12 +179,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimize app"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Use your operating system's title bar and window controls. Requires an app restart."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "System window frame"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Show how long you've played each instance."
|
||||
},
|
||||
@@ -230,6 +218,15 @@
|
||||
"app.behavior-settings.startup-and-navigation.title": {
|
||||
"message": "Startup and navigation"
|
||||
},
|
||||
"app.behavior-settings.sync-across-devices.description": {
|
||||
"message": "Use these behavior settings everywhere you're signed in. Turn this off to keep separate settings on this device."
|
||||
},
|
||||
"app.behavior-settings.sync-across-devices.signed-out-tooltip": {
|
||||
"message": "Sign into a Modrinth account to sync settings."
|
||||
},
|
||||
"app.behavior-settings.sync-across-devices.title": {
|
||||
"message": "Sync behavior across devices"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Adding server to instance"
|
||||
},
|
||||
@@ -332,6 +329,9 @@
|
||||
"app.home.jump-back-in.new-instance": {
|
||||
"message": "New instance"
|
||||
},
|
||||
"app.home.jump-back-in.resize": {
|
||||
"message": "Drag to resize"
|
||||
},
|
||||
"app.home.jump-back-in.title": {
|
||||
"message": "Jump in"
|
||||
},
|
||||
@@ -1082,6 +1082,9 @@
|
||||
"app.library.sort.date-modified": {
|
||||
"message": "Date modified"
|
||||
},
|
||||
"app.library.sort.game-version": {
|
||||
"message": "Game version"
|
||||
},
|
||||
"app.library.sort.hours-played": {
|
||||
"message": "Hours played"
|
||||
},
|
||||
@@ -1091,6 +1094,9 @@
|
||||
"app.library.sort.last-played": {
|
||||
"message": "Last played"
|
||||
},
|
||||
"app.library.sort.loader": {
|
||||
"message": "Loader"
|
||||
},
|
||||
"app.library.sort.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
@@ -1742,6 +1748,9 @@
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"content.shared-instance.change-version-body": {
|
||||
"message": "Changing the version only changes your local copy. Future shared instance updates may restore or change it again."
|
||||
},
|
||||
@@ -2069,6 +2078,12 @@
|
||||
"instance.icon-editor.symbol.engine": {
|
||||
"message": "Engine"
|
||||
},
|
||||
"instance.icon-editor.symbol.fabric": {
|
||||
"message": "Fabric"
|
||||
},
|
||||
"instance.icon-editor.symbol.forge": {
|
||||
"message": "Forge"
|
||||
},
|
||||
"instance.icon-editor.symbol.furnace": {
|
||||
"message": "Furnace"
|
||||
},
|
||||
@@ -2090,6 +2105,9 @@
|
||||
"instance.icon-editor.symbol.mr-pack": {
|
||||
"message": "Mr Pack"
|
||||
},
|
||||
"instance.icon-editor.symbol.neoforge": {
|
||||
"message": "NeoForge"
|
||||
},
|
||||
"instance.icon-editor.symbol.orb": {
|
||||
"message": "Orb"
|
||||
},
|
||||
@@ -2105,6 +2123,9 @@
|
||||
"instance.icon-editor.symbol.poke-ball": {
|
||||
"message": "Poke Ball"
|
||||
},
|
||||
"instance.icon-editor.symbol.quilt": {
|
||||
"message": "Quilt"
|
||||
},
|
||||
"instance.icon-editor.symbol.redstone-block": {
|
||||
"message": "Redstone Block"
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Tu privacidad y como los anuncios apoyan a Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Activar efectos visuales como los efectos de desenfoque. Esto puede reducir el rendimiento si no tienes aceleración de hardware."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderizado avanzado"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Escoge el esquema de colores usado por la aplicación de Modrinth."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Color de la interfaz"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Inicio"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar app"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa el borde de ventana de tu sistema. Necesita reiniciar la aplicación."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decoraciones nativas"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Muestra cuanto tiempo has jugado en cada instancia."
|
||||
},
|
||||
|
||||
@@ -95,12 +95,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Ver registros"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderizado avanzado"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema de color"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Inicio"
|
||||
},
|
||||
@@ -122,12 +116,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar lanzador"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa el marco de ventana del sistema (requiere reiniciar la aplicación)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decoraciones nativas"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
|
||||
},
|
||||
|
||||
@@ -71,12 +71,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Katso lokeja"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Edistynyt renderointi"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Väriteema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Koti"
|
||||
},
|
||||
@@ -98,12 +92,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Piilota käynnistysohjelma"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Käytä järjestelmän ikkuna kehystä (Vaatii sovelluksen uudelleen käynnistämisen)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Natiivit koristukset"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
|
||||
},
|
||||
|
||||
@@ -47,12 +47,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Tignan ang logs"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Masalimuot na pagrerender"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema ng kulay"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Tahanan"
|
||||
},
|
||||
@@ -74,12 +68,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Liitin ang launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Gamitin ang system window frame (kailangan i-restart ang app)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Native decorations"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Votre vie privée et comment les publicités soutiennent Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Activer les effets visuels tel que le flou d'arrière-plan. Ceci peut réduire les performances sans accélération matérielle."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Rendu avancé"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Choisir la couleur de thème utilisée par Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Couleur du thème"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Accueil"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimiser l'application"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Utilise la barre de titre et les contrôles de fenêtre de votre système d'exploitation. Nécessite le redémarrage de l'application."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Cadre de fenêtre système"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Montre combien de temps vous avez joué à chaque instance."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Adataid védelme és a hirdetések szerepe a Modrinth működésében"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Engedélyezi a vizuális effektusokat, például a háttér elmosását. Hardveres gyorsítás nélkül ez ronthatja a rendszer teljesítményét."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Haladó megjelenítési beállítások"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Válaszd ki a Modrinth App által használt színsémát."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Téma"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Kezdőlap"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Alkalmazás bezárása"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Az operációs rendszer címsorának és ablakvezérlőinek használata. A módosítás érvénybe lépéséhez újra kell indítani az alkalmazást."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Rendszerablakkeret"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Megjeleníti, hogy mennyi időt töltöttél egy példányban."
|
||||
},
|
||||
|
||||
@@ -44,12 +44,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Lihat catatan"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderasi tingkat lanjut"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema warna"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Beranda"
|
||||
},
|
||||
@@ -71,12 +65,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Kecilkan peluncur"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Gunakan bingkai jendela sistem (wajib menjalankan ulang aplikasi)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Hiasan asli"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "La tua privacy e come le pubblicità supportano Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Abilita effetti come lo sfondo sfocato. Questo potrebbe ridurre le prestazioni senza accelerazione hardware."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Rendering avanzato"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Scegli il tema usato da Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Home"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizza app"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa la barra e i controlli delle finestre di sistema. Richiede un riavvio."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Usa cornice di sistema"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Mostra quanto tempo hai giocato a un'istanza."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "プライバシーと広告がModrinthを支える仕組み"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "背景のぼかしなどの視覚効果を有効にします。ハードウェアアクセラレーションが無効の場合、パフォーマンスが低下する可能性があります。"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "高度なレンダリング"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Modrinth Appのテーマを選びましょう。"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "テーマ"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "ホーム"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "ランチャーを最小化"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "OS標準のタイトルバーとウィンドウ操作ボタンを使用します。反映にはアプリの再起動が必要です。"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "システムウィンドウフレーム"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "各インスタンスのプレイ時間を表示します。"
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "개인정보와 광고가 Modrinth를 지원하는 방식"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "배경 흐림 효과와 같은 시각 효과를 활성화합니다. 하드웨어 가속이 지원되지 않는 경우 성능이 저하될 수 있습니다."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "고급 렌더링"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Modrinth 앱에서 사용할 색상 테마를 선택하세요."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "색상 테마"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "홈"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "앱 최소화"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "운영 체제의 제목 표시줄 및 창 제어 기능을 사용합니다. 앱 재시작이 필요합니다."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "시스템 창 프레임"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "각 인스턴스를 얼마나 플레이했는지 표시합니다."
|
||||
},
|
||||
|
||||
@@ -53,12 +53,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Lihat log"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Pemaparan lanjutan"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema warna"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Laman utama"
|
||||
},
|
||||
@@ -80,12 +74,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimumkan pelancar"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Gunakan bingkai tetingkap sistem (aplikasi perlu dimulakan semula)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Hiasan asli"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Melangkau pengesahan berisiko rendah secara automatik seperti pemasangan pek mod pendua, pemadaman kandungan biasa, kemas kini pukal, menyahpautkan pek mod dan gesaan pembaikan. Amaran berbahaya masih akan ditunjukkan."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Je privacy en hoe advertenties Modrinth ondersteunen"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Schakel visuele effecten zoals achtergrondvervaging in. Zonder hardwareversnelling kan dit ten koste gaan van de prestaties."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Geavanceerde weergave"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Kies het kleurenthema dat door de Modrinth-app wordt gebruikt."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Kleurenthema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Start"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "App minimaliseren"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Gebruik de titelbalk en vensterbedieningselementen van je besturingssysteem. Hiervoor moet de app opnieuw worden opgestart."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Systeemvensterframe"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Laat zien hoe lang je elke instantie hebt gespeeld."
|
||||
},
|
||||
|
||||
@@ -56,12 +56,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Vis logger"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Avansert rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Fargetema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Hjem"
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Twoja prywatność i jak reklamy wspierają Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Włącz efekty wizualne, takie jak rozmycie tła. Może to negatywnie wpłynąć na wydajność, jeżeli akceleracja sprzętowa jest wyłączona."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Zaawansowane renderowanie"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wybierz motyw użyty przez Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Motyw"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Strona główna"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimalizuj aplikację"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Używaj pasek tytułu i kontrolki okna systemu operacyjnego. Wymaga ponownego uruchomienia aplikacji."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Systemowa ramka okna"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Pokazuje czas gry spędzony w danej instancji."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Sua privacidade e como anúncios apoiam o Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Ativa os efeitos visuais como o desfoque de fundo. Isso talvez reduza o desempenho sem o aceleramento de hardware."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderização avançada"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Escolha o tema usado pelo Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Início"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar aplicativo"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa a barra de título e os controles de janela do seu sistema operacional. Exige a reinicialização do aplicativo."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Moldura de janela do sistema"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Exibe o tempo de jogo de cada instância."
|
||||
},
|
||||
|
||||
@@ -140,12 +140,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "A sua privacidade e como os anúncios são financiados pela Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderização avançada"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Cor de tema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Casa"
|
||||
},
|
||||
@@ -167,12 +161,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Utilizar a moldura da janela do sistema (é necessário reiniciar a aplicação)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decorações nativas"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Apresenta quanto tempo passou a jogar uma instância."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Конфиденциальность и реклама на Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Использовать эффекты вроде размытия фона. Без аппаратного ускорения возможна потеря производительности."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Расширенные эффекты"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Выберите тему для Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Тема"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Главная"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Сворачивание приложения"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Использовать системное оформление заголовка и кнопок окна. Требуется перезапуск."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Системная рамка окна"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Показывать проведённое время в сборке."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Tvoja bezbednost i kako reklame pomažu Modrinth-u"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Uključi vizuelne efekte kao što je blur pozadine. Ovo može da pogorša performanse bez hardverskog ubrzanja."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Napredno renderovanje"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Izaberi boju teme koju koristi Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema boje"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Početna stranica"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Smanji aplikaciju"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Koristi naslovnu traku i kontrole prozora tvog operativnog sistema. Restart aplikacije obavezan."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Sistemski okvir prozora"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Prikaži koliko dugo ste igrali svaku instancu."
|
||||
},
|
||||
|
||||
@@ -140,15 +140,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Din integritet och hur reklam stödjer Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Avancerad rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Välj färgtemat som används av Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Färgtema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Hem"
|
||||
},
|
||||
@@ -170,12 +161,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimera app"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Använd systemets fönsterram (omstart av app krävs)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Inbyggda fönsterdekorationer"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Visa hur länge du spelat varje instans."
|
||||
},
|
||||
|
||||
@@ -59,12 +59,6 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "ดู Log"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "การเรนเดอร์ขั้นสูง"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "ธีมสี"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "หน้าหลัก"
|
||||
},
|
||||
@@ -86,12 +80,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "ย่อลันเชอร์"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "ใช้กรอบหน้าต่างระบบ (ต้องรีสตาร์ทแอป)"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "ของตกแต่งดั้งเดิม"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์"
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Sizin mahremiyetiniz ve reklamlar Modrinth'i nasıl destekler"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Arka plan bulanıklığı gibi görsel efektleri etkinleştirin. Bu, donanım ivmelendirme yokken performansı etkileyebilir."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Gelişmiş işleme"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Modrinth App tarafından kullanılacak renk şemasını seçin."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Renk teması"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Ana Sayfa"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Uygulamayı küçült"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "İşletim sisteminizin başlık çubuğu ve pencere kontrollerini kullanın. Uygulamanın yeniden başlatılmasını gerektirir."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Sistem pencere çerçevesi"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Her bir örneği ne kadar süreyle oynadığını göster."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Ваша приватність та як реклама підтримує Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Умикає візуальні ефекти, як-от розмиття фону. Це може знизити продуктивність без апаратного прискорення."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Розширене промальовування"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Оберіть тему для Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Колірна тема"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Головна"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Згортати застосунок"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Використовувати заголовок вікна та елементи керування вашої операційної системи. Потребує перезапуску застосунку."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Системна рамка вікна"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Показувати награний час для кожної збірки."
|
||||
},
|
||||
|
||||
@@ -134,12 +134,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "Quyền riêng tư của bạn và cách quảng cáo hỗ trợ Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Dựng hình nâng cao"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Màu chủ đề"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Trang chủ"
|
||||
},
|
||||
@@ -161,12 +155,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Thu nhỏ launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Sử dụng khung cửa sổ hệ thống (yêu cầu khởi động lại ứng dụng)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Giao diện cửa sổ hệ thống"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Hiển thị thời gian bạn đã dành để chơi bằng một cấu hình."
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "您的隐私以及广告如何支持 Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "启用模糊效果等高级渲染。可能会在没有硬件加速渲染的情况下造成性能问题。"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "高级渲染"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "为 Modrinth App 选择偏好的色彩主题。"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "色彩主题"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "首页"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "最小化 App"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "使用操作系统的标题栏和窗口控件。需要重启应用。"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "系统窗口框架"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "显示你在每个实例中游玩的时间。"
|
||||
},
|
||||
|
||||
@@ -143,18 +143,6 @@
|
||||
"app.ads-consent.title": {
|
||||
"message": "關於你的隱私,以及廣告如何幫助 Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "啟用視覺效果(如背景模糊),若無硬體加速可能會降低效能。"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "進階繪製"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "選擇 Modrinth App 使用的色彩主題。"
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "色彩主題"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "首頁"
|
||||
},
|
||||
@@ -179,12 +167,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "最小化應用程式"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "使用作業系統的標題列和視窗控制項。需要重新啟動應用程式。"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "系統視窗外框"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "顯示你在各個實例的遊玩時數。"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as Sentry from '@sentry/vue'
|
||||
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
||||
import { VueQueryPlugin } from '@tanstack/vue-query'
|
||||
import FloatingVue from 'floating-vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from '@/App.vue'
|
||||
@@ -21,8 +20,6 @@ const vueScan = new VueScanPlugin({
|
||||
playSound: false, // Play sound on each render
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
let app = createApp(App)
|
||||
|
||||
Sentry.init({
|
||||
@@ -35,7 +32,6 @@ Sentry.init({
|
||||
app.use(VueQueryPlugin)
|
||||
app.use(vueScan)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(FloatingVue, {
|
||||
themes: {
|
||||
'ribbit-popout': {
|
||||
|
||||
@@ -34,13 +34,14 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { get_project, get_search_results_v3, get_version_many } from '@/helpers/cache.js'
|
||||
import {
|
||||
get_installed_project_ids as getInstalledProjectIds,
|
||||
@@ -63,7 +64,6 @@ import {
|
||||
createServerInstallContent,
|
||||
provideServerInstallContent,
|
||||
} from '@/providers/setup/server-install-content'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -109,7 +109,7 @@ const breadcrumbLabel = computed(() => {
|
||||
),
|
||||
})
|
||||
})
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const browseRouteActive = computed(() => route.path.startsWith('/browse/'))
|
||||
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
||||
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
|
||||
@@ -384,7 +384,7 @@ async function initInstanceContext() {
|
||||
}
|
||||
|
||||
function setBrowseHideInstalledFlag(flag: 'hide_installed_modpacks', value: boolean) {
|
||||
themeStore.featureFlags[flag] = value
|
||||
appSettings.featureFlags[flag] = value
|
||||
getSettings()
|
||||
.then((settings) => {
|
||||
settings.feature_flags[flag] = value
|
||||
@@ -394,7 +394,7 @@ function setBrowseHideInstalledFlag(flag: 'hide_installed_modpacks', value: bool
|
||||
}
|
||||
|
||||
const hideInstalledModpacks = computed({
|
||||
get: () => themeStore.getFeatureFlag('hide_installed_modpacks'),
|
||||
get: () => appSettings.getFeatureFlag('hide_installed_modpacks'),
|
||||
set: (value: boolean) => setBrowseHideInstalledFlag('hide_installed_modpacks', value),
|
||||
})
|
||||
|
||||
@@ -530,13 +530,20 @@ const {
|
||||
})
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
const handleOffline = () => {
|
||||
debugLog('went offline')
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
}
|
||||
const handleOnline = () => {
|
||||
debugLog('went online')
|
||||
offline.value = false
|
||||
}
|
||||
window.addEventListener('offline', handleOffline)
|
||||
window.addEventListener('online', handleOnline)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -742,7 +749,7 @@ const installContext = computed(() => {
|
||||
queuedCount: queuedServerInstallCount.value,
|
||||
selectedProjects: selectedServerInstallProjects.value,
|
||||
isInstallingSelected: isInstallingQueuedServerInstalls.value,
|
||||
skipNonEssentialWarnings: themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
skipNonEssentialWarnings: appSettings.getFeatureFlag('skip_non_essential_warnings'),
|
||||
installProgress: queuedInstallProgress.value,
|
||||
clearQueued: clearQueuedServerInstalls,
|
||||
clearSelected: clearQueuedServerInstalls,
|
||||
@@ -1222,9 +1229,9 @@ function getProjectBrowseQuery() {
|
||||
}
|
||||
|
||||
const advancedFiltersCollapsed = computed({
|
||||
get: () => themeStore.getFeatureFlag('advanced_filters_collapsed'),
|
||||
get: () => appSettings.getFeatureFlag('advanced_filters_collapsed'),
|
||||
set: (value) => {
|
||||
themeStore.featureFlags['advanced_filters_collapsed'] = value
|
||||
appSettings.featureFlags['advanced_filters_collapsed'] = value
|
||||
getSettings()
|
||||
.then((settings) => {
|
||||
settings.feature_flags['advanced_filters_collapsed'] = value
|
||||
@@ -1235,9 +1242,9 @@ const advancedFiltersCollapsed = computed({
|
||||
})
|
||||
|
||||
const dismissedPhotosensitivityFilterWarning = computed({
|
||||
get: () => themeStore.getFeatureFlag('dismissed_photosensitivity_filter_warning'),
|
||||
get: () => appSettings.getFeatureFlag('dismissed_photosensitivity_filter_warning'),
|
||||
set: (value) => {
|
||||
themeStore.featureFlags['dismissed_photosensitivity_filter_warning'] = value
|
||||
appSettings.featureFlags['dismissed_photosensitivity_filter_warning'] = value
|
||||
getSettings()
|
||||
.then((settings) => {
|
||||
settings.feature_flags['dismissed_photosensitivity_filter_warning'] = value
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { HomeIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { PlayIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, inject, onActivated, ref } from 'vue'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import LibrarySection from '@/components/ui/library/index.vue'
|
||||
import WelcomeScreen from '@/components/ui/WelcomeScreen.vue'
|
||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import { list } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { injectOnboardingChecklist } from '@/providers/onboarding-checklist'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
defineOptions({
|
||||
name: 'LibraryPage',
|
||||
@@ -25,7 +25,7 @@ const { formatMessage } = useVIntl()
|
||||
const { hasCreatedInstance, isReady } = injectOnboardingChecklist()
|
||||
const showCreationModal = inject<() => void>('showCreationModal')
|
||||
const pageOptions = ref<InstanceType<typeof ContextMenu>>()
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
|
||||
const messages = defineMessages({
|
||||
home: {
|
||||
@@ -43,7 +43,7 @@ const homeBreadcrumb = useRootBreadcrumb({
|
||||
id: 'home',
|
||||
label: formatMessage(messages.home),
|
||||
to: '/',
|
||||
visual: { type: 'icon', component: HomeIcon },
|
||||
visual: { type: 'icon', component: PlayIcon },
|
||||
})
|
||||
onActivated(homeBreadcrumb.reset)
|
||||
|
||||
@@ -102,11 +102,11 @@ function handlePageOption({ option }: { option: string }) {
|
||||
<div
|
||||
v-else-if="isReady"
|
||||
data-library-page-background
|
||||
class="flex flex-col gap-6 p-6"
|
||||
class="flex flex-col gap-3 p-6"
|
||||
@contextmenu="openPageContextMenu"
|
||||
>
|
||||
<RecentWorldsList
|
||||
v-if="recentInstances?.length > 0 && themeStore.getFeatureFlag('worlds_in_home')"
|
||||
v-if="recentInstances?.length > 0 && appSettings.getFeatureFlag('worlds_in_home')"
|
||||
:recent-instances="recentInstances"
|
||||
/>
|
||||
<LibrarySection :instances="instances" />
|
||||
|
||||
@@ -16,7 +16,7 @@ const client = injectModrinthClient()
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'servers',
|
||||
label: 'Servers',
|
||||
label: 'Hosting',
|
||||
to: '/hosting/manage/',
|
||||
visual: { type: 'icon', component: ServerStackIcon },
|
||||
})
|
||||
|
||||
@@ -32,6 +32,8 @@ import EarsModIcon from '@/assets/skins/ears-mod.png'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
||||
import VirtualSkinSectionList from '@/components/ui/skin/VirtualSkinSectionList.vue'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable, get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
@@ -58,8 +60,6 @@ import {
|
||||
} from '@/helpers/skins.ts'
|
||||
import { hasPride26Badge } from '@/helpers/user-campaigns.ts'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { appMessages } from '@/utils/app-messages'
|
||||
|
||||
useRootBreadcrumb({
|
||||
@@ -217,7 +217,7 @@ const { addNotification, handleError } = notifications
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const skins = ref<Skin[]>([])
|
||||
const capes = ref<Cape[]>([])
|
||||
const offline = ref(!navigator.onLine)
|
||||
@@ -339,7 +339,7 @@ const skinTexture = computedAsync(async () => {
|
||||
})
|
||||
const capeTexture = computed(() => currentCape.value?.texture)
|
||||
const skinVariant = computed(() => selectedSkin.value?.variant)
|
||||
const skinNametag = computed(() => (themeStore.hideNametagSkinsPage ? undefined : username.value))
|
||||
const skinNametag = computed(() => (appSettings.hideNametagSkinsPage ? undefined : username.value))
|
||||
const isSkinManagementReadOnly = computed(
|
||||
() =>
|
||||
!!currentUser.value &&
|
||||
|
||||
@@ -11,19 +11,20 @@
|
||||
>
|
||||
<template #project-actions="{ project }">
|
||||
<Button
|
||||
v-if="project.minecraft_server == null"
|
||||
type="outlined"
|
||||
class="!text-brand [&>svg]:!text-brand !shadow-[inset_0_0_0_1px_var(--color-brand)]"
|
||||
:disabled="isProjectInstalling(project.id)"
|
||||
@click.stop="installProject(project)"
|
||||
>
|
||||
<SpinnerIcon v-if="isProjectInstalling(project.id)" class="animate-spin" />
|
||||
<DownloadIcon v-else-if="project.project_type === 'modpack'" />
|
||||
<DownloadIcon v-else-if="project.project_types.includes('modpack')" />
|
||||
<PlusIcon v-else />
|
||||
{{
|
||||
formatMessage(
|
||||
isProjectInstalling(project.id)
|
||||
? commonMessages.installingLabel
|
||||
: project.project_type === 'modpack'
|
||||
: project.project_types.includes('modpack')
|
||||
? commonMessages.installButton
|
||||
: messages.installToInstance,
|
||||
)
|
||||
@@ -96,7 +97,7 @@ function setProjectInstalling(projectId: string, installing: boolean): void {
|
||||
installingProjectIds.value = next
|
||||
}
|
||||
|
||||
async function installProject(project: Labrinth.Projects.v2.Project): Promise<void> {
|
||||
async function installProject(project: Labrinth.Projects.v3.Project): Promise<void> {
|
||||
if (isProjectInstalling(project.id)) return
|
||||
|
||||
setProjectInstalling(project.id, true)
|
||||
@@ -130,14 +131,19 @@ const userProfile = provideUserProfile({
|
||||
unblockUser: unblock_user,
|
||||
})
|
||||
|
||||
const userId = computed(() => {
|
||||
const value = route.params.user
|
||||
return Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
|
||||
})
|
||||
const projectType = computed(() => {
|
||||
const value = route.params.projectType
|
||||
return Array.isArray(value) ? value[0] : value
|
||||
})
|
||||
function readRouteParam(param: unknown): string | undefined {
|
||||
const value = Array.isArray(param) ? param[0] : param
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
const userId = ref(readRouteParam(route.params.user) ?? '')
|
||||
watch(
|
||||
() => readRouteParam(route.params.user),
|
||||
(next) => {
|
||||
if (next) userId.value = next
|
||||
},
|
||||
)
|
||||
const projectType = computed(() => readRouteParam(route.params.projectType))
|
||||
|
||||
function getCachedUserSummary(id: string) {
|
||||
return queryClient.getQueryData<Labrinth.Users.v3.User>(['users', 'summary', id])
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:layout-mode="isContainedServerRoute ? 'contained' : 'page'"
|
||||
:reload-page="() => router.go(0)"
|
||||
:resolve-viewer="resolveViewer"
|
||||
:show-copy-id-action="themeStore.devMode"
|
||||
:show-copy-id-action="appSettings.devMode"
|
||||
:auth-user="authUser"
|
||||
:navigate-to-billing="() => openUrl('https://modrinth.com/settings/billing')"
|
||||
:navigate-to-servers="() => router.push('/hosting/manage')"
|
||||
@@ -61,17 +61,17 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { get as getCreds } from '@/helpers/mr_auth'
|
||||
import { provideBreadcrumbParent, useBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const isContainedServerRoute = computed(() => route.name === 'ServerManageOverview')
|
||||
|
||||
+3
-3
@@ -16,6 +16,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_version } from '@/helpers/cache'
|
||||
import {
|
||||
@@ -34,7 +35,6 @@ import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
import { provideInstanceBackup } from '@/providers/instance-backup'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import type { Manifest } from '../../../../helpers/types'
|
||||
import { instanceKeys } from '../../query-options.ts'
|
||||
@@ -47,12 +47,12 @@ const filePicker = injectFilePicker()
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('AppInstallationSettings')
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
|
||||
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
|
||||
const managedContentPolicy = useManagedContentPolicy(instance)
|
||||
const skipNonEssentialWarnings = computed(() =>
|
||||
themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
appSettings.getFeatureFlag('skip_non_essential_warnings'),
|
||||
)
|
||||
|
||||
debug('metadata load: start', {
|
||||
|
||||
@@ -127,6 +127,7 @@ import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { type FeatureFlag, useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import {
|
||||
@@ -149,8 +150,6 @@ import type { CacheBehaviour } from '@/helpers/types'
|
||||
import { highlightModInInstance } from '@/helpers/utils.js'
|
||||
import { type AppEventPayload, injectAppEvents } from '@/providers/app-events'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { FeatureFlag } from '@/store/theme'
|
||||
|
||||
import { injectInstancePage } from '../instance-context'
|
||||
import { instanceContentQueryOptions, instanceKeys } from '../query-options'
|
||||
@@ -234,10 +233,10 @@ const { installingItems, installRevisionByInstance, installFailureRevisionByInst
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('Mods:ContentUpdate')
|
||||
const themeStore = useTheming()
|
||||
const appSettings = useAppSettings()
|
||||
const skipUnknownFileWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
|
||||
const skipNonEssentialWarnings = computed(() =>
|
||||
themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
appSettings.getFeatureFlag('skip_non_essential_warnings'),
|
||||
)
|
||||
|
||||
const instancePage = injectInstancePage()
|
||||
@@ -759,7 +758,7 @@ async function handleUploadFiles() {
|
||||
}
|
||||
|
||||
function confirmUnknownFileInstallation(fileName: string) {
|
||||
if (themeStore.getFeatureFlag(skipUnknownFileWarningFeatureFlag)) {
|
||||
if (appSettings.getFeatureFlag(skipUnknownFileWarningFeatureFlag)) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
@@ -779,7 +778,7 @@ function resolveUnknownFileWarning(confirmed: boolean) {
|
||||
|
||||
async function handleUnknownFileContinue(dontShowAgain: boolean) {
|
||||
if (dontShowAgain) {
|
||||
themeStore.featureFlags[skipUnknownFileWarningFeatureFlag] = true
|
||||
appSettings.featureFlags[skipUnknownFileWarningFeatureFlag] = true
|
||||
try {
|
||||
const settings = await getSettings()
|
||||
settings.feature_flags[skipUnknownFileWarningFeatureFlag] = true
|
||||
|
||||
@@ -122,7 +122,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, type ComputedRef, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||
@@ -133,6 +133,8 @@ import {
|
||||
getFreshCachedServerStatus,
|
||||
} from '@/composables/instances/use-server-status-query'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
import { handleSevereError } from '@/composables/use-error.js'
|
||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { toError } from '@/helpers/errors'
|
||||
@@ -159,8 +161,6 @@ import type { ServerStatus } from '@/helpers/worlds'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { provideInstanceBackup } from '@/providers/instance-backup'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import InstanceAdmonitions from './components/admonitions/index.vue'
|
||||
import InstancePageHeader from './components/page-header/index.vue'
|
||||
@@ -216,8 +216,8 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const themeStore = useTheming()
|
||||
const showInstancePlayTime = computed(() => themeStore.getFeatureFlag('show_instance_play_time'))
|
||||
const appSettings = useAppSettings()
|
||||
const showInstancePlayTime = computed(() => appSettings.getFeatureFlag('show_instance_play_time'))
|
||||
|
||||
const online = useOnline()
|
||||
const offline = computed(() => !online.value)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user