You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -3,8 +3,7 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = "tab"
|
||||||
indent_size = 4
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
@@ -15,5 +14,5 @@ indent_size = 2
|
|||||||
max_line_length = off
|
max_line_length = off
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{json,yml,yaml,ts,vue,scss,css,html,js,cjs,mjs,gltf,prettierrc}]
|
[*.{json,yml,yaml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|||||||
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
@@ -1,10 +1,15 @@
|
|||||||
{
|
{
|
||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||||
"editor.detectIndentation": true,
|
"editor.detectIndentation": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.insertSpaces": false,
|
||||||
"source.fixAll.eslint": "explicit",
|
"files.eol": "\n",
|
||||||
"source.organizeImports": "always",
|
"files.trimTrailingWhitespace": true,
|
||||||
}
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "always"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
**/dist
|
**/dist
|
||||||
*.gltf
|
*.gltf
|
||||||
|
src/locales/
|
||||||
|
|||||||
@@ -1,22 +1,2 @@
|
|||||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||||
import { fixupPluginRules } from '@eslint/compat'
|
export default config
|
||||||
import turboPlugin from 'eslint-plugin-turbo'
|
|
||||||
|
|
||||||
export default createConfigForNuxt().append([
|
|
||||||
{
|
|
||||||
name: 'turbo',
|
|
||||||
plugins: {
|
|
||||||
turbo: fixupPluginRules(turboPlugin),
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'turbo/no-undeclared-env-vars': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'modrinth',
|
|
||||||
rules: {
|
|
||||||
'vue/html-self-closing': 'off',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" class="dark-mode">
|
<html lang="en" class="dark-mode">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Modrinth App</title>
|
<title>Modrinth App</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="https://tally.so/widgets/embed.js" async></script>
|
<script src="https://tally.so/widgets/embed.js" async></script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,64 +1,63 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app-frontend",
|
"name": "@modrinth/app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0-local",
|
"version": "1.0.0-local",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
"test": "vue-tsc --noEmit"
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@sentry/vue": "^8.27.0",
|
"@sentry/vue": "^8.27.0",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-http": "^2.5.0",
|
"@tauri-apps/plugin-http": "^2.5.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"@types/three": "^0.172.0",
|
"@types/three": "^0.172.0",
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@vintl/vintl": "^4.4.1",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@vueuse/core": "^11.1.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"floating-vue": "^5.2.2",
|
"floating-vue": "^5.2.2",
|
||||||
"ofetch": "^1.3.4",
|
"ofetch": "^1.3.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"posthog-js": "^1.158.2",
|
"posthog-js": "^1.158.2",
|
||||||
"three": "^0.172.0",
|
"three": "^0.172.0",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-multiselect": "3.0.0",
|
"vue-multiselect": "3.0.0",
|
||||||
"vue-router": "4.3.0",
|
"vue-router": "4.3.0",
|
||||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.1.1",
|
"@modrinth/tooling-config": "workspace:*",
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@formatjs/cli": "^6.2.12",
|
||||||
"@taijased/vue-render-tracker": "^1.0.7",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@taijased/vue-render-tracker": "^1.0.7",
|
||||||
"autoprefixer": "^10.4.19",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"eslint": "^9.9.1",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-turbo": "^2.5.4",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"tsconfig": "workspace:*",
|
"typescript": "^5.5.4",
|
||||||
"typescript": "^5.5.4",
|
"vite": "^5.4.6",
|
||||||
"vite": "^5.4.6",
|
"vue-tsc": "^2.1.6"
|
||||||
"vue-tsc": "^2.1.6"
|
},
|
||||||
},
|
"packageManager": "pnpm@9.4.0",
|
||||||
"packageManager": "pnpm@9.4.0",
|
"web-types": "../../web-types.json"
|
||||||
"web-types": "../../web-types.json"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
24
apps/app-frontend/src/assets/external/index.js
vendored
24
apps/app-frontend/src/assets/external/index.js
vendored
@@ -1,18 +1,18 @@
|
|||||||
|
export { default as ATLauncherIcon } from './atlauncher.svg'
|
||||||
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
||||||
export { default as DiscordIcon } from './discord.svg'
|
export { default as DiscordIcon } from './discord.svg'
|
||||||
|
export { default as GDLauncherIcon } from './gdlauncher.png'
|
||||||
|
export { default as GithubIcon } from './github.svg'
|
||||||
|
export { default as GitLabIcon } from './gitlab.svg'
|
||||||
|
export { default as GoogleIcon } from './google.svg'
|
||||||
export { default as KoFiIcon } from './kofi.svg'
|
export { default as KoFiIcon } from './kofi.svg'
|
||||||
|
export { default as MastodonIcon } from './mastodon.svg'
|
||||||
|
export { default as MicrosoftIcon } from './microsoft.svg'
|
||||||
|
export { default as MultiMCIcon } from './multimc.webp'
|
||||||
|
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
||||||
export { default as PatreonIcon } from './patreon.svg'
|
export { default as PatreonIcon } from './patreon.svg'
|
||||||
export { default as PaypalIcon } from './paypal.svg'
|
export { default as PaypalIcon } from './paypal.svg'
|
||||||
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
|
||||||
export { default as TwitterIcon } from './twitter.svg'
|
|
||||||
export { default as GithubIcon } from './github.svg'
|
|
||||||
export { default as MastodonIcon } from './mastodon.svg'
|
|
||||||
export { default as RedditIcon } from './reddit.svg'
|
|
||||||
export { default as GoogleIcon } from './google.svg'
|
|
||||||
export { default as MicrosoftIcon } from './microsoft.svg'
|
|
||||||
export { default as SteamIcon } from './steam.svg'
|
|
||||||
export { default as GitLabIcon } from './gitlab.svg'
|
|
||||||
export { default as ATLauncherIcon } from './atlauncher.svg'
|
|
||||||
export { default as GDLauncherIcon } from './gdlauncher.png'
|
|
||||||
export { default as MultiMCIcon } from './multimc.webp'
|
|
||||||
export { default as PrismIcon } from './prism.svg'
|
export { default as PrismIcon } from './prism.svg'
|
||||||
|
export { default as RedditIcon } from './reddit.svg'
|
||||||
|
export { default as SteamIcon } from './steam.svg'
|
||||||
|
export { default as TwitterIcon } from './twitter.svg'
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export { default as SwapIcon } from './arrow-left-right.svg'
|
|
||||||
export { default as ToggleIcon } from './toggle.svg'
|
|
||||||
export { default as PackageIcon } from './package.svg'
|
|
||||||
export { default as VersionIcon } from './milestone.svg'
|
|
||||||
export { default as TextInputIcon } from './text-cursor-input.svg'
|
|
||||||
export { default as AddProjectImage } from './add-project.svg'
|
export { default as AddProjectImage } from './add-project.svg'
|
||||||
export { default as NewInstanceImage } from './new-instance.svg'
|
export { default as SwapIcon } from './arrow-left-right.svg'
|
||||||
export { default as MenuIcon } from './menu.svg'
|
export { default as MenuIcon } from './menu.svg'
|
||||||
export { default as ChatIcon } from './messages-square.svg'
|
export { default as ChatIcon } from './messages-square.svg'
|
||||||
|
export { default as VersionIcon } from './milestone.svg'
|
||||||
|
export { default as NewInstanceImage } from './new-instance.svg'
|
||||||
|
export { default as PackageIcon } from './package.svg'
|
||||||
|
export { default as TextInputIcon } from './text-cursor-input.svg'
|
||||||
|
export { default as ToggleIcon } from './toggle.svg'
|
||||||
|
|||||||
@@ -3,158 +3,158 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-minecraft {
|
.font-minecraft {
|
||||||
font-family: 'bundled-minecraft-font-mrapp', monospace;
|
font-family: 'bundled-minecraft-font-mrapp', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: var(--font-standard, sans-serif), sans-serif;
|
font-family: var(--font-standard, sans-serif), sans-serif;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--view-width: calc(100% - 5rem);
|
--view-width: calc(100% - 5rem);
|
||||||
--expanded-view-width: calc(100% - 13rem);
|
--expanded-view-width: calc(100% - 13rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-divider {
|
.card-divider {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--color-button-bg);
|
color: var(--color-button-bg);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: var(--gap-sm) 0;
|
margin: var(--gap-sm) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap {
|
.no-wrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-select {
|
.no-select {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
padding-block: var(--gap-sm);
|
padding-block: var(--gap-sm);
|
||||||
padding-inline: var(--gap-lg);
|
padding-inline: var(--gap-lg);
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.1rem;
|
width: 1.1rem;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.featured {
|
&.featured {
|
||||||
background-color: var(--color-brand-highlight);
|
background-color: var(--color-brand-highlight);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: auto;
|
scrollbar-width: auto;
|
||||||
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome, Edge, and Safari */
|
/* Chrome, Edge, and Safari */
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar:hover {
|
*::-webkit-scrollbar:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--color-scrollbar);
|
background-color: var(--color-scrollbar);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border: 5px solid transparent;
|
border: 5px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighted {
|
.highlighted {
|
||||||
box-shadow: 0 0 1rem var(--color-brand) !important;
|
box-shadow: 0 0 1rem var(--color-brand) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gecko {
|
.gecko {
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-shadow {
|
.card-shadow {
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '@modrinth/assets/omorphia.scss';
|
@import '@modrinth/assets/omorphia.scss';
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
img {
|
img {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,38 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import { duplicate, remove } from '@/helpers/profile.js'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { formatCategoryHeader } from '@modrinth/utils'
|
import { formatCategoryHeader } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { duplicate, remove } from '@/helpers/profile.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const instanceOptions = ref(null)
|
const instanceOptions = ref(null)
|
||||||
const instanceComponents = ref(null)
|
const instanceComponents = ref(null)
|
||||||
@@ -40,84 +41,84 @@ const currentDeleteInstance = ref(null)
|
|||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
if (currentDeleteInstance.value) {
|
if (currentDeleteInstance.value) {
|
||||||
instanceComponents.value = instanceComponents.value.filter(
|
instanceComponents.value = instanceComponents.value.filter(
|
||||||
(x) => x.instance.path !== currentDeleteInstance.value,
|
(x) => x.instance.path !== currentDeleteInstance.value,
|
||||||
)
|
)
|
||||||
await remove(currentDeleteInstance.value).catch(handleError)
|
await remove(currentDeleteInstance.value).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateProfile(p) {
|
async function duplicateProfile(p) {
|
||||||
await duplicate(p).catch(handleError)
|
await duplicate(p).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRightClick = (event, profilePathId) => {
|
const handleRightClick = (event, profilePathId) => {
|
||||||
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
{ name: 'add_content' },
|
{ name: 'add_content' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ name: 'edit' },
|
{ name: 'edit' },
|
||||||
{ name: 'duplicate' },
|
{ name: 'duplicate' },
|
||||||
{ name: 'open' },
|
{ name: 'open' },
|
||||||
{ name: 'copy' },
|
{ name: 'copy' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
instanceOptions.value.showMenu(
|
instanceOptions.value.showMenu(
|
||||||
event,
|
event,
|
||||||
item,
|
item,
|
||||||
item.playing
|
item.playing
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
name: 'play',
|
name: 'play',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsClick = async (args) => {
|
const handleOptionsClick = async (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'play':
|
case 'play':
|
||||||
args.item.play(null, 'InstanceGridContextMenu')
|
args.item.play(null, 'InstanceGridContextMenu')
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
args.item.stop(null, 'InstanceGridContextMenu')
|
args.item.stop(null, 'InstanceGridContextMenu')
|
||||||
break
|
break
|
||||||
case 'add_content':
|
case 'add_content':
|
||||||
await args.item.addContent()
|
await args.item.addContent()
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await args.item.seeInstance()
|
await args.item.seeInstance()
|
||||||
break
|
break
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
if (args.item.instance.install_stage == 'installed')
|
if (args.item.instance.install_stage == 'installed')
|
||||||
await duplicateProfile(args.item.instance.path)
|
await duplicateProfile(args.item.instance.path)
|
||||||
break
|
break
|
||||||
case 'open':
|
case 'open':
|
||||||
await args.item.openFolder()
|
await args.item.openFolder()
|
||||||
break
|
break
|
||||||
case 'copy':
|
case 'copy':
|
||||||
await navigator.clipboard.writeText(args.item.instance.path)
|
await navigator.clipboard.writeText(args.item.instance.path)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
currentDeleteInstance.value = args.item.instance.path
|
currentDeleteInstance.value = args.item.instance.path
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
@@ -125,261 +126,261 @@ const group = ref('Group')
|
|||||||
const sortBy = ref('Name')
|
const sortBy = ref('Name')
|
||||||
|
|
||||||
const filteredResults = computed(() => {
|
const filteredResults = computed(() => {
|
||||||
const instances = props.instances.filter((instance) => {
|
const instances = props.instances.filter((instance) => {
|
||||||
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
||||||
})
|
})
|
||||||
|
|
||||||
if (sortBy.value === 'Name') {
|
if (sortBy.value === 'Name') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Game version') {
|
if (sortBy.value === 'Game version') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Last played') {
|
if (sortBy.value === 'Last played') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date created') {
|
if (sortBy.value === 'Date created') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date modified') {
|
if (sortBy.value === 'Date modified') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceMap = new Map()
|
const instanceMap = new Map()
|
||||||
|
|
||||||
if (group.value === 'Loader') {
|
if (group.value === 'Loader') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
const loader = formatCategoryHeader(instance.loader)
|
const loader = formatCategoryHeader(instance.loader)
|
||||||
if (!instanceMap.has(loader)) {
|
if (!instanceMap.has(loader)) {
|
||||||
instanceMap.set(loader, [])
|
instanceMap.set(loader, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceMap.get(loader).push(instance)
|
instanceMap.get(loader).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Game version') {
|
} else if (group.value === 'Game version') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (!instanceMap.has(instance.game_version)) {
|
if (!instanceMap.has(instance.game_version)) {
|
||||||
instanceMap.set(instance.game_version, [])
|
instanceMap.set(instance.game_version, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceMap.get(instance.game_version).push(instance)
|
instanceMap.get(instance.game_version).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Group') {
|
} else if (group.value === 'Group') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (instance.groups.length === 0) {
|
if (instance.groups.length === 0) {
|
||||||
instance.groups.push('None')
|
instance.groups.push('None')
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const category of instance.groups) {
|
for (const category of instance.groups) {
|
||||||
if (!instanceMap.has(category)) {
|
if (!instanceMap.has(category)) {
|
||||||
instanceMap.set(category, [])
|
instanceMap.set(category, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceMap.get(category).push(instance)
|
instanceMap.get(category).push(instance)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return instanceMap.set('None', instances)
|
return instanceMap.set('None', instances)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
||||||
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
||||||
if (sortBy.value === 'Name') {
|
if (sortBy.value === 'Name') {
|
||||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
// None should always be first
|
// None should always be first
|
||||||
if (a[0] === 'None' && b[0] !== 'None') {
|
if (a[0] === 'None' && b[0] !== 'None') {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
if (a[0] !== 'None' && b[0] === 'None') {
|
if (a[0] !== 'None' && b[0] === 'None') {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return a[0].localeCompare(b[0])
|
return a[0].localeCompare(b[0])
|
||||||
})
|
})
|
||||||
instanceMap.clear()
|
instanceMap.clear()
|
||||||
sortedEntries.forEach((entry) => {
|
sortedEntries.forEach((entry) => {
|
||||||
instanceMap.set(entry[0], entry[1])
|
instanceMap.set(entry[0], entry[1])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||||
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||||
if (group.value === 'Game version') {
|
if (group.value === 'Game version') {
|
||||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||||
})
|
})
|
||||||
instanceMap.clear()
|
instanceMap.clear()
|
||||||
sortedEntries.forEach((entry) => {
|
sortedEntries.forEach((entry) => {
|
||||||
instanceMap.set(entry[0], entry[1])
|
instanceMap.set(entry[0], entry[1])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return instanceMap
|
return instanceMap
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="iconified-input flex-1">
|
<div class="iconified-input flex-1">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
<input v-model="search" type="text" placeholder="Search" />
|
<input v-model="search" type="text" placeholder="Search" />
|
||||||
<Button class="r-btn" @click="() => (search = '')">
|
<Button class="r-btn" @click="() => (search = '')">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="sortBy"
|
v-model="sortBy"
|
||||||
name="Sort Dropdown"
|
name="Sort Dropdown"
|
||||||
class="max-w-[16rem]"
|
class="max-w-[16rem]"
|
||||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">Sort by: </span>
|
<span class="font-semibold text-primary">Sort by: </span>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</DropdownSelect>
|
</DropdownSelect>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="group"
|
v-model="group"
|
||||||
class="max-w-[16rem]"
|
class="max-w-[16rem]"
|
||||||
name="Group Dropdown"
|
name="Group Dropdown"
|
||||||
:options="['Group', 'Loader', 'Game version', 'None']"
|
:options="['Group', 'Loader', 'Game version', 'None']"
|
||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">Group by: </span>
|
<span class="font-semibold text-primary">Group by: </span>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</DropdownSelect>
|
</DropdownSelect>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
}))"
|
}))"
|
||||||
:key="instanceSection.key"
|
:key="instanceSection.key"
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div v-if="instanceSection.key !== 'None'" class="divider">
|
<div v-if="instanceSection.key !== 'None'" class="divider">
|
||||||
<p>{{ instanceSection.key }}</p>
|
<p>{{ instanceSection.key }}</p>
|
||||||
<hr aria-hidden="true" />
|
<hr aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<section class="instances">
|
<section class="instances">
|
||||||
<Instance
|
<Instance
|
||||||
v-for="instance in instanceSection.value"
|
v-for="instance in instanceSection.value"
|
||||||
ref="instanceComponents"
|
ref="instanceComponents"
|
||||||
:key="instance.path + instance.install_stage"
|
:key="instance.path + instance.install_stage"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="confirmModal"
|
ref="confirmModal"
|
||||||
title="Are you sure you want to delete this instance?"
|
title="Are you sure you want to delete this instance?"
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
@proceed="deleteProfile"
|
@proceed="deleteProfile"
|
||||||
/>
|
/>
|
||||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #play> <PlayIcon /> Play </template>
|
||||||
<template #stop> <StopCircleIcon /> Stop </template>
|
<template #stop> <StopCircleIcon /> Stop </template>
|
||||||
<template #add_content> <PlusIcon /> Add content </template>
|
<template #add_content> <PlusIcon /> Add content </template>
|
||||||
<template #edit> <EyeIcon /> View instance </template>
|
<template #edit> <EyeIcon /> View instance </template>
|
||||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||||
<template #delete> <TrashIcon /> Delete </template>
|
<template #delete> <TrashIcon /> Delete </template>
|
||||||
<template #open> <FolderOpenIcon /> Open folder </template>
|
<template #open> <FolderOpenIcon /> Open folder </template>
|
||||||
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
background-color: var(--color-gray);
|
background-color: var(--color-gray);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: inherit;
|
align-items: inherit;
|
||||||
margin: 1rem 1rem 0 !important;
|
margin: 1rem 1rem 0 !important;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
|
|
||||||
.iconified-input {
|
.iconified-input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-dropdown {
|
.sort-dropdown {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-dropdown {
|
.filter-dropdown {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-dropdown {
|
.group-dropdown {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labeled_button {
|
.labeled_button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useLoading } from '@/store/state.js'
|
import { useLoading } from '@/store/state.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
throttle: {
|
throttle: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1000,
|
default: 1000,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 2,
|
default: 2,
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'var(--loading-bar-gradient)',
|
default: 'var(--loading-bar-gradient)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const indicator = useLoadingIndicator({
|
const indicator = useLoadingIndicator({
|
||||||
duration: props.duration,
|
duration: props.duration,
|
||||||
throttle: props.throttle,
|
throttle: props.throttle,
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => indicator.clear)
|
onBeforeUnmount(() => indicator.clear)
|
||||||
@@ -31,111 +32,111 @@ onBeforeUnmount(() => indicator.clear)
|
|||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
|
|
||||||
watch(loading, (newValue) => {
|
watch(loading, (newValue) => {
|
||||||
if (newValue.barEnabled) {
|
if (newValue.barEnabled) {
|
||||||
if (newValue.loading) {
|
if (newValue.loading) {
|
||||||
indicator.start()
|
indicator.start()
|
||||||
} else {
|
} else {
|
||||||
indicator.finish()
|
indicator.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function useLoadingIndicator(opts) {
|
function useLoadingIndicator(opts) {
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const step = computed(() => 10000 / opts.duration)
|
const step = computed(() => 10000 / opts.duration)
|
||||||
|
|
||||||
let _timer = null
|
let _timer = null
|
||||||
let _throttle = null
|
let _throttle = null
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
clear()
|
clear()
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
if (opts.throttle) {
|
if (opts.throttle) {
|
||||||
_throttle = setTimeout(() => {
|
_throttle = setTimeout(() => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
_startTimer()
|
_startTimer()
|
||||||
}, opts.throttle)
|
}, opts.throttle)
|
||||||
} else {
|
} else {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
_startTimer()
|
_startTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
progress.value = 100
|
progress.value = 100
|
||||||
_hide()
|
_hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
clearInterval(_timer)
|
clearInterval(_timer)
|
||||||
clearTimeout(_throttle)
|
clearTimeout(_throttle)
|
||||||
_timer = null
|
_timer = null
|
||||||
_throttle = null
|
_throttle = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function _increase(num) {
|
function _increase(num) {
|
||||||
progress.value = Math.min(100, progress.value + num)
|
progress.value = Math.min(100, progress.value + num)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _hide() {
|
function _hide() {
|
||||||
clear()
|
clear()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
}, 400)
|
}, 400)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _startTimer() {
|
function _startTimer() {
|
||||||
_timer = setInterval(() => {
|
_timer = setInterval(() => {
|
||||||
_increase(step.value)
|
_increase(step.value)
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { progress, isLoading, start, finish, clear }
|
return { progress, isLoading, start, finish, clear }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="loading-indicator-bar"
|
class="loading-indicator-bar"
|
||||||
:style="{
|
:style="{
|
||||||
'--_width': `${indicator.progress.value}%`,
|
'--_width': `${indicator.progress.value}%`,
|
||||||
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
||||||
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
||||||
top: `0`,
|
top: `0`,
|
||||||
right: `0`,
|
right: `0`,
|
||||||
left: `${props.offsetWidth}`,
|
left: `${props.offsetWidth}`,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
width: `var(--_width)`,
|
width: `var(--_width)`,
|
||||||
height: `var(--_height)`,
|
height: `var(--_height)`,
|
||||||
borderRadius: `var(--_height)`,
|
borderRadius: `var(--_height)`,
|
||||||
// opacity: `var(--_opacity)`,
|
// opacity: `var(--_opacity)`,
|
||||||
background: `${props.color}`,
|
background: `${props.color}`,
|
||||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||||
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
||||||
zIndex: 6,
|
zIndex: 6,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.loading-indicator-bar::before {
|
.loading-indicator-bar::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: var(--_width);
|
width: var(--_width);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
||||||
opacity: calc(var(--_opacity) * 0.1);
|
opacity: calc(var(--_opacity) * 0.1);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
transition:
|
transition:
|
||||||
width 0.1s ease-in-out,
|
width 0.1s ease-in-out,
|
||||||
opacity 0.1s ease-out;
|
opacity 0.1s ease-out;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
EyeIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
@@ -9,45 +26,29 @@ import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
|||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
import {
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
ExternalIcon,
|
|
||||||
EyeIcon,
|
|
||||||
FolderOpenIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
PlayIcon,
|
|
||||||
PlusIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
canPaginate: Boolean,
|
canPaginate: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const actualInstances = computed(() =>
|
const actualInstances = computed(() =>
|
||||||
props.instances.filter(
|
props.instances.filter(
|
||||||
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
|
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const modsRow = ref(null)
|
const modsRow = ref(null)
|
||||||
@@ -59,124 +60,124 @@ const deleteConfirmModal = ref(null)
|
|||||||
const currentDeleteInstance = ref(null)
|
const currentDeleteInstance = ref(null)
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
if (currentDeleteInstance.value) {
|
if (currentDeleteInstance.value) {
|
||||||
await remove(currentDeleteInstance.value).catch(handleError)
|
await remove(currentDeleteInstance.value).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateProfile(p) {
|
async function duplicateProfile(p) {
|
||||||
await duplicate(p).catch(handleError)
|
await duplicate(p).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInstanceRightClick = async (event, passedInstance) => {
|
const handleInstanceRightClick = async (event, passedInstance) => {
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
{ name: 'add_content' },
|
{ name: 'add_content' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ name: 'edit' },
|
{ name: 'edit' },
|
||||||
{ name: 'duplicate' },
|
{ name: 'duplicate' },
|
||||||
{ name: 'open_folder' },
|
{ name: 'open_folder' },
|
||||||
{ name: 'copy_path' },
|
{ name: 'copy_path' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
runningProcesses.length > 0
|
runningProcesses.length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
name: 'play',
|
name: 'play',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
|
|
||||||
instanceOptions.value.showMenu(event, passedInstance, options)
|
instanceOptions.value.showMenu(event, passedInstance, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProjectClick = (event, passedInstance) => {
|
const handleProjectClick = (event, passedInstance) => {
|
||||||
instanceOptions.value.showMenu(event, passedInstance, [
|
instanceOptions.value.showMenu(event, passedInstance, [
|
||||||
{
|
{
|
||||||
name: 'install',
|
name: 'install',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'open_link',
|
name: 'open_link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'copy_link',
|
name: 'copy_link',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsClick = async (args) => {
|
const handleOptionsClick = async (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'play':
|
case 'play':
|
||||||
await run(args.item.path).catch((err) =>
|
await run(args.item.path).catch((err) =>
|
||||||
handleSevereError(err, { profilePath: args.item.path }),
|
handleSevereError(err, { profilePath: args.item.path }),
|
||||||
)
|
)
|
||||||
trackEvent('InstanceStart', {
|
trackEvent('InstanceStart', {
|
||||||
loader: args.item.loader,
|
loader: args.item.loader,
|
||||||
game_version: args.item.game_version,
|
game_version: args.item.game_version,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
await kill(args.item.path).catch(handleError)
|
await kill(args.item.path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: args.item.loader,
|
loader: args.item.loader,
|
||||||
game_version: args.item.game_version,
|
game_version: args.item.game_version,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'add_content':
|
case 'add_content':
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||||
query: { i: args.item.path },
|
query: { i: args.item.path },
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
currentDeleteInstance.value = args.item.path
|
currentDeleteInstance.value = args.item.path
|
||||||
deleteConfirmModal.value.show()
|
deleteConfirmModal.value.show()
|
||||||
break
|
break
|
||||||
case 'open_folder':
|
case 'open_folder':
|
||||||
await showProfileInFolder(args.item.path)
|
await showProfileInFolder(args.item.path)
|
||||||
break
|
break
|
||||||
case 'copy_path':
|
case 'copy_path':
|
||||||
await navigator.clipboard.writeText(args.item.path)
|
await navigator.clipboard.writeText(args.item.path)
|
||||||
break
|
break
|
||||||
case 'install': {
|
case 'install': {
|
||||||
await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
|
await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'open_link':
|
case 'open_link':
|
||||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||||
break
|
break
|
||||||
case 'copy_link':
|
case 'copy_link':
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxInstancesPerCompactRow = ref(1)
|
const maxInstancesPerCompactRow = ref(1)
|
||||||
@@ -184,184 +185,184 @@ const maxInstancesPerRow = ref(1)
|
|||||||
const maxProjectsPerRow = ref(1)
|
const maxProjectsPerRow = ref(1)
|
||||||
|
|
||||||
const calculateCardsPerRow = () => {
|
const calculateCardsPerRow = () => {
|
||||||
if (rows.value.length === 0) {
|
if (rows.value.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate how many cards fit in one row
|
// Calculate how many cards fit in one row
|
||||||
const containerWidth = rows.value[0].clientWidth
|
const containerWidth = rows.value[0].clientWidth
|
||||||
// Convert container width from pixels to rem
|
// Convert container width from pixels to rem
|
||||||
const containerWidthInRem =
|
const containerWidthInRem =
|
||||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
|
||||||
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
||||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||||
|
|
||||||
if (maxInstancesPerRow.value < 5) {
|
if (maxInstancesPerRow.value < 5) {
|
||||||
maxInstancesPerRow.value *= 2
|
maxInstancesPerRow.value *= 2
|
||||||
}
|
}
|
||||||
if (maxInstancesPerCompactRow.value < 5) {
|
if (maxInstancesPerCompactRow.value < 5) {
|
||||||
maxInstancesPerCompactRow.value *= 2
|
maxInstancesPerCompactRow.value *= 2
|
||||||
}
|
}
|
||||||
if (maxProjectsPerRow.value < 3) {
|
if (maxProjectsPerRow.value < 3) {
|
||||||
maxProjectsPerRow.value *= 2
|
maxProjectsPerRow.value *= 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowContainer = ref(null)
|
const rowContainer = ref(null)
|
||||||
const resizeObserver = ref(null)
|
const resizeObserver = ref(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calculateCardsPerRow()
|
calculateCardsPerRow()
|
||||||
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
||||||
if (rowContainer.value) {
|
if (rowContainer.value) {
|
||||||
resizeObserver.value.observe(rowContainer.value)
|
resizeObserver.value.observe(rowContainer.value)
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', calculateCardsPerRow)
|
window.addEventListener('resize', calculateCardsPerRow)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', calculateCardsPerRow)
|
window.removeEventListener('resize', calculateCardsPerRow)
|
||||||
if (rowContainer.value) {
|
if (rowContainer.value) {
|
||||||
resizeObserver.value.unobserve(rowContainer.value)
|
resizeObserver.value.unobserve(rowContainer.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="deleteConfirmModal"
|
ref="deleteConfirmModal"
|
||||||
title="Are you sure you want to delete this instance?"
|
title="Are you sure you want to delete this instance?"
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
@proceed="deleteProfile"
|
@proceed="deleteProfile"
|
||||||
/>
|
/>
|
||||||
<div ref="rowContainer" class="flex flex-col gap-4">
|
<div ref="rowContainer" class="flex flex-col gap-4">
|
||||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||||
<HeadingLink class="mt-1" :to="row.route">
|
<HeadingLink class="mt-1" :to="row.route">
|
||||||
{{ row.label }}
|
{{ row.label }}
|
||||||
</HeadingLink>
|
</HeadingLink>
|
||||||
<section
|
<section
|
||||||
v-if="row.instance"
|
v-if="row.instance"
|
||||||
ref="modsRow"
|
ref="modsRow"
|
||||||
class="instances"
|
class="instances"
|
||||||
:class="{ compact: row.compact }"
|
:class="{ compact: row.compact }"
|
||||||
>
|
>
|
||||||
<Instance
|
<Instance
|
||||||
v-for="(instance, instanceIndex) in row.instances.slice(
|
v-for="(instance, instanceIndex) in row.instances.slice(
|
||||||
0,
|
0,
|
||||||
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
||||||
)"
|
)"
|
||||||
:key="row.label + instance.path"
|
:key="row.label + instance.path"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
:compact="row.compact"
|
:compact="row.compact"
|
||||||
:first="instanceIndex === 0"
|
:first="instanceIndex === 0"
|
||||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section v-else ref="modsRow" class="projects">
|
<section v-else ref="modsRow" class="projects">
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
||||||
:key="project?.project_id"
|
:key="project?.project_id"
|
||||||
ref="instanceComponents"
|
ref="instanceComponents"
|
||||||
class="item"
|
class="item"
|
||||||
:project="project"
|
:project="project"
|
||||||
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #play> <PlayIcon /> Play </template>
|
||||||
<template #stop> <StopCircleIcon /> Stop </template>
|
<template #stop> <StopCircleIcon /> Stop </template>
|
||||||
<template #add_content> <PlusIcon /> Add content </template>
|
<template #add_content> <PlusIcon /> Add content </template>
|
||||||
<template #edit> <EyeIcon /> View instance </template>
|
<template #edit> <EyeIcon /> View instance </template>
|
||||||
<template #delete> <TrashIcon /> Delete </template>
|
<template #delete> <TrashIcon /> Delete </template>
|
||||||
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
||||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||||
<template #install> <DownloadIcon /> Install </template>
|
<template #install> <DownloadIcon /> Install </template>
|
||||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
grid-gap: 0.75rem;
|
grid-gap: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects {
|
.projects {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
grid-gap: 0.75rem;
|
grid-gap: 0.75rem;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,102 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="mode !== 'isolated'"
|
v-if="mode !== 'isolated'"
|
||||||
ref="button"
|
ref="button"
|
||||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||||
:class="{ expanded: mode === 'expanded' }"
|
:class="{ expanded: mode === 'expanded' }"
|
||||||
@click="toggleMenu"
|
@click="toggleMenu"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="36px"
|
size="36px"
|
||||||
:src="
|
:src="
|
||||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||||
<span class="text-secondary text-xs">Minecraft account</span>
|
<span class="text-secondary text-xs">Minecraft account</span>
|
||||||
</div>
|
</div>
|
||||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<Card
|
<Card
|
||||||
v-if="showCard || mode === 'isolated'"
|
v-if="showCard || mode === 'isolated'"
|
||||||
ref="card"
|
ref="card"
|
||||||
class="account-card"
|
class="account-card"
|
||||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||||
>
|
>
|
||||||
<div v-if="selectedAccount" class="selected account">
|
<div v-if="selectedAccount" class="selected account">
|
||||||
<Avatar size="xs" :src="avatarUrl" />
|
<Avatar size="xs" :src="avatarUrl" />
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||||
<p>Selected</p>
|
<p>Selected</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip="'Log out'"
|
v-tooltip="'Log out'"
|
||||||
icon-only
|
icon-only
|
||||||
color="raised"
|
color="raised"
|
||||||
@click="logout(selectedAccount.profile.id)"
|
@click="logout(selectedAccount.profile.id)"
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="logged-out account">
|
<div v-else class="logged-out account">
|
||||||
<h4>Not signed in</h4>
|
<h4>Not signed in</h4>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip="'Log in'"
|
v-tooltip="'Log in'"
|
||||||
:disabled="loginDisabled"
|
:disabled="loginDisabled"
|
||||||
icon-only
|
icon-only
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="login()"
|
@click="login()"
|
||||||
>
|
>
|
||||||
<LogInIcon v-if="!loginDisabled" />
|
<LogInIcon v-if="!loginDisabled" />
|
||||||
<SpinnerIcon v-else class="animate-spin" />
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||||
<Button class="option account" @click="setAccount(account)">
|
<Button class="option account" @click="setAccount(account)">
|
||||||
<Avatar :src="getAccountAvatarUrl(account)" class="icon" />
|
<Avatar :src="getAccountAvatarUrl(account)" class="icon" />
|
||||||
<p>{{ account.profile.name }}</p>
|
<p>{{ account.profile.name }}</p>
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button v-if="accounts.length > 0" @click="login()">
|
<Button v-if="accounts.length > 0" @click="login()">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Add account
|
Add account
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { DropdownIcon, LogInIcon, PlusIcon, SpinnerIcon, TrashIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import {
|
import {
|
||||||
get_default_user,
|
get_default_user,
|
||||||
login as login_flow,
|
login as login_flow,
|
||||||
remove_user,
|
remove_user,
|
||||||
set_default_user,
|
set_default_user,
|
||||||
users,
|
users,
|
||||||
} from '@/helpers/auth'
|
} from '@/helpers/auth'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
import { get_available_skins } from '@/helpers/skins'
|
import { get_available_skins } from '@/helpers/skins'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { DropdownIcon, LogInIcon, PlusIcon, SpinnerIcon, TrashIcon } from '@modrinth/assets'
|
|
||||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
default: 'normal',
|
default: 'normal',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
@@ -108,370 +109,370 @@ const equippedSkin = ref(null)
|
|||||||
const headUrlCache = ref(new Map())
|
const headUrlCache = ref(new Map())
|
||||||
|
|
||||||
async function refreshValues() {
|
async function refreshValues() {
|
||||||
defaultUser.value = await get_default_user().catch(handleError)
|
defaultUser.value = await get_default_user().catch(handleError)
|
||||||
accounts.value = await users().catch(handleError)
|
accounts.value = await users().catch(handleError)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const skins = await get_available_skins()
|
const skins = await get_available_skins()
|
||||||
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
||||||
|
|
||||||
if (equippedSkin.value) {
|
if (equippedSkin.value) {
|
||||||
try {
|
try {
|
||||||
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
|
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
|
||||||
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
|
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to get head render for equipped skin:', error)
|
console.warn('Failed to get head render for equipped skin:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
equippedSkin.value = null
|
equippedSkin.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLoginDisabled(value) {
|
function setLoginDisabled(value) {
|
||||||
loginDisabled.value = value
|
loginDisabled.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refreshValues,
|
refreshValues,
|
||||||
setLoginDisabled,
|
setLoginDisabled,
|
||||||
loginDisabled,
|
loginDisabled,
|
||||||
})
|
})
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
const displayAccounts = computed(() =>
|
const displayAccounts = computed(() =>
|
||||||
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
const avatarUrl = computed(() => {
|
const avatarUrl = computed(() => {
|
||||||
if (equippedSkin.value?.texture_key) {
|
if (equippedSkin.value?.texture_key) {
|
||||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||||
if (cachedUrl) {
|
if (cachedUrl) {
|
||||||
return cachedUrl
|
return cachedUrl
|
||||||
}
|
}
|
||||||
return `https://mc-heads.net/avatar/${equippedSkin.value.texture_key}/128`
|
return `https://mc-heads.net/avatar/${equippedSkin.value.texture_key}/128`
|
||||||
}
|
}
|
||||||
if (selectedAccount.value?.profile?.id) {
|
if (selectedAccount.value?.profile?.id) {
|
||||||
return `https://mc-heads.net/avatar/${selectedAccount.value.profile.id}/128`
|
return `https://mc-heads.net/avatar/${selectedAccount.value.profile.id}/128`
|
||||||
}
|
}
|
||||||
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
})
|
})
|
||||||
|
|
||||||
function getAccountAvatarUrl(account) {
|
function getAccountAvatarUrl(account) {
|
||||||
if (
|
if (
|
||||||
account.profile.id === selectedAccount.value?.profile?.id &&
|
account.profile.id === selectedAccount.value?.profile?.id &&
|
||||||
equippedSkin.value?.texture_key
|
equippedSkin.value?.texture_key
|
||||||
) {
|
) {
|
||||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||||
if (cachedUrl) {
|
if (cachedUrl) {
|
||||||
return cachedUrl
|
return cachedUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedAccount = computed(() =>
|
const selectedAccount = computed(() =>
|
||||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function setAccount(account) {
|
async function setAccount(account) {
|
||||||
defaultUser.value = account.profile.id
|
defaultUser.value = account.profile.id
|
||||||
await set_default_user(account.profile.id).catch(handleError)
|
await set_default_user(account.profile.id).catch(handleError)
|
||||||
emit('change')
|
emit('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
loginDisabled.value = true
|
loginDisabled.value = true
|
||||||
const loggedIn = await login_flow().catch(handleSevereError)
|
const loggedIn = await login_flow().catch(handleSevereError)
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await setAccount(loggedIn)
|
await setAccount(loggedIn)
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('AccountLogIn')
|
trackEvent('AccountLogIn')
|
||||||
loginDisabled.value = false
|
loginDisabled.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = async (id) => {
|
const logout = async (id) => {
|
||||||
await remove_user(id).catch(handleError)
|
await remove_user(id).catch(handleError)
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
if (!selectedAccount.value && accounts.value.length > 0) {
|
if (!selectedAccount.value && accounts.value.length > 0) {
|
||||||
await setAccount(accounts.value[0])
|
await setAccount(accounts.value[0])
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
} else {
|
} else {
|
||||||
emit('change')
|
emit('change')
|
||||||
}
|
}
|
||||||
trackEvent('AccountLogOut')
|
trackEvent('AccountLogOut')
|
||||||
}
|
}
|
||||||
|
|
||||||
const showCard = ref(false)
|
const showCard = ref(false)
|
||||||
const card = ref(null)
|
const card = ref(null)
|
||||||
const button = ref(null)
|
const button = ref(null)
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
card.value &&
|
card.value &&
|
||||||
card.value.$el !== event.target &&
|
card.value.$el !== event.target &&
|
||||||
!elements.includes(card.value.$el) &&
|
!elements.includes(card.value.$el) &&
|
||||||
!button.value.contains(event.target)
|
!button.value.contains(event.target)
|
||||||
) {
|
) {
|
||||||
toggleMenu(false)
|
toggleMenu(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenu(override = true) {
|
function toggleMenu(override = true) {
|
||||||
if (showCard.value || !override) {
|
if (showCard.value || !override) {
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
} else {
|
} else {
|
||||||
showCard.value = true
|
showCard.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlisten = await process_listener(async (e) => {
|
const unlisten = await process_listener(async (e) => {
|
||||||
if (e.event === 'launched') {
|
if (e.event === 'launched') {
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', handleClickOutside)
|
window.addEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', handleClickOutside)
|
window.removeEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten()
|
unlisten()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.selected {
|
.selected {
|
||||||
background: var(--color-brand-highlight);
|
background: var(--color-brand-highlight);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logged-out {
|
.logged-out {
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account {
|
.account {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
h4,
|
h4,
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
right: 2rem;
|
right: 2rem;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
max-height: 98vh;
|
max-height: 98vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
border-top-right-radius: 1rem;
|
border-top-right-radius: 1rem;
|
||||||
border-bottom-right-radius: 1rem;
|
border-bottom-right-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
border-top-right-radius: 1rem;
|
border-top-right-radius: 1rem;
|
||||||
border-bottom-right-radius: 1rem;
|
border-bottom-right-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
left: 13.5rem;
|
left: 13.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.isolated {
|
&.isolated {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accounts-title {
|
.accounts-title {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-group {
|
.account-group {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
width: calc(100% - 2.25rem);
|
width: calc(100% - 2.25rem);
|
||||||
background: var(--color-raised-bg);
|
background: var(--color-raised-bg);
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
--size: 1.5rem !important;
|
--size: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-row {
|
.account-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition:
|
transition:
|
||||||
opacity 0.25s ease,
|
opacity 0.25s ease,
|
||||||
translate 0.25s ease,
|
translate 0.25s ease,
|
||||||
scale 0.25s ease;
|
scale 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
translate: 0 -2rem;
|
translate: 0 -2rem;
|
||||||
scale: 0.9;
|
scale: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-button {
|
.avatar-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-text {
|
.avatar-text {
|
||||||
margin: auto 0 auto 0.25rem;
|
margin: auto 0 auto 0.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accounts-text {
|
.accounts-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-code {
|
.qr-code {
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-lg);
|
gap: var(--gap-lg);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--gap-xl);
|
padding: var(--gap-xl);
|
||||||
|
|
||||||
.modal-text {
|
.modal-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
h2,
|
h2,
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-text {
|
.code-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
border: solid 1px var(--color-button-bg);
|
border: solid 1px var(--color-button-bg);
|
||||||
font-family: var(--mono-font);
|
font-family: var(--mono-font);
|
||||||
letter-spacing: var(--gap-md);
|
letter-spacing: var(--gap-md);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
|
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
color: var(--color-brand);
|
color: var(--color-brand);
|
||||||
padding: 0.05rem 0.1rem;
|
padding: 0.05rem 0.1rem;
|
||||||
// row not column
|
// row not column
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--color-base);
|
background: var(--color-base);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,61 +1,62 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { add_project_from_path } from '@/helpers/profile.js'
|
|
||||||
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { add_project_from_path } from '@/helpers/profile.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleAddContentFromFile = async () => {
|
const handleAddContentFromFile = async () => {
|
||||||
const newProject = await open({ multiple: true })
|
const newProject = await open({ multiple: true })
|
||||||
if (!newProject) return
|
if (!newProject) return
|
||||||
|
|
||||||
for (const project of newProject) {
|
for (const project of newProject) {
|
||||||
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchContent = async () => {
|
const handleSearchContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="joined-buttons">
|
<div class="joined-buttons">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="handleSearchContent">
|
<button @click="handleSearchContent">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Install content
|
Install content
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'from_file',
|
id: 'from_file',
|
||||||
action: handleAddContentFromFile,
|
action: handleAddContentFromFile,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
<template #from_file>
|
<template #from_file>
|
||||||
<FolderOpenIcon />
|
<FolderOpenIcon />
|
||||||
<span class="no-wrap"> Add from file </span>
|
<span class="no-wrap"> Add from file </span>
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,63 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
||||||
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="false"
|
v-if="false"
|
||||||
class="breadcrumbs__forward transparent"
|
class="breadcrumbs__forward transparent"
|
||||||
icon-only
|
icon-only
|
||||||
@click="$router.forward()"
|
@click="$router.forward()"
|
||||||
>
|
>
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="breadcrumb.link"
|
v-if="breadcrumb.link"
|
||||||
:to="{
|
:to="{
|
||||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
|
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
|
||||||
query: breadcrumb.query,
|
query: breadcrumb.query,
|
||||||
}"
|
}"
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
>{{
|
>{{
|
||||||
breadcrumb.name.charAt(0) === '?'
|
breadcrumb.name.charAt(0) === '?'
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||||
: breadcrumb.name
|
: breadcrumb.name
|
||||||
}}
|
}}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
class="text-contrast font-semibold cursor-default select-none"
|
class="text-contrast font-semibold cursor-default select-none"
|
||||||
>{{
|
>{{
|
||||||
breadcrumb.name.charAt(0) === '?'
|
breadcrumb.name.charAt(0) === '?'
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||||
: breadcrumb.name
|
: breadcrumb.name
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from '@modrinth/assets'
|
import { ChevronLeftIcon, ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button } from '@modrinth/ui'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const breadcrumbData = useBreadcrumbs()
|
const breadcrumbData = useBreadcrumbs()
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
const additionalContext =
|
const additionalContext =
|
||||||
route.meta.useContext === true
|
route.meta.useContext === true
|
||||||
? breadcrumbData.context
|
? breadcrumbData.context
|
||||||
: route.meta.useRootContext === true
|
: route.meta.useRootContext === true
|
||||||
? breadcrumbData.rootContext
|
? breadcrumbData.rootContext
|
||||||
: null
|
: null
|
||||||
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-show="shown"
|
v-show="shown"
|
||||||
ref="contextMenu"
|
ref="contextMenu"
|
||||||
class="context-menu"
|
class="context-menu"
|
||||||
:style="{
|
:style="{
|
||||||
left: left,
|
left: left,
|
||||||
top: top,
|
top: top,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||||
<hr v-if="option.type === 'divider'" class="divider" />
|
<hr v-if="option.type === 'divider'" class="divider" />
|
||||||
<div
|
<div
|
||||||
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
||||||
class="item clickable"
|
class="item clickable"
|
||||||
:class="[option.color ?? 'base']"
|
:class="[option.color ?? 'base']"
|
||||||
>
|
>
|
||||||
<slot :name="option.name" />
|
<slot :name="option.name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -36,141 +36,141 @@ const top = ref('0px')
|
|||||||
const shown = ref(false)
|
const shown = ref(false)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showMenu: (event, passedItem, passedOptions) => {
|
showMenu: (event, passedItem, passedOptions) => {
|
||||||
item.value = passedItem
|
item.value = passedItem
|
||||||
options.value = passedOptions
|
options.value = passedOptions
|
||||||
|
|
||||||
const menuWidth = contextMenu.value.clientWidth
|
const menuWidth = contextMenu.value.clientWidth
|
||||||
const menuHeight = contextMenu.value.clientHeight
|
const menuHeight = contextMenu.value.clientHeight
|
||||||
|
|
||||||
if (menuWidth + event.pageX >= window.innerWidth) {
|
if (menuWidth + event.pageX >= window.innerWidth) {
|
||||||
left.value = event.pageX - menuWidth + 2 + 'px'
|
left.value = event.pageX - menuWidth + 2 + 'px'
|
||||||
} else {
|
} else {
|
||||||
left.value = event.pageX - 2 + 'px'
|
left.value = event.pageX - 2 + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuHeight + event.pageY >= window.innerHeight) {
|
if (menuHeight + event.pageY >= window.innerHeight) {
|
||||||
top.value = event.pageY - menuHeight + 2 + 'px'
|
top.value = event.pageY - menuHeight + 2 + 'px'
|
||||||
} else {
|
} else {
|
||||||
top.value = event.pageY - 2 + 'px'
|
top.value = event.pageY - 2 + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
shown.value = true
|
shown.value = true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLinkedData = (item) => {
|
const isLinkedData = (item) => {
|
||||||
if (item.instance != undefined && item.instance.linked_data) {
|
if (item.instance != undefined && item.instance.linked_data) {
|
||||||
return true
|
return true
|
||||||
} else if (item != undefined && item.linked_data) {
|
} else if (item != undefined && item.linked_data) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
shown.value = false
|
shown.value = false
|
||||||
emit('menu-closed')
|
emit('menu-closed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionClicked = (option) => {
|
const optionClicked = (option) => {
|
||||||
emit('option-clicked', {
|
emit('option-clicked', {
|
||||||
item: item.value,
|
item: item.value,
|
||||||
option: option,
|
option: option,
|
||||||
})
|
})
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEscKeyRelease = (event) => {
|
const onEscKeyRelease = (event) => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
contextMenu.value &&
|
contextMenu.value &&
|
||||||
contextMenu.value.$el !== event.target &&
|
contextMenu.value.$el !== event.target &&
|
||||||
!elements.includes(contextMenu.value.$el)
|
!elements.includes(contextMenu.value.$el)
|
||||||
) {
|
) {
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', handleClickOutside)
|
window.addEventListener('click', handleClickOutside)
|
||||||
document.body.addEventListener('keyup', onEscKeyRelease)
|
document.body.addEventListener('keyup', onEscKeyRelease)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', handleClickOutside)
|
window.removeEventListener('click', handleClickOutside)
|
||||||
document.removeEventListener('keyup', onEscKeyRelease)
|
document.removeEventListener('keyup', onEscKeyRelease)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.context-menu {
|
.context-menu {
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
box-shadow: var(--shadow-floating);
|
box-shadow: var(--shadow-floating);
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
&.base {
|
&.base {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
background-color: var(--color-red);
|
background-color: var(--color-red);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.contrast {
|
&.contrast {
|
||||||
background-color: var(--color-orange);
|
background-color: var(--color-orange);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
margin: var(--gap-sm);
|
margin: var(--gap-sm);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
HammerIcon,
|
||||||
|
LogInIcon,
|
||||||
|
UpdatedIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { ChatIcon } from '@/assets/icons'
|
import { ChatIcon } from '@/assets/icons'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
@@ -6,17 +18,6 @@ import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
|||||||
import { install } from '@/helpers/profile.js'
|
import { install } from '@/helpers/profile.js'
|
||||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
CopyIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
HammerIcon,
|
|
||||||
LogInIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
@@ -31,111 +32,111 @@ const supportLink = ref('https://support.modrinth.com')
|
|||||||
const metadata = ref({})
|
const metadata = ref({})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(errorVal, context, canClose = true, source = null) {
|
async show(errorVal, context, canClose = true, source = null) {
|
||||||
closable.value = canClose
|
closable.value = canClose
|
||||||
|
|
||||||
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||||
title.value = 'Unable to sign in to Minecraft'
|
title.value = 'Unable to sign in to Minecraft'
|
||||||
errorType.value = 'minecraft_auth'
|
errorType.value = 'minecraft_auth'
|
||||||
supportLink.value =
|
supportLink.value =
|
||||||
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
||||||
|
|
||||||
if (
|
if (
|
||||||
errorVal.message.includes('existing connection was forcibly closed') ||
|
errorVal.message.includes('existing connection was forcibly closed') ||
|
||||||
errorVal.message.includes('error sending request for url')
|
errorVal.message.includes('error sending request for url')
|
||||||
) {
|
) {
|
||||||
metadata.value.network = true
|
metadata.value.network = true
|
||||||
}
|
}
|
||||||
if (errorVal.message.includes('because the target machine actively refused it')) {
|
if (errorVal.message.includes('because the target machine actively refused it')) {
|
||||||
metadata.value.hostsFile = true
|
metadata.value.hostsFile = true
|
||||||
}
|
}
|
||||||
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
|
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
|
||||||
title.value = 'Sign in to Minecraft'
|
title.value = 'Sign in to Minecraft'
|
||||||
errorType.value = 'minecraft_sign_in'
|
errorType.value = 'minecraft_sign_in'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
|
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
|
||||||
title.value = 'Could not change app directory'
|
title.value = 'Could not change app directory'
|
||||||
errorType.value = 'directory_move'
|
errorType.value = 'directory_move'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
|
|
||||||
if (errorVal.message.includes('directory is not writeable')) {
|
if (errorVal.message.includes('directory is not writeable')) {
|
||||||
metadata.value.readOnly = true
|
metadata.value.readOnly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorVal.message.includes('Not enough space')) {
|
if (errorVal.message.includes('Not enough space')) {
|
||||||
metadata.value.notEnoughSpace = true
|
metadata.value.notEnoughSpace = true
|
||||||
}
|
}
|
||||||
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
|
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
|
||||||
title.value = 'No loader selected'
|
title.value = 'No loader selected'
|
||||||
errorType.value = 'no_loader_version'
|
errorType.value = 'no_loader_version'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
metadata.value.profilePath = context.profilePath
|
metadata.value.profilePath = context.profilePath
|
||||||
} else if (source === 'state_init') {
|
} else if (source === 'state_init') {
|
||||||
title.value = 'Error initializing Modrinth App'
|
title.value = 'Error initializing Modrinth App'
|
||||||
errorType.value = 'state_init'
|
errorType.value = 'state_init'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
} else {
|
} else {
|
||||||
title.value = 'An error occurred'
|
title.value = 'An error occurred'
|
||||||
errorType.value = 'unknown'
|
errorType.value = 'unknown'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
metadata.value = {}
|
metadata.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
error.value = errorVal
|
error.value = errorVal
|
||||||
errorModal.value.show()
|
errorModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadingMinecraft = ref(false)
|
const loadingMinecraft = ref(false)
|
||||||
async function loginMinecraft() {
|
async function loginMinecraft() {
|
||||||
try {
|
try {
|
||||||
loadingMinecraft.value = true
|
loadingMinecraft.value = true
|
||||||
const loggedIn = await login_flow()
|
const loggedIn = await login_flow()
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await set_default_user(loggedIn.profile.id).catch(handleError)
|
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||||
loadingMinecraft.value = false
|
loadingMinecraft.value = false
|
||||||
errorModal.value.hide()
|
errorModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loadingMinecraft.value = false
|
loadingMinecraft.value = false
|
||||||
handleSevereError(err)
|
handleSevereError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelDirectoryChange() {
|
async function cancelDirectoryChange() {
|
||||||
try {
|
try {
|
||||||
await cancel_directory_change()
|
await cancel_directory_change()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retryDirectoryChange() {
|
function retryDirectoryChange() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingRepair = ref(false)
|
const loadingRepair = ref(false)
|
||||||
async function repairInstance() {
|
async function repairInstance() {
|
||||||
loadingRepair.value = true
|
loadingRepair.value = true
|
||||||
try {
|
try {
|
||||||
await install(metadata.value.profilePath, false)
|
await install(metadata.value.profilePath, false)
|
||||||
errorModal.value.hide()
|
errorModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleSevereError(err)
|
handleSevereError(err)
|
||||||
}
|
}
|
||||||
loadingRepair.value = false
|
loadingRepair.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDebugInfo = computed(
|
const hasDebugInfo = computed(
|
||||||
() =>
|
() =>
|
||||||
errorType.value === 'directory_move' ||
|
errorType.value === 'directory_move' ||
|
||||||
errorType.value === 'minecraft_auth' ||
|
errorType.value === 'minecraft_auth' ||
|
||||||
errorType.value === 'state_init' ||
|
errorType.value === 'state_init' ||
|
||||||
errorType.value === 'no_loader_version',
|
errorType.value === 'no_loader_version',
|
||||||
)
|
)
|
||||||
|
|
||||||
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
|
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
|
||||||
@@ -143,236 +144,236 @@ const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error
|
|||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
copied.value = true
|
copied.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied.value = false
|
copied.value = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<template v-if="errorType === 'minecraft_auth'">
|
<template v-if="errorType === 'minecraft_auth'">
|
||||||
<template v-if="metadata.network">
|
<template v-if="metadata.network">
|
||||||
<h3>Network issues</h3>
|
<h3>Network issues</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like there were issues with the Modrinth App connecting to Microsoft's
|
It looks like there were issues with the Modrinth App connecting to Microsoft's
|
||||||
servers. This is often the result of a poor connection, so we recommend trying again
|
servers. This is often the result of a poor connection, so we recommend trying again
|
||||||
to see if it works. If issues continue to persist, follow the steps in
|
to see if it works. If issues continue to persist, follow the steps in
|
||||||
<a
|
<a
|
||||||
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
||||||
>
|
>
|
||||||
our support article
|
our support article
|
||||||
</a>
|
</a>
|
||||||
to troubleshoot.
|
to troubleshoot.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="metadata.hostsFile">
|
<template v-else-if="metadata.hostsFile">
|
||||||
<h3>Network issues</h3>
|
<h3>Network issues</h3>
|
||||||
<p>
|
<p>
|
||||||
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
||||||
remote server rejected the connection. This may indicate that these services are
|
remote server rejected the connection. This may indicate that these services are
|
||||||
blocked by the hosts file. Please visit
|
blocked by the hosts file. Please visit
|
||||||
<a
|
<a
|
||||||
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
||||||
>
|
>
|
||||||
our support article
|
our support article
|
||||||
</a>
|
</a>
|
||||||
for steps on how to fix the issue.
|
for steps on how to fix the issue.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h3>Try another Microsoft account</h3>
|
<h3>Try another Microsoft account</h3>
|
||||||
<p>
|
<p>
|
||||||
Double check you've signed in with the right account. You may own Minecraft on a
|
Double check you've signed in with the right account. You may own Minecraft on a
|
||||||
different Microsoft account.
|
different Microsoft account.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Try another account
|
<LogInIcon /> Try another account
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
|
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
|
||||||
<p>
|
<p>
|
||||||
Try signing in with the
|
Try signing in with the
|
||||||
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
|
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
|
||||||
first. Once you're done, come back here and sign in!
|
first. Once you're done, come back here and sign in!
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Try signing in again
|
<LogInIcon /> Try signing in again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="errorType === 'directory_move'">
|
<template v-if="errorType === 'directory_move'">
|
||||||
<template v-if="metadata.readOnly">
|
<template v-if="metadata.readOnly">
|
||||||
<h3>Change directory permissions</h3>
|
<h3>Change directory permissions</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like the Modrinth App is unable to write to the directory you selected.
|
It looks like the Modrinth App is unable to write to the directory you selected.
|
||||||
Please adjust the permissions of the directory and try again or cancel the directory
|
Please adjust the permissions of the directory and try again or cancel the directory
|
||||||
change.
|
change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="metadata.notEnoughSpace">
|
<template v-else-if="metadata.notEnoughSpace">
|
||||||
<h3>Not enough space</h3>
|
<h3>Not enough space</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like there is not enough space on the disk containing the directory you
|
It looks like there is not enough space on the disk containing the directory you
|
||||||
selected. Please free up some space and try again or cancel the directory change.
|
selected. Please free up some space and try again or cancel the directory change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>
|
<p>
|
||||||
The Modrinth App is unable to migrate to the new directory you selected. Please
|
The Modrinth App is unable to migrate to the new directory you selected. Please
|
||||||
contact support for help or cancel the directory change.
|
contact support for help or cancel the directory change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn" @click="retryDirectoryChange">
|
<button class="btn" @click="retryDirectoryChange">
|
||||||
<UpdatedIcon /> Retry directory change
|
<UpdatedIcon /> Retry directory change
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @click="cancelDirectoryChange">
|
<button class="btn btn-danger" @click="cancelDirectoryChange">
|
||||||
<XIcon /> Cancel directory change
|
<XIcon /> Cancel directory change
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="errorType === 'minecraft_sign_in'">
|
<div v-else-if="errorType === 'minecraft_sign_in'">
|
||||||
<p>
|
<p>
|
||||||
To play this instance, you must sign in through Microsoft below. If you don't have a
|
To play this instance, you must sign in through Microsoft below. If you don't have a
|
||||||
Minecraft account, you can purchase the game on the
|
Minecraft account, you can purchase the game on the
|
||||||
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
|
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
|
||||||
>Minecraft website</a
|
>Minecraft website</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Sign in to Minecraft
|
<LogInIcon /> Sign in to Minecraft
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-else-if="errorType === 'state_init'">
|
<template v-else-if="errorType === 'state_init'">
|
||||||
<p>
|
<p>
|
||||||
Modrinth App failed to load correctly. This may be because of a corrupted file, or
|
Modrinth App failed to load correctly. This may be because of a corrupted file, or
|
||||||
because the app is missing crucial files.
|
because the app is missing crucial files.
|
||||||
</p>
|
</p>
|
||||||
<p>You may be able to fix it through one of the following ways:</p>
|
<p>You may be able to fix it through one of the following ways:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
||||||
<li>Redownloading the app.</li>
|
<li>Redownloading the app.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="errorType === 'no_loader_version'">
|
<template v-else-if="errorType === 'no_loader_version'">
|
||||||
<p>The Modrinth App failed to find the loader version for this instance.</p>
|
<p>The Modrinth App failed to find the loader version for this instance.</p>
|
||||||
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
|
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
|
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
|
||||||
<HammerIcon /> Repair instance
|
<HammerIcon /> Repair instance
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ debugInfo }}
|
{{ debugInfo }}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="hasDebugInfo">
|
<template v-if="hasDebugInfo">
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
If nothing is working and you need help, visit
|
If nothing is working and you need help, visit
|
||||||
<a :href="supportLink">our support page</a>
|
<a :href="supportLink">our support page</a>
|
||||||
and start a chat using the widget in the bottom right and we will be more than happy to
|
and start a chat using the widget in the bottom right and we will be more than happy to
|
||||||
assist! Make sure to provide the following debug information to the agent:
|
assist! Make sure to provide the following debug information to the agent:
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="closable">
|
<ButtonStyled v-if="closable">
|
||||||
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="hasDebugInfo">
|
<ButtonStyled v-if="hasDebugInfo">
|
||||||
<button :disabled="copied" @click="copyToClipboard(debugInfo)">
|
<button :disabled="copied" @click="copyToClipboard(debugInfo)">
|
||||||
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
||||||
<template v-else> <CopyIcon /> Copy debug info </template>
|
<template v-else> <CopyIcon /> Copy debug info </template>
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="hasDebugInfo">
|
<template v-if="hasDebugInfo">
|
||||||
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
|
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
||||||
@click="errorCollapsed = !errorCollapsed"
|
@click="errorCollapsed = !errorCollapsed"
|
||||||
>
|
>
|
||||||
<span class="text-contrast font-extrabold m-0">Debug information:</span>
|
<span class="text-contrast font-extrabold m-0">Debug information:</span>
|
||||||
<DropdownIcon
|
<DropdownIcon
|
||||||
class="h-5 w-5 text-secondary transition-transform"
|
class="h-5 w-5 text-secondary transition-transform"
|
||||||
:class="{ 'rotate-180': !errorCollapsed }"
|
:class="{ 'rotate-180': !errorCollapsed }"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Collapsible :collapsed="errorCollapsed">
|
<Collapsible :collapsed="errorCollapsed">
|
||||||
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
|
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.light-mode {
|
.light-mode {
|
||||||
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode,
|
.dark-mode,
|
||||||
.oled-mode {
|
.oled-mode {
|
||||||
--color-orange-bg: rgba(224, 131, 37, 0.2);
|
--color-orange-bg: rgba(224, 131, 37, 0.2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.cta-button {
|
.cta-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner {
|
.warning-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: var(--gap-lg);
|
padding: var(--gap-lg);
|
||||||
background-color: var(--color-orange-bg);
|
background-color: var(--color-orange-bg);
|
||||||
border: 2px solid var(--color-orange);
|
border: 2px solid var(--color-orange);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner__title {
|
.warning-banner__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-orange);
|
color: var(--color-orange);
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
|
||||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
exportModal.value.show()
|
exportModal.value.show()
|
||||||
initFiles()
|
initFiles()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const exportModal = ref(null)
|
const exportModal = ref(null)
|
||||||
@@ -32,273 +33,273 @@ const folders = ref([])
|
|||||||
const showingFiles = ref(false)
|
const showingFiles = ref(false)
|
||||||
|
|
||||||
const initFiles = async () => {
|
const initFiles = async () => {
|
||||||
const newFolders = new Map()
|
const newFolders = new Map()
|
||||||
const sep = '/'
|
const sep = '/'
|
||||||
files.value = []
|
files.value = []
|
||||||
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
||||||
filePaths
|
filePaths
|
||||||
.map((folder) => ({
|
.map((folder) => ({
|
||||||
path: folder,
|
path: folder,
|
||||||
name: folder.split(sep).pop(),
|
name: folder.split(sep).pop(),
|
||||||
selected:
|
selected:
|
||||||
folder.startsWith('mods') ||
|
folder.startsWith('mods') ||
|
||||||
folder.startsWith('datapacks') ||
|
folder.startsWith('datapacks') ||
|
||||||
folder.startsWith('resourcepacks') ||
|
folder.startsWith('resourcepacks') ||
|
||||||
folder.startsWith('shaderpacks') ||
|
folder.startsWith('shaderpacks') ||
|
||||||
folder.startsWith('config'),
|
folder.startsWith('config'),
|
||||||
disabled:
|
disabled:
|
||||||
folder === 'profile.json' ||
|
folder === 'profile.json' ||
|
||||||
folder.startsWith('modrinth_logs') ||
|
folder.startsWith('modrinth_logs') ||
|
||||||
folder.startsWith('.fabric'),
|
folder.startsWith('.fabric'),
|
||||||
}))
|
}))
|
||||||
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
||||||
.forEach((pathData) => {
|
.forEach((pathData) => {
|
||||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||||
if (parent !== '') {
|
if (parent !== '') {
|
||||||
if (newFolders.has(parent)) {
|
if (newFolders.has(parent)) {
|
||||||
newFolders.get(parent).push(pathData)
|
newFolders.get(parent).push(pathData)
|
||||||
} else {
|
} else {
|
||||||
newFolders.set(parent, [pathData])
|
newFolders.set(parent, [pathData])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
files.value.push(pathData)
|
files.value.push(pathData)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
showingMore: false,
|
showingMore: false,
|
||||||
},
|
},
|
||||||
value,
|
value,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
await initFiles()
|
await initFiles()
|
||||||
|
|
||||||
const exportPack = async () => {
|
const exportPack = async () => {
|
||||||
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
||||||
folders.value.forEach((args) => {
|
folders.value.forEach((args) => {
|
||||||
args[1].forEach((child) => {
|
args[1].forEach((child) => {
|
||||||
if (child.selected) {
|
if (child.selected) {
|
||||||
filesToExport.push(child.path)
|
filesToExport.push(child.path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const outputPath = await open({
|
const outputPath = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (outputPath) {
|
if (outputPath) {
|
||||||
export_profile_mrpack(
|
export_profile_mrpack(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
||||||
filesToExport,
|
filesToExport,
|
||||||
versionInput.value,
|
versionInput.value,
|
||||||
exportDescription.value,
|
exportDescription.value,
|
||||||
nameInput.value,
|
nameInput.value,
|
||||||
).catch((err) => handleError(err))
|
).catch((err) => handleError(err))
|
||||||
exportModal.value.hide()
|
exportModal.value.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="exportModal" header="Export modpack">
|
<ModalWrapper ref="exportModal" header="Export modpack">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="labeled_input">
|
<div class="labeled_input">
|
||||||
<p>Modpack Name</p>
|
<p>Modpack Name</p>
|
||||||
<div class="iconified-input">
|
<div class="iconified-input">
|
||||||
<PackageIcon />
|
<PackageIcon />
|
||||||
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
||||||
<Button class="r-btn" @click="nameInput = ''">
|
<Button class="r-btn" @click="nameInput = ''">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="labeled_input">
|
<div class="labeled_input">
|
||||||
<p>Version number</p>
|
<p>Version number</p>
|
||||||
<div class="iconified-input">
|
<div class="iconified-input">
|
||||||
<VersionIcon />
|
<VersionIcon />
|
||||||
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
||||||
<Button class="r-btn" @click="versionInput = ''">
|
<Button class="r-btn" @click="versionInput = ''">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="adjacent-input">
|
<div class="adjacent-input">
|
||||||
<div class="labeled_input">
|
<div class="labeled_input">
|
||||||
<p>Description</p>
|
<p>Description</p>
|
||||||
|
|
||||||
<div class="textarea-wrapper">
|
<div class="textarea-wrapper">
|
||||||
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-head">
|
<div class="table-head">
|
||||||
<div class="table-cell row-wise">
|
<div class="table-cell row-wise">
|
||||||
Select files and folders to include in pack
|
Select files and folders to include in pack
|
||||||
<Button
|
<Button
|
||||||
class="sleek-primary collapsed-button"
|
class="sleek-primary collapsed-button"
|
||||||
icon-only
|
icon-only
|
||||||
@click="() => (showingFiles = !showingFiles)"
|
@click="() => (showingFiles = !showingFiles)"
|
||||||
>
|
>
|
||||||
<PlusIcon v-if="!showingFiles" />
|
<PlusIcon v-if="!showingFiles" />
|
||||||
<XIcon v-else />
|
<XIcon v-else />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showingFiles" class="table-content">
|
<div v-if="showingFiles" class="table-content">
|
||||||
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
||||||
<div class="table-cell file-entry">
|
<div class="table-cell file-entry">
|
||||||
<div class="file-primary">
|
<div class="file-primary">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="children.every((child) => child.selected)"
|
:model-value="children.every((child) => child.selected)"
|
||||||
:label="path.name"
|
:label="path.name"
|
||||||
class="select-checkbox"
|
class="select-checkbox"
|
||||||
:disabled="children.every((x) => x.disabled)"
|
:disabled="children.every((x) => x.disabled)"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="path.showingMore"
|
v-model="path.showingMore"
|
||||||
class="select-checkbox dropdown"
|
class="select-checkbox dropdown"
|
||||||
collapsing-toggle-style
|
collapsing-toggle-style
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="path.showingMore" class="file-secondary">
|
<div v-if="path.showingMore" class="file-secondary">
|
||||||
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="child.selected"
|
v-model="child.selected"
|
||||||
:label="child.name"
|
:label="child.name"
|
||||||
class="select-checkbox"
|
class="select-checkbox"
|
||||||
:disabled="child.disabled"
|
:disabled="child.disabled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="file in files" :key="file.path" class="table-row">
|
<div v-for="file in files" :key="file.path" class="table-row">
|
||||||
<div class="table-cell file-entry">
|
<div class="table-cell file-entry">
|
||||||
<div class="file-primary">
|
<div class="file-primary">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="file.selected"
|
v-model="file.selected"
|
||||||
:label="file.name"
|
:label="file.name"
|
||||||
:disabled="file.disabled"
|
:disabled="file.disabled"
|
||||||
class="select-checkbox"
|
class="select-checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row push-right">
|
<div class="button-row push-right">
|
||||||
<Button @click="exportModal.hide">
|
<Button @click="exportModal.hide">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" @click="exportPack">
|
<Button color="primary" @click="exportPack">
|
||||||
<PackageIcon />
|
<PackageIcon />
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.labeled_input {
|
.labeled_input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-checkbox {
|
.select-checkbox {
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
|
|
||||||
button.checkbox {
|
button.checkbox {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dropdown {
|
&.dropdown {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-content {
|
.table-content {
|
||||||
max-height: 18rem;
|
max-height: 18rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
border: 1px solid var(--color-bg);
|
border: 1px solid var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-entry {
|
.file-entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-primary {
|
.file-primary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-secondary {
|
.file-secondary {
|
||||||
margin-left: var(--gap-xl);
|
margin-left: var(--gap-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-secondary-row {
|
.file-secondary-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-wise {
|
.row-wise {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-wrapper {
|
.textarea-wrapper {
|
||||||
// margin-top: 1rem;
|
// margin-top: 1rem;
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
max-height: 12rem;
|
max-height: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { process_listener } from '@/helpers/events'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { finish_install, kill, run } from '@/helpers/profile'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
@@ -19,33 +13,40 @@ import dayjs from 'dayjs'
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { finish_install, kill, run } from '@/helpers/profile'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
first: {
|
first: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const modLoading = computed(
|
const modLoading = computed(
|
||||||
() =>
|
() =>
|
||||||
loading.value ||
|
loading.value ||
|
||||||
currentEvent.value === 'installing' ||
|
currentEvent.value === 'installing' ||
|
||||||
(currentEvent.value === 'launched' && !playing.value),
|
(currentEvent.value === 'launched' && !playing.value),
|
||||||
)
|
)
|
||||||
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
||||||
const installed = computed(() => props.instance.install_stage === 'installed')
|
const installed = computed(() => props.instance.install_stage === 'installed')
|
||||||
@@ -53,78 +54,78 @@ const installed = computed(() => props.instance.install_stage === 'installed')
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const seeInstance = async () => {
|
const seeInstance = async () => {
|
||||||
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkProcess = async () => {
|
const checkProcess = async () => {
|
||||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
playing.value = runningProcesses.length > 0
|
playing.value = runningProcesses.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const play = async (e, context) => {
|
const play = async (e, context) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await run(props.instance.path)
|
await run(props.instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstancePlay', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: context,
|
source: context,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async (e, context) => {
|
const stop = async (e, context) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
playing.value = false
|
playing.value = false
|
||||||
|
|
||||||
await kill(props.instance.path).catch(handleError)
|
await kill(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: context,
|
source: context,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const repair = async (e) => {
|
const repair = async (e) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
|
|
||||||
await finish_install(props.instance)
|
await finish_install(props.instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFolder = async () => {
|
const openFolder = async () => {
|
||||||
await showProfileInFolder(props.instance.path)
|
await showProfileInFolder(props.instance.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addContent = async () => {
|
const addContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
play,
|
play,
|
||||||
stop,
|
stop,
|
||||||
seeInstance,
|
seeInstance,
|
||||||
openFolder,
|
openFolder,
|
||||||
addContent,
|
addContent,
|
||||||
instance: props.instance,
|
instance: props.instance,
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentEvent = ref(null)
|
const currentEvent = ref(null)
|
||||||
|
|
||||||
const unlisten = await process_listener((e) => {
|
const unlisten = await process_listener((e) => {
|
||||||
if (e.profile_path_id === props.instance.path) {
|
if (e.profile_path_id === props.instance.path) {
|
||||||
currentEvent.value = e.event
|
currentEvent.value = e.event
|
||||||
if (e.event === 'finished') {
|
if (e.event === 'finished') {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => checkProcess())
|
onMounted(() => checkProcess())
|
||||||
@@ -132,118 +133,118 @@ onUnmounted(() => unlisten())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="compact">
|
<template v-if="compact">
|
||||||
<div
|
<div
|
||||||
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="seeInstance"
|
@click="seeInstance"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="48px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
/>
|
/>
|
||||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||||
<button v-tooltip="'Instance is loading...'" disabled>
|
<button v-tooltip="'Instance is loading...'" disabled>
|
||||||
<SpinnerIcon class="animate-spin" />
|
<SpinnerIcon class="animate-spin" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Play'"
|
v-tooltip="'Play'"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<!-- Translate for optical centering -->
|
<!-- Translate for optical centering -->
|
||||||
<PlayIcon class="translate-x-[1px]" />
|
<PlayIcon class="translate-x-[1px]" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||||
<TimerIcon />
|
<TimerIcon />
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
<template v-if="instance.last_played">
|
<template v-if="instance.last_played">
|
||||||
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else> Never played </template>
|
<template v-else> Never played </template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
||||||
@click="seeInstance"
|
@click="seeInstance"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="48px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Stop'"
|
v-tooltip="'Stop'"
|
||||||
:class="{ 'scale-100 opacity-100': playing }"
|
:class="{ 'scale-100 opacity-100': playing }"
|
||||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||||
@click="(e) => stop(e, 'InstanceCard')"
|
@click="(e) => stop(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<SpinnerIcon
|
<SpinnerIcon
|
||||||
v-else-if="modLoading || installing"
|
v-else-if="modLoading || installing"
|
||||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||||
class="animate-spin w-8 h-8"
|
class="animate-spin w-8 h-8"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Repair'"
|
v-tooltip="'Repair'"
|
||||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||||
@click="(e) => repair(e)"
|
@click="(e) => repair(e)"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else size="large" color="brand" circular>
|
<ButtonStyled v-else size="large" color="brand" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Play'"
|
v-tooltip="'Play'"
|
||||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<PlayIcon class="translate-x-[2px]" />
|
<PlayIcon class="translate-x-[2px]" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||||
<GameIcon class="shrink-0" />
|
<GameIcon class="shrink-0" />
|
||||||
<span class="text-sm capitalize">
|
<span class="text-sm capitalize">
|
||||||
{{ instance.loader }} {{ instance.game_version }}
|
{{ instance.loader }} {{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +1,53 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
|
||||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||||
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
game_version: string
|
game_version: string
|
||||||
loader: string
|
loader: string
|
||||||
path: string
|
path: string
|
||||||
install_stage: string
|
install_stage: string
|
||||||
icon_path?: string
|
icon_path?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
instance: Instance
|
instance: Instance
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class="flex flex-col gap-4 text-primary"
|
class="flex flex-col gap-4 text-primary"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
:alt="instance.name"
|
:alt="instance.name"
|
||||||
size="48px"
|
size="48px"
|
||||||
/>
|
/>
|
||||||
<span class="flex flex-col gap-2">
|
<span class="flex flex-col gap-2">
|
||||||
<span class="font-extrabold bold text-contrast">
|
<span class="font-extrabold bold text-contrast">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-secondary flex items-center gap-2 font-semibold">
|
<span class="text-secondary flex items-center gap-2 font-semibold">
|
||||||
<GameIcon class="h-5 w-5 text-secondary" />
|
<GameIcon class="h-5 w-5 text-secondary" />
|
||||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||||
<LeftArrowIcon /> Back to instance
|
<LeftArrowIcon /> Back to instance
|
||||||
</router-link>
|
</router-link>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
||||||
<div class="auto-detect-modal">
|
<div class="auto-detect-modal">
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-row table-head">
|
<div class="table-row table-head">
|
||||||
<div class="table-cell table-text">Version</div>
|
<div class="table-cell table-text">Version</div>
|
||||||
<div class="table-cell table-text">Path</div>
|
<div class="table-cell table-text">Path</div>
|
||||||
<div class="table-cell table-text">Actions</div>
|
<div class="table-cell table-text">Actions</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
|
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
|
||||||
<div class="table-cell table-text">
|
<div class="table-cell table-text">
|
||||||
<span>{{ javaInstall.version }}</span>
|
<span>{{ javaInstall.version }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-tooltip="javaInstall.path" class="table-cell table-text">
|
<div v-tooltip="javaInstall.path" class="table-cell table-text">
|
||||||
<span>{{ javaInstall.path }}</span>
|
<span>{{ javaInstall.path }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-cell table-text manage">
|
<div class="table-cell table-text manage">
|
||||||
<Button v-if="currentSelected.path === javaInstall.path" disabled
|
<Button v-if="currentSelected.path === javaInstall.path" disabled
|
||||||
><CheckIcon /> Selected</Button
|
><CheckIcon /> Selected</Button
|
||||||
>
|
>
|
||||||
<Button v-else @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</Button>
|
<Button v-else @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
||||||
<div class="table-cell table-text">No java installations found!</div>
|
<div class="table-cell table-text">No java installations found!</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button @click="$refs.detectJavaModal.hide()">
|
<Button @click="$refs.detectJavaModal.hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
|
||||||
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const chosenInstallOptions = ref([])
|
const chosenInstallOptions = ref([])
|
||||||
@@ -49,48 +50,48 @@ const detectJavaModal = ref(null)
|
|||||||
const currentSelected = ref({})
|
const currentSelected = ref({})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: async (version, currentSelectedJava) => {
|
show: async (version, currentSelectedJava) => {
|
||||||
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
|
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
|
||||||
|
|
||||||
currentSelected.value = currentSelectedJava
|
currentSelected.value = currentSelectedJava
|
||||||
if (!currentSelected.value) {
|
if (!currentSelected.value) {
|
||||||
currentSelected.value = { path: '', version: '' }
|
currentSelected.value = { path: '', version: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
detectJavaModal.value.show()
|
detectJavaModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['submit'])
|
const emit = defineEmits(['submit'])
|
||||||
|
|
||||||
function setJavaInstall(javaInstall) {
|
function setJavaInstall(javaInstall) {
|
||||||
emit('submit', javaInstall)
|
emit('submit', javaInstall)
|
||||||
detectJavaModal.value.hide()
|
detectJavaModal.value.hide()
|
||||||
trackEvent('JavaAutoDetect', {
|
trackEvent('JavaAutoDetect', {
|
||||||
path: javaInstall.path,
|
path: javaInstall.path,
|
||||||
version: javaInstall.version,
|
version: javaInstall.version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.auto-detect-modal {
|
.auto-detect-modal {
|
||||||
.table {
|
.table {
|
||||||
.table-row {
|
.table-row {
|
||||||
grid-template-columns: 1fr 4fr min-content;
|
grid-template-columns: 1fr 4fr min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inherit;
|
display: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.manage {
|
.manage {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,101 +1,102 @@
|
|||||||
<template>
|
<template>
|
||||||
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
||||||
<div class="toggle-setting" :class="{ compact }">
|
<div class="toggle-setting" :class="{ compact }">
|
||||||
<input
|
<input
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:value="props.modelValue ? props.modelValue.path : ''"
|
:value="props.modelValue ? props.modelValue.path : ''"
|
||||||
type="text"
|
type="text"
|
||||||
class="installation-input"
|
class="installation-input"
|
||||||
:placeholder="placeholder ?? '/path/to/java'"
|
:placeholder="placeholder ?? '/path/to/java'"
|
||||||
@input="
|
@input="
|
||||||
(val) => {
|
(val) => {
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
...props.modelValue,
|
...props.modelValue,
|
||||||
path: val.target.value,
|
path: val.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<span class="installation-buttons">
|
<span class="installation-buttons">
|
||||||
<Button
|
<Button
|
||||||
v-if="props.version"
|
v-if="props.version"
|
||||||
:disabled="props.disabled || installingJava"
|
:disabled="props.disabled || installingJava"
|
||||||
@click="reinstallJava"
|
@click="reinstallJava"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="props.disabled" @click="autoDetect">
|
<Button :disabled="props.disabled" @click="autoDetect">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
Detect
|
Detect
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||||
<FolderSearchIcon />
|
<FolderSearchIcon />
|
||||||
Browse
|
Browse
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="testingJava" disabled> Testing... </Button>
|
<Button v-if="testingJava" disabled> Testing... </Button>
|
||||||
<Button v-else-if="testingJavaSuccess === true">
|
<Button v-else-if="testingJavaSuccess === true">
|
||||||
<CheckIcon class="test-success" />
|
<CheckIcon class="test-success" />
|
||||||
Success
|
Success
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else-if="testingJavaSuccess === false">
|
<Button v-else-if="testingJavaSuccess === false">
|
||||||
<XIcon class="test-fail" />
|
<XIcon class="test-fail" />
|
||||||
Failed
|
Failed
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else :disabled="props.disabled" @click="testJava">
|
<Button v-else :disabled="props.disabled" @click="testJava">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
FolderSearchIcon,
|
FolderSearchIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
path: '',
|
path: '',
|
||||||
version: '',
|
version: '',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
@@ -106,115 +107,115 @@ const testingJavaSuccess = ref(null)
|
|||||||
const installingJava = ref(false)
|
const installingJava = ref(false)
|
||||||
|
|
||||||
async function testJava() {
|
async function testJava() {
|
||||||
testingJava.value = true
|
testingJava.value = true
|
||||||
testingJavaSuccess.value = await test_jre(
|
testingJavaSuccess.value = await test_jre(
|
||||||
props.modelValue ? props.modelValue.path : '',
|
props.modelValue ? props.modelValue.path : '',
|
||||||
props.version,
|
props.version,
|
||||||
)
|
)
|
||||||
testingJava.value = false
|
testingJava.value = false
|
||||||
|
|
||||||
trackEvent('JavaTest', {
|
trackEvent('JavaTest', {
|
||||||
path: props.modelValue ? props.modelValue.path : '',
|
path: props.modelValue ? props.modelValue.path : '',
|
||||||
success: testingJavaSuccess.value,
|
success: testingJavaSuccess.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
testingJavaSuccess.value = null
|
testingJavaSuccess.value = null
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleJavaFileInput() {
|
async function handleJavaFileInput() {
|
||||||
const filePath = await open()
|
const filePath = await open()
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: filePath.path ?? filePath,
|
path: filePath.path ?? filePath,
|
||||||
version: props.version.toString(),
|
version: props.version.toString(),
|
||||||
architecture: 'x86',
|
architecture: 'x86',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('JavaManualSelect', {
|
trackEvent('JavaManualSelect', {
|
||||||
version: props.version,
|
version: props.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', result)
|
emit('update:modelValue', result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
async function autoDetect() {
|
async function autoDetect() {
|
||||||
if (!props.compact) {
|
if (!props.compact) {
|
||||||
detectJavaModal.value.show(props.version, props.modelValue)
|
detectJavaModal.value.show(props.version, props.modelValue)
|
||||||
} else {
|
} else {
|
||||||
const versions = await find_filtered_jres(props.version).catch(handleError)
|
const versions = await find_filtered_jres(props.version).catch(handleError)
|
||||||
if (versions.length > 0) {
|
if (versions.length > 0) {
|
||||||
emit('update:modelValue', versions[0])
|
emit('update:modelValue', versions[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reinstallJava() {
|
async function reinstallJava() {
|
||||||
installingJava.value = true
|
installingJava.value = true
|
||||||
const path = await auto_install_java(props.version).catch(handleError)
|
const path = await auto_install_java(props.version).catch(handleError)
|
||||||
let result = await get_jre(path)
|
let result = await get_jre(path)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: path,
|
path: path,
|
||||||
version: props.version.toString(),
|
version: props.version.toString(),
|
||||||
architecture: 'x86',
|
architecture: 'x86',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('JavaReInstall', {
|
trackEvent('JavaReInstall', {
|
||||||
path: path,
|
path: path,
|
||||||
version: props.version,
|
version: props.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', result)
|
emit('update:modelValue', result)
|
||||||
installingJava.value = false
|
installingJava.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.installation-input {
|
.installation-input {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-setting {
|
.toggle-setting {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.installation-buttons {
|
.installation-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-success {
|
.test-success {
|
||||||
color: var(--color-green);
|
color: var(--color-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-fail {
|
.test-fail {
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { CheckIcon } from '@modrinth/assets'
|
import { CheckIcon } from '@modrinth/assets'
|
||||||
import { Button, Badge } from '@modrinth/ui'
|
import { Badge, Button } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
|
||||||
import { releaseColor } from '@/helpers/utils'
|
|
||||||
import { SwapIcon } from '@/assets/icons/index.js'
|
import { SwapIcon } from '@/assets/icons/index.js'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||||
|
import { releaseColor } from '@/helpers/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
modpackVersionModal.value.show()
|
modpackVersionModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['finish-install'])
|
const emit = defineEmits(['finish-install'])
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
return props.versions
|
return props.versions
|
||||||
})
|
})
|
||||||
|
|
||||||
const modpackVersionModal = ref(null)
|
const modpackVersionModal = ref(null)
|
||||||
@@ -36,160 +37,160 @@ const installing = computed(() => props.instance.install_stage !== 'installed')
|
|||||||
const inProgress = ref(false)
|
const inProgress = ref(false)
|
||||||
|
|
||||||
const switchVersion = async (versionId) => {
|
const switchVersion = async (versionId) => {
|
||||||
modpackVersionModal.value.hide()
|
modpackVersionModal.value.hide()
|
||||||
inProgress.value = true
|
inProgress.value = true
|
||||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||||
inProgress.value = false
|
inProgress.value = false
|
||||||
emit('finish-install')
|
emit('finish-install')
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHide = () => {
|
const onHide = () => {
|
||||||
if (!inProgress.value) {
|
if (!inProgress.value) {
|
||||||
emit('finish-install')
|
emit('finish-install')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper
|
<ModalWrapper
|
||||||
ref="modpackVersionModal"
|
ref="modpackVersionModal"
|
||||||
class="modpack-version-modal"
|
class="modpack-version-modal"
|
||||||
header="Change modpack version"
|
header="Change modpack version"
|
||||||
:on-hide="onHide"
|
:on-hide="onHide"
|
||||||
>
|
>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div v-if="instance.linked_data" class="mod-card">
|
<div v-if="instance.linked_data" class="mod-card">
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-row with-columns table-head">
|
<div class="table-row with-columns table-head">
|
||||||
<div class="table-cell table-text download-cell" />
|
<div class="table-cell table-text download-cell" />
|
||||||
<div class="name-cell table-cell table-text">Name</div>
|
<div class="name-cell table-cell table-text">Name</div>
|
||||||
<div class="table-cell table-text">Supports</div>
|
<div class="table-cell table-text">Supports</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
<div
|
<div
|
||||||
v-for="version in filteredVersions"
|
v-for="version in filteredVersions"
|
||||||
:key="version.id"
|
:key="version.id"
|
||||||
class="table-row with-columns selectable"
|
class="table-row with-columns selectable"
|
||||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||||
>
|
>
|
||||||
<div class="table-cell table-text">
|
<div class="table-cell table-text">
|
||||||
<Button
|
<Button
|
||||||
:color="version.id === installedVersion ? '' : 'primary'"
|
:color="version.id === installedVersion ? '' : 'primary'"
|
||||||
icon-only
|
icon-only
|
||||||
:disabled="inProgress || installing || version.id === installedVersion"
|
:disabled="inProgress || installing || version.id === installedVersion"
|
||||||
@click.stop="() => switchVersion(version.id)"
|
@click.stop="() => switchVersion(version.id)"
|
||||||
>
|
>
|
||||||
<SwapIcon v-if="version.id !== installedVersion" />
|
<SwapIcon v-if="version.id !== installedVersion" />
|
||||||
<CheckIcon v-else />
|
<CheckIcon v-else />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="name-cell table-cell table-text">
|
<div class="name-cell table-cell table-text">
|
||||||
<div class="version-link">
|
<div class="version-link">
|
||||||
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
||||||
<div class="version-badge">
|
<div class="version-badge">
|
||||||
<div class="channel-indicator">
|
<div class="channel-indicator">
|
||||||
<Badge
|
<Badge
|
||||||
:color="releaseColor(version.version_type)"
|
:color="releaseColor(version.version_type)"
|
||||||
:type="
|
:type="
|
||||||
version.version_type.charAt(0).toUpperCase() +
|
version.version_type.charAt(0).toUpperCase() +
|
||||||
version.version_type.slice(1)
|
version.version_type.slice(1)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ version.version_number }}
|
{{ version.version_number }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-cell table-text stacked-text">
|
<div class="table-cell table-text stacked-text">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
version.loaders
|
version.loaders
|
||||||
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
||||||
.join(', ')
|
.join(', ')
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ version.game_versions.join(', ') }}
|
{{ version.game_versions.join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.filter-header {
|
.filter-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-columns {
|
.with-columns {
|
||||||
grid-template-columns: min-content 1fr 1fr;
|
grid-template-columns: min-content 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 25rem;
|
max-height: 25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-row {
|
.card-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-card {
|
.mod-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-link {
|
.version-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.version-badge {
|
.version-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.channel-indicator {
|
.channel-indicator {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stacked-text {
|
.stacked-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-cell {
|
.download-cell {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
border: 1px solid var(--color-bg);
|
border: 1px solid var(--color-bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="typeof to === 'string'"
|
v-if="typeof to === 'string'"
|
||||||
:to="to"
|
:to="to"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:class="{
|
:class="{
|
||||||
'router-link-active': isPrimary && isPrimary(route),
|
'router-link-active': isPrimary && isPrimary(route),
|
||||||
'subpage-active': isSubpage && isSubpage(route),
|
'subpage-active': isSubpage && isSubpage(route),
|
||||||
}"
|
}"
|
||||||
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
@click="to"
|
@click="to"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -30,30 +30,30 @@ const route = useRoute()
|
|||||||
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
to: (() => void) | string
|
to: (() => void) | string
|
||||||
isPrimary?: RouteFunction
|
isPrimary?: RouteFunction
|
||||||
isSubpage?: RouteFunction
|
isSubpage?: RouteFunction
|
||||||
highlightOverride?: boolean
|
highlightOverride?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.router-link-active,
|
.router-link-active,
|
||||||
.subpage-active {
|
.subpage-active {
|
||||||
svg {
|
svg {
|
||||||
filter: drop-shadow(0 0 0.5rem black);
|
filter: drop-shadow(0 0 0.5rem black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-active {
|
.subpage-active {
|
||||||
@apply text-contrast bg-button-bg;
|
@apply text-contrast bg-button-bg;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav
|
<nav
|
||||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||||
>
|
>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-for="(link, index) in filteredLinks"
|
v-for="(link, index) in filteredLinks"
|
||||||
v-show="link.shown === undefined ? true : link.shown"
|
v-show="link.shown === undefined ? true : link.shown"
|
||||||
:key="index"
|
:key="index"
|
||||||
ref="tabLinkElements"
|
ref="tabLinkElements"
|
||||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||||
:class="`button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
|
:class="`button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
|
||||||
>
|
>
|
||||||
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
||||||
<span class="text-nowrap">{{ link.label }}</span>
|
<span class="text-nowrap">{{ link.label }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div
|
<div
|
||||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
||||||
:style="{
|
:style="{
|
||||||
left: sliderLeftPx,
|
left: sliderLeftPx,
|
||||||
top: sliderTopPx,
|
top: sliderTopPx,
|
||||||
right: sliderRightPx,
|
right: sliderRightPx,
|
||||||
bottom: sliderBottomPx,
|
bottom: sliderBottomPx,
|
||||||
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
||||||
}"
|
}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
></div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import { useRoute, RouterLink } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
label: string
|
label: string
|
||||||
href: string | RouteLocationRaw
|
href: string | RouteLocationRaw
|
||||||
shown?: boolean
|
shown?: boolean
|
||||||
icon?: unknown
|
icon?: unknown
|
||||||
subpages?: string[]
|
subpages?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
links: Tab[]
|
links: Tab[]
|
||||||
query?: string
|
query?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const sliderLeft = ref(4)
|
const sliderLeft = ref(4)
|
||||||
@@ -56,7 +56,7 @@ const oldIndex = ref(-1)
|
|||||||
const subpageSelected = ref(false)
|
const subpageSelected = ref(false)
|
||||||
|
|
||||||
const filteredLinks = computed(() =>
|
const filteredLinks = computed(() =>
|
||||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||||
)
|
)
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||||
@@ -64,97 +64,97 @@ const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
|||||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||||
|
|
||||||
function pickLink() {
|
function pickLink() {
|
||||||
let index = -1
|
let index = -1
|
||||||
subpageSelected.value = false
|
subpageSelected.value = false
|
||||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||||
const link = filteredLinks.value[i]
|
const link = filteredLinks.value[i]
|
||||||
|
|
||||||
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
||||||
index = i
|
index = i
|
||||||
break
|
break
|
||||||
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
||||||
index = i
|
index = i
|
||||||
subpageSelected.value = true
|
subpageSelected.value = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeIndex.value = index
|
activeIndex.value = index
|
||||||
|
|
||||||
if (activeIndex.value !== -1) {
|
if (activeIndex.value !== -1) {
|
||||||
startAnimation()
|
startAnimation()
|
||||||
} else {
|
} else {
|
||||||
oldIndex.value = -1
|
oldIndex.value = -1
|
||||||
sliderLeft.value = 0
|
sliderLeft.value = 0
|
||||||
sliderRight.value = 0
|
sliderRight.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabLinkElements = ref()
|
const tabLinkElements = ref()
|
||||||
|
|
||||||
function startAnimation() {
|
function startAnimation() {
|
||||||
const el = tabLinkElements.value[activeIndex.value].$el
|
const el = tabLinkElements.value[activeIndex.value].$el
|
||||||
|
|
||||||
if (!el || !el.offsetParent) return
|
if (!el || !el.offsetParent) return
|
||||||
|
|
||||||
const newValues = {
|
const newValues = {
|
||||||
left: el.offsetLeft,
|
left: el.offsetLeft,
|
||||||
top: el.offsetTop,
|
top: el.offsetTop,
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||||
sliderLeft.value = newValues.left
|
sliderLeft.value = newValues.left
|
||||||
sliderRight.value = newValues.right
|
sliderRight.value = newValues.right
|
||||||
sliderTop.value = newValues.top
|
sliderTop.value = newValues.top
|
||||||
sliderBottom.value = newValues.bottom
|
sliderBottom.value = newValues.bottom
|
||||||
} else {
|
} else {
|
||||||
const delay = 200
|
const delay = 200
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
if (newValues.left < sliderLeft.value) {
|
||||||
sliderLeft.value = newValues.left
|
sliderLeft.value = newValues.left
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderRight.value = newValues.right
|
sliderRight.value = newValues.right
|
||||||
}, delay)
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderRight.value = newValues.right
|
sliderRight.value = newValues.right
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderLeft.value = newValues.left
|
sliderLeft.value = newValues.left
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
if (newValues.top < sliderTop.value) {
|
||||||
sliderTop.value = newValues.top
|
sliderTop.value = newValues.top
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderBottom.value = newValues.bottom
|
sliderBottom.value = newValues.bottom
|
||||||
}, delay)
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderBottom.value = newValues.bottom
|
sliderBottom.value = newValues.bottom
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderTop.value = newValues.top
|
sliderTop.value = newValues.top
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', pickLink)
|
window.addEventListener('resize', pickLink)
|
||||||
pickLink()
|
pickLink()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', pickLink)
|
window.removeEventListener('resize', pickLink)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
pickLink()
|
pickLink()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.navtabs-transition {
|
.navtabs-transition {
|
||||||
/* Delay on opacity is to hide any jankiness as the page loads */
|
/* Delay on opacity is to hide any jankiness as the page loads */
|
||||||
transition:
|
transition:
|
||||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-bar__fill" :style="{ width: `${progress}%` }"></div>
|
<div class="progress-bar__fill" :style="{ width: `${progress}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
progress: {
|
progress: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
validator(value) {
|
validator(value) {
|
||||||
return value >= 0 && value <= 100
|
return value >= 0 && value <= 100
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar__fill {
|
.progress-bar__fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Avatar, TagItem } from '@modrinth/ui'
|
|
||||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { Avatar, TagItem } from '@modrinth/ui'
|
||||||
import { computed } from 'vue'
|
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@@ -12,107 +12,107 @@ dayjs.extend(relativeTime)
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const featuredCategory = computed(() => {
|
const featuredCategory = computed(() => {
|
||||||
if (props.project.display_categories.includes('optimization')) {
|
if (props.project.display_categories.includes('optimization')) {
|
||||||
return 'optimization'
|
return 'optimization'
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.project.display_categories[0] ?? props.project.categories[0]
|
return props.project.display_categories[0] ?? props.project.categories[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
const toColor = computed(() => {
|
const toColor = computed(() => {
|
||||||
let color = props.project.color
|
let color = props.project.color
|
||||||
|
|
||||||
color >>>= 0
|
color >>>= 0
|
||||||
const b = color & 0xff
|
const b = color & 0xff
|
||||||
const g = (color >>> 8) & 0xff
|
const g = (color >>> 8) & 0xff
|
||||||
const r = (color >>> 16) & 0xff
|
const r = (color >>> 16) & 0xff
|
||||||
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
||||||
})
|
})
|
||||||
|
|
||||||
const toTransparent = computed(() => {
|
const toTransparent = computed(() => {
|
||||||
let color = props.project.color
|
let color = props.project.color
|
||||||
|
|
||||||
color >>>= 0
|
color >>>= 0
|
||||||
const b = color & 0xff
|
const b = color & 0xff
|
||||||
const g = (color >>> 8) & 0xff
|
const g = (color >>> 8) & 0xff
|
||||||
const r = (color >>> 16) & 0xff
|
const r = (color >>> 16) & 0xff
|
||||||
return (
|
return (
|
||||||
'linear-gradient(rgba(' +
|
'linear-gradient(rgba(' +
|
||||||
[r, g, b, 0.03].join(',') +
|
[r, g, b, 0.03].join(',') +
|
||||||
'), 65%, rgba(' +
|
'), 65%, rgba(' +
|
||||||
[r, g, b, 0.3].join(',') +
|
[r, g, b, 0.3].join(',') +
|
||||||
'))'
|
'))'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="router.push(`/project/${project.slug}`)"
|
@click="router.push(`/project/${project.slug}`)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
||||||
:style="{
|
:style="{
|
||||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||||
'background-image': `url(${
|
'background-image': `url(${
|
||||||
project.featured_gallery ??
|
project.featured_gallery ??
|
||||||
project.gallery[0] ??
|
project.gallery[0] ??
|
||||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||||
})`,
|
})`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="badges-wrapper"
|
class="badges-wrapper"
|
||||||
:class="{
|
:class="{
|
||||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||||
}"
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<Avatar size="48px" :src="project.icon_url" />
|
<Avatar size="48px" :src="project.icon_url" />
|
||||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||||
<span class="line-clamp-2">{{ project.title }}</span>
|
<span class="line-clamp-2">{{ project.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{{ formatNumber(project.downloads) }}
|
{{ formatNumber(project.downloads) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||||
>
|
>
|
||||||
<HeartIcon />
|
<HeartIcon />
|
||||||
{{ formatNumber(project.follows) }}
|
{{ formatNumber(project.follows) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 pr-2">
|
<div class="flex items-center gap-1 pr-2">
|
||||||
<TagIcon />
|
<TagIcon />
|
||||||
<TagItem>
|
<TagItem>
|
||||||
{{ formatCategory(featuredCategory) }}
|
{{ formatCategory(featuredCategory) }}
|
||||||
</TagItem>
|
</TagItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { init_ads_window } from '@/helpers/ads.js'
|
import { init_ads_window } from '@/helpers/ads.js'
|
||||||
|
|
||||||
const adsWrapper = ref(null)
|
const adsWrapper = ref(null)
|
||||||
@@ -7,58 +8,58 @@ const adsWrapper = ref(null)
|
|||||||
let devicePixelRatioWatcher = null
|
let devicePixelRatioWatcher = null
|
||||||
|
|
||||||
function initDevicePixelRatioWatcher() {
|
function initDevicePixelRatioWatcher() {
|
||||||
if (devicePixelRatioWatcher) {
|
if (devicePixelRatioWatcher) {
|
||||||
devicePixelRatioWatcher.removeEventListener('change', updateAdPosition)
|
devicePixelRatioWatcher.removeEventListener('change', updateAdPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
devicePixelRatioWatcher = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
devicePixelRatioWatcher = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
||||||
devicePixelRatioWatcher.addEventListener('change', updateAdPosition)
|
devicePixelRatioWatcher.addEventListener('change', updateAdPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateAdPosition()
|
updateAdPosition()
|
||||||
|
|
||||||
window.addEventListener('resize', updateAdPosition)
|
window.addEventListener('resize', updateAdPosition)
|
||||||
initDevicePixelRatioWatcher()
|
initDevicePixelRatioWatcher()
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateAdPosition() {
|
function updateAdPosition() {
|
||||||
if (adsWrapper.value) {
|
if (adsWrapper.value) {
|
||||||
init_ads_window()
|
init_ads_window()
|
||||||
initDevicePixelRatioWatcher()
|
initDevicePixelRatioWatcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
||||||
<a
|
<a
|
||||||
href="https://modrinth.gg?from=app-placeholder"
|
href="https://modrinth.gg?from=app-placeholder"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp"
|
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp"
|
||||||
alt="Host your next server with Modrinth Servers"
|
alt="Host your next server with Modrinth Servers"
|
||||||
class="hidden light-image rounded-[inherit]"
|
class="hidden light-image rounded-[inherit]"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp"
|
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp"
|
||||||
alt="Host your next server with Modrinth Servers"
|
alt="Host your next server with Modrinth Servers"
|
||||||
class="dark-image rounded-[inherit]"
|
class="dark-image rounded-[inherit]"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.light,
|
.light,
|
||||||
.light-mode {
|
.light-mode {
|
||||||
.dark-image {
|
.dark-image {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light-image {
|
.light-image {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,74 +1,75 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import { list } from '@/helpers/profile'
|
|
||||||
import { SpinnerIcon } from '@modrinth/assets'
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { onUnmounted, ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
|
import { list } from '@/helpers/profile'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const recentInstances = ref([])
|
const recentInstances = ref([])
|
||||||
const getInstances = async () => {
|
const getInstances = async () => {
|
||||||
const profiles = await list().catch(handleError)
|
const profiles = await list().catch(handleError)
|
||||||
|
|
||||||
recentInstances.value = profiles
|
recentInstances.value = profiles
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const dateACreated = dayjs(a.created)
|
const dateACreated = dayjs(a.created)
|
||||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||||
|
|
||||||
const dateBCreated = dayjs(b.created)
|
const dateBCreated = dayjs(b.created)
|
||||||
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
||||||
|
|
||||||
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||||
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||||
|
|
||||||
if (dateA.isSame(dateB)) {
|
if (dateA.isSame(dateB)) {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dateB - dateA
|
return dateB - dateA
|
||||||
})
|
})
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
await getInstances()
|
await getInstances()
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(async (event) => {
|
const unlistenProfile = await profile_listener(async (event) => {
|
||||||
if (event.event !== 'synced') {
|
if (event.event !== 'synced') {
|
||||||
await getInstances()
|
await getInstances()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProfile()
|
unlistenProfile()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NavButton
|
<NavButton
|
||||||
v-for="instance in recentInstances"
|
v-for="instance in recentInstances"
|
||||||
:key="instance.id"
|
:key="instance.id"
|
||||||
v-tooltip.right="instance.name"
|
v-tooltip.right="instance.name"
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
size="28px"
|
size="28px"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="instance.install_stage !== 'installed'"
|
v-if="instance.install_stage !== 'installed'"
|
||||||
class="absolute inset-0 flex items-center justify-center z-10"
|
class="absolute inset-0 flex items-center justify-center z-10"
|
||||||
>
|
>
|
||||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,115 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-groups">
|
<div class="action-groups">
|
||||||
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
||||||
<button ref="infoButton" @click="toggleCard()">
|
<button ref="infoButton" @click="toggleCard()">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<div v-if="offline" class="status">
|
<div v-if="offline" class="status">
|
||||||
<UnplugIcon />
|
<UnplugIcon />
|
||||||
<div class="running-text">
|
<div class="running-text">
|
||||||
<span> Offline </span>
|
<span> Offline </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedProcess" class="status">
|
<div v-if="selectedProcess" class="status">
|
||||||
<span class="circle running" />
|
<span class="circle running" />
|
||||||
<div ref="profileButton" class="running-text">
|
<div ref="profileButton" class="running-text">
|
||||||
<router-link
|
<router-link
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
||||||
>
|
>
|
||||||
{{ selectedProcess.profile.name }}
|
{{ selectedProcess.profile.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div
|
<div
|
||||||
v-if="currentProcesses.length > 1"
|
v-if="currentProcesses.length > 1"
|
||||||
class="arrow button-base"
|
class="arrow button-base"
|
||||||
:class="{ rotate: showProfiles }"
|
:class="{ rotate: showProfiles }"
|
||||||
@click="toggleProfiles()"
|
@click="toggleProfiles()"
|
||||||
>
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip="'Stop instance'"
|
v-tooltip="'Stop instance'"
|
||||||
icon-only
|
icon-only
|
||||||
class="icon-button stop"
|
class="icon-button stop"
|
||||||
@click="stop(selectedProcess)"
|
@click="stop(selectedProcess)"
|
||||||
>
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||||
<TerminalSquareIcon />
|
<TerminalSquareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="status">
|
<div v-else class="status">
|
||||||
<span class="circle stopped" />
|
<span class="circle stopped" />
|
||||||
<span class="running-text"> No instances running </span>
|
<span class="running-text"> No instances running </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="download">
|
<transition name="download">
|
||||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||||
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
|
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
|
||||||
<h3 class="info-title">
|
<h3 class="info-title">
|
||||||
{{ loadingBar.title }}
|
{{ loadingBar.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }}
|
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}%
|
||||||
</div>
|
{{ loadingBar.message }}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</transition>
|
</Card>
|
||||||
<transition name="download">
|
</transition>
|
||||||
<Card
|
<transition name="download">
|
||||||
v-if="showProfiles === true && currentProcesses.length > 0"
|
<Card
|
||||||
ref="profiles"
|
v-if="showProfiles === true && currentProcesses.length > 0"
|
||||||
class="profile-card"
|
ref="profiles"
|
||||||
>
|
class="profile-card"
|
||||||
<Button
|
>
|
||||||
v-for="process in currentProcesses"
|
<Button
|
||||||
:key="process.uuid"
|
v-for="process in currentProcesses"
|
||||||
class="profile-button"
|
:key="process.uuid"
|
||||||
@click="selectProcess(process)"
|
class="profile-button"
|
||||||
>
|
@click="selectProcess(process)"
|
||||||
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
>
|
||||||
<Button
|
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
||||||
v-tooltip="'Stop instance'"
|
<Button
|
||||||
icon-only
|
v-tooltip="'Stop instance'"
|
||||||
class="icon-button stop"
|
icon-only
|
||||||
@click.stop="stop(process)"
|
class="icon-button stop"
|
||||||
>
|
@click.stop="stop(process)"
|
||||||
<StopCircleIcon />
|
>
|
||||||
</Button>
|
<StopCircleIcon />
|
||||||
<Button
|
</Button>
|
||||||
v-tooltip="'View logs'"
|
<Button
|
||||||
icon-only
|
v-tooltip="'View logs'"
|
||||||
class="icon-button"
|
icon-only
|
||||||
@click.stop="goToTerminal(process.profile.path)"
|
class="icon-button"
|
||||||
>
|
@click.stop="goToTerminal(process.profile.path)"
|
||||||
<TerminalSquareIcon />
|
>
|
||||||
</Button>
|
<TerminalSquareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Button>
|
||||||
</transition>
|
</Card>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
DownloadIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TerminalSquareIcon,
|
||||||
|
UnplugIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { loading_listener, process_listener } from '@/helpers/events'
|
import { loading_listener, process_listener } from '@/helpers/events'
|
||||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||||
import { get_many } from '@/helpers/profile.js'
|
import { get_many } from '@/helpers/profile.js'
|
||||||
import { progress_bars_list } from '@/helpers/state.js'
|
import { progress_bars_list } from '@/helpers/state.js'
|
||||||
import {
|
|
||||||
DownloadIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TerminalSquareIcon,
|
|
||||||
UnplugIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
@@ -126,347 +128,347 @@ const currentProcesses = ref([])
|
|||||||
const selectedProcess = ref()
|
const selectedProcess = ref()
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const processes = await getRunningProcesses().catch(handleError)
|
const processes = await getRunningProcesses().catch(handleError)
|
||||||
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
|
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
|
||||||
|
|
||||||
currentProcesses.value = processes.map((x) => ({
|
currentProcesses.value = processes.map((x) => ({
|
||||||
profile: profiles.find((prof) => x.profile_path === prof.path),
|
profile: profiles.find((prof) => x.profile_path === prof.path),
|
||||||
...x,
|
...x,
|
||||||
}))
|
}))
|
||||||
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
||||||
selectedProcess.value = currentProcesses.value[0]
|
selectedProcess.value = currentProcesses.value[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await refresh()
|
await refresh()
|
||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
const offline = ref(!navigator.onLine)
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
offline.value = true
|
offline.value = true
|
||||||
})
|
})
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
offline.value = false
|
offline.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const unlistenProcess = await process_listener(async () => {
|
const unlistenProcess = await process_listener(async () => {
|
||||||
await refresh()
|
await refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
const stop = async (process) => {
|
const stop = async (process) => {
|
||||||
try {
|
try {
|
||||||
await killProcess(process.uuid).catch(handleError)
|
await killProcess(process.uuid).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: process.profile.loader,
|
loader: process.profile.loader,
|
||||||
game_version: process.profile.game_version,
|
game_version: process.profile.game_version,
|
||||||
source: 'AppBar',
|
source: 'AppBar',
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
await refresh()
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToTerminal = (path) => {
|
const goToTerminal = (path) => {
|
||||||
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
|
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLoadingBars = ref([])
|
const currentLoadingBars = ref([])
|
||||||
|
|
||||||
const refreshInfo = async () => {
|
const refreshInfo = async () => {
|
||||||
const currentLoadingBarCount = currentLoadingBars.value.length
|
const currentLoadingBarCount = currentLoadingBars.value.length
|
||||||
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)).map(
|
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)).map(
|
||||||
(x) => {
|
(x) => {
|
||||||
if (x.bar_type.type === 'java_download') {
|
if (x.bar_type.type === 'java_download') {
|
||||||
x.title = 'Downloading Java ' + x.bar_type.version
|
x.title = 'Downloading Java ' + x.bar_type.version
|
||||||
}
|
}
|
||||||
if (x.bar_type.profile_path) {
|
if (x.bar_type.profile_path) {
|
||||||
x.title = x.bar_type.profile_path
|
x.title = x.bar_type.profile_path
|
||||||
}
|
}
|
||||||
if (x.bar_type.pack_name) {
|
if (x.bar_type.pack_name) {
|
||||||
x.title = x.bar_type.pack_name
|
x.title = x.bar_type.pack_name
|
||||||
}
|
}
|
||||||
|
|
||||||
return x
|
return x
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
currentLoadingBars.value.sort((a, b) => {
|
currentLoadingBars.value.sort((a, b) => {
|
||||||
if (a.loading_bar_uuid < b.loading_bar_uuid) {
|
if (a.loading_bar_uuid < b.loading_bar_uuid) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
if (a.loading_bar_uuid > b.loading_bar_uuid) {
|
if (a.loading_bar_uuid > b.loading_bar_uuid) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if (currentLoadingBars.value.length === 0) {
|
if (currentLoadingBars.value.length === 0) {
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
|
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
|
||||||
showCard.value = true
|
showCard.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshInfo()
|
await refreshInfo()
|
||||||
const unlistenLoading = await loading_listener(async () => {
|
const unlistenLoading = await loading_listener(async () => {
|
||||||
await refreshInfo()
|
await refreshInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectProcess = (process) => {
|
const selectProcess = (process) => {
|
||||||
selectedProcess.value = process
|
selectedProcess.value = process
|
||||||
showProfiles.value = false
|
showProfiles.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutsideCard = (event) => {
|
const handleClickOutsideCard = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
card.value &&
|
card.value &&
|
||||||
card.value.$el !== event.target &&
|
card.value.$el !== event.target &&
|
||||||
!elements.includes(card.value.$el) &&
|
!elements.includes(card.value.$el) &&
|
||||||
infoButton.value &&
|
infoButton.value &&
|
||||||
!infoButton.value.contains(event.target)
|
!infoButton.value.contains(event.target)
|
||||||
) {
|
) {
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutsideProfile = (event) => {
|
const handleClickOutsideProfile = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
profiles.value &&
|
profiles.value &&
|
||||||
profiles.value.$el !== event.target &&
|
profiles.value.$el !== event.target &&
|
||||||
!elements.includes(profiles.value.$el) &&
|
!elements.includes(profiles.value.$el) &&
|
||||||
!profileButton.value.contains(event.target)
|
!profileButton.value.contains(event.target)
|
||||||
) {
|
) {
|
||||||
showProfiles.value = false
|
showProfiles.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCard = async () => {
|
const toggleCard = async () => {
|
||||||
showCard.value = !showCard.value
|
showCard.value = !showCard.value
|
||||||
showProfiles.value = false
|
showProfiles.value = false
|
||||||
await refreshInfo()
|
await refreshInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleProfiles = async () => {
|
const toggleProfiles = async () => {
|
||||||
if (currentProcesses.value.length === 1) return
|
if (currentProcesses.value.length === 1) return
|
||||||
showProfiles.value = !showProfiles.value
|
showProfiles.value = !showProfiles.value
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', handleClickOutsideCard)
|
window.addEventListener('click', handleClickOutsideCard)
|
||||||
window.addEventListener('click', handleClickOutsideProfile)
|
window.addEventListener('click', handleClickOutsideProfile)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', handleClickOutsideCard)
|
window.removeEventListener('click', handleClickOutsideCard)
|
||||||
window.removeEventListener('click', handleClickOutsideProfile)
|
window.removeEventListener('click', handleClickOutsideProfile)
|
||||||
unlistenProcess()
|
unlistenProcess()
|
||||||
unlistenLoading()
|
unlistenLoading()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.action-groups {
|
.action-groups {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&.rotate {
|
&.rotate {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
padding: var(--gap-sm) var(--gap-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.running-text {
|
.running-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-user-select: none; /* Safari */
|
-webkit-user-select: none; /* Safari */
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.clickable:hover {
|
&.clickable:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
|
|
||||||
&.running {
|
&.running {
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stopped {
|
&.stopped {
|
||||||
background-color: var(--color-base);
|
background-color: var(--color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
width: 1.25rem !important;
|
width: 1.25rem !important;
|
||||||
height: 1.25rem !important;
|
height: 1.25rem !important;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stop {
|
&.stop {
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3.5rem;
|
top: 3.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: var(--shadow-raised);
|
box-shadow: var(--shadow-raised);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-option {
|
.loading-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
background-color: var(--color-raised-bg-hover);
|
background-color: var(--color-raised-bg-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-icon {
|
.loading-icon {
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
:deep(svg) {
|
:deep(svg) {
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-enter-active,
|
.download-enter-active,
|
||||||
.download-leave-active {
|
.download-leave-active {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-enter-from,
|
.download-enter-from,
|
||||||
.download-leave-to {
|
.download-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-text {
|
.info-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-title {
|
.info-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-button {
|
.profile-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-card {
|
.profile-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3.5rem;
|
top: 3.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: var(--shadow-raised);
|
box-shadow: var(--shadow-raised);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
padding: var(--gap-md);
|
padding: var(--gap-md);
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,159 +1,160 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
emit('open')
|
emit('open')
|
||||||
$router.push({
|
$router.push({
|
||||||
path: `/project/${project.project_id ?? project.id}`,
|
path: `/project/${project.project_id ?? project.id}`,
|
||||||
query: { i: props.instance ? props.instance.path : undefined },
|
query: { i: props.instance ? props.instance.path : undefined },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="icon w-[96px] h-[96px] relative">
|
<div class="icon w-[96px] h-[96px] relative">
|
||||||
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 overflow-hidden">
|
<div class="flex flex-col gap-2 overflow-hidden">
|
||||||
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
||||||
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
||||||
{{ project.title }}
|
{{ project.title }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-0 line-clamp-2">
|
<div class="m-0 line-clamp-2">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
||||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||||
<div
|
<div
|
||||||
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
||||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||||
>
|
>
|
||||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
||||||
Client or server
|
Client or server
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
(project.client_side === 'optional' || project.client_side === 'required') &&
|
(project.client_side === 'optional' || project.client_side === 'required') &&
|
||||||
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Client
|
Client
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
(project.server_side === 'optional' || project.server_side === 'required') &&
|
(project.server_side === 'optional' || project.server_side === 'required') &&
|
||||||
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Server
|
Server
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Unsupported
|
Unsupported
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
||||||
>
|
>
|
||||||
Client and server
|
Client and server
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="tag in categories"
|
v-for="tag in categories"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||||
>
|
>
|
||||||
{{ formatCategory(tag.name) }}
|
{{ formatCategory(tag.name) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<DownloadIcon class="shrink-0" />
|
<DownloadIcon class="shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatNumber(project.downloads) }}
|
{{ formatNumber(project.downloads) }}
|
||||||
<span class="text-secondary">downloads</span>
|
<span class="text-secondary">downloads</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<HeartIcon class="shrink-0" />
|
<HeartIcon class="shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatNumber(project.follows ?? project.followers) }}
|
{{ formatNumber(project.follows ?? project.followers) }}
|
||||||
<span class="text-secondary">followers</span>
|
<span class="text-secondary">followers</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-auto relative">
|
<div class="mt-auto relative">
|
||||||
<div class="absolute bottom-0 right-0 w-fit">
|
<div class="absolute bottom-0 right-0 w-fit">
|
||||||
<ButtonStyled color="brand" type="outlined">
|
<ButtonStyled color="brand" type="outlined">
|
||||||
<button
|
<button
|
||||||
:disabled="installed || installing"
|
:disabled="installed || installing"
|
||||||
class="shrink-0 no-wrap"
|
class="shrink-0 no-wrap"
|
||||||
@click.stop="install()"
|
@click.stop="install()"
|
||||||
>
|
>
|
||||||
<template v-if="!installed">
|
<template v-if="!installed">
|
||||||
<DownloadIcon v-if="modpack || instance" />
|
<DownloadIcon v-if="modpack || instance" />
|
||||||
<PlusIcon v-else />
|
<PlusIcon v-else />
|
||||||
</template>
|
</template>
|
||||||
<CheckIcon v-else />
|
<CheckIcon v-else />
|
||||||
{{
|
{{
|
||||||
installing
|
installing
|
||||||
? 'Installing'
|
? 'Installing'
|
||||||
: installed
|
: installed
|
||||||
? 'Installed'
|
? 'Installed'
|
||||||
: modpack || instance
|
: modpack || instance
|
||||||
? 'Install'
|
? 'Install'
|
||||||
: 'Add to an instance'
|
: 'Add to an instance'
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
import { CheckIcon, DownloadIcon, HeartIcon, PlusIcon, TagsIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { ref, computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
featured: {
|
featured: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
installed: {
|
installed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['open', 'install'])
|
const emit = defineEmits(['open', 'install'])
|
||||||
@@ -161,20 +162,20 @@ const emit = defineEmits(['open', 'install'])
|
|||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
await installVersion(
|
await installVersion(
|
||||||
props.project.project_id ?? props.project.id,
|
props.project.project_id ?? props.project.id,
|
||||||
null,
|
null,
|
||||||
props.instance ? props.instance.path : null,
|
props.instance ? props.instance.path : null,
|
||||||
'SearchCard',
|
'SearchCard',
|
||||||
() => {
|
() => {
|
||||||
installing.value = false
|
installing.value = false
|
||||||
emit('install', props.project.project_id ?? props.project.id)
|
emit('install', props.project.project_id ?? props.project.id)
|
||||||
},
|
},
|
||||||
(profile) => {
|
(profile) => {
|
||||||
router.push(`/instance/${profile}`)
|
router.push(`/instance/${profile}`)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modpack = computed(() => props.project.project_type === 'modpack')
|
const modpack = computed(() => props.project.project_type === 'modpack')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||||
import { get_project, get_version } from '@/helpers/cache.js'
|
import { get_project, get_version } from '@/helpers/cache.js'
|
||||||
import { get_categories } from '@/helpers/tags.js'
|
import { get_categories } from '@/helpers/tags.js'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
@@ -16,86 +17,86 @@ const categories = ref(null)
|
|||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(event) {
|
async show(event) {
|
||||||
if (event.event === 'InstallVersion') {
|
if (event.event === 'InstallVersion') {
|
||||||
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
||||||
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
|
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
|
||||||
handleError,
|
handleError,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
|
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
|
||||||
version.value = await get_version(
|
version.value = await get_version(
|
||||||
project.value.versions[project.value.versions.length - 1],
|
project.value.versions[project.value.versions.length - 1],
|
||||||
'must_revalidate',
|
'must_revalidate',
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
categories.value = (await get_categories().catch(handleError)).filter(
|
categories.value = (await get_categories().catch(handleError)).filter(
|
||||||
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
|
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
|
||||||
)
|
)
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
confirmModal.value.hide()
|
confirmModal.value.hide()
|
||||||
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
|
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<SearchCard
|
<SearchCard
|
||||||
:project="project"
|
:project="project"
|
||||||
class="project-card"
|
class="project-card"
|
||||||
:categories="categories"
|
:categories="categories"
|
||||||
@open="confirmModal.hide()"
|
@open="confirmModal.hide()"
|
||||||
/>
|
/>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<p>
|
<p>
|
||||||
Installing <code>{{ version.id }}</code> from Modrinth
|
Installing <code>{{ version.id }}</code> from Modrinth
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card {
|
.project-card {
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
:deep(.badge) {
|
:deep(.badge) {
|
||||||
border: 1px solid var(--color-raised-bg);
|
border: 1px solid var(--color-raised-bg);
|
||||||
background-color: var(--color-accent-contrast);
|
background-color: var(--color-accent-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { get_user_many } from '@/helpers/cache'
|
|
||||||
import { friend_listener } from '@/helpers/events'
|
|
||||||
import { add_friend, friend_statuses, friends, remove_friend } from '@/helpers/friends'
|
|
||||||
import {
|
import {
|
||||||
MailIcon,
|
MailIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { get_user_many } from '@/helpers/cache'
|
||||||
|
import { friend_listener } from '@/helpers/events'
|
||||||
|
import { add_friend, friend_statuses, friends, remove_friend } from '@/helpers/friends'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
credentials: unknown | null
|
credentials: unknown | null
|
||||||
signIn: () => void
|
signIn: () => void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const userCredentials = computed(() => props.credentials)
|
const userCredentials = computed(() => props.credentials)
|
||||||
@@ -40,328 +41,328 @@ const friendInvitesModal = ref()
|
|||||||
const username = ref('')
|
const username = ref('')
|
||||||
const addFriendModal = ref()
|
const addFriendModal = ref()
|
||||||
async function addFriendFromModal() {
|
async function addFriendFromModal() {
|
||||||
addFriendModal.value.hide()
|
addFriendModal.value.hide()
|
||||||
await add_friend(username.value).catch(handleError)
|
await add_friend(username.value).catch(handleError)
|
||||||
username.value = ''
|
username.value = ''
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
}
|
}
|
||||||
|
|
||||||
const friendOptions = ref()
|
const friendOptions = ref()
|
||||||
async function handleFriendOptions(args) {
|
async function handleFriendOptions(args) {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'remove-friend':
|
case 'remove-friend':
|
||||||
await removeFriend(args.item)
|
await removeFriend(args.item)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addFriend(friend: Friend) {
|
async function addFriend(friend: Friend) {
|
||||||
await add_friend(
|
await add_friend(
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFriend(friend: Friend) {
|
async function removeFriend(friend: Friend) {
|
||||||
await remove_friend(
|
await remove_friend(
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Friend = {
|
type Friend = {
|
||||||
id: string
|
id: string
|
||||||
friend_id: string | null
|
friend_id: string | null
|
||||||
status: string | null
|
status: string | null
|
||||||
last_updated: Dayjs | null
|
last_updated: Dayjs | null
|
||||||
created: Dayjs
|
created: Dayjs
|
||||||
username: string
|
username: string
|
||||||
accepted: boolean
|
accepted: boolean
|
||||||
online: boolean
|
online: boolean
|
||||||
avatar: string
|
avatar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFriends = ref<Friend[]>([])
|
const userFriends = ref<Friend[]>([])
|
||||||
const acceptedFriends = computed(() =>
|
const acceptedFriends = computed(() =>
|
||||||
userFriends.value
|
userFriends.value
|
||||||
.filter((x) => x.accepted)
|
.filter((x) => x.accepted)
|
||||||
.toSorted((a, b) => {
|
.toSorted((a, b) => {
|
||||||
if (a.last_updated === null && b.last_updated === null) {
|
if (a.last_updated === null && b.last_updated === null) {
|
||||||
return 0 // Both are null, equal in sorting
|
return 0 // Both are null, equal in sorting
|
||||||
}
|
}
|
||||||
if (a.last_updated === null) {
|
if (a.last_updated === null) {
|
||||||
return 1 // `a` is null, move it after `b`
|
return 1 // `a` is null, move it after `b`
|
||||||
}
|
}
|
||||||
if (b.last_updated === null) {
|
if (b.last_updated === null) {
|
||||||
return -1 // `b` is null, move it after `a`
|
return -1 // `b` is null, move it after `a`
|
||||||
}
|
}
|
||||||
// Both are non-null, sort by date
|
// Both are non-null, sort by date
|
||||||
return b.last_updated.diff(a.last_updated)
|
return b.last_updated.diff(a.last_updated)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const pendingFriends = computed(() =>
|
const pendingFriends = computed(() =>
|
||||||
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
|
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
async function loadFriends(timeout = false) {
|
async function loadFriends(timeout = false) {
|
||||||
loading.value = timeout
|
loading.value = timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const friendsList = await friends()
|
const friendsList = await friends()
|
||||||
|
|
||||||
if (friendsList.length === 0) {
|
if (friendsList.length === 0) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
} else {
|
} else {
|
||||||
const friendStatuses = await friend_statuses()
|
const friendStatuses = await friend_statuses()
|
||||||
const users = await get_user_many(
|
const users = await get_user_many(
|
||||||
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
|
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
|
||||||
)
|
)
|
||||||
|
|
||||||
userFriends.value = friendsList.map((friend) => {
|
userFriends.value = friendsList.map((friend) => {
|
||||||
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
|
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
|
||||||
const status = friendStatuses.find(
|
const status = friendStatuses.find(
|
||||||
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
|
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
id: friend.id,
|
id: friend.id,
|
||||||
friend_id: friend.friend_id,
|
friend_id: friend.friend_id,
|
||||||
status: status?.profile_name,
|
status: status?.profile_name,
|
||||||
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
|
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
|
||||||
created: dayjs(friend.created),
|
created: dayjs(friend.created),
|
||||||
avatar: user?.avatar_url,
|
avatar: user?.avatar_url,
|
||||||
username: user?.username,
|
username: user?.username,
|
||||||
online: !!status,
|
online: !!status,
|
||||||
accepted: friend.accepted,
|
accepted: friend.accepted,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading friends', e)
|
console.error('Error loading friends', e)
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
setTimeout(() => loadFriends(), 15 * 1000)
|
setTimeout(() => loadFriends(), 15 * 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
userCredentials,
|
userCredentials,
|
||||||
() => {
|
() => {
|
||||||
if (userCredentials.value === undefined) {
|
if (userCredentials.value === undefined) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
} else if (userCredentials.value === null) {
|
} else if (userCredentials.value === null) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
loading.value = false
|
loading.value = false
|
||||||
} else {
|
} else {
|
||||||
loadFriends(true)
|
loadFriends(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const unlisten = await friend_listener(() => loadFriends())
|
const unlisten = await friend_listener(() => loadFriends())
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten()
|
unlisten()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
||||||
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
||||||
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
||||||
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
||||||
<div
|
<div
|
||||||
v-for="friend in acceptedFriends.filter(
|
v-for="friend in acceptedFriends.filter(
|
||||||
(x) => !search || x.username.toLowerCase().includes(search),
|
(x) => !search || x.username.toLowerCase().includes(search),
|
||||||
)"
|
)"
|
||||||
:key="friend.username"
|
:key="friend.username"
|
||||||
class="flex gap-2 items-center"
|
class="flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
<span
|
<span
|
||||||
v-if="friend.online"
|
v-if="friend.online"
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ friend.username }}</div>
|
<div>{{ friend.username }}</div>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="removeFriend(friend)">
|
<button @click="removeFriend(friend)">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
||||||
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
|
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
|
||||||
<div v-else class="flex flex-col gap-4">
|
<div v-else class="flex flex-col gap-4">
|
||||||
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
|
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
<template v-if="friend.id === userCredentials.user_id">
|
||||||
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
<p class="m-0 text-sm text-secondary">
|
<p class="m-0 text-sm text-secondary">
|
||||||
{{ formatRelativeTime(friend.created.toISOString()) }}
|
{{ formatRelativeTime(friend.created.toISOString()) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
<template v-if="friend.id === userCredentials.user_id">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button @click="addFriend(friend)">
|
<button @click="addFriend(friend)">
|
||||||
<UserPlusIcon />
|
<UserPlusIcon />
|
||||||
Accept
|
Accept
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="removeFriend(friend)">
|
<button @click="removeFriend(friend)">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Ignore
|
Ignore
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="removeFriend(friend)">
|
<button @click="removeFriend(friend)">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
||||||
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||||
<UserPlusIcon />
|
<UserPlusIcon />
|
||||||
Add friend
|
Add friend
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="text-lg m-0">Friends</h3>
|
<h3 class="text-lg m-0">Friends</h3>
|
||||||
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'add-friend',
|
id: 'add-friend',
|
||||||
action: () => addFriendModal.show(),
|
action: () => addFriendModal.show(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'manage-friends',
|
id: 'manage-friends',
|
||||||
action: () => manageFriendsModal.show(),
|
action: () => manageFriendsModal.show(),
|
||||||
shown: acceptedFriends.length > 0,
|
shown: acceptedFriends.length > 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'view-requests',
|
id: 'view-requests',
|
||||||
action: () => friendInvitesModal.show(),
|
action: () => friendInvitesModal.show(),
|
||||||
shown: pendingFriends.length > 0,
|
shown: pendingFriends.length > 0,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
aria-label="More options"
|
aria-label="More options"
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #add-friend>
|
<template #add-friend>
|
||||||
<UserPlusIcon aria-hidden="true" />
|
<UserPlusIcon aria-hidden="true" />
|
||||||
Add friend
|
Add friend
|
||||||
</template>
|
</template>
|
||||||
<template #manage-friends>
|
<template #manage-friends>
|
||||||
<SettingsIcon aria-hidden="true" />
|
<SettingsIcon aria-hidden="true" />
|
||||||
Manage friends
|
Manage friends
|
||||||
<div
|
<div
|
||||||
v-if="acceptedFriends.length > 0"
|
v-if="acceptedFriends.length > 0"
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
||||||
>
|
>
|
||||||
{{ acceptedFriends.length }}
|
{{ acceptedFriends.length }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #view-requests>
|
<template #view-requests>
|
||||||
<MailIcon aria-hidden="true" />
|
<MailIcon aria-hidden="true" />
|
||||||
View friend requests
|
View friend requests
|
||||||
<div
|
<div
|
||||||
v-if="pendingFriends.length > 0"
|
v-if="pendingFriends.length > 0"
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
||||||
>
|
>
|
||||||
{{ pendingFriends.length }}
|
{{ pendingFriends.length }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 mt-2">
|
<div class="flex flex-col gap-2 mt-2">
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
||||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||||
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="acceptedFriends.length === 0">
|
<template v-else-if="acceptedFriends.length === 0">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div v-if="!userCredentials">
|
<div v-if="!userCredentials">
|
||||||
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
||||||
to share what you're playing!
|
to share what you're playing!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
||||||
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
v-for="friend in acceptedFriends.slice(0, 5)"
|
v-for="friend in acceptedFriends.slice(0, 5)"
|
||||||
:key="friend.username"
|
:key="friend.username"
|
||||||
class="flex gap-2 items-center"
|
class="flex gap-2 items-center"
|
||||||
:class="{ grayscale: !friend.online }"
|
:class="{ grayscale: !friend.online }"
|
||||||
@contextmenu.prevent.stop="
|
@contextmenu.prevent.stop="
|
||||||
(event) =>
|
(event) =>
|
||||||
friendOptions.showMenu(event, friend, [
|
friendOptions.showMenu(event, friend, [
|
||||||
{
|
{
|
||||||
name: 'remove-friend',
|
name: 'remove-friend',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
<span
|
<span
|
||||||
v-if="friend.online"
|
v-if="friend.online"
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
||||||
{{ friend.username }}
|
{{ friend.username }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,70 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<p>
|
||||||
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
||||||
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
|
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
|
||||||
installed.
|
installed.
|
||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="header">
|
<tr class="header">
|
||||||
<th>{{ instance?.name }}</th>
|
<th>{{ instance?.name }}</th>
|
||||||
<th>{{ project.title }}</th>
|
<th>{{ project.title }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="content">
|
<tr class="content">
|
||||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||||
<td>
|
<td>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-if="versions?.length > 1"
|
v-if="versions?.length > 1"
|
||||||
v-model="selectedVersion"
|
v-model="selectedVersion"
|
||||||
:options="versions"
|
:options="versions"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
placeholder="Select version"
|
placeholder="Select version"
|
||||||
open-direction="top"
|
open-direction="top"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:custom-label="
|
:custom-label="
|
||||||
(version) =>
|
(version) =>
|
||||||
`${version?.name} (${version?.loaders
|
`${version?.name} (${version?.loaders
|
||||||
.map((name) => formatCategory(name))
|
.map((name) => formatCategory(name))
|
||||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||||
"
|
"
|
||||||
:max-height="150"
|
:max-height="150"
|
||||||
/>
|
/>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<span>
|
<span>
|
||||||
{{ selectedVersion?.name }} ({{
|
{{ selectedVersion?.name }} ({{
|
||||||
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
||||||
}}
|
}}
|
||||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
- {{ selectedVersion?.game_versions.join(', ') }})
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
||||||
<Button color="primary" :disabled="installing" @click="install()">
|
<Button color="primary" :disabled="installing" @click="install()">
|
||||||
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { formatCategory } from '@modrinth/utils'
|
import { formatCategory } from '@modrinth/utils'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const instance = ref(null)
|
const instance = ref(null)
|
||||||
@@ -77,91 +78,91 @@ const installing = ref(false)
|
|||||||
const onInstall = ref(() => {})
|
const onInstall = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
|
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
|
||||||
instance.value = instanceVal
|
instance.value = instanceVal
|
||||||
versions.value = projectVersions
|
versions.value = projectVersions
|
||||||
selectedVersion.value = selected ?? projectVersions[0]
|
selectedVersion.value = selected ?? projectVersions[0]
|
||||||
|
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
installing.value = false
|
installing.value = false
|
||||||
|
|
||||||
incompatibleModal.value.show()
|
incompatibleModal.value.show()
|
||||||
|
|
||||||
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
|
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const install = async () => {
|
const install = async () => {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
||||||
installing.value = false
|
installing.value = false
|
||||||
onInstall.value(selectedVersion.value.id)
|
onInstall.value(selectedVersion.value.id)
|
||||||
incompatibleModal.value.hide()
|
incompatibleModal.value.hide()
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
trackEvent('ProjectInstall', {
|
||||||
loader: instance.value.loader,
|
loader: instance.value.loader,
|
||||||
game_version: instance.value.game_version,
|
game_version: instance.value.game_version,
|
||||||
id: project.value,
|
id: project.value,
|
||||||
version_id: selectedVersion.value.id,
|
version_id: selectedVersion.value.id,
|
||||||
project_type: project.value.project_type,
|
project_type: project.value.project_type,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ProjectIncompatibilityWarningModal',
|
source: 'ProjectIncompatibilityWarningModal',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.data {
|
.data {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
box-shadow: 0 0 0 1px var(--color-button-bg);
|
box-shadow: 0 0 0 1px var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-bottom: 1px solid var(--color-button-bg);
|
border-bottom: 1px solid var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
th:first-child {
|
th:first-child {
|
||||||
border-top-left-radius: var(--radius-lg);
|
border-top-left-radius: var(--radius-lg);
|
||||||
border-right: 1px solid var(--color-button-bg);
|
border-right: 1px solid var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
th:last-child {
|
th:last-child {
|
||||||
border-top-right-radius: var(--radius-lg);
|
border-top-right-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
td:first-child {
|
td:first-child {
|
||||||
border-right: 1px solid var(--color-button-bg);
|
border-right: 1px solid var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
:deep(.animated-dropdown .options) {
|
:deep(.animated-dropdown .options) {
|
||||||
max-height: 13.375rem;
|
max-height: 13.375rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const versionId = ref()
|
const versionId = ref()
|
||||||
@@ -17,60 +18,60 @@ const onInstall = ref(() => {})
|
|||||||
const onCreateInstance = ref(() => {})
|
const onCreateInstance = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
versionId.value = versionIdVal
|
versionId.value = versionIdVal
|
||||||
installing.value = false
|
installing.value = false
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
onCreateInstance.value = createInstanceCallback
|
onCreateInstance.value = createInstanceCallback
|
||||||
|
|
||||||
trackEvent('PackInstallStart')
|
trackEvent('PackInstallStart')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
confirmModal.value.hide()
|
confirmModal.value.hide()
|
||||||
|
|
||||||
await pack_install(
|
await pack_install(
|
||||||
project.value.id,
|
project.value.id,
|
||||||
versionId.value,
|
versionId.value,
|
||||||
project.value.title,
|
project.value.title,
|
||||||
project.value.icon_url,
|
project.value.icon_url,
|
||||||
onCreateInstance.value,
|
onCreateInstance.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
trackEvent('PackInstall', {
|
trackEvent('PackInstall', {
|
||||||
id: project.value.id,
|
id: project.value.id,
|
||||||
version_id: versionId.value,
|
version_id: versionId.value,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ConfirmModal',
|
source: 'ConfirmModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
onInstall.value(versionId.value)
|
onInstall.value(versionId.value)
|
||||||
installing.value = false
|
installing.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
|
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
|
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
|
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
|
||||||
<Button color="primary" :disabled="installing" @click="install()"
|
<Button color="primary" :disabled="installing" @click="install()"
|
||||||
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
|
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import {
|
import {
|
||||||
check_installed,
|
CheckIcon,
|
||||||
create,
|
DownloadIcon,
|
||||||
get,
|
PlusIcon,
|
||||||
add_project_from_version as installMod,
|
RightArrowIcon,
|
||||||
list,
|
UploadIcon,
|
||||||
} from '@/helpers/profile'
|
XIcon,
|
||||||
import { installVersionDependencies } from '@/store/install.js'
|
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
PlusIcon,
|
|
||||||
RightArrowIcon,
|
|
||||||
UploadIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
@@ -23,6 +13,17 @@ import { open } from '@tauri-apps/plugin-dialog'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import {
|
||||||
|
add_project_from_version as installMod,
|
||||||
|
check_installed,
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
list,
|
||||||
|
} from '@/helpers/profile'
|
||||||
|
import { installVersionDependencies } from '@/store/install.js'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -43,361 +44,361 @@ const creatingInstance = ref(false)
|
|||||||
const profiles = ref([])
|
const profiles = ref([])
|
||||||
|
|
||||||
const shownProfiles = computed(() =>
|
const shownProfiles = computed(() =>
|
||||||
profiles.value
|
profiles.value
|
||||||
.filter((profile) => {
|
.filter((profile) => {
|
||||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||||
})
|
})
|
||||||
.filter((profile) => {
|
.filter((profile) => {
|
||||||
const loaders = versions.value.flatMap((v) => v.loaders)
|
const loaders = versions.value.flatMap((v) => v.loaders)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
|
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
|
||||||
(project.value.project_type === 'mod'
|
(project.value.project_type === 'mod'
|
||||||
? loaders.includes(profile.loader) || loaders.includes('minecraft')
|
? loaders.includes(profile.loader) || loaders.includes('minecraft')
|
||||||
: true)
|
: true)
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onInstall = ref(() => {})
|
const onInstall = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: async (projectVal, versionsVal, callback) => {
|
show: async (projectVal, versionsVal, callback) => {
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
versions.value = versionsVal
|
versions.value = versionsVal
|
||||||
searchFilter.value = ''
|
searchFilter.value = ''
|
||||||
|
|
||||||
showCreation.value = false
|
showCreation.value = false
|
||||||
name.value = null
|
name.value = null
|
||||||
icon.value = null
|
icon.value = null
|
||||||
display_icon.value = null
|
display_icon.value = null
|
||||||
gameVersion.value = null
|
gameVersion.value = null
|
||||||
loader.value = null
|
loader.value = null
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
|
|
||||||
const profilesVal = await list().catch(handleError)
|
const profilesVal = await list().catch(handleError)
|
||||||
for (const profile of profilesVal) {
|
for (const profile of profilesVal) {
|
||||||
profile.installing = false
|
profile.installing = false
|
||||||
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
||||||
handleError,
|
handleError,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
profiles.value = profilesVal
|
profiles.value = profilesVal
|
||||||
|
|
||||||
installModal.value.show()
|
installModal.value.show()
|
||||||
|
|
||||||
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install(instance) {
|
async function install(instance) {
|
||||||
instance.installing = true
|
instance.installing = true
|
||||||
const version = versions.value.find((v) => {
|
const version = versions.value.find((v) => {
|
||||||
return (
|
return (
|
||||||
v.game_versions.includes(instance.game_version) &&
|
v.game_versions.includes(instance.game_version) &&
|
||||||
(project.value.project_type === 'mod'
|
(project.value.project_type === 'mod'
|
||||||
? v.loaders.includes(instance.loader) || v.loaders.includes('minecraft')
|
? v.loaders.includes(instance.loader) || v.loaders.includes('minecraft')
|
||||||
: true)
|
: true)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
instance.installing = false
|
instance.installing = false
|
||||||
handleError('No compatible version found')
|
handleError('No compatible version found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await installMod(instance.path, version.id).catch(handleError)
|
await installMod(instance.path, version.id).catch(handleError)
|
||||||
await installVersionDependencies(instance, version)
|
await installVersionDependencies(instance, version)
|
||||||
|
|
||||||
instance.installedMod = true
|
instance.installedMod = true
|
||||||
instance.installing = false
|
instance.installing = false
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
trackEvent('ProjectInstall', {
|
||||||
loader: instance.loader,
|
loader: instance.loader,
|
||||||
game_version: instance.game_version,
|
game_version: instance.game_version,
|
||||||
id: project.value.id,
|
id: project.value.id,
|
||||||
version_id: version.id,
|
version_id: version.id,
|
||||||
project_type: project.value.project_type,
|
project_type: project.value.project_type,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ProjectInstallModal',
|
source: 'ProjectInstallModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
onInstall.value(version.id)
|
onInstall.value(version.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCreation = () => {
|
const toggleCreation = () => {
|
||||||
showCreation.value = !showCreation.value
|
showCreation.value = !showCreation.value
|
||||||
name.value = null
|
name.value = null
|
||||||
icon.value = null
|
icon.value = null
|
||||||
display_icon.value = null
|
display_icon.value = null
|
||||||
gameVersion.value = null
|
gameVersion.value = null
|
||||||
loader.value = null
|
loader.value = null
|
||||||
|
|
||||||
if (showCreation.value) {
|
if (showCreation.value) {
|
||||||
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
|
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload_icon = async () => {
|
const upload_icon = async () => {
|
||||||
const res = await open({
|
const res = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Image',
|
name: 'Image',
|
||||||
extensions: ['png', 'jpeg'],
|
extensions: ['png', 'jpeg'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
icon.value = res.path ?? res
|
icon.value = res.path ?? res
|
||||||
|
|
||||||
if (!icon.value) return
|
if (!icon.value) return
|
||||||
display_icon.value = convertFileSrc(icon.value)
|
display_icon.value = convertFileSrc(icon.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset_icon = () => {
|
const reset_icon = () => {
|
||||||
icon.value = null
|
icon.value = null
|
||||||
display_icon.value = null
|
display_icon.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const createInstance = async () => {
|
const createInstance = async () => {
|
||||||
creatingInstance.value = true
|
creatingInstance.value = true
|
||||||
|
|
||||||
const loader =
|
const loader =
|
||||||
versions.value[0].loaders[0] !== 'forge' &&
|
versions.value[0].loaders[0] !== 'forge' &&
|
||||||
versions.value[0].loaders[0] !== 'fabric' &&
|
versions.value[0].loaders[0] !== 'fabric' &&
|
||||||
versions.value[0].loaders[0] !== 'quilt'
|
versions.value[0].loaders[0] !== 'quilt'
|
||||||
? 'vanilla'
|
? 'vanilla'
|
||||||
: versions.value[0].loaders[0]
|
: versions.value[0].loaders[0]
|
||||||
|
|
||||||
const id = await create(
|
const id = await create(
|
||||||
name.value,
|
name.value,
|
||||||
versions.value[0].game_versions[0],
|
versions.value[0].game_versions[0],
|
||||||
loader,
|
loader,
|
||||||
'latest',
|
'latest',
|
||||||
icon.value,
|
icon.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
|
|
||||||
await installMod(id, versions.value[0].id).catch(handleError)
|
await installMod(id, versions.value[0].id).catch(handleError)
|
||||||
|
|
||||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||||
|
|
||||||
const instance = await get(id, true)
|
const instance = await get(id, true)
|
||||||
await installVersionDependencies(instance, versions.value[0])
|
await installVersionDependencies(instance, versions.value[0])
|
||||||
|
|
||||||
trackEvent('InstanceCreate', {
|
trackEvent('InstanceCreate', {
|
||||||
profile_name: name.value,
|
profile_name: name.value,
|
||||||
game_version: versions.value[0].game_versions[0],
|
game_version: versions.value[0].game_versions[0],
|
||||||
loader: loader,
|
loader: loader,
|
||||||
loader_version: 'latest',
|
loader_version: 'latest',
|
||||||
has_icon: !!icon.value,
|
has_icon: !!icon.value,
|
||||||
source: 'ProjectInstallModal',
|
source: 'ProjectInstallModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
trackEvent('ProjectInstall', {
|
||||||
loader: loader,
|
loader: loader,
|
||||||
game_version: versions.value[0].game_versions[0],
|
game_version: versions.value[0].game_versions[0],
|
||||||
id: project.value,
|
id: project.value,
|
||||||
version_id: versions.value[0].id,
|
version_id: versions.value[0].id,
|
||||||
project_type: project.value.project_type,
|
project_type: project.value.project_type,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ProjectInstallModal',
|
source: 'ProjectInstallModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
onInstall.value(versions.value[0].id)
|
onInstall.value(versions.value[0].id)
|
||||||
|
|
||||||
if (installModal.value) installModal.value.hide()
|
if (installModal.value) installModal.value.hide()
|
||||||
creatingInstance.value = false
|
creatingInstance.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input
|
<input
|
||||||
v-model="searchFilter"
|
v-model="searchFilter"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
class="search"
|
class="search"
|
||||||
placeholder="Search for an instance"
|
placeholder="Search for an instance"
|
||||||
/>
|
/>
|
||||||
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
||||||
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
||||||
<router-link
|
<router-link
|
||||||
class="btn btn-transparent profile-button"
|
class="btn btn-transparent profile-button"
|
||||||
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
||||||
@click="installModal.hide()"
|
@click="installModal.hide()"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||||
class="profile-image"
|
class="profile-image"
|
||||||
/>
|
/>
|
||||||
{{ profile.name }}
|
{{ profile.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div
|
<div
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
profile.linked_data?.locked && !profile.installedMod
|
profile.linked_data?.locked && !profile.installedMod
|
||||||
? 'Unpair or unlock an instance to add mods.'
|
? 'Unpair or unlock an instance to add mods.'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
:disabled="profile.installedMod || profile.installing"
|
:disabled="profile.installedMod || profile.installing"
|
||||||
@click="install(profile)"
|
@click="install(profile)"
|
||||||
>
|
>
|
||||||
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
||||||
<CheckIcon v-else-if="profile.installedMod" />
|
<CheckIcon v-else-if="profile.installedMod" />
|
||||||
{{
|
{{
|
||||||
profile.installing
|
profile.installing
|
||||||
? 'Installing...'
|
? 'Installing...'
|
||||||
: profile.installedMod
|
: profile.installedMod
|
||||||
? 'Installed'
|
? 'Installed'
|
||||||
: 'Install'
|
: 'Install'
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card v-if="showCreation" class="creation-card">
|
<Card v-if="showCreation" class="creation-card">
|
||||||
<div class="creation-container">
|
<div class="creation-container">
|
||||||
<div class="creation-icon">
|
<div class="creation-icon">
|
||||||
<Avatar size="md" class="icon" :src="display_icon" />
|
<Avatar size="md" class="icon" :src="display_icon" />
|
||||||
<div class="creation-icon__description">
|
<div class="creation-icon__description">
|
||||||
<Button @click="upload_icon()">
|
<Button @click="upload_icon()">
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
<span class="no-wrap"> Select icon </span>
|
<span class="no-wrap"> Select icon </span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="!display_icon" @click="reset_icon()">
|
<Button :disabled="!display_icon" @click="reset_icon()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span class="no-wrap"> Remove icon </span>
|
<span class="no-wrap"> Remove icon </span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="creation-settings">
|
<div class="creation-settings">
|
||||||
<input
|
<input
|
||||||
v-model="name"
|
v-model="name"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
class="creation-input"
|
class="creation-input"
|
||||||
/>
|
/>
|
||||||
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
|
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="installModal.hide()">Cancel</Button>
|
<Button @click="installModal.hide()">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.creation-card {
|
.creation-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-container {
|
.creation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-icon {
|
.creation-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
.creation-icon__description {
|
.creation-icon__description {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-input {
|
.creation-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap {
|
.no-wrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-dropdown {
|
.creation-dropdown {
|
||||||
width: min-content !important;
|
width: min-content !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-settings {
|
.creation-settings {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profiles {
|
.profiles {
|
||||||
max-height: 12rem;
|
max-height: 12rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
&.hide-creation {
|
&.hide-creation {
|
||||||
max-height: 21rem;
|
max-height: 21rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
background: var(--color-raised-bg);
|
background: var(--color-raised-bg);
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-button {
|
.profile-button {
|
||||||
align-content: start;
|
align-content: start;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-image {
|
.profile-image {
|
||||||
--size: 2rem !important;
|
--size: 2rem !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
|
||||||
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
|
||||||
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
@@ -34,299 +36,299 @@ const newCategoryInput = ref('')
|
|||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||||
|
|
||||||
async function duplicateProfile() {
|
async function duplicateProfile() {
|
||||||
await duplicate(props.instance.path).catch(handleError)
|
await duplicate(props.instance.path).catch(handleError)
|
||||||
trackEvent('InstanceDuplicate', {
|
trackEvent('InstanceDuplicate', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const allInstances = ref((await list()) as GameInstance[])
|
const allInstances = ref((await list()) as GameInstance[])
|
||||||
const availableGroups = computed(() => [
|
const availableGroups = computed(() => [
|
||||||
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
||||||
])
|
])
|
||||||
|
|
||||||
async function resetIcon() {
|
async function resetIcon() {
|
||||||
icon.value = undefined
|
icon.value = undefined
|
||||||
await edit_icon(props.instance.path, null).catch(handleError)
|
await edit_icon(props.instance.path, null).catch(handleError)
|
||||||
trackEvent('InstanceRemoveIcon')
|
trackEvent('InstanceRemoveIcon')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setIcon() {
|
async function setIcon() {
|
||||||
const value = await open({
|
const value = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Image',
|
name: 'Image',
|
||||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!value) return
|
if (!value) return
|
||||||
|
|
||||||
icon.value = value
|
icon.value = value
|
||||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceSetIcon')
|
trackEvent('InstanceSetIcon')
|
||||||
}
|
}
|
||||||
|
|
||||||
const editProfileObject = computed(() => ({
|
const editProfileObject = computed(() => ({
|
||||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toggleGroup = (group: string) => {
|
const toggleGroup = (group: string) => {
|
||||||
if (groups.value.includes(group)) {
|
if (groups.value.includes(group)) {
|
||||||
groups.value = groups.value.filter((x) => x !== group)
|
groups.value = groups.value.filter((x) => x !== group)
|
||||||
} else {
|
} else {
|
||||||
groups.value.push(group)
|
groups.value.push(group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addCategory = () => {
|
const addCategory = () => {
|
||||||
const text = newCategoryInput.value.trim()
|
const text = newCategoryInput.value.trim()
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
groups.value.push(text.substring(0, 32))
|
groups.value.push(text.substring(0, 32))
|
||||||
newCategoryInput.value = ''
|
newCategoryInput.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[title, groups, groups],
|
[title, groups, groups],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const removing = ref(false)
|
const removing = ref(false)
|
||||||
async function removeProfile() {
|
async function removeProfile() {
|
||||||
removing.value = true
|
removing.value = true
|
||||||
await remove(props.instance.path).catch(handleError)
|
await remove(props.instance.path).catch(handleError)
|
||||||
removing.value = false
|
removing.value = false
|
||||||
|
|
||||||
trackEvent('InstanceRemove', {
|
trackEvent('InstanceRemove', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
})
|
})
|
||||||
|
|
||||||
await router.push({ path: '/' })
|
await router.push({ path: '/' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.settings.tabs.general.name',
|
id: 'instance.settings.tabs.general.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
libraryGroups: {
|
libraryGroups: {
|
||||||
id: 'instance.settings.tabs.general.library-groups',
|
id: 'instance.settings.tabs.general.library-groups',
|
||||||
defaultMessage: 'Library groups',
|
defaultMessage: 'Library groups',
|
||||||
},
|
},
|
||||||
libraryGroupsDescription: {
|
libraryGroupsDescription: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.description',
|
id: 'instance.settings.tabs.general.library-groups.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Library groups allow you to organize your instances into different sections in your library.',
|
'Library groups allow you to organize your instances into different sections in your library.',
|
||||||
},
|
},
|
||||||
libraryGroupsEnterName: {
|
libraryGroupsEnterName: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
||||||
defaultMessage: 'Enter group name',
|
defaultMessage: 'Enter group name',
|
||||||
},
|
},
|
||||||
libraryGroupsCreate: {
|
libraryGroupsCreate: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.create',
|
id: 'instance.settings.tabs.general.library-groups.create',
|
||||||
defaultMessage: 'Create new group',
|
defaultMessage: 'Create new group',
|
||||||
},
|
},
|
||||||
editIcon: {
|
editIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon',
|
id: 'instance.settings.tabs.general.edit-icon',
|
||||||
defaultMessage: 'Edit icon',
|
defaultMessage: 'Edit icon',
|
||||||
},
|
},
|
||||||
selectIcon: {
|
selectIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.select',
|
id: 'instance.settings.tabs.general.edit-icon.select',
|
||||||
defaultMessage: 'Select icon',
|
defaultMessage: 'Select icon',
|
||||||
},
|
},
|
||||||
replaceIcon: {
|
replaceIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.replace',
|
id: 'instance.settings.tabs.general.edit-icon.replace',
|
||||||
defaultMessage: 'Replace icon',
|
defaultMessage: 'Replace icon',
|
||||||
},
|
},
|
||||||
removeIcon: {
|
removeIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.remove',
|
id: 'instance.settings.tabs.general.edit-icon.remove',
|
||||||
defaultMessage: 'Remove icon',
|
defaultMessage: 'Remove icon',
|
||||||
},
|
},
|
||||||
duplicateInstance: {
|
duplicateInstance: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-instance',
|
id: 'instance.settings.tabs.general.duplicate-instance',
|
||||||
defaultMessage: 'Duplicate instance',
|
defaultMessage: 'Duplicate instance',
|
||||||
},
|
},
|
||||||
duplicateInstanceDescription: {
|
duplicateInstanceDescription: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
||||||
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
||||||
},
|
},
|
||||||
duplicateButtonTooltipInstalling: {
|
duplicateButtonTooltipInstalling: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
||||||
defaultMessage: 'Cannot duplicate while installing.',
|
defaultMessage: 'Cannot duplicate while installing.',
|
||||||
},
|
},
|
||||||
duplicateButton: {
|
duplicateButton: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-button',
|
id: 'instance.settings.tabs.general.duplicate-button',
|
||||||
defaultMessage: 'Duplicate',
|
defaultMessage: 'Duplicate',
|
||||||
},
|
},
|
||||||
deleteInstance: {
|
deleteInstance: {
|
||||||
id: 'instance.settings.tabs.general.delete',
|
id: 'instance.settings.tabs.general.delete',
|
||||||
defaultMessage: 'Delete instance',
|
defaultMessage: 'Delete instance',
|
||||||
},
|
},
|
||||||
deleteInstanceDescription: {
|
deleteInstanceDescription: {
|
||||||
id: 'instance.settings.tabs.general.delete.description',
|
id: 'instance.settings.tabs.general.delete.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
||||||
},
|
},
|
||||||
deleteInstanceButton: {
|
deleteInstanceButton: {
|
||||||
id: 'instance.settings.tabs.general.delete.button',
|
id: 'instance.settings.tabs.general.delete.button',
|
||||||
defaultMessage: 'Delete instance',
|
defaultMessage: 'Delete instance',
|
||||||
},
|
},
|
||||||
deletingInstanceButton: {
|
deletingInstanceButton: {
|
||||||
id: 'instance.settings.tabs.general.deleting.button',
|
id: 'instance.settings.tabs.general.deleting.button',
|
||||||
defaultMessage: 'Deleting...',
|
defaultMessage: 'Deleting...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="deleteConfirmModal"
|
ref="deleteConfirmModal"
|
||||||
title="Are you sure you want to delete this instance?"
|
title="Are you sure you want to delete this instance?"
|
||||||
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
:show-ad-on-close="false"
|
:show-ad-on-close="false"
|
||||||
@proceed="removeProfile"
|
@proceed="removeProfile"
|
||||||
/>
|
/>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="float-end ml-4 relative group">
|
<div class="float-end ml-4 relative group">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
v-tooltip="formatMessage(messages.editIcon)"
|
v-tooltip="formatMessage(messages.editIcon)"
|
||||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
action: () => setIcon(),
|
action: () => setIcon(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'remove',
|
id: 'remove',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
action: () => resetIcon(),
|
action: () => resetIcon(),
|
||||||
shown: !!icon,
|
shown: !!icon,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="icon ? convertFileSrc(icon) : icon"
|
:src="icon ? convertFileSrc(icon) : icon"
|
||||||
size="108px"
|
size="108px"
|
||||||
class="!border-4 group-hover:brightness-75"
|
class="!border-4 group-hover:brightness-75"
|
||||||
:tint-by="props.instance.path"
|
:tint-by="props.instance.path"
|
||||||
no-shadow
|
no-shadow
|
||||||
/>
|
/>
|
||||||
<div class="absolute top-0 right-0 m-2">
|
<div class="absolute top-0 right-0 m-2">
|
||||||
<div
|
<div
|
||||||
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
||||||
>
|
>
|
||||||
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #select>
|
<template #select>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||||
</template>
|
</template>
|
||||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</div>
|
</div>
|
||||||
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.name) }}
|
{{ formatMessage(messages.name) }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<input
|
<input
|
||||||
id="instance-name"
|
id="instance-name"
|
||||||
v-model="title"
|
v-model="title"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
class="flex-grow"
|
class="flex-grow"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="instance.install_stage == 'installed'">
|
<template v-if="instance.install_stage == 'installed'">
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
id="duplicate-instance-label"
|
id="duplicate-instance-label"
|
||||||
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
||||||
>
|
>
|
||||||
{{ formatMessage(messages.duplicateInstance) }}
|
{{ formatMessage(messages.duplicateInstance) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0 mb-2">
|
<p class="m-0 mb-2">
|
||||||
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button
|
<button
|
||||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||||
aria-labelledby="duplicate-instance-label"
|
aria-labelledby="duplicate-instance-label"
|
||||||
:disabled="installing"
|
:disabled="installing"
|
||||||
@click="duplicateProfile"
|
@click="duplicateProfile"
|
||||||
>
|
>
|
||||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.libraryGroups) }}
|
{{ formatMessage(messages.libraryGroups) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0 mb-2">
|
<p class="m-0 mb-2">
|
||||||
{{ formatMessage(messages.libraryGroupsDescription) }}
|
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-for="group in availableGroups"
|
v-for="group in availableGroups"
|
||||||
:key="group"
|
:key="group"
|
||||||
:model-value="groups.includes(group)"
|
:model-value="groups.includes(group)"
|
||||||
:label="group"
|
:label="group"
|
||||||
@click="toggleGroup(group)"
|
@click="toggleGroup(group)"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
v-model="newCategoryInput"
|
v-model="newCategoryInput"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||||
@submit="() => addCategory"
|
@submit="() => addCategory"
|
||||||
/>
|
/>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button class="w-fit" @click="() => addCategory()">
|
<button class="w-fit" @click="() => addCategory()">
|
||||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.deleteInstance) }}
|
{{ formatMessage(messages.deleteInstance) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0 mb-2">
|
<p class="m-0 mb-2">
|
||||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled color="red">
|
<ButtonStyled color="red">
|
||||||
<button
|
<button
|
||||||
aria-labelledby="delete-instance-label"
|
aria-labelledby="delete-instance-label"
|
||||||
:disabled="removing"
|
:disabled="removing"
|
||||||
@click="deleteConfirmModal.show()"
|
@click="deleteConfirmModal.show()"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||||
<TrashIcon v-else />
|
<TrashIcon v-else />
|
||||||
{{
|
{{
|
||||||
removing
|
removing
|
||||||
? formatMessage(messages.deletingInstanceButton)
|
? formatMessage(messages.deletingInstanceButton)
|
||||||
: formatMessage(messages.deleteInstanceButton)
|
: formatMessage(messages.deleteInstanceButton)
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.hovering-icon-shadow {
|
.hovering-icon-shadow {
|
||||||
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { edit } from '@/helpers/profile'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
@@ -14,139 +16,139 @@ const props = defineProps<InstanceSettingsTabProps>()
|
|||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideHooks = ref(
|
const overrideHooks = ref(
|
||||||
!!props.instance.hooks.pre_launch ||
|
!!props.instance.hooks.pre_launch ||
|
||||||
!!props.instance.hooks.wrapper ||
|
!!props.instance.hooks.wrapper ||
|
||||||
!!props.instance.hooks.post_exit,
|
!!props.instance.hooks.post_exit,
|
||||||
)
|
)
|
||||||
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
// When hooks are not overridden per-instance, we want to clear them
|
// When hooks are not overridden per-instance, we want to clear them
|
||||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[overrideHooks, hooks],
|
[overrideHooks, hooks],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
hooks: {
|
hooks: {
|
||||||
id: 'instance.settings.tabs.hooks.title',
|
id: 'instance.settings.tabs.hooks.title',
|
||||||
defaultMessage: 'Game launch hooks',
|
defaultMessage: 'Game launch hooks',
|
||||||
},
|
},
|
||||||
hooksDescription: {
|
hooksDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.description',
|
id: 'instance.settings.tabs.hooks.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
||||||
},
|
},
|
||||||
customHooks: {
|
customHooks: {
|
||||||
id: 'instance.settings.tabs.hooks.custom-hooks',
|
id: 'instance.settings.tabs.hooks.custom-hooks',
|
||||||
defaultMessage: 'Custom launch hooks',
|
defaultMessage: 'Custom launch hooks',
|
||||||
},
|
},
|
||||||
preLaunch: {
|
preLaunch: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch',
|
id: 'instance.settings.tabs.hooks.pre-launch',
|
||||||
defaultMessage: 'Pre-launch',
|
defaultMessage: 'Pre-launch',
|
||||||
},
|
},
|
||||||
preLaunchDescription: {
|
preLaunchDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
||||||
defaultMessage: 'Ran before the instance is launched.',
|
defaultMessage: 'Ran before the instance is launched.',
|
||||||
},
|
},
|
||||||
preLaunchEnter: {
|
preLaunchEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
||||||
defaultMessage: 'Enter pre-launch command...',
|
defaultMessage: 'Enter pre-launch command...',
|
||||||
},
|
},
|
||||||
wrapper: {
|
wrapper: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper',
|
id: 'instance.settings.tabs.hooks.wrapper',
|
||||||
defaultMessage: 'Wrapper',
|
defaultMessage: 'Wrapper',
|
||||||
},
|
},
|
||||||
wrapperDescription: {
|
wrapperDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper.description',
|
id: 'instance.settings.tabs.hooks.wrapper.description',
|
||||||
defaultMessage: 'Wrapper command for launching Minecraft.',
|
defaultMessage: 'Wrapper command for launching Minecraft.',
|
||||||
},
|
},
|
||||||
wrapperEnter: {
|
wrapperEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
||||||
defaultMessage: 'Enter wrapper command...',
|
defaultMessage: 'Enter wrapper command...',
|
||||||
},
|
},
|
||||||
postExit: {
|
postExit: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit',
|
id: 'instance.settings.tabs.hooks.post-exit',
|
||||||
defaultMessage: 'Post-exit',
|
defaultMessage: 'Post-exit',
|
||||||
},
|
},
|
||||||
postExitDescription: {
|
postExitDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit.description',
|
id: 'instance.settings.tabs.hooks.post-exit.description',
|
||||||
defaultMessage: 'Ran after the game closes.',
|
defaultMessage: 'Ran after the game closes.',
|
||||||
},
|
},
|
||||||
postExitEnter: {
|
postExitEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
||||||
defaultMessage: 'Enter post-exit command...',
|
defaultMessage: 'Enter post-exit command...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.hooks) }}
|
{{ formatMessage(messages.hooks) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.hooksDescription) }}
|
{{ formatMessage(messages.hooksDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
||||||
|
|
||||||
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.preLaunch) }}
|
{{ formatMessage(messages.preLaunch) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.preLaunchDescription) }}
|
{{ formatMessage(messages.preLaunchDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="pre-launch"
|
id="pre-launch"
|
||||||
v-model="hooks.pre_launch"
|
v-model="hooks.pre_launch"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideHooks"
|
:disabled="!overrideHooks"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.wrapper) }}
|
{{ formatMessage(messages.wrapper) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.wrapperDescription) }}
|
{{ formatMessage(messages.wrapperDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="wrapper"
|
id="wrapper"
|
||||||
v-model="hooks.wrapper"
|
v-model="hooks.wrapper"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideHooks"
|
:disabled="!overrideHooks"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.postExit) }}
|
{{ formatMessage(messages.postExit) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.postExitDescription) }}
|
{{ formatMessage(messages.postExitDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="post-exit"
|
id="post-exit"
|
||||||
v-model="hooks.post_exit"
|
v-model="hooks.post_exit"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideHooks"
|
:disabled="!overrideHooks"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.postExitEnter)"
|
:placeholder="formatMessage(messages.postExitEnter)"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
import useMemorySlider from '@/composables/useMemorySlider'
|
|
||||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||||
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, readonly, ref, watch } from 'vue'
|
import { computed, readonly, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
import type { AppSettings, InstanceSettingsTabProps, MemorySettings } from '../../../helpers/types'
|
import type { AppSettings, InstanceSettingsTabProps, MemorySettings } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
@@ -22,14 +24,14 @@ const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
|||||||
|
|
||||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
||||||
const javaArgs = ref(
|
const javaArgs = ref(
|
||||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
||||||
const envVars = ref(
|
const envVars = ref(
|
||||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||||
.map((x) => x.join('='))
|
.map((x) => x.join('='))
|
||||||
.join(' '),
|
.join(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||||
@@ -37,154 +39,154 @@ const memory = ref(props.instance.memory ?? globalSettings.memory)
|
|||||||
const { maxMemory, snapPoints } = await useMemorySlider()
|
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
java_path?: string
|
java_path?: string
|
||||||
extra_launch_args?: string[]
|
extra_launch_args?: string[]
|
||||||
custom_env_vars?: string[][]
|
custom_env_vars?: string[][]
|
||||||
memory?: MemorySettings
|
memory?: MemorySettings
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (overrideJavaInstall.value) {
|
if (overrideJavaInstall.value) {
|
||||||
if (javaInstall.value.path !== '') {
|
if (javaInstall.value.path !== '') {
|
||||||
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overrideJavaArgs.value) {
|
if (overrideJavaArgs.value) {
|
||||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overrideEnvVars.value) {
|
if (overrideEnvVars.value) {
|
||||||
editProfile.custom_env_vars = envVars.value
|
editProfile.custom_env_vars = envVars.value
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overrideMemorySettings.value) {
|
if (overrideMemorySettings.value) {
|
||||||
editProfile.memory = memory.value
|
editProfile.memory = memory.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
overrideJavaInstall,
|
overrideJavaInstall,
|
||||||
javaInstall,
|
javaInstall,
|
||||||
overrideJavaArgs,
|
overrideJavaArgs,
|
||||||
javaArgs,
|
javaArgs,
|
||||||
overrideEnvVars,
|
overrideEnvVars,
|
||||||
envVars,
|
envVars,
|
||||||
overrideMemorySettings,
|
overrideMemorySettings,
|
||||||
memory,
|
memory,
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
javaInstallation: {
|
javaInstallation: {
|
||||||
id: 'instance.settings.tabs.java.java-installation',
|
id: 'instance.settings.tabs.java.java-installation',
|
||||||
defaultMessage: 'Java installation',
|
defaultMessage: 'Java installation',
|
||||||
},
|
},
|
||||||
javaArguments: {
|
javaArguments: {
|
||||||
id: 'instance.settings.tabs.java.java-arguments',
|
id: 'instance.settings.tabs.java.java-arguments',
|
||||||
defaultMessage: 'Java arguments',
|
defaultMessage: 'Java arguments',
|
||||||
},
|
},
|
||||||
javaEnvironmentVariables: {
|
javaEnvironmentVariables: {
|
||||||
id: 'instance.settings.tabs.java.environment-variables',
|
id: 'instance.settings.tabs.java.environment-variables',
|
||||||
defaultMessage: 'Environment variables',
|
defaultMessage: 'Environment variables',
|
||||||
},
|
},
|
||||||
javaMemory: {
|
javaMemory: {
|
||||||
id: 'instance.settings.tabs.java.java-memory',
|
id: 'instance.settings.tabs.java.java-memory',
|
||||||
defaultMessage: 'Memory allocated',
|
defaultMessage: 'Memory allocated',
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
id: 'instance.settings.tabs.java.hooks',
|
id: 'instance.settings.tabs.java.hooks',
|
||||||
defaultMessage: 'Hooks',
|
defaultMessage: 'Hooks',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.javaInstallation) }}
|
{{ formatMessage(messages.javaInstallation) }}
|
||||||
</h2>
|
</h2>
|
||||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
||||||
<template v-if="!overrideJavaInstall">
|
<template v-if="!overrideJavaInstall">
|
||||||
<div class="flex my-2 items-center gap-2 font-semibold">
|
<div class="flex my-2 items-center gap-2 font-semibold">
|
||||||
<template v-if="javaInstall">
|
<template v-if="javaInstall">
|
||||||
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
||||||
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="optimalJava">
|
<template v-else-if="optimalJava">
|
||||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||||
<span
|
<span
|
||||||
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
||||||
one below:</span
|
one below:</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||||
<span
|
<span
|
||||||
>Could not automatically determine a Java installation to use. Please set one
|
>Could not automatically determine a Java installation to use. Please set one
|
||||||
below:</span
|
below:</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="javaInstall && !overrideJavaInstall"
|
v-if="javaInstall && !overrideJavaInstall"
|
||||||
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
||||||
>
|
>
|
||||||
{{ javaInstall.path }}
|
{{ javaInstall.path }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.javaMemory) }}
|
{{ formatMessage(messages.javaMemory) }}
|
||||||
</h2>
|
</h2>
|
||||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
||||||
<Slider
|
<Slider
|
||||||
id="max-memory"
|
id="max-memory"
|
||||||
v-model="memory.maximum"
|
v-model="memory.maximum"
|
||||||
:disabled="!overrideMemorySettings"
|
:disabled="!overrideMemorySettings"
|
||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
:snap-points="snapPoints"
|
:snap-points="snapPoints"
|
||||||
:snap-range="512"
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.javaArguments) }}
|
{{ formatMessage(messages.javaArguments) }}
|
||||||
</h2>
|
</h2>
|
||||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||||
<input
|
<input
|
||||||
id="java-args"
|
id="java-args"
|
||||||
v-model="javaArgs"
|
v-model="javaArgs"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideJavaArgs"
|
:disabled="!overrideJavaArgs"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="Enter java arguments..."
|
placeholder="Enter java arguments..."
|
||||||
/>
|
/>
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||||
</h2>
|
</h2>
|
||||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
||||||
<input
|
<input
|
||||||
id="env-vars"
|
id="env-vars"
|
||||||
v-model="envVars"
|
v-model="envVars"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideEnvVars"
|
:disabled="!overrideEnvVars"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="Enter environmental variables..."
|
placeholder="Enter environmental variables..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { edit } from '@/helpers/profile'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
@@ -14,151 +16,151 @@ const props = defineProps<InstanceSettingsTabProps>()
|
|||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideWindowSettings = ref(
|
const overrideWindowSettings = ref(
|
||||||
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
||||||
)
|
)
|
||||||
const resolution: Ref<[number, number]> = ref(
|
const resolution: Ref<[number, number]> = ref(
|
||||||
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
||||||
)
|
)
|
||||||
const fullscreenSetting: Ref<boolean> = ref(
|
const fullscreenSetting: Ref<boolean> = ref(
|
||||||
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||||
)
|
)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
force_fullscreen?: boolean
|
force_fullscreen?: boolean
|
||||||
game_resolution?: [number, number]
|
game_resolution?: [number, number]
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (overrideWindowSettings.value) {
|
if (overrideWindowSettings.value) {
|
||||||
editProfile.force_fullscreen = fullscreenSetting.value
|
editProfile.force_fullscreen = fullscreenSetting.value
|
||||||
|
|
||||||
if (!fullscreenSetting.value) {
|
if (!fullscreenSetting.value) {
|
||||||
editProfile.game_resolution = resolution.value
|
editProfile.game_resolution = resolution.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[overrideWindowSettings, resolution, fullscreenSetting],
|
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
customWindowSettings: {
|
customWindowSettings: {
|
||||||
id: 'instance.settings.tabs.window.custom-window-settings',
|
id: 'instance.settings.tabs.window.custom-window-settings',
|
||||||
defaultMessage: 'Custom window settings',
|
defaultMessage: 'Custom window settings',
|
||||||
},
|
},
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
id: 'instance.settings.tabs.window.fullscreen',
|
id: 'instance.settings.tabs.window.fullscreen',
|
||||||
defaultMessage: 'Fullscreen',
|
defaultMessage: 'Fullscreen',
|
||||||
},
|
},
|
||||||
fullscreenDescription: {
|
fullscreenDescription: {
|
||||||
id: 'instance.settings.tabs.window.fullscreen.description',
|
id: 'instance.settings.tabs.window.fullscreen.description',
|
||||||
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
id: 'instance.settings.tabs.window.width',
|
id: 'instance.settings.tabs.window.width',
|
||||||
defaultMessage: 'Width',
|
defaultMessage: 'Width',
|
||||||
},
|
},
|
||||||
widthDescription: {
|
widthDescription: {
|
||||||
id: 'instance.settings.tabs.window.width.description',
|
id: 'instance.settings.tabs.window.width.description',
|
||||||
defaultMessage: 'The width of the game window when launched.',
|
defaultMessage: 'The width of the game window when launched.',
|
||||||
},
|
},
|
||||||
enterWidth: {
|
enterWidth: {
|
||||||
id: 'instance.settings.tabs.window.width.enter',
|
id: 'instance.settings.tabs.window.width.enter',
|
||||||
defaultMessage: 'Enter width...',
|
defaultMessage: 'Enter width...',
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
id: 'instance.settings.tabs.window.height',
|
id: 'instance.settings.tabs.window.height',
|
||||||
defaultMessage: 'Height',
|
defaultMessage: 'Height',
|
||||||
},
|
},
|
||||||
heightDescription: {
|
heightDescription: {
|
||||||
id: 'instance.settings.tabs.window.height.description',
|
id: 'instance.settings.tabs.window.height.description',
|
||||||
defaultMessage: 'The height of the game window when launched.',
|
defaultMessage: 'The height of the game window when launched.',
|
||||||
},
|
},
|
||||||
enterHeight: {
|
enterHeight: {
|
||||||
id: 'instance.settings.tabs.window.height.enter',
|
id: 'instance.settings.tabs.window.height.enter',
|
||||||
defaultMessage: 'Enter height...',
|
defaultMessage: 'Enter height...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="overrideWindowSettings"
|
v-model="overrideWindowSettings"
|
||||||
:label="formatMessage(messages.customWindowSettings)"
|
:label="formatMessage(messages.customWindowSettings)"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(value) => {
|
(value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
resolution = globalSettings.game_resolution
|
resolution = globalSettings.game_resolution
|
||||||
fullscreenSetting = globalSettings.force_fullscreen
|
fullscreenSetting = globalSettings.force_fullscreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.fullscreen) }}
|
{{ formatMessage(messages.fullscreen) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.fullscreenDescription) }}
|
{{ formatMessage(messages.fullscreenDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
id="fullscreen"
|
id="fullscreen"
|
||||||
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
||||||
:disabled="!overrideWindowSettings"
|
:disabled="!overrideWindowSettings"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(e) => {
|
(e) => {
|
||||||
fullscreenSetting = e
|
fullscreenSetting = e
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.width) }}
|
{{ formatMessage(messages.width) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.widthDescription) }}
|
{{ formatMessage(messages.widthDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="width"
|
id="width"
|
||||||
v-model="resolution[0]"
|
v-model="resolution[0]"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="formatMessage(messages.enterWidth)"
|
:placeholder="formatMessage(messages.enterWidth)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.height) }}
|
{{ formatMessage(messages.height) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.heightDescription) }}
|
{{ formatMessage(messages.heightDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="height"
|
id="height"
|
||||||
v-model="resolution[1]"
|
v-model="resolution[1]"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="formatMessage(messages.enterHeight)"
|
:placeholder="formatMessage(messages.enterHeight)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ReportIcon,
|
CoffeeIcon,
|
||||||
ModrinthIcon,
|
GameIcon,
|
||||||
ShieldIcon,
|
GaugeIcon,
|
||||||
SettingsIcon,
|
ModrinthIcon,
|
||||||
GaugeIcon,
|
PaintbrushIcon,
|
||||||
PaintbrushIcon,
|
ReportIcon,
|
||||||
GameIcon,
|
SettingsIcon,
|
||||||
CoffeeIcon,
|
ShieldIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { TabbedModal } from '@modrinth/ui'
|
import { TabbedModal } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
|
||||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
|
||||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
|
||||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
|
||||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
|
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||||
import { useTheming } from '@/store/state'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
|
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||||
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
|
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||||
|
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||||
|
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -31,66 +32,66 @@ const { formatMessage } = useVIntl()
|
|||||||
const devModeCounter = ref(0)
|
const devModeCounter = ref(0)
|
||||||
|
|
||||||
const developerModeEnabled = defineMessage({
|
const developerModeEnabled = defineMessage({
|
||||||
id: 'app.settings.developer-mode-enabled',
|
id: 'app.settings.developer-mode-enabled',
|
||||||
defaultMessage: 'Developer mode enabled.',
|
defaultMessage: 'Developer mode enabled.',
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.appearance',
|
id: 'app.settings.tabs.appearance',
|
||||||
defaultMessage: 'Appearance',
|
defaultMessage: 'Appearance',
|
||||||
}),
|
}),
|
||||||
icon: PaintbrushIcon,
|
icon: PaintbrushIcon,
|
||||||
content: AppearanceSettings,
|
content: AppearanceSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.privacy',
|
id: 'app.settings.tabs.privacy',
|
||||||
defaultMessage: 'Privacy',
|
defaultMessage: 'Privacy',
|
||||||
}),
|
}),
|
||||||
icon: ShieldIcon,
|
icon: ShieldIcon,
|
||||||
content: PrivacySettings,
|
content: PrivacySettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.java-installations',
|
id: 'app.settings.tabs.java-installations',
|
||||||
defaultMessage: 'Java installations',
|
defaultMessage: 'Java installations',
|
||||||
}),
|
}),
|
||||||
icon: CoffeeIcon,
|
icon: CoffeeIcon,
|
||||||
content: JavaSettings,
|
content: JavaSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.default-instance-options',
|
id: 'app.settings.tabs.default-instance-options',
|
||||||
defaultMessage: 'Default instance options',
|
defaultMessage: 'Default instance options',
|
||||||
}),
|
}),
|
||||||
icon: GameIcon,
|
icon: GameIcon,
|
||||||
content: DefaultInstanceSettings,
|
content: DefaultInstanceSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.resource-management',
|
id: 'app.settings.tabs.resource-management',
|
||||||
defaultMessage: 'Resource management',
|
defaultMessage: 'Resource management',
|
||||||
}),
|
}),
|
||||||
icon: GaugeIcon,
|
icon: GaugeIcon,
|
||||||
content: ResourceManagementSettings,
|
content: ResourceManagementSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.feature-flags',
|
id: 'app.settings.tabs.feature-flags',
|
||||||
defaultMessage: 'Feature flags',
|
defaultMessage: 'Feature flags',
|
||||||
}),
|
}),
|
||||||
icon: ReportIcon,
|
icon: ReportIcon,
|
||||||
content: FeatureFlagSettings,
|
content: FeatureFlagSettings,
|
||||||
developerOnly: true,
|
developerOnly: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = computed(() => modal.value?.isOpen)
|
const isOpen = computed(() => modal.value?.isOpen)
|
||||||
@@ -103,59 +104,62 @@ const osVersion = getOsVersion()
|
|||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function devModeCount() {
|
function devModeCount() {
|
||||||
devModeCounter.value++
|
devModeCounter.value++
|
||||||
if (devModeCounter.value > 5) {
|
if (devModeCounter.value > 5) {
|
||||||
themeStore.devMode = !themeStore.devMode
|
themeStore.devMode = !themeStore.devMode
|
||||||
settings.value.developer_mode = !!themeStore.devMode
|
settings.value.developer_mode = !!themeStore.devMode
|
||||||
devModeCounter.value = 0
|
devModeCounter.value = 0
|
||||||
|
|
||||||
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
||||||
modal.value.setTab(0)
|
modal.value.setTab(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
<SettingsIcon /> Settings
|
<SettingsIcon /> Settings
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="mt-auto text-secondary text-sm">
|
<div class="mt-auto text-secondary text-sm">
|
||||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
{{ formatMessage(developerModeEnabled) }}
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
:class="{
|
||||||
@click="devModeCount"
|
'text-brand': themeStore.devMode,
|
||||||
>
|
'text-secondary': !themeStore.devMode,
|
||||||
<ModrinthIcon class="w-6 h-6" />
|
}"
|
||||||
</button>
|
@click="devModeCount"
|
||||||
<div>
|
>
|
||||||
<p class="m-0">Modrinth App {{ version }}</p>
|
<ModrinthIcon class="w-6 h-6" />
|
||||||
<p class="m-0">
|
</button>
|
||||||
<span v-if="osPlatform === 'macos'">MacOS</span>
|
<div>
|
||||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
<p class="m-0">Modrinth App {{ version }}</p>
|
||||||
{{ osVersion }}
|
<p class="m-0">
|
||||||
</p>
|
<span v-if="osPlatform === 'macos'">MacOS</span>
|
||||||
</div>
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
</div>
|
{{ osVersion }}
|
||||||
</div>
|
</p>
|
||||||
</template>
|
</div>
|
||||||
</TabbedModal>
|
</div>
|
||||||
</ModalWrapper>
|
</div>
|
||||||
|
</template>
|
||||||
|
</TabbedModal>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,42 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
onFlowCancel: {
|
onFlowCancel: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
return async () => {}
|
return async () => {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
<LogInIcon /> Sign in
|
<LogInIcon /> Sign in
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-secondary">
|
<p class="text-sm text-secondary">
|
||||||
Please sign in at the browser window that just opened to continue.
|
Please sign in at the browser window that just opened to continue.
|
||||||
</p>
|
</p>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,90 +1,91 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ConfirmModal } from '@modrinth/ui'
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
import { useTheming } from '@/store/theme.ts'
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
confirmationText: {
|
confirmationText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
hasToType: {
|
hasToType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'No title defined',
|
default: 'No title defined',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'No description defined',
|
default: 'No description defined',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
proceedIcon: {
|
proceedIcon: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
proceedLabel: {
|
proceedLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Proceed',
|
default: 'Proceed',
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
showAdOnClose: {
|
showAdOnClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
markdown: {
|
markdown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['proceed'])
|
const emit = defineEmits(['proceed'])
|
||||||
const modal = ref(null)
|
const modal = ref(null)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
hide_ads_window()
|
hide_ads_window()
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
onModalHide()
|
onModalHide()
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
if (props.showAdOnClose) {
|
if (props.showAdOnClose) {
|
||||||
show_ads_window()
|
show_ads_window()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function proceed() {
|
function proceed() {
|
||||||
emit('proceed')
|
emit('proceed')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:confirmation-text="confirmationText"
|
:confirmation-text="confirmationText"
|
||||||
:has-to-type="hasToType"
|
:has-to-type="hasToType"
|
||||||
:title="title"
|
:title="title"
|
||||||
:description="description"
|
:description="description"
|
||||||
:proceed-icon="proceedIcon"
|
:proceed-icon="proceedIcon"
|
||||||
:proceed-label="proceedLabel"
|
:proceed-label="proceedLabel"
|
||||||
:on-hide="onModalHide"
|
:on-hide="onModalHide"
|
||||||
:noblur="!themeStore.advancedRendering"
|
:noblur="!themeStore.advancedRendering"
|
||||||
:danger="danger"
|
:danger="danger"
|
||||||
:markdown="markdown"
|
:markdown="markdown"
|
||||||
@proceed="proceed"
|
@proceed="proceed"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
import { ChevronRightIcon } from '@modrinth/assets'
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Avatar } from '@modrinth/ui'
|
import { Avatar } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
size="24px"
|
size="24px"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
/>
|
/>
|
||||||
{{ instance.name }} <ChevronRightIcon />
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CoffeeIcon,
|
CodeIcon,
|
||||||
InfoIcon,
|
CoffeeIcon,
|
||||||
WrenchIcon,
|
InfoIcon,
|
||||||
MonitorIcon,
|
MonitorIcon,
|
||||||
CodeIcon,
|
WrenchIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||||
|
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||||
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
||||||
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -24,75 +26,75 @@ const { formatMessage } = useVIntl()
|
|||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|
||||||
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.general',
|
id: 'instance.settings.tabs.general',
|
||||||
defaultMessage: 'General',
|
defaultMessage: 'General',
|
||||||
}),
|
}),
|
||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
content: GeneralSettings,
|
content: GeneralSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.installation',
|
id: 'instance.settings.tabs.installation',
|
||||||
defaultMessage: 'Installation',
|
defaultMessage: 'Installation',
|
||||||
}),
|
}),
|
||||||
icon: WrenchIcon,
|
icon: WrenchIcon,
|
||||||
content: InstallationSettings,
|
content: InstallationSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.window',
|
id: 'instance.settings.tabs.window',
|
||||||
defaultMessage: 'Window',
|
defaultMessage: 'Window',
|
||||||
}),
|
}),
|
||||||
icon: MonitorIcon,
|
icon: MonitorIcon,
|
||||||
content: WindowSettings,
|
content: WindowSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.java',
|
id: 'instance.settings.tabs.java',
|
||||||
defaultMessage: 'Java and memory',
|
defaultMessage: 'Java and memory',
|
||||||
}),
|
}),
|
||||||
icon: CoffeeIcon,
|
icon: CoffeeIcon,
|
||||||
content: JavaSettings,
|
content: JavaSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.hooks',
|
id: 'instance.settings.tabs.hooks',
|
||||||
defaultMessage: 'Launch hooks',
|
defaultMessage: 'Launch hooks',
|
||||||
}),
|
}),
|
||||||
icon: CodeIcon,
|
icon: CodeIcon,
|
||||||
content: HooksSettings,
|
content: HooksSettings,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const titleMessage = defineMessage({
|
const titleMessage = defineMessage({
|
||||||
id: 'instance.settings.title',
|
id: 'instance.settings.title',
|
||||||
defaultMessage: 'Settings',
|
defaultMessage: 'Settings',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
size="24px"
|
size="24px"
|
||||||
:tint-by="props.instance.path"
|
:tint-by="props.instance.path"
|
||||||
/>
|
/>
|
||||||
{{ instance.name }} <ChevronRightIcon />
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,57 +1,58 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef } from 'vue'
|
|
||||||
import { NewModal as Modal } from '@modrinth/ui'
|
import { NewModal as Modal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
import { useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
import { useTheming } from '@/store/theme.ts'
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
header: {
|
header: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
closable: {
|
closable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
onHide: {
|
onHide: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
return () => {}
|
return () => {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showAdOnClose: {
|
showAdOnClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (e: MouseEvent) => {
|
show: (e: MouseEvent) => {
|
||||||
hide_ads_window()
|
hide_ads_window()
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
onModalHide()
|
onModalHide()
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
if (props.showAdOnClose) {
|
if (props.showAdOnClose) {
|
||||||
show_ads_window()
|
show_ads_window()
|
||||||
}
|
}
|
||||||
props.onHide?.()
|
props.onHide?.()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
||||||
<template #title>
|
<template #title>
|
||||||
<slot name="title" />
|
<slot name="title" />
|
||||||
</template>
|
</template>
|
||||||
<slot />
|
<slot />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,61 +1,62 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ShareModal } from '@modrinth/ui'
|
import { ShareModal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
import { useTheming } from '@/store/theme.ts'
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
header: {
|
header: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Share',
|
default: 'Share',
|
||||||
},
|
},
|
||||||
shareTitle: {
|
shareTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Modrinth',
|
default: 'Modrinth',
|
||||||
},
|
},
|
||||||
shareText: {
|
shareText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
openInNewTab: {
|
openInNewTab: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref(null)
|
const modal = ref(null)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (passedContent) => {
|
show: (passedContent) => {
|
||||||
hide_ads_window()
|
hide_ads_window()
|
||||||
modal.value.show(passedContent)
|
modal.value.show(passedContent)
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
onModalHide()
|
onModalHide()
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
show_ads_window()
|
show_ads_window()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ShareModal
|
<ShareModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:header="header"
|
:header="header"
|
||||||
:share-title="shareTitle"
|
:share-title="shareTitle"
|
||||||
:share-text="shareText"
|
:share-text="shareText"
|
||||||
:link="link"
|
:link="link"
|
||||||
:open-in-new-tab="openInNewTab"
|
:open-in-new-tab="openInNewTab"
|
||||||
:on-hide="onModalHide"
|
:on-hide="onModalHide"
|
||||||
:noblur="!themeStore.advancedRendering"
|
:noblur="!themeStore.advancedRendering"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { getOS } from '@/helpers/utils'
|
import { getOS } from '@/helpers/utils'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
import type { ColorTheme } from '@/store/theme.ts'
|
import type { ColorTheme } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
@@ -12,119 +13,119 @@ const os = ref(await getOS())
|
|||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
||||||
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||||
|
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
:update-color-theme="
|
:update-color-theme="
|
||||||
(theme: ColorTheme) => {
|
(theme: ColorTheme) => {
|
||||||
themeStore.setThemeState(theme)
|
themeStore.setThemeState(theme)
|
||||||
settings.theme = theme
|
settings.theme = theme
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
:current-theme="settings.theme"
|
:current-theme="settings.theme"
|
||||||
:theme-options="themeStore.getThemeOptions()"
|
:theme-options="themeStore.getThemeOptions()"
|
||||||
system-theme-color="system"
|
system-theme-color="system"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
||||||
<p class="m-0 mt-1">
|
<p class="m-0 mt-1">
|
||||||
Enables advanced rendering such as blur effects that may cause performance issues without
|
Enables advanced rendering such as blur effects that may cause performance issues without
|
||||||
hardware-accelerated rendering.
|
hardware-accelerated rendering.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
id="advanced-rendering"
|
id="advanced-rendering"
|
||||||
:model-value="themeStore.advancedRendering"
|
:model-value="themeStore.advancedRendering"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(e) => {
|
(e) => {
|
||||||
themeStore.advancedRendering = e
|
themeStore.advancedRendering = e
|
||||||
settings.advanced_rendering = themeStore.advancedRendering
|
settings.advanced_rendering = themeStore.advancedRendering
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
||||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
||||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
||||||
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
||||||
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||||
</div>
|
</div>
|
||||||
<TeleportDropdownMenu
|
<TeleportDropdownMenu
|
||||||
id="opening-page"
|
id="opening-page"
|
||||||
v-model="settings.default_page"
|
v-model="settings.default_page"
|
||||||
name="Opening page dropdown"
|
name="Opening page dropdown"
|
||||||
class="w-40"
|
class="w-40"
|
||||||
:options="['Home', 'Library']"
|
:options="['Home', 'Library']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
||||||
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
() => {
|
() => {
|
||||||
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
||||||
themeStore.featureFlags['worlds_in_home'] = newValue
|
themeStore.featureFlags['worlds_in_home'] = newValue
|
||||||
settings.feature_flags['worlds_in_home'] = newValue
|
settings.feature_flags['worlds_in_home'] = newValue
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||||
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
|
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
id="toggle-sidebar"
|
id="toggle-sidebar"
|
||||||
:model-value="settings.toggle_sidebar"
|
:model-value="settings.toggle_sidebar"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(e) => {
|
(e) => {
|
||||||
settings.toggle_sidebar = e
|
settings.toggle_sidebar = e
|
||||||
themeStore.toggleSidebar = settings.toggle_sidebar
|
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { Slider, Toggle } from '@modrinth/ui'
|
import { Slider, Toggle } from '@modrinth/ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import useMemorySlider from '@/composables/useMemorySlider'
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
const fetchSettings = await get()
|
const fetchSettings = await get()
|
||||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||||
@@ -13,161 +14,161 @@ const settings = ref(fetchSettings)
|
|||||||
const { maxMemory, snapPoints } = await useMemorySlider()
|
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||||
|
|
||||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||||
setSettings.custom_env_vars = setSettings.envVars
|
setSettings.custom_env_vars = setSettings.envVars
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
|
|
||||||
if (!setSettings.hooks.pre_launch) {
|
if (!setSettings.hooks.pre_launch) {
|
||||||
setSettings.hooks.pre_launch = null
|
setSettings.hooks.pre_launch = null
|
||||||
}
|
}
|
||||||
if (!setSettings.hooks.wrapper) {
|
if (!setSettings.hooks.wrapper) {
|
||||||
setSettings.hooks.wrapper = null
|
setSettings.hooks.wrapper = null
|
||||||
}
|
}
|
||||||
if (!setSettings.hooks.post_exit) {
|
if (!setSettings.hooks.post_exit) {
|
||||||
setSettings.hooks.post_exit = null
|
setSettings.hooks.post_exit = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!setSettings.custom_dir) {
|
if (!setSettings.custom_dir) {
|
||||||
setSettings.custom_dir = null
|
setSettings.custom_dir = null
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(setSettings)
|
await set(setSettings)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
Overwrites the options.txt file to start in full screen when launched.
|
Overwrites the options.txt file to start in full screen when launched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The width of the game window when launched.
|
The width of the game window when launched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="width"
|
id="width"
|
||||||
v-model="settings.game_resolution[0]"
|
v-model="settings.game_resolution[0]"
|
||||||
:disabled="settings.force_fullscreen"
|
:disabled="settings.force_fullscreen"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Enter width..."
|
placeholder="Enter width..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The height of the game window when launched.
|
The height of the game window when launched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="height"
|
id="height"
|
||||||
v-model="settings.game_resolution[1]"
|
v-model="settings.game_resolution[1]"
|
||||||
:disabled="settings.force_fullscreen"
|
:disabled="settings.force_fullscreen"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="number"
|
type="number"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="Enter height..."
|
placeholder="Enter height..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||||
<Slider
|
<Slider
|
||||||
id="max-memory"
|
id="max-memory"
|
||||||
v-model="settings.memory.maximum"
|
v-model="settings.memory.maximum"
|
||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
:snap-points="snapPoints"
|
:snap-points="snapPoints"
|
||||||
:snap-range="512"
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
||||||
<input
|
<input
|
||||||
id="java-args"
|
id="java-args"
|
||||||
v-model="settings.launchArgs"
|
v-model="settings.launchArgs"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter java arguments..."
|
placeholder="Enter java arguments..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
||||||
<input
|
<input
|
||||||
id="env-vars"
|
id="env-vars"
|
||||||
v-model="settings.envVars"
|
v-model="settings.envVars"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter environmental variables..."
|
placeholder="Enter environmental variables..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
||||||
<input
|
<input
|
||||||
id="pre-launch"
|
id="pre-launch"
|
||||||
v-model="settings.hooks.pre_launch"
|
v-model="settings.hooks.pre_launch"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter pre-launch command..."
|
placeholder="Enter pre-launch command..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
Wrapper command for launching Minecraft.
|
Wrapper command for launching Minecraft.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="wrapper"
|
id="wrapper"
|
||||||
v-model="settings.hooks.wrapper"
|
v-model="settings.hooks.wrapper"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter wrapper command..."
|
placeholder="Enter wrapper command..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
||||||
<input
|
<input
|
||||||
id="post-exit"
|
id="post-exit"
|
||||||
v-model="settings.hooks.post_exit"
|
v-model="settings.hooks.post_exit"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter post-exit command..."
|
placeholder="Enter post-exit command..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { get as getSettings, set as setSettings } from '@/helpers/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'
|
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
@@ -11,30 +12,30 @@ const settings = ref(await getSettings())
|
|||||||
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||||
|
|
||||||
function setFeatureFlag(key: string, value: boolean) {
|
function setFeatureFlag(key: string, value: boolean) {
|
||||||
themeStore.featureFlags[key] = value
|
themeStore.featureFlags[key] = value
|
||||||
settings.value.feature_flags[key] = value
|
settings.value.feature_flags[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await setSettings(settings.value)
|
await setSettings(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||||
{{ option.replaceAll('_', ' ') }}
|
{{ option.replaceAll('_', ' ') }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
id="advanced-rendering"
|
id="advanced-rendering"
|
||||||
:model-value="themeStore.getFeatureFlag(option)"
|
:model-value="themeStore.getFeatureFlag(option)"
|
||||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||||
async function updateJavaVersion(version) {
|
async function updateJavaVersion(version) {
|
||||||
if (version?.path === '') {
|
if (version?.path === '') {
|
||||||
version.path = undefined
|
version.path = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version?.path) {
|
if (version?.path) {
|
||||||
version.path = version.path.replace('java.exe', 'javaw.exe')
|
version.path = version.path.replace('java.exe', 'javaw.exe')
|
||||||
}
|
}
|
||||||
|
|
||||||
await set_java_version(version).catch(handleError)
|
await set_java_version(version).catch(handleError)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||||
Java {{ javaVersion }} location
|
Java {{ javaVersion }} location
|
||||||
</h2>
|
</h2>
|
||||||
<JavaSelector
|
<JavaSelector
|
||||||
:id="'java-selector-' + javaVersion"
|
:id="'java-selector-' + javaVersion"
|
||||||
v-model="javaVersions[javaVersion]"
|
v-model="javaVersions[javaVersion]"
|
||||||
:version="javaVersion"
|
:version="javaVersion"
|
||||||
@update:model-value="updateJavaVersion"
|
@update:model-value="updateJavaVersion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,62 +1,63 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
if (settings.value.telemetry) {
|
if (settings.value.telemetry) {
|
||||||
optInAnalytics()
|
optInAnalytics()
|
||||||
} else {
|
} else {
|
||||||
optOutAnalytics()
|
optOutAnalytics()
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||||
option, you opt out and ads will no longer be shown based on your interests.
|
option, you opt out and ads will no longer be shown based on your interests.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="personalized-ads" v-model="settings.personalized_ads" />
|
<Toggle id="personalized-ads" v-model="settings.personalized_ads" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between gap-4">
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||||
customize your experience. By disabling this option, you opt out and your data will no
|
customize your experience. By disabling this option, you opt out and your data will no
|
||||||
longer be collected.
|
longer be collected.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="opt-out-analytics" v-model="settings.telemetry" />
|
<Toggle id="opt-out-analytics" v-model="settings.telemetry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between gap-4">
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
||||||
longer show up as a game or app you are using on your Discord profile.
|
longer show up as a game or app you are using on your Discord profile.
|
||||||
</p>
|
</p>
|
||||||
<p class="m-0 mt-2 text-sm">
|
<p class="m-0 mt-2 text-sm">
|
||||||
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
||||||
as those added by mods. (app restart required to take effect)
|
as those added by mods. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
|
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,117 +1,118 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||||
|
import { Button, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
import { purge_cache_types } from '@/helpers/cache.js'
|
import { purge_cache_types } from '@/helpers/cache.js'
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
|
||||||
import { Button, Slider, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||||
|
|
||||||
if (!setSettings.custom_dir) {
|
if (!setSettings.custom_dir) {
|
||||||
setSettings.custom_dir = null
|
setSettings.custom_dir = null
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(setSettings)
|
await set(setSettings)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
async function purgeCache() {
|
async function purgeCache() {
|
||||||
await purge_cache_types([
|
await purge_cache_types([
|
||||||
'project',
|
'project',
|
||||||
'version',
|
'version',
|
||||||
'user',
|
'user',
|
||||||
'team',
|
'team',
|
||||||
'organization',
|
'organization',
|
||||||
'loader_manifest',
|
'loader_manifest',
|
||||||
'minecraft_manifest',
|
'minecraft_manifest',
|
||||||
'categories',
|
'categories',
|
||||||
'report_types',
|
'report_types',
|
||||||
'loaders',
|
'loaders',
|
||||||
'game_versions',
|
'game_versions',
|
||||||
'donation_platforms',
|
'donation_platforms',
|
||||||
'file_update',
|
'file_update',
|
||||||
'search_results',
|
'search_results',
|
||||||
]).catch(handleError)
|
]).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findLauncherDir() {
|
async function findLauncherDir() {
|
||||||
const newDir = await open({
|
const newDir = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
directory: true,
|
directory: true,
|
||||||
title: 'Select a new app directory',
|
title: 'Select a new app directory',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (newDir) {
|
if (newDir) {
|
||||||
settings.value.custom_dir = newDir
|
settings.value.custom_dir = newDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The directory where the launcher stores all of its files. Changes will be applied after
|
The directory where the launcher stores all of its files. Changes will be applied after
|
||||||
restarting the launcher.
|
restarting the launcher.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="m-1 my-2">
|
<div class="m-1 my-2">
|
||||||
<div class="iconified-input w-full">
|
<div class="iconified-input w-full">
|
||||||
<BoxIcon />
|
<BoxIcon />
|
||||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||||
<Button class="r-btn" @click="findLauncherDir">
|
<Button class="r-btn" @click="findLauncherDir">
|
||||||
<FolderSearchIcon />
|
<FolderSearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="purgeCacheConfirmModal"
|
ref="purgeCacheConfirmModal"
|
||||||
title="Are you sure you want to purge the cache?"
|
title="Are you sure you want to purge the cache?"
|
||||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Purge cache"
|
proceed-label="Purge cache"
|
||||||
:show-ad-on-close="false"
|
:show-ad-on-close="false"
|
||||||
@proceed="purgeCache"
|
@proceed="purgeCache"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||||
app to reload data. This may slow down the app temporarily.
|
app to reload data. This may slow down the app temporarily.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
Purge cache
|
Purge cache
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||||
value if you have a poor internet connection. (app restart required to take effect)
|
value if you have a poor internet connection. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
<Slider
|
<Slider
|
||||||
id="max-downloads"
|
id="max-downloads"
|
||||||
v-model="settings.max_concurrent_downloads"
|
v-model="settings.max_concurrent_downloads"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="10"
|
:max="10"
|
||||||
:step="1"
|
:step="1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,136 +1,137 @@
|
|||||||
<template>
|
<template>
|
||||||
<UploadSkinModal ref="uploadModal" />
|
<UploadSkinModal ref="uploadModal" />
|
||||||
<ModalWrapper ref="modal" @on-hide="resetState">
|
<ModalWrapper ref="modal" @on-hide="resetState">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="text-lg font-extrabold text-contrast">
|
<span class="text-lg font-extrabold text-contrast">
|
||||||
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||||
<SkinPreviewRenderer
|
<SkinPreviewRenderer
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
:texture-src="previewSkin || ''"
|
:texture-src="previewSkin || ''"
|
||||||
:cape-src="selectedCapeTexture"
|
:cape-src="selectedCapeTexture"
|
||||||
:scale="1.4"
|
:scale="1.4"
|
||||||
:fov="50"
|
:fov="50"
|
||||||
:initial-rotation="Math.PI / 8"
|
:initial-rotation="Math.PI / 8"
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
||||||
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
||||||
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
||||||
</template>
|
</template>
|
||||||
</RadioButtons>
|
</RadioButtons>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<CapeButton
|
<CapeButton
|
||||||
v-if="defaultCape"
|
v-if="defaultCape"
|
||||||
:id="defaultCape.id"
|
:id="defaultCape.id"
|
||||||
:texture="defaultCape.texture"
|
:texture="defaultCape.texture"
|
||||||
:name="undefined"
|
:name="undefined"
|
||||||
:selected="!selectedCape"
|
:selected="!selectedCape"
|
||||||
faded
|
faded
|
||||||
@select="selectCape(undefined)"
|
@select="selectCape(undefined)"
|
||||||
>
|
>
|
||||||
<span>Use default cape</span>
|
<span>Use default cape</span>
|
||||||
</CapeButton>
|
</CapeButton>
|
||||||
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
||||||
<span>Use default cape</span>
|
<span>Use default cape</span>
|
||||||
</CapeLikeTextButton>
|
</CapeLikeTextButton>
|
||||||
|
|
||||||
<CapeButton
|
<CapeButton
|
||||||
v-for="cape in visibleCapeList"
|
v-for="cape in visibleCapeList"
|
||||||
:id="cape.id"
|
:id="cape.id"
|
||||||
:key="cape.id"
|
:key="cape.id"
|
||||||
:texture="cape.texture"
|
:texture="cape.texture"
|
||||||
:name="cape.name || 'Cape'"
|
:name="cape.name || 'Cape'"
|
||||||
:selected="selectedCape?.id === cape.id"
|
:selected="selectedCape?.id === cape.id"
|
||||||
@select="selectCape(cape)"
|
@select="selectCape(cape)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CapeLikeTextButton
|
<CapeLikeTextButton
|
||||||
v-if="(capes?.length ?? 0) > 2"
|
v-if="(capes?.length ?? 0) > 2"
|
||||||
tooltip="View more capes"
|
tooltip="View more capes"
|
||||||
@mouseup="openSelectCapeModal"
|
@mouseup="openSelectCapeModal"
|
||||||
>
|
>
|
||||||
<template #icon><ChevronRightIcon /></template>
|
<template #icon><ChevronRightIcon /></template>
|
||||||
<span>More</span>
|
<span>More</span>
|
||||||
</CapeLikeTextButton>
|
</CapeLikeTextButton>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 mt-12">
|
<div class="flex gap-2 mt-12">
|
||||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||||
<CheckIcon v-else-if="mode === 'new'" />
|
<CheckIcon v-else-if="mode === 'new'" />
|
||||||
<SaveIcon v-else />
|
<SaveIcon v-else />
|
||||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
|
|
||||||
<SelectCapeModal
|
<SelectCapeModal
|
||||||
ref="selectCapeModal"
|
ref="selectCapeModal"
|
||||||
:capes="capes || []"
|
:capes="capes || []"
|
||||||
@select="handleCapeSelected"
|
@select="handleCapeSelected"
|
||||||
@cancel="handleCapeCancel"
|
@cancel="handleCapeCancel"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
SaveIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
UploadIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonStyled,
|
||||||
|
CapeButton,
|
||||||
|
CapeLikeTextButton,
|
||||||
|
injectNotificationManager,
|
||||||
|
RadioButtons,
|
||||||
|
SkinPreviewRenderer,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||||
import {
|
import {
|
||||||
add_and_equip_custom_skin,
|
add_and_equip_custom_skin,
|
||||||
determineModelType,
|
type Cape,
|
||||||
get_normalized_skin_texture,
|
determineModelType,
|
||||||
remove_custom_skin,
|
get_normalized_skin_texture,
|
||||||
unequip_skin,
|
remove_custom_skin,
|
||||||
type Cape,
|
type Skin,
|
||||||
type Skin,
|
type SkinModel,
|
||||||
type SkinModel,
|
unequip_skin,
|
||||||
} from '@/helpers/skins.ts'
|
} from '@/helpers/skins.ts'
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SaveIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
UploadIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonStyled,
|
|
||||||
CapeButton,
|
|
||||||
CapeLikeTextButton,
|
|
||||||
injectNotificationManager,
|
|
||||||
RadioButtons,
|
|
||||||
SkinPreviewRenderer,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
@@ -152,264 +153,264 @@ const selectedCapeTexture = computed(() => selectedCape.value?.texture)
|
|||||||
const visibleCapeList = ref<Cape[]>([])
|
const visibleCapeList = ref<Cape[]>([])
|
||||||
|
|
||||||
const sortedCapes = computed(() => {
|
const sortedCapes = computed(() => {
|
||||||
return [...(props.capes || [])].sort((a, b) => {
|
return [...(props.capes || [])].sort((a, b) => {
|
||||||
const nameA = (a.name || '').toLowerCase()
|
const nameA = (a.name || '').toLowerCase()
|
||||||
const nameB = (b.name || '').toLowerCase()
|
const nameB = (b.name || '').toLowerCase()
|
||||||
return nameA.localeCompare(nameB)
|
return nameA.localeCompare(nameB)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function initVisibleCapeList() {
|
function initVisibleCapeList() {
|
||||||
if (!props.capes || props.capes.length === 0) {
|
if (!props.capes || props.capes.length === 0) {
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibleCapeList.value.length === 0) {
|
if (visibleCapeList.value.length === 0) {
|
||||||
if (selectedCape.value) {
|
if (selectedCape.value) {
|
||||||
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
||||||
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
||||||
} else {
|
} else {
|
||||||
visibleCapeList.value = getSortedCapes(2)
|
visibleCapeList.value = getSortedCapes(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedCapes(count: number): Cape[] {
|
function getSortedCapes(count: number): Cape[] {
|
||||||
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
||||||
return sortedCapes.value.slice(0, count)
|
return sortedCapes.value.slice(0, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
||||||
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
||||||
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPreviewSkin() {
|
async function loadPreviewSkin() {
|
||||||
if (uploadedTextureUrl.value) {
|
if (uploadedTextureUrl.value) {
|
||||||
previewSkin.value = uploadedTextureUrl.value
|
previewSkin.value = uploadedTextureUrl.value
|
||||||
} else if (currentSkin.value) {
|
} else if (currentSkin.value) {
|
||||||
try {
|
try {
|
||||||
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
|
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load skin texture:', error)
|
console.error('Failed to load skin texture:', error)
|
||||||
previewSkin.value = '/src/assets/skins/steve.png'
|
previewSkin.value = '/src/assets/skins/steve.png'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
previewSkin.value = '/src/assets/skins/steve.png'
|
previewSkin.value = '/src/assets/skins/steve.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasEdits = computed(() => {
|
const hasEdits = computed(() => {
|
||||||
if (mode.value !== 'edit') return true
|
if (mode.value !== 'edit') return true
|
||||||
if (uploadedTextureUrl.value) return true
|
if (uploadedTextureUrl.value) return true
|
||||||
if (!currentSkin.value) return false
|
if (!currentSkin.value) return false
|
||||||
if (variant.value !== currentSkin.value.variant) return true
|
if (variant.value !== currentSkin.value.variant) return true
|
||||||
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
|
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const disableSave = computed(
|
const disableSave = computed(
|
||||||
() =>
|
() =>
|
||||||
(mode.value === 'new' && !uploadedTextureUrl.value) ||
|
(mode.value === 'new' && !uploadedTextureUrl.value) ||
|
||||||
(mode.value === 'edit' && !hasEdits.value),
|
(mode.value === 'edit' && !hasEdits.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveTooltip = computed(() => {
|
const saveTooltip = computed(() => {
|
||||||
if (isSaving.value) return 'Saving...'
|
if (isSaving.value) return 'Saving...'
|
||||||
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
||||||
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
mode.value = 'new'
|
mode.value = 'new'
|
||||||
currentSkin.value = null
|
currentSkin.value = null
|
||||||
uploadedTextureUrl.value = null
|
uploadedTextureUrl.value = null
|
||||||
previewSkin.value = ''
|
previewSkin.value = ''
|
||||||
variant.value = 'CLASSIC'
|
variant.value = 'CLASSIC'
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function show(e: MouseEvent, skin?: Skin) {
|
async function show(e: MouseEvent, skin?: Skin) {
|
||||||
mode.value = skin ? 'edit' : 'new'
|
mode.value = skin ? 'edit' : 'new'
|
||||||
currentSkin.value = skin ?? null
|
currentSkin.value = skin ?? null
|
||||||
if (skin) {
|
if (skin) {
|
||||||
variant.value = skin.variant
|
variant.value = skin.variant
|
||||||
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
|
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
|
||||||
} else {
|
} else {
|
||||||
variant.value = 'CLASSIC'
|
variant.value = 'CLASSIC'
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
}
|
}
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
|
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
|
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
||||||
mode.value = 'new'
|
mode.value = 'new'
|
||||||
currentSkin.value = null
|
currentSkin.value = null
|
||||||
uploadedTextureUrl.value = skinTextureUrl
|
uploadedTextureUrl.value = skinTextureUrl
|
||||||
variant.value = await determineModelType(skinTextureUrl)
|
variant.value = await determineModelType(skinTextureUrl)
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
|
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
|
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreWithNewTexture(skinTextureUrl: string) {
|
async function restoreWithNewTexture(skinTextureUrl: string) {
|
||||||
uploadedTextureUrl.value = skinTextureUrl
|
uploadedTextureUrl.value = skinTextureUrl
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
|
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
setTimeout(() => resetState(), 250)
|
setTimeout(() => resetState(), 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCape(cape: Cape | undefined) {
|
function selectCape(cape: Cape | undefined) {
|
||||||
if (cape && selectedCape.value?.id !== cape.id) {
|
if (cape && selectedCape.value?.id !== cape.id) {
|
||||||
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
||||||
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
||||||
visibleCapeList.value.splice(0, 1, cape)
|
visibleCapeList.value.splice(0, 1, cape)
|
||||||
|
|
||||||
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
||||||
const otherCape = getSortedCapeExcluding(cape.id)
|
const otherCape = getSortedCapeExcluding(cape.id)
|
||||||
if (otherCape) {
|
if (otherCape) {
|
||||||
visibleCapeList.value.splice(1, 1, otherCape)
|
visibleCapeList.value.splice(1, 1, otherCape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectedCape.value = cape
|
selectedCape.value = cape
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCapeSelected(cape: Cape | undefined) {
|
function handleCapeSelected(cape: Cape | undefined) {
|
||||||
selectCape(cape)
|
selectCape(cape)
|
||||||
|
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCapeCancel() {
|
function handleCapeCancel() {
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSelectCapeModal(e: MouseEvent) {
|
function openSelectCapeModal(e: MouseEvent) {
|
||||||
if (!selectCapeModal.value) return
|
if (!selectCapeModal.value) return
|
||||||
|
|
||||||
shouldRestoreModal.value = true
|
shouldRestoreModal.value = true
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
selectCapeModal.value?.show(
|
selectCapeModal.value?.show(
|
||||||
e,
|
e,
|
||||||
currentSkin.value?.texture_key,
|
currentSkin.value?.texture_key,
|
||||||
selectedCape.value,
|
selectedCape.value,
|
||||||
previewSkin.value,
|
previewSkin.value,
|
||||||
variant.value,
|
variant.value,
|
||||||
)
|
)
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openUploadSkinModal(e: MouseEvent) {
|
function openUploadSkinModal(e: MouseEvent) {
|
||||||
shouldRestoreModal.value = true
|
shouldRestoreModal.value = true
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
emit('open-upload-modal', e)
|
emit('open-upload-modal', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreModal() {
|
function restoreModal() {
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const fakeEvent = new MouseEvent('click')
|
const fakeEvent = new MouseEvent('click')
|
||||||
modal.value?.show(fakeEvent)
|
modal.value?.show(fakeEvent)
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let textureUrl: string
|
let textureUrl: string
|
||||||
|
|
||||||
if (uploadedTextureUrl.value) {
|
if (uploadedTextureUrl.value) {
|
||||||
textureUrl = uploadedTextureUrl.value
|
textureUrl = uploadedTextureUrl.value
|
||||||
} else {
|
} else {
|
||||||
textureUrl = currentSkin.value!.texture
|
textureUrl = currentSkin.value!.texture
|
||||||
}
|
}
|
||||||
|
|
||||||
await unequip_skin()
|
await unequip_skin()
|
||||||
|
|
||||||
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
||||||
|
|
||||||
if (mode.value === 'new') {
|
if (mode.value === 'new') {
|
||||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||||
emit('saved')
|
emit('saved')
|
||||||
} else {
|
} else {
|
||||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||||
await remove_custom_skin(currentSkin.value!)
|
await remove_custom_skin(currentSkin.value!)
|
||||||
emit('saved')
|
emit('saved')
|
||||||
}
|
}
|
||||||
|
|
||||||
hide()
|
hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([uploadedTextureUrl, currentSkin], async () => {
|
watch([uploadedTextureUrl, currentSkin], async () => {
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.capes,
|
() => props.capes,
|
||||||
() => {
|
() => {
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'saved'): void
|
(event: 'saved'): void
|
||||||
(event: 'deleted', skin: Skin): void
|
(event: 'deleted', skin: Skin): void
|
||||||
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
showNew,
|
showNew,
|
||||||
restoreWithNewTexture,
|
restoreWithNewTexture,
|
||||||
hide,
|
hide,
|
||||||
shouldRestoreModal,
|
shouldRestoreModal,
|
||||||
restoreModal,
|
restoreModal,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef, ref, computed } from 'vue'
|
|
||||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
|
||||||
import {
|
|
||||||
ButtonStyled,
|
|
||||||
ScrollablePanel,
|
|
||||||
CapeButton,
|
|
||||||
CapeLikeTextButton,
|
|
||||||
SkinPreviewRenderer,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
CapeButton,
|
||||||
|
CapeLikeTextButton,
|
||||||
|
ScrollablePanel,
|
||||||
|
SkinPreviewRenderer,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
||||||
|
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'select', cape: Cape | undefined): void
|
(e: 'select', cape: Cape | undefined): void
|
||||||
(e: 'cancel'): void
|
(e: 'cancel'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
capes: Cape[]
|
capes: Cape[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const sortedCapes = computed(() => {
|
const sortedCapes = computed(() => {
|
||||||
return [...props.capes].sort((a, b) => {
|
return [...props.capes].sort((a, b) => {
|
||||||
const nameA = (a.name || '').toLowerCase()
|
const nameA = (a.name || '').toLowerCase()
|
||||||
const nameB = (b.name || '').toLowerCase()
|
const nameB = (b.name || '').toLowerCase()
|
||||||
return nameA.localeCompare(nameB)
|
return nameA.localeCompare(nameB)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSkinId = ref<string | undefined>()
|
const currentSkinId = ref<string | undefined>()
|
||||||
@@ -37,104 +38,104 @@ const currentCapeTexture = computed<string | undefined>(() => currentCape.value?
|
|||||||
const currentCape = ref<Cape | undefined>()
|
const currentCape = ref<Cape | undefined>()
|
||||||
|
|
||||||
function show(
|
function show(
|
||||||
e: MouseEvent,
|
e: MouseEvent,
|
||||||
skinId?: string,
|
skinId?: string,
|
||||||
selected?: Cape,
|
selected?: Cape,
|
||||||
skinTexture?: string,
|
skinTexture?: string,
|
||||||
variant?: SkinModel,
|
variant?: SkinModel,
|
||||||
) {
|
) {
|
||||||
currentSkinId.value = skinId
|
currentSkinId.value = skinId
|
||||||
currentSkinTexture.value = skinTexture
|
currentSkinTexture.value = skinTexture
|
||||||
currentSkinVariant.value = variant || 'CLASSIC'
|
currentSkinVariant.value = variant || 'CLASSIC'
|
||||||
currentCape.value = selected
|
currentCape.value = selected
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
emit('select', currentCape.value)
|
emit('select', currentCape.value)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedCape(cape: Cape | undefined) {
|
function updateSelectedCape(cape: Cape | undefined) {
|
||||||
currentCape.value = cape
|
currentCape.value = cape
|
||||||
}
|
}
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide,
|
hide,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||||
<SkinPreviewRenderer
|
<SkinPreviewRenderer
|
||||||
v-if="currentSkinTexture"
|
v-if="currentSkinTexture"
|
||||||
:cape-src="currentCapeTexture"
|
:cape-src="currentCapeTexture"
|
||||||
:texture-src="currentSkinTexture"
|
:texture-src="currentSkinTexture"
|
||||||
:variant="currentSkinVariant"
|
:variant="currentSkinVariant"
|
||||||
:scale="1.4"
|
:scale="1.4"
|
||||||
:fov="50"
|
:fov="50"
|
||||||
:initial-rotation="Math.PI + Math.PI / 8"
|
:initial-rotation="Math.PI + Math.PI / 8"
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 w-full my-auto">
|
<div class="flex flex-col gap-4 w-full my-auto">
|
||||||
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
||||||
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
||||||
<CapeLikeTextButton
|
<CapeLikeTextButton
|
||||||
tooltip="No Cape"
|
tooltip="No Cape"
|
||||||
:highlighted="!currentCape"
|
:highlighted="!currentCape"
|
||||||
@click="updateSelectedCape(undefined)"
|
@click="updateSelectedCape(undefined)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</template>
|
</template>
|
||||||
<span>None</span>
|
<span>None</span>
|
||||||
</CapeLikeTextButton>
|
</CapeLikeTextButton>
|
||||||
<CapeButton
|
<CapeButton
|
||||||
v-for="cape in sortedCapes"
|
v-for="cape in sortedCapes"
|
||||||
:id="cape.id"
|
:id="cape.id"
|
||||||
:key="cape.id"
|
:key="cape.id"
|
||||||
:name="cape.name"
|
:name="cape.name"
|
||||||
:texture="cape.texture"
|
:texture="cape.texture"
|
||||||
:selected="currentCape?.id === cape.id"
|
:selected="currentCape?.id === cape.id"
|
||||||
@select="updateSelectedCape(cape)"
|
@select="updateSelectedCape(cape)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollablePanel>
|
</ScrollablePanel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button @click="select">
|
<button @click="select">
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
Select
|
Select
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide">
|
<button @click="hide">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
||||||
</template>
|
</template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
||||||
@click="triggerFileInput"
|
@click="triggerFileInput"
|
||||||
>
|
>
|
||||||
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
||||||
<UploadIcon /> Select skin texture file
|
<UploadIcon /> Select skin texture file
|
||||||
</p>
|
</p>
|
||||||
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
||||||
Drag and drop or click here to browse
|
Drag and drop or click here to browse
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/png"
|
accept="image/png"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="handleInputFileChange"
|
@change="handleInputFileChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
|
||||||
import { UploadIcon } from '@modrinth/assets'
|
import { UploadIcon } from '@modrinth/assets'
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -42,98 +43,98 @@ const unlisten = ref<() => void>()
|
|||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'uploaded', data: ArrayBuffer): void
|
(e: 'uploaded', data: ArrayBuffer): void
|
||||||
(e: 'canceled'): void
|
(e: 'canceled'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function show(e?: MouseEvent) {
|
function show(e?: MouseEvent) {
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
setupDragDropListener()
|
setupDragDropListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide(emitCanceled = false) {
|
function hide(emitCanceled = false) {
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
modalVisible.value = false
|
modalVisible.value = false
|
||||||
cleanupDragDropListener()
|
cleanupDragDropListener()
|
||||||
resetState()
|
resetState()
|
||||||
if (emitCanceled) {
|
if (emitCanceled) {
|
||||||
emit('canceled')
|
emit('canceled')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
if (fileInput.value) fileInput.value.value = ''
|
if (fileInput.value) fileInput.value.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerFileInput() {
|
function triggerFileInput() {
|
||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInputFileChange(e: Event) {
|
async function handleInputFileChange(e: Event) {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files || files.length === 0) {
|
if (!files || files.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const file = files[0]
|
const file = files[0]
|
||||||
const buffer = await file.arrayBuffer()
|
const buffer = await file.arrayBuffer()
|
||||||
await processData(buffer)
|
await processData(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDragDropListener() {
|
async function setupDragDropListener() {
|
||||||
try {
|
try {
|
||||||
if (modalVisible.value) {
|
if (modalVisible.value) {
|
||||||
await cleanupDragDropListener()
|
await cleanupDragDropListener()
|
||||||
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||||
if (event.payload.type !== 'drop') {
|
if (event.payload.type !== 'drop') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.payload.paths || event.payload.paths.length === 0) {
|
if (!event.payload.paths || event.payload.paths.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = event.payload.paths[0]
|
const filePath = event.payload.paths[0]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await get_dragged_skin_data(filePath)
|
const data = await get_dragged_skin_data(filePath)
|
||||||
await processData(data.buffer)
|
await processData(data.buffer)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addNotification({
|
addNotification({
|
||||||
title: 'Error processing file',
|
title: 'Error processing file',
|
||||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set up drag and drop listener:', error)
|
console.error('Failed to set up drag and drop listener:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupDragDropListener() {
|
async function cleanupDragDropListener() {
|
||||||
if (unlisten.value) {
|
if (unlisten.value) {
|
||||||
unlisten.value()
|
unlisten.value()
|
||||||
unlisten.value = undefined
|
unlisten.value = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processData(buffer: ArrayBuffer) {
|
async function processData(buffer: ArrayBuffer) {
|
||||||
emit('uploaded', buffer)
|
emit('uploaded', buffer)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(modalVisible, (isVisible) => {
|
watch(modalVisible, (isVisible) => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
setupDragDropListener()
|
setupDragDropListener()
|
||||||
} else {
|
} else {
|
||||||
cleanupDragDropListener()
|
cleanupDragDropListener()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
cleanupDragDropListener()
|
cleanupDragDropListener()
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
defineExpose({ show, hide })
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { get_project } from '@/helpers/cache'
|
|
||||||
import { process_listener } from '@/helpers/events'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { kill, run } from '@/helpers/profile'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils'
|
|
||||||
import { handleSevereError } from '@/store/error'
|
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
SmartClickable,
|
SmartClickable,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { capitalizeString } from '@modrinth/utils'
|
import { capitalizeString } from '@modrinth/utils'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
@@ -32,6 +24,15 @@ import dayjs from 'dayjs'
|
|||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_project } from '@/helpers/cache'
|
||||||
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { kill, run } from '@/helpers/profile'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
@@ -39,12 +40,12 @@ const formatRelativeTime = useRelativeTime()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play' | 'stop'): void
|
(e: 'play' | 'stop'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
last_played: Dayjs
|
last_played: Dayjs
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const loadingModpack = ref(!!props.instance.linked_data)
|
const loadingModpack = ref(!!props.instance.linked_data)
|
||||||
@@ -52,180 +53,180 @@ const loadingModpack = ref(!!props.instance.linked_data)
|
|||||||
const modpack = ref()
|
const modpack = ref()
|
||||||
|
|
||||||
if (props.instance.linked_data) {
|
if (props.instance.linked_data) {
|
||||||
nextTick().then(async () => {
|
nextTick().then(async () => {
|
||||||
modpack.value = await get_project(props.instance.linked_data?.project_id, 'must_revalidate')
|
modpack.value = await get_project(props.instance.linked_data?.project_id, 'must_revalidate')
|
||||||
loadingModpack.value = false
|
loadingModpack.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceIcon = computed(() => props.instance.icon_path)
|
const instanceIcon = computed(() => props.instance.icon_path)
|
||||||
|
|
||||||
const loader = computed(() => {
|
const loader = computed(() => {
|
||||||
if (props.instance.loader === 'vanilla') {
|
if (props.instance.loader === 'vanilla') {
|
||||||
return 'Minecraft'
|
return 'Minecraft'
|
||||||
} else if (props.instance.loader === 'neoforge') {
|
} else if (props.instance.loader === 'neoforge') {
|
||||||
return 'NeoForge'
|
return 'NeoForge'
|
||||||
} else {
|
} else {
|
||||||
return capitalizeString(props.instance.loader)
|
return capitalizeString(props.instance.loader)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
|
|
||||||
const play = async (event: MouseEvent) => {
|
const play = async (event: MouseEvent) => {
|
||||||
event?.stopPropagation()
|
event?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await run(props.instance.path)
|
await run(props.instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstancePlay', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: 'InstanceItem',
|
source: 'InstanceItem',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
emit('play')
|
emit('play')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async (event: MouseEvent) => {
|
const stop = async (event: MouseEvent) => {
|
||||||
event?.stopPropagation()
|
event?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await kill(props.instance.path).catch(handleError)
|
await kill(props.instance.path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: 'InstanceItem',
|
source: 'InstanceItem',
|
||||||
})
|
})
|
||||||
emit('stop')
|
emit('stop')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlistenProcesses = await process_listener(async () => {
|
const unlistenProcesses = await process_listener(async () => {
|
||||||
await checkProcess()
|
await checkProcess()
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkProcess = async () => {
|
const checkProcess = async () => {
|
||||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
playing.value = runningProcesses.length > 0
|
playing.value = runningProcesses.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkProcess()
|
checkProcess()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<SmartClickable>
|
<SmartClickable>
|
||||||
<template #clickable>
|
<template #clickable>
|
||||||
<router-link
|
<router-link
|
||||||
class="no-click-animation"
|
class="no-click-animation"
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover"
|
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
size="48px"
|
size="48px"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col col-span-2 justify-between h-full">
|
<div class="flex flex-col col-span-2 justify-between h-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary">
|
<div class="flex items-center gap-2 text-sm text-secondary">
|
||||||
<div
|
<div
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
instance.last_played
|
instance.last_played
|
||||||
? dayjs(instance.last_played).format('MMMM D, YYYY [at] h:mm A')
|
? dayjs(instance.last_played).format('MMMM D, YYYY [at] h:mm A')
|
||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
class="w-fit shrink-0"
|
class="w-fit shrink-0"
|
||||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
|
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
|
||||||
>
|
>
|
||||||
<template v-if="last_played">
|
<template v-if="last_played">
|
||||||
{{
|
{{
|
||||||
formatMessage(commonMessages.playedLabel, {
|
formatMessage(commonMessages.playedLabel, {
|
||||||
time: formatRelativeTime(last_played.toISOString?.()),
|
time: formatRelativeTime(last_played.toISOString?.()),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-else> Not played yet </template>
|
<template v-else> Not played yet </template>
|
||||||
</div>
|
</div>
|
||||||
•
|
•
|
||||||
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
||||||
<router-link
|
<router-link
|
||||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||||
:to="`/project/${modpack.id}`"
|
:to="`/project/${modpack.id}`"
|
||||||
>
|
>
|
||||||
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
||||||
<span class="truncate">{{ modpack.title }}</span>
|
<span class="truncate">{{ modpack.title }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
({{ loader }} {{ instance.game_version }})
|
({{ loader }} {{ instance.game_version }})
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="loadingModpack" class="flex items-center gap-1 truncate text-secondary">
|
<span v-else-if="loadingModpack" class="flex items-center gap-1 truncate text-secondary">
|
||||||
<SpinnerIcon class="animate-spin shrink-0" />
|
<SpinnerIcon class="animate-spin shrink-0" />
|
||||||
<span class="truncate">Loading modpack...</span>
|
<span class="truncate">Loading modpack...</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="flex items-center gap-1 truncate text-secondary">
|
<span v-else class="flex items-center gap-1 truncate text-secondary">
|
||||||
{{ loader }}
|
{{ loader }}
|
||||||
{{ instance.game_version }}
|
{{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||||
<ButtonStyled v-if="playing && !loading" color="red">
|
<ButtonStyled v-if="playing && !loading" color="red">
|
||||||
<button @click="stop">
|
<button @click="stop">
|
||||||
<StopCircleIcon aria-hidden="true" />
|
<StopCircleIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.stopButton) }}
|
{{ formatMessage(commonMessages.stopButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else>
|
<ButtonStyled v-else>
|
||||||
<button
|
<button
|
||||||
v-tooltip="playing ? 'Instance is already open' : null"
|
v-tooltip="playing ? 'Instance is already open' : null"
|
||||||
:disabled="playing || loading"
|
:disabled="playing || loading"
|
||||||
@click="play"
|
@click="play"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="loading" class="animate-spin" />
|
<SpinnerIcon v-if="loading" class="animate-spin" />
|
||||||
<PlayIcon v-else aria-hidden="true" />
|
<PlayIcon v-else aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled circular type="transparent">
|
<ButtonStyled circular type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instance.path,
|
shown: !!instance.path,
|
||||||
action: () => router.push(encodeURI(`/instance/${instance.path}`)),
|
action: () => router.push(encodeURI(`/instance/${instance.path}`)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'open-folder',
|
id: 'open-folder',
|
||||||
action: () => showProfileInFolder(instance.path),
|
action: () => showProfileInFolder(instance.path),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #open-instance>
|
<template #open-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
View instance
|
View instance
|
||||||
</template>
|
</template>
|
||||||
<template #open-folder>
|
<template #open-folder>
|
||||||
<FolderOpenIcon aria-hidden="true" />
|
<FolderOpenIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SmartClickable>
|
</SmartClickable>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
@@ -7,28 +12,24 @@ import { get_all } from '@/helpers/process'
|
|||||||
import { kill, run } from '@/helpers/profile'
|
import { kill, run } from '@/helpers/profile'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import {
|
import {
|
||||||
type ProtocolVersion,
|
get_profile_protocol_version,
|
||||||
type ServerData,
|
get_recent_worlds,
|
||||||
type ServerWorld,
|
getWorldIdentifier,
|
||||||
type WorldWithProfile,
|
type ProtocolVersion,
|
||||||
getWorldIdentifier,
|
refreshServerData,
|
||||||
get_profile_protocol_version,
|
type ServerData,
|
||||||
get_recent_worlds,
|
type ServerWorld,
|
||||||
refreshServerData,
|
start_join_server,
|
||||||
start_join_server,
|
start_join_singleplayer_world,
|
||||||
start_join_singleplayer_world,
|
type WorldWithProfile,
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { handleSevereError } from '@/store/error'
|
||||||
import { useTheming } from '@/store/theme.ts'
|
import { useTheming } from '@/store/theme.ts'
|
||||||
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import type { Dayjs } from 'dayjs'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
recentInstances: GameInstance[]
|
recentInstances: GameInstance[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const theme = useTheming()
|
const theme = useTheming()
|
||||||
@@ -42,17 +43,17 @@ const MAX_JUMP_BACK_IN = 6
|
|||||||
const TWO_WEEKS_AGO = dayjs().subtract(14, 'day')
|
const TWO_WEEKS_AGO = dayjs().subtract(14, 'day')
|
||||||
|
|
||||||
type BaseJumpBackInItem = {
|
type BaseJumpBackInItem = {
|
||||||
last_played: Dayjs
|
last_played: Dayjs
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceJumpBackInItem = BaseJumpBackInItem & {
|
type InstanceJumpBackInItem = BaseJumpBackInItem & {
|
||||||
type: 'instance'
|
type: 'instance'
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorldJumpBackInItem = BaseJumpBackInItem & {
|
type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||||
type: 'world'
|
type: 'world'
|
||||||
world: WorldWithProfile
|
world: WorldWithProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||||
@@ -60,244 +61,244 @@ type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
|||||||
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
||||||
|
|
||||||
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||||
await populateJumpBackIn().catch(() => {
|
await populateJumpBackIn().catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
console.error('Failed to populate jump back in')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await populateJumpBackIn().catch(() => {
|
await populateJumpBackIn().catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
console.error('Failed to populate jump back in')
|
||||||
})
|
})
|
||||||
|
|
||||||
async function populateJumpBackIn() {
|
async function populateJumpBackIn() {
|
||||||
console.info('Repopulating jump back in...')
|
console.info('Repopulating jump back in...')
|
||||||
|
|
||||||
const worldItems: WorldJumpBackInItem[] = []
|
const worldItems: WorldJumpBackInItem[] = []
|
||||||
|
|
||||||
if (showWorlds.value) {
|
if (showWorlds.value) {
|
||||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||||
|
|
||||||
worlds.forEach((world) => {
|
worlds.forEach((world) => {
|
||||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||||
|
|
||||||
if (!instance || !world.last_played) {
|
if (!instance || !world.last_played) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
worldItems.push({
|
worldItems.push({
|
||||||
type: 'world',
|
type: 'world',
|
||||||
last_played: dayjs(world.last_played ?? 0),
|
last_played: dayjs(world.last_played ?? 0),
|
||||||
world: world,
|
world: world,
|
||||||
instance: instance,
|
instance: instance,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const servers: {
|
const servers: {
|
||||||
instancePath: string
|
instancePath: string
|
||||||
address: string
|
address: string
|
||||||
}[] = worldItems
|
}[] = worldItems
|
||||||
.filter((item) => item.world.type === 'server' && item.instance)
|
.filter((item) => item.world.type === 'server' && item.instance)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
instancePath: item.instance.path,
|
instancePath: item.instance.path,
|
||||||
address: (item.world as ServerWorld).address,
|
address: (item.world as ServerWorld).address,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// fetch protocol versions for all unique MC versions with server worlds
|
// fetch protocol versions for all unique MC versions with server worlds
|
||||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...uniqueServerInstances].map((path) =>
|
[...uniqueServerInstances].map((path) =>
|
||||||
get_profile_protocol_version(path)
|
get_profile_protocol_version(path)
|
||||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error(`Failed to get profile protocol for: ${path} `)
|
console.error(`Failed to get profile protocol for: ${path} `)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// initialize server data
|
// initialize server data
|
||||||
servers.forEach(({ address }) => {
|
servers.forEach(({ address }) => {
|
||||||
if (!serverData.value[address]) {
|
if (!serverData.value[address]) {
|
||||||
serverData.value[address] = {
|
serverData.value[address] = {
|
||||||
refreshing: true,
|
refreshing: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
servers.forEach(({ instancePath, address }) =>
|
servers.forEach(({ instancePath, address }) =>
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceItems: InstanceJumpBackInItem[] = []
|
const instanceItems: InstanceJumpBackInItem[] = []
|
||||||
for (const instance of props.recentInstances) {
|
for (const instance of props.recentInstances) {
|
||||||
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||||
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceItems.push({
|
instanceItems.push({
|
||||||
type: 'instance',
|
type: 'instance',
|
||||||
last_played: dayjs(instance.last_played ?? 0),
|
last_played: dayjs(instance.last_played ?? 0),
|
||||||
instance: instance,
|
instance: instance,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||||
items.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
|
items.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
|
||||||
jumpBackInItems.value = items
|
jumpBackInItems.value = items
|
||||||
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
||||||
.slice(0, MAX_JUMP_BACK_IN)
|
.slice(0, MAX_JUMP_BACK_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshServer(address: string, instancePath: string) {
|
function refreshServer(address: string, instancePath: string) {
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinWorld(world: WorldWithProfile) {
|
async function joinWorld(world: WorldWithProfile) {
|
||||||
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
||||||
if (world.type === 'server') {
|
if (world.type === 'server') {
|
||||||
await start_join_server(world.profile, world.address).catch(handleError)
|
await start_join_server(world.profile, world.address).catch(handleError)
|
||||||
} else if (world.type === 'singleplayer') {
|
} else if (world.type === 'singleplayer') {
|
||||||
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
|
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playInstance(instance: GameInstance) {
|
async function playInstance(instance: GameInstance) {
|
||||||
await run(instance.path)
|
await run(instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstancePlay', {
|
||||||
loader: instance.loader,
|
loader: instance.loader,
|
||||||
game_version: instance.game_version,
|
game_version: instance.game_version,
|
||||||
source: 'WorldItem',
|
source: 'WorldItem',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopInstance(path: string) {
|
async function stopInstance(path: string) {
|
||||||
await kill(path).catch(handleError)
|
await kill(path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
source: 'RecentWorldsList',
|
source: 'RecentWorldsList',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentProfile = ref<string>()
|
const currentProfile = ref<string>()
|
||||||
const currentWorld = ref<string>()
|
const currentWorld = ref<string>()
|
||||||
|
|
||||||
const unlistenProcesses = await process_listener(async () => {
|
const unlistenProcesses = await process_listener(async () => {
|
||||||
await checkProcesses()
|
await checkProcesses()
|
||||||
})
|
})
|
||||||
|
|
||||||
const unlistenProfiles = await profile_listener(async () => {
|
const unlistenProfiles = await profile_listener(async () => {
|
||||||
await populateJumpBackIn().catch(() => {
|
await populateJumpBackIn().catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
console.error('Failed to populate jump back in')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const runningInstances = ref<string[]>([])
|
const runningInstances = ref<string[]>([])
|
||||||
|
|
||||||
type ProcessMetadata = {
|
type ProcessMetadata = {
|
||||||
uuid: string
|
uuid: string
|
||||||
profile_path: string
|
profile_path: string
|
||||||
start_time: string
|
start_time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkProcesses = async () => {
|
const checkProcesses = async () => {
|
||||||
const runningProcesses: ProcessMetadata[] = await get_all().catch(handleError)
|
const runningProcesses: ProcessMetadata[] = await get_all().catch(handleError)
|
||||||
|
|
||||||
const runningPaths = runningProcesses.map((x) => x.profile_path)
|
const runningPaths = runningProcesses.map((x) => x.profile_path)
|
||||||
|
|
||||||
const stoppedInstances = runningInstances.value.filter((x) => !runningPaths.includes(x))
|
const stoppedInstances = runningInstances.value.filter((x) => !runningPaths.includes(x))
|
||||||
if (currentProfile.value && stoppedInstances.includes(currentProfile.value)) {
|
if (currentProfile.value && stoppedInstances.includes(currentProfile.value)) {
|
||||||
currentProfile.value = undefined
|
currentProfile.value = undefined
|
||||||
currentWorld.value = undefined
|
currentWorld.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
runningInstances.value = runningPaths
|
runningInstances.value = runningPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkProcesses()
|
checkProcesses()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
unlistenProfiles()
|
unlistenProfiles()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||||
Jump back in
|
Jump back in
|
||||||
</HeadingLink>
|
</HeadingLink>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
|
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
|
||||||
>
|
>
|
||||||
Jump back in
|
Jump back in
|
||||||
</span>
|
</span>
|
||||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||||
<template
|
<template
|
||||||
v-for="item in jumpBackInItems"
|
v-for="item in jumpBackInItems"
|
||||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||||
>
|
>
|
||||||
<WorldItem
|
<WorldItem
|
||||||
v-if="item.type === 'world'"
|
v-if="item.type === 'world'"
|
||||||
:world="item.world"
|
:world="item.world"
|
||||||
:playing-instance="runningInstances.includes(item.instance.path)"
|
:playing-instance="runningInstances.includes(item.instance.path)"
|
||||||
:playing-world="
|
:playing-world="
|
||||||
currentProfile === item.instance.path && currentWorld === getWorldIdentifier(item.world)
|
currentProfile === item.instance.path && currentWorld === getWorldIdentifier(item.world)
|
||||||
"
|
"
|
||||||
:refreshing="
|
:refreshing="
|
||||||
item.world.type === 'server'
|
item.world.type === 'server'
|
||||||
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
supports-quick-play
|
supports-quick-play
|
||||||
:server-status="
|
:server-status="
|
||||||
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
||||||
"
|
"
|
||||||
:rendered-motd="
|
:rendered-motd="
|
||||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||||
"
|
"
|
||||||
:current-protocol="protocolVersions[item.instance.path]"
|
:current-protocol="protocolVersions[item.instance.path]"
|
||||||
:game-mode="
|
:game-mode="
|
||||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||||
"
|
"
|
||||||
:instance-path="item.instance.path"
|
:instance-path="item.instance.path"
|
||||||
:instance-name="item.instance.name"
|
:instance-name="item.instance.name"
|
||||||
:instance-icon="item.instance.icon_path"
|
:instance-icon="item.instance.icon_path"
|
||||||
@refresh="
|
@refresh="
|
||||||
() =>
|
() =>
|
||||||
item.world.type === 'server'
|
item.world.type === 'server'
|
||||||
? refreshServer(item.world.address, item.instance.path)
|
? refreshServer(item.world.address, item.instance.path)
|
||||||
: {}
|
: {}
|
||||||
"
|
"
|
||||||
@update="() => populateJumpBackIn()"
|
@update="() => populateJumpBackIn()"
|
||||||
@play="
|
@play="
|
||||||
() => {
|
() => {
|
||||||
currentProfile = item.instance.path
|
currentProfile = item.instance.path
|
||||||
currentWorld = getWorldIdentifier(item.world)
|
currentWorld = getWorldIdentifier(item.world)
|
||||||
joinWorld(item.world)
|
joinWorld(item.world)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@play-instance="
|
@play-instance="
|
||||||
() => {
|
() => {
|
||||||
currentProfile = item.instance.path
|
currentProfile = item.instance.path
|
||||||
playInstance(item.instance)
|
playInstance(item.instance)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@stop="() => stopInstance(item.instance.path)"
|
@stop="() => stopInstance(item.instance.path)"
|
||||||
/>
|
/>
|
||||||
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
|
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.grid-when-huge {
|
.grid-when-huge {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,48 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type {
|
|
||||||
ProtocolVersion,
|
|
||||||
ServerStatus,
|
|
||||||
ServerWorld,
|
|
||||||
SingleplayerWorld,
|
|
||||||
World,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
ClipboardCopyIcon,
|
||||||
Avatar,
|
EditIcon,
|
||||||
ButtonStyled,
|
EyeIcon,
|
||||||
commonMessages,
|
FolderOpenIcon,
|
||||||
OverflowMenu,
|
IssuesIcon,
|
||||||
SmartClickable,
|
MoreVerticalIcon,
|
||||||
} from '@modrinth/ui'
|
NoSignalIcon,
|
||||||
import {
|
PlayIcon,
|
||||||
IssuesIcon,
|
SignalIcon,
|
||||||
EyeIcon,
|
SkullIcon,
|
||||||
ClipboardCopyIcon,
|
SpinnerIcon,
|
||||||
EditIcon,
|
StopCircleIcon,
|
||||||
FolderOpenIcon,
|
TrashIcon,
|
||||||
MoreVerticalIcon,
|
UpdatedIcon,
|
||||||
NoSignalIcon,
|
UserIcon,
|
||||||
PlayIcon,
|
XIcon,
|
||||||
SignalIcon,
|
|
||||||
SkullIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TrashIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
UserIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
OverflowMenu,
|
||||||
|
SmartClickable,
|
||||||
|
useRelativeTime,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import type { MessageDescriptor } from '@vintl/vintl'
|
import type { MessageDescriptor } from '@vintl/vintl'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Tooltip } from 'floating-vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { copyToClipboard } from '@/helpers/utils'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Tooltip } from 'floating-vue'
|
|
||||||
|
import { copyToClipboard } from '@/helpers/utils'
|
||||||
|
import type {
|
||||||
|
ProtocolVersion,
|
||||||
|
ServerStatus,
|
||||||
|
ServerWorld,
|
||||||
|
SingleplayerWorld,
|
||||||
|
World,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
|
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
@@ -50,466 +51,473 @@ const formatRelativeTime = useRelativeTime()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||||
(e: 'open-folder', world: SingleplayerWorld): void
|
(e: 'open-folder', world: SingleplayerWorld): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
world: World
|
world: World
|
||||||
playingInstance?: boolean
|
playingInstance?: boolean
|
||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsServerQuickPlay?: boolean
|
supportsServerQuickPlay?: boolean
|
||||||
supportsWorldQuickPlay?: boolean
|
supportsWorldQuickPlay?: boolean
|
||||||
currentProtocol?: ProtocolVersion | null
|
currentProtocol?: ProtocolVersion | null
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
|
||||||
// Server only
|
// Server only
|
||||||
refreshing?: boolean
|
refreshing?: boolean
|
||||||
serverStatus?: ServerStatus
|
serverStatus?: ServerStatus
|
||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
|
|
||||||
// Singleplayer only
|
// Singleplayer only
|
||||||
gameMode?: {
|
gameMode?: {
|
||||||
icon: Component
|
icon: Component
|
||||||
message: MessageDescriptor
|
message: MessageDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance
|
// Instance
|
||||||
instancePath?: string
|
instancePath?: string
|
||||||
instanceName?: string
|
instanceName?: string
|
||||||
instanceIcon?: string
|
instanceIcon?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
playingInstance: false,
|
playingInstance: false,
|
||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsServerQuickPlay: true,
|
supportsServerQuickPlay: true,
|
||||||
supportsWorldQuickPlay: false,
|
supportsWorldQuickPlay: false,
|
||||||
currentProtocol: null,
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
serverStatus: undefined,
|
serverStatus: undefined,
|
||||||
renderedMotd: undefined,
|
renderedMotd: undefined,
|
||||||
|
|
||||||
gameMode: undefined,
|
gameMode: undefined,
|
||||||
|
|
||||||
instancePath: undefined,
|
instancePath: undefined,
|
||||||
instanceName: undefined,
|
instanceName: undefined,
|
||||||
instanceIcon: undefined,
|
instanceIcon: undefined,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const playingOtherWorld = computed(() => props.playingInstance && !props.playingWorld)
|
const playingOtherWorld = computed(() => props.playingInstance && !props.playingWorld)
|
||||||
const hasPlayersTooltip = computed(
|
const hasPlayersTooltip = computed(
|
||||||
() => !!props.serverStatus?.players?.sample && props.serverStatus.players?.sample?.length > 0,
|
() => !!props.serverStatus?.players?.sample && props.serverStatus.players?.sample?.length > 0,
|
||||||
)
|
)
|
||||||
const serverIncompatible = computed(
|
const serverIncompatible = computed(
|
||||||
() =>
|
() =>
|
||||||
!!props.serverStatus &&
|
!!props.serverStatus &&
|
||||||
!!props.serverStatus.version?.protocol &&
|
!!props.serverStatus.version?.protocol &&
|
||||||
!!props.currentProtocol &&
|
!!props.currentProtocol &&
|
||||||
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
||||||
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
hardcore: {
|
hardcore: {
|
||||||
id: 'instance.worlds.hardcore',
|
id: 'instance.worlds.hardcore',
|
||||||
defaultMessage: 'Hardcore mode',
|
defaultMessage: 'Hardcore mode',
|
||||||
},
|
},
|
||||||
cantConnect: {
|
cantConnect: {
|
||||||
id: 'instance.worlds.cant_connect',
|
id: 'instance.worlds.cant_connect',
|
||||||
defaultMessage: "Can't connect to server",
|
defaultMessage: "Can't connect to server",
|
||||||
},
|
},
|
||||||
aMinecraftServer: {
|
aMinecraftServer: {
|
||||||
id: 'instance.worlds.a_minecraft_server',
|
id: 'instance.worlds.a_minecraft_server',
|
||||||
defaultMessage: 'A Minecraft Server',
|
defaultMessage: 'A Minecraft Server',
|
||||||
},
|
},
|
||||||
noServerQuickPlay: {
|
noServerQuickPlay: {
|
||||||
id: 'instance.worlds.no_server_quick_play',
|
id: 'instance.worlds.no_server_quick_play',
|
||||||
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+',
|
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+',
|
||||||
},
|
},
|
||||||
noSingleplayerQuickPlay: {
|
noSingleplayerQuickPlay: {
|
||||||
id: 'instance.worlds.no_singleplayer_quick_play',
|
id: 'instance.worlds.no_singleplayer_quick_play',
|
||||||
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
||||||
},
|
},
|
||||||
gameAlreadyOpen: {
|
gameAlreadyOpen: {
|
||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.game_already_open',
|
||||||
defaultMessage: 'Instance is already open',
|
defaultMessage: 'Instance is already open',
|
||||||
},
|
},
|
||||||
noContact: {
|
noContact: {
|
||||||
id: 'instance.worlds.no_contact',
|
id: 'instance.worlds.no_contact',
|
||||||
defaultMessage: "Server couldn't be contacted",
|
defaultMessage: "Server couldn't be contacted",
|
||||||
},
|
},
|
||||||
incompatibleServer: {
|
incompatibleServer: {
|
||||||
id: 'instance.worlds.incompatible_server',
|
id: 'instance.worlds.incompatible_server',
|
||||||
defaultMessage: 'Server is incompatible',
|
defaultMessage: 'Server is incompatible',
|
||||||
},
|
},
|
||||||
copyAddress: {
|
copyAddress: {
|
||||||
id: 'instance.worlds.copy_address',
|
id: 'instance.worlds.copy_address',
|
||||||
defaultMessage: 'Copy address',
|
defaultMessage: 'Copy address',
|
||||||
},
|
},
|
||||||
viewInstance: {
|
viewInstance: {
|
||||||
id: 'instance.worlds.view_instance',
|
id: 'instance.worlds.view_instance',
|
||||||
defaultMessage: 'View instance',
|
defaultMessage: 'View instance',
|
||||||
},
|
},
|
||||||
playInstance: {
|
playInstance: {
|
||||||
id: 'instance.worlds.play_instance',
|
id: 'instance.worlds.play_instance',
|
||||||
defaultMessage: 'Play instance',
|
defaultMessage: 'Play instance',
|
||||||
},
|
},
|
||||||
worldInUse: {
|
worldInUse: {
|
||||||
id: 'instance.worlds.world_in_use',
|
id: 'instance.worlds.world_in_use',
|
||||||
defaultMessage: 'World is in use',
|
defaultMessage: 'World is in use',
|
||||||
},
|
},
|
||||||
dontShowOnHome: {
|
dontShowOnHome: {
|
||||||
id: 'instance.worlds.dont_show_on_home',
|
id: 'instance.worlds.dont_show_on_home',
|
||||||
defaultMessage: `Don't show on Home`,
|
defaultMessage: `Don't show on Home`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<SmartClickable>
|
<SmartClickable>
|
||||||
<template v-if="instancePath" #clickable>
|
<template v-if="instancePath" #clickable>
|
||||||
<router-link
|
<router-link
|
||||||
class="no-click-animation"
|
class="no-click-animation"
|
||||||
:to="`/instance/${encodeURIComponent(instancePath)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
|
:to="`/instance/${encodeURIComponent(instancePath)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl"
|
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl"
|
||||||
:class="{
|
:class="{
|
||||||
'world-item-highlighted': highlighted,
|
'world-item-highlighted': highlighted,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="
|
:src="
|
||||||
world.type === 'server' && serverStatus ? serverStatus.favicon ?? world.icon : world.icon
|
world.type === 'server' && serverStatus ? serverStatus.favicon ?? world.icon : world.icon
|
||||||
"
|
"
|
||||||
size="48px"
|
size="48px"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between h-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
||||||
{{ world.name }}
|
{{ world.name }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="world.type === 'singleplayer'"
|
v-if="world.type === 'singleplayer'"
|
||||||
class="text-sm text-secondary flex items-center gap-1 font-semibold"
|
class="text-sm text-secondary flex items-center gap-1 font-semibold"
|
||||||
>
|
>
|
||||||
<UserIcon
|
<UserIcon
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="h-4 w-4 text-secondary shrink-0"
|
class="h-4 w-4 text-secondary shrink-0"
|
||||||
stroke-width="3px"
|
stroke-width="3px"
|
||||||
/>
|
/>
|
||||||
{{ formatMessage(commonMessages.singleplayerLabel) }}
|
{{ formatMessage(commonMessages.singleplayerLabel) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="world.type === 'server'"
|
v-else-if="world.type === 'server'"
|
||||||
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
|
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<template v-if="refreshing">
|
<template v-if="refreshing">
|
||||||
<SpinnerIcon aria-hidden="true" class="animate-spin shrink-0" />
|
<SpinnerIcon aria-hidden="true" class="animate-spin shrink-0" />
|
||||||
Loading...
|
Loading...
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="serverStatus">
|
<template v-else-if="serverStatus">
|
||||||
<template v-if="serverIncompatible">
|
<template v-if="serverIncompatible">
|
||||||
<IssuesIcon class="shrink-0 text-orange" aria-hidden="true" />
|
<IssuesIcon class="shrink-0 text-orange" aria-hidden="true" />
|
||||||
<span class="text-orange">
|
<span class="text-orange">
|
||||||
Incompatible version {{ serverStatus.version?.name }}
|
Incompatible version {{ serverStatus.version?.name }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<SignalIcon
|
<SignalIcon
|
||||||
v-tooltip="serverStatus ? `${serverStatus.ping}ms` : null"
|
v-tooltip="serverStatus ? `${serverStatus.ping}ms` : null"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
:style="`--_signal-${getPingLevel(serverStatus.ping || 0)}: var(--color-green)`"
|
:style="`--_signal-${getPingLevel(serverStatus.ping || 0)}: var(--color-green)`"
|
||||||
stroke-width="3px"
|
stroke-width="3px"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
:class="{
|
:class="{
|
||||||
'smart-clickable:allow-pointer-events': serverStatus,
|
'smart-clickable:allow-pointer-events': serverStatus,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<Tooltip :disabled="!hasPlayersTooltip">
|
<Tooltip :disabled="!hasPlayersTooltip">
|
||||||
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
||||||
{{ formatNumber(serverStatus.players?.online, false) }} online
|
{{ formatNumber(serverStatus.players?.online, false) }}
|
||||||
</span>
|
online
|
||||||
<template #popper>
|
</span>
|
||||||
<div class="flex flex-col gap-1">
|
<template #popper>
|
||||||
<span v-for="player in serverStatus.players?.sample" :key="player.name">
|
<div class="flex flex-col gap-1">
|
||||||
{{ player.name }}
|
<span v-for="player in serverStatus.players?.sample" :key="player.name">
|
||||||
</span>
|
{{ player.name }}
|
||||||
</div>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</Tooltip>
|
</template>
|
||||||
</template>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
</template>
|
||||||
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" /> Offline
|
<template v-else>
|
||||||
</template>
|
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" />
|
||||||
</div>
|
Offline
|
||||||
</div>
|
</template>
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary">
|
</div>
|
||||||
<div
|
</div>
|
||||||
v-tooltip="
|
<div class="flex items-center gap-2 text-sm text-secondary">
|
||||||
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
<div
|
||||||
"
|
v-tooltip="
|
||||||
class="w-fit shrink-0"
|
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
||||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': world.last_played }"
|
"
|
||||||
>
|
class="w-fit shrink-0"
|
||||||
<template v-if="world.last_played">
|
:class="{
|
||||||
{{
|
'cursor-help smart-clickable:allow-pointer-events': world.last_played,
|
||||||
formatMessage(commonMessages.playedLabel, {
|
}"
|
||||||
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
>
|
||||||
})
|
<template v-if="world.last_played">
|
||||||
}}
|
{{
|
||||||
</template>
|
formatMessage(commonMessages.playedLabel, {
|
||||||
<template v-else> Not played yet </template>
|
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
||||||
</div>
|
})
|
||||||
<template v-if="instancePath">
|
}}
|
||||||
•
|
</template>
|
||||||
<router-link
|
<template v-else> Not played yet </template>
|
||||||
class="flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
</div>
|
||||||
:to="`/instance/${instancePath}`"
|
<template v-if="instancePath">
|
||||||
>
|
•
|
||||||
<Avatar
|
<router-link
|
||||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
class="flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||||
size="16px"
|
:to="`/instance/${instancePath}`"
|
||||||
:tint-by="instancePath"
|
>
|
||||||
class="shrink-0"
|
<Avatar
|
||||||
/>
|
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||||
<span class="truncate">{{ instanceName }}</span>
|
size="16px"
|
||||||
</router-link>
|
:tint-by="instancePath"
|
||||||
</template>
|
class="shrink-0"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<span class="truncate">{{ instanceName }}</span>
|
||||||
<div
|
</router-link>
|
||||||
class="font-semibold flex items-center gap-1 justify-center text-center"
|
</template>
|
||||||
:class="world.type === 'singleplayer' && world.hardcore ? `text-red` : 'text-secondary'"
|
</div>
|
||||||
>
|
</div>
|
||||||
<template v-if="world.type === 'server'">
|
<div
|
||||||
<template v-if="refreshing">
|
class="font-semibold flex items-center gap-1 justify-center text-center"
|
||||||
<SpinnerIcon aria-hidden="true" class="animate-spin" />
|
:class="world.type === 'singleplayer' && world.hardcore ? `text-red` : 'text-secondary'"
|
||||||
{{ formatMessage(commonMessages.loadingLabel) }}
|
>
|
||||||
</template>
|
<template v-if="world.type === 'server'">
|
||||||
<div
|
<template v-if="refreshing">
|
||||||
v-else-if="renderedMotd"
|
<SpinnerIcon aria-hidden="true" class="animate-spin" />
|
||||||
class="motd-renderer font-normal font-minecraft line-clamp-2 text-secondary leading-5"
|
{{ formatMessage(commonMessages.loadingLabel) }}
|
||||||
v-html="renderedMotd"
|
</template>
|
||||||
/>
|
<div
|
||||||
<div v-else-if="!serverStatus" class="font-normal font-minecraft text-red leading-5">
|
v-else-if="renderedMotd"
|
||||||
{{ formatMessage(messages.cantConnect) }}
|
class="motd-renderer font-normal font-minecraft line-clamp-2 text-secondary leading-5"
|
||||||
</div>
|
v-html="renderedMotd"
|
||||||
<div v-else class="font-normal font-minecraft text-secondary leading-5">
|
/>
|
||||||
{{ formatMessage(messages.aMinecraftServer) }}
|
<div v-else-if="!serverStatus" class="font-normal font-minecraft text-red leading-5">
|
||||||
</div>
|
{{ formatMessage(messages.cantConnect) }}
|
||||||
</template>
|
</div>
|
||||||
<template v-else-if="world.type === 'singleplayer' && gameMode">
|
<div v-else class="font-normal font-minecraft text-secondary leading-5">
|
||||||
<template v-if="world.hardcore">
|
{{ formatMessage(messages.aMinecraftServer) }}
|
||||||
<SkullIcon aria-hidden="true" class="h-4 w-4 shrink-0" />
|
</div>
|
||||||
{{ formatMessage(messages.hardcore) }}
|
</template>
|
||||||
</template>
|
<template v-else-if="world.type === 'singleplayer' && gameMode">
|
||||||
<template v-else>
|
<template v-if="world.hardcore">
|
||||||
<component :is="gameMode.icon" aria-hidden="true" class="h-4 w-4 shrink-0" />
|
<SkullIcon aria-hidden="true" class="h-4 w-4 shrink-0" />
|
||||||
{{ formatMessage(gameMode.message) }}
|
{{ formatMessage(messages.hardcore) }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
</div>
|
<component :is="gameMode.icon" aria-hidden="true" class="h-4 w-4 shrink-0" />
|
||||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
{{ formatMessage(gameMode.message) }}
|
||||||
<ButtonStyled
|
</template>
|
||||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
</template>
|
||||||
color="red"
|
</div>
|
||||||
>
|
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||||
<button @click="emit('stop')">
|
<ButtonStyled
|
||||||
<StopCircleIcon aria-hidden="true" />
|
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||||
{{ formatMessage(commonMessages.stopButton) }}
|
color="red"
|
||||||
</button>
|
>
|
||||||
</ButtonStyled>
|
<button @click="emit('stop')">
|
||||||
<ButtonStyled v-else>
|
<StopCircleIcon aria-hidden="true" />
|
||||||
<button
|
{{ formatMessage(commonMessages.stopButton) }}
|
||||||
v-tooltip="
|
</button>
|
||||||
world.type == 'server' && !supportsServerQuickPlay
|
</ButtonStyled>
|
||||||
? formatMessage(messages.noServerQuickPlay)
|
<ButtonStyled v-else>
|
||||||
: world.type == 'singleplayer' && !supportsWorldQuickPlay
|
<button
|
||||||
? formatMessage(messages.noSingleplayerQuickPlay)
|
v-tooltip="
|
||||||
: playingOtherWorld || locked
|
world.type == 'server' && !supportsServerQuickPlay
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
? formatMessage(messages.noServerQuickPlay)
|
||||||
: !serverStatus
|
: world.type == 'singleplayer' && !supportsWorldQuickPlay
|
||||||
? formatMessage(messages.noContact)
|
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||||
: serverIncompatible
|
: playingOtherWorld || locked
|
||||||
? formatMessage(messages.incompatibleServer)
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
: null
|
: !serverStatus
|
||||||
"
|
? formatMessage(messages.noContact)
|
||||||
:disabled="
|
: serverIncompatible
|
||||||
playingOtherWorld ||
|
? formatMessage(messages.incompatibleServer)
|
||||||
startingInstance ||
|
: null
|
||||||
(world.type == 'server' && !supportsServerQuickPlay) ||
|
"
|
||||||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
:disabled="
|
||||||
"
|
playingOtherWorld ||
|
||||||
@click="emit('play')"
|
startingInstance ||
|
||||||
>
|
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||||
<PlayIcon v-else aria-hidden="true" />
|
"
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
@click="emit('play')"
|
||||||
</button>
|
>
|
||||||
</ButtonStyled>
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
<ButtonStyled circular type="transparent">
|
<PlayIcon v-else aria-hidden="true" />
|
||||||
<OverflowMenu
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
:options="[
|
</button>
|
||||||
{
|
</ButtonStyled>
|
||||||
id: 'play-instance',
|
<ButtonStyled circular type="transparent">
|
||||||
shown: !!instancePath,
|
<OverflowMenu
|
||||||
disabled: playingInstance,
|
:options="[
|
||||||
action: () => emit('play-instance'),
|
{
|
||||||
},
|
id: 'play-instance',
|
||||||
{
|
shown: !!instancePath,
|
||||||
id: 'open-instance',
|
disabled: playingInstance,
|
||||||
shown: !!instancePath,
|
action: () => emit('play-instance'),
|
||||||
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'open-instance',
|
||||||
id: 'refresh',
|
shown: !!instancePath,
|
||||||
shown: world.type === 'server',
|
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||||
action: () => emit('refresh'),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'refresh',
|
||||||
id: 'copy-address',
|
shown: world.type === 'server',
|
||||||
shown: world.type === 'server',
|
action: () => emit('refresh'),
|
||||||
action: () => copyToClipboard((world as ServerWorld).address),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'copy-address',
|
||||||
id: 'edit',
|
shown: world.type === 'server',
|
||||||
action: () => emit('edit'),
|
action: () => copyToClipboard((world as ServerWorld).address),
|
||||||
shown: !instancePath,
|
},
|
||||||
disabled: locked,
|
{
|
||||||
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
id: 'edit',
|
||||||
},
|
action: () => emit('edit'),
|
||||||
{
|
shown: !instancePath,
|
||||||
id: 'open-folder',
|
disabled: locked,
|
||||||
shown: world.type === 'singleplayer',
|
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
||||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'open-folder',
|
||||||
divider: true,
|
shown: world.type === 'singleplayer',
|
||||||
shown: !!instancePath,
|
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dont-show-on-home',
|
divider: true,
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
action: () => {
|
},
|
||||||
set_world_display_status(
|
{
|
||||||
instancePath,
|
id: 'dont-show-on-home',
|
||||||
world.type,
|
shown: !!instancePath,
|
||||||
getWorldIdentifier(world),
|
action: () => {
|
||||||
'hidden',
|
set_world_display_status(
|
||||||
).then(() => {
|
instancePath,
|
||||||
emit('update')
|
world.type,
|
||||||
})
|
getWorldIdentifier(world),
|
||||||
},
|
'hidden',
|
||||||
},
|
).then(() => {
|
||||||
{
|
emit('update')
|
||||||
divider: true,
|
})
|
||||||
shown: !instancePath,
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'delete',
|
divider: true,
|
||||||
color: 'red',
|
shown: !instancePath,
|
||||||
hoverFilled: true,
|
},
|
||||||
action: () => emit('delete'),
|
{
|
||||||
shown: !instancePath,
|
id: 'delete',
|
||||||
disabled: locked,
|
color: 'red',
|
||||||
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
hoverFilled: true,
|
||||||
},
|
action: () => emit('delete'),
|
||||||
]"
|
shown: !instancePath,
|
||||||
>
|
disabled: locked,
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
||||||
<template #play-instance>
|
},
|
||||||
<PlayIcon aria-hidden="true" />
|
]"
|
||||||
{{ formatMessage(messages.playInstance) }}
|
>
|
||||||
</template>
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #open-instance>
|
<template #play-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.viewInstance) }}
|
{{ formatMessage(messages.playInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #edit>
|
<template #open-instance>
|
||||||
<EditIcon aria-hidden="true" /> {{ formatMessage(commonMessages.editButton) }}
|
<EyeIcon aria-hidden="true" />
|
||||||
</template>
|
{{ formatMessage(messages.viewInstance) }}
|
||||||
<template #open-folder>
|
</template>
|
||||||
<FolderOpenIcon aria-hidden="true" />
|
<template #edit>
|
||||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
<EditIcon aria-hidden="true" />
|
||||||
</template>
|
{{ formatMessage(commonMessages.editButton) }}
|
||||||
<template #copy-address>
|
</template>
|
||||||
<ClipboardCopyIcon aria-hidden="true" /> {{ formatMessage(messages.copyAddress) }}
|
<template #open-folder>
|
||||||
</template>
|
<FolderOpenIcon aria-hidden="true" />
|
||||||
<template #refresh>
|
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
</template>
|
||||||
</template>
|
<template #copy-address>
|
||||||
<template #dont-show-on-home>
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
<XIcon aria-hidden="true" />
|
{{ formatMessage(messages.copyAddress) }}
|
||||||
{{ formatMessage(messages.dontShowOnHome) }}
|
</template>
|
||||||
</template>
|
<template #refresh>
|
||||||
<template #delete>
|
<UpdatedIcon aria-hidden="true" />
|
||||||
<TrashIcon aria-hidden="true" />
|
{{ formatMessage(commonMessages.refreshButton) }}
|
||||||
{{
|
</template>
|
||||||
formatMessage(
|
<template #dont-show-on-home>
|
||||||
world.type === 'server'
|
<XIcon aria-hidden="true" />
|
||||||
? commonMessages.removeButton
|
{{ formatMessage(messages.dontShowOnHome) }}
|
||||||
: commonMessages.deleteLabel,
|
</template>
|
||||||
)
|
<template #delete>
|
||||||
}}
|
<TrashIcon aria-hidden="true" />
|
||||||
</template>
|
{{
|
||||||
</OverflowMenu>
|
formatMessage(
|
||||||
</ButtonStyled>
|
world.type === 'server'
|
||||||
</div>
|
? commonMessages.removeButton
|
||||||
</div>
|
: commonMessages.deleteLabel,
|
||||||
</SmartClickable>
|
)
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SmartClickable>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.world-item-highlighted {
|
.world-item-highlighted {
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: fade-highlight 4s ease-out;
|
animation: fade-highlight 4s ease-out;
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@apply rounded-xl inset-0 absolute;
|
@apply rounded-xl inset-0 absolute;
|
||||||
|
|
||||||
animation: fade-opacity 4s ease-out;
|
animation: fade-opacity 4s ease-out;
|
||||||
|
|
||||||
content: '';
|
content: '';
|
||||||
box-shadow: 0 0 8px 2px var(--color-brand);
|
box-shadow: 0 0 8px 2px var(--color-brand);
|
||||||
border: 1.5px solid var(--color-brand);
|
border: 1.5px solid var(--color-brand);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-highlight {
|
@keyframes fade-highlight {
|
||||||
0% {
|
0% {
|
||||||
filter: brightness(1.25);
|
filter: brightness(1.25);
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
filter: brightness(1.25);
|
filter: brightness(1.25);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-opacity {
|
@keyframes fade-opacity {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.light-mode .motd-renderer {
|
.light-mode .motd-renderer {
|
||||||
filter: brightness(0.75);
|
filter: brightness(0.75);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
|
||||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [server: ServerWorld, play: boolean]
|
submit: [server: ServerWorld, play: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -27,89 +28,89 @@ const address = ref()
|
|||||||
const resourcePack = ref<ServerPackStatus>('enabled')
|
const resourcePack = ref<ServerPackStatus>('enabled')
|
||||||
|
|
||||||
async function addServer(play: boolean) {
|
async function addServer(play: boolean) {
|
||||||
const serverName = name.value ? name.value : address.value
|
const serverName = name.value ? name.value : address.value
|
||||||
const resourcePackStatus = resourcePack.value
|
const resourcePackStatus = resourcePack.value
|
||||||
const index =
|
const index =
|
||||||
(await add_server_to_profile(
|
(await add_server_to_profile(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
serverName,
|
serverName,
|
||||||
address.value,
|
address.value,
|
||||||
resourcePackStatus,
|
resourcePackStatus,
|
||||||
).catch(handleError)) ?? 0
|
).catch(handleError)) ?? 0
|
||||||
emit(
|
emit(
|
||||||
'submit',
|
'submit',
|
||||||
{
|
{
|
||||||
name: serverName,
|
name: serverName,
|
||||||
type: 'server',
|
type: 'server',
|
||||||
index,
|
index,
|
||||||
address: address.value,
|
address: address.value,
|
||||||
pack_status: resourcePackStatus,
|
pack_status: resourcePackStatus,
|
||||||
},
|
},
|
||||||
play,
|
play,
|
||||||
)
|
)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
name.value = ''
|
name.value = ''
|
||||||
address.value = ''
|
address.value = ''
|
||||||
resourcePack.value = 'enabled'
|
resourcePack.value = 'enabled'
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
id: 'instance.add-server.title',
|
id: 'instance.add-server.title',
|
||||||
defaultMessage: 'Add a server',
|
defaultMessage: 'Add a server',
|
||||||
},
|
},
|
||||||
addServer: {
|
addServer: {
|
||||||
id: 'instance.add-server.add-server',
|
id: 'instance.add-server.add-server',
|
||||||
defaultMessage: 'Add server',
|
defaultMessage: 'Add server',
|
||||||
},
|
},
|
||||||
addAndPlay: {
|
addAndPlay: {
|
||||||
id: 'instance.add-server.add-and-play',
|
id: 'instance.add-server.add-and-play',
|
||||||
defaultMessage: 'Add and play',
|
defaultMessage: 'Add and play',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
<InstanceModalTitlePrefix :instance="instance" />
|
<InstanceModalTitlePrefix :instance="instance" />
|
||||||
<span class="font-extrabold text-contrast">{{ formatMessage(messages.title) }}</span>
|
<span class="font-extrabold text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<ServerModalBody
|
<ServerModalBody
|
||||||
v-model:name="name"
|
v-model:name="name"
|
||||||
v-model:address="address"
|
v-model:address="address"
|
||||||
v-model:resource-pack="resourcePack"
|
v-model:resource-pack="resourcePack"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="!address" @click="addServer(true)">
|
<button :disabled="!address" @click="addServer(true)">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
{{ formatMessage(messages.addAndPlay) }}
|
{{ formatMessage(messages.addAndPlay) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button :disabled="!address" @click="addServer(false)">
|
<button :disabled="!address" @click="addServer(false)">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
{{ formatMessage(messages.addServer) }}
|
{{ formatMessage(messages.addServer) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide()">
|
<button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import {
|
|
||||||
edit_server_in_profile,
|
|
||||||
set_world_display_status,
|
|
||||||
type DisplayStatus,
|
|
||||||
type ServerPackStatus,
|
|
||||||
type ServerWorld,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import {
|
||||||
|
type DisplayStatus,
|
||||||
|
edit_server_in_profile,
|
||||||
|
type ServerPackStatus,
|
||||||
|
type ServerWorld,
|
||||||
|
set_world_display_status,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [server: ServerWorld]
|
submit: [server: ServerWorld]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -38,81 +39,81 @@ const hideFromHome = ref(false)
|
|||||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||||
|
|
||||||
async function saveServer() {
|
async function saveServer() {
|
||||||
const serverName = name.value ? name.value : address.value
|
const serverName = name.value ? name.value : address.value
|
||||||
const resourcePackStatus = resourcePack.value
|
const resourcePackStatus = resourcePack.value
|
||||||
await edit_server_in_profile(
|
await edit_server_in_profile(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
index.value,
|
index.value,
|
||||||
serverName,
|
serverName,
|
||||||
address.value,
|
address.value,
|
||||||
resourcePackStatus,
|
resourcePackStatus,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
|
|
||||||
if (newDisplayStatus.value !== displayStatus.value) {
|
if (newDisplayStatus.value !== displayStatus.value) {
|
||||||
await set_world_display_status(
|
await set_world_display_status(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
'server',
|
'server',
|
||||||
address.value,
|
address.value,
|
||||||
newDisplayStatus.value,
|
newDisplayStatus.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
type: 'server',
|
type: 'server',
|
||||||
index: index.value,
|
index: index.value,
|
||||||
address: address.value,
|
address: address.value,
|
||||||
pack_status: resourcePackStatus,
|
pack_status: resourcePackStatus,
|
||||||
display_status: newDisplayStatus.value,
|
display_status: newDisplayStatus.value,
|
||||||
})
|
})
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(server: ServerWorld) {
|
function show(server: ServerWorld) {
|
||||||
name.value = server.name
|
name.value = server.name
|
||||||
address.value = server.address
|
address.value = server.address
|
||||||
resourcePack.value = server.pack_status
|
resourcePack.value = server.pack_status
|
||||||
index.value = server.index
|
index.value = server.index
|
||||||
displayStatus.value = server.display_status
|
displayStatus.value = server.display_status
|
||||||
hideFromHome.value = server.display_status === 'hidden'
|
hideFromHome.value = server.display_status === 'hidden'
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const titleMessage = defineMessage({
|
const titleMessage = defineMessage({
|
||||||
id: 'instance.edit-server.title',
|
id: 'instance.edit-server.title',
|
||||||
defaultMessage: 'Edit server',
|
defaultMessage: 'Edit server',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(titleMessage) }}</span>
|
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<ServerModalBody
|
<ServerModalBody
|
||||||
v-model:name="name"
|
v-model:name="name"
|
||||||
v-model:address="address"
|
v-model:address="address"
|
||||||
v-model:resource-pack="resourcePack"
|
v-model:resource-pack="resourcePack"
|
||||||
/>
|
/>
|
||||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="!address" @click="saveServer">
|
<button :disabled="!address" @click="saveServer">
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide()">
|
<button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
|
||||||
import { rename_world, reset_world_icon, set_world_display_status } from '@/helpers/worlds.ts'
|
|
||||||
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||||
|
import { rename_world, reset_world_icon, set_world_display_status } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -32,98 +33,98 @@ const hideFromHome = ref(false)
|
|||||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||||
|
|
||||||
async function saveWorld() {
|
async function saveWorld() {
|
||||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||||
|
|
||||||
if (removeIcon.value) {
|
if (removeIcon.value) {
|
||||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
||||||
}
|
}
|
||||||
if (newDisplayStatus.value !== displayStatus.value) {
|
if (newDisplayStatus.value !== displayStatus.value) {
|
||||||
await set_world_display_status(
|
await set_world_display_status(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
'singleplayer',
|
'singleplayer',
|
||||||
path.value,
|
path.value,
|
||||||
newDisplayStatus.value,
|
newDisplayStatus.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(world: SingleplayerWorld) {
|
function show(world: SingleplayerWorld) {
|
||||||
name.value = world.name
|
name.value = world.name
|
||||||
path.value = world.path
|
path.value = world.path
|
||||||
icon.value = world.icon
|
icon.value = world.icon
|
||||||
displayStatus.value = world.display_status
|
displayStatus.value = world.display_status
|
||||||
hideFromHome.value = world.display_status === 'hidden'
|
hideFromHome.value = world.display_status === 'hidden'
|
||||||
removeIcon.value = false
|
removeIcon.value = false
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
id: 'instance.edit-world.title',
|
id: 'instance.edit-world.title',
|
||||||
defaultMessage: 'Edit world',
|
defaultMessage: 'Edit world',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.edit-world.name',
|
id: 'instance.edit-world.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
placeholderName: {
|
placeholderName: {
|
||||||
id: 'instance.edit-world.placeholder-name',
|
id: 'instance.edit-world.placeholder-name',
|
||||||
defaultMessage: 'Minecraft World',
|
defaultMessage: 'Minecraft World',
|
||||||
},
|
},
|
||||||
resetIcon: {
|
resetIcon: {
|
||||||
id: 'instance.edit-world.reset-icon',
|
id: 'instance.edit-world.reset-icon',
|
||||||
defaultMessage: 'Reset icon',
|
defaultMessage: 'Reset icon',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<Avatar :src="removeIcon || !icon ? undefined : icon" size="24px" />
|
<Avatar :src="removeIcon || !icon ? undefined : icon" size="24px" />
|
||||||
{{ instance.name }} <ChevronRightIcon />
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(messages.title) }}</span>
|
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-[450px]">
|
<div class="w-[450px]">
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
||||||
{{ formatMessage(messages.name) }}
|
{{ formatMessage(messages.name) }}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<input
|
||||||
v-model="name"
|
v-model="name"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.placeholderName)"
|
:placeholder="formatMessage(messages.placeholderName)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button @click="saveWorld">
|
<button @click="saveWorld">
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
<button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
||||||
<UndoIcon />
|
<UndoIcon />
|
||||||
{{ formatMessage(messages.resetIcon) }}
|
{{ formatMessage(messages.resetIcon) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide()">
|
<button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Checkbox } from '@modrinth/ui'
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Checkbox } from '@modrinth/ui'
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const value = defineModel<boolean>({ required: true })
|
const value = defineModel<boolean>({ required: true })
|
||||||
|
|
||||||
const labelMessage = defineMessage({
|
const labelMessage = defineMessage({
|
||||||
id: 'instance.edit-world.hide-from-home',
|
id: 'instance.edit-world.hide-from-home',
|
||||||
defaultMessage: `Hide from the Home page`,
|
defaultMessage: `Hide from the Home page`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = computed(() => formatMessage(labelMessage))
|
const label = computed(() => formatMessage(labelMessage))
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Checkbox v-model="value" :label="label" />
|
<Checkbox v-model="value" :label="label" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TeleportDropdownMenu } from '@modrinth/ui'
|
import { TeleportDropdownMenu } from '@modrinth/ui'
|
||||||
|
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||||
|
|
||||||
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
||||||
import { type MessageDescriptor, defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
@@ -12,75 +13,75 @@ const resourcePack = defineModel<ServerPackStatus>('resourcePack')
|
|||||||
const resourcePackOptions: ServerPackStatus[] = ['enabled', 'prompt', 'disabled']
|
const resourcePackOptions: ServerPackStatus[] = ['enabled', 'prompt', 'disabled']
|
||||||
|
|
||||||
const resourcePackOptionMessages: Record<ServerPackStatus, MessageDescriptor> = defineMessages({
|
const resourcePackOptionMessages: Record<ServerPackStatus, MessageDescriptor> = defineMessages({
|
||||||
enabled: {
|
enabled: {
|
||||||
id: 'instance.add-server.resource-pack.enabled',
|
id: 'instance.add-server.resource-pack.enabled',
|
||||||
defaultMessage: 'Enabled',
|
defaultMessage: 'Enabled',
|
||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
id: 'instance.add-server.resource-pack.prompt',
|
id: 'instance.add-server.resource-pack.prompt',
|
||||||
defaultMessage: 'Prompt',
|
defaultMessage: 'Prompt',
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
id: 'instance.add-server.resource-pack.disabled',
|
id: 'instance.add-server.resource-pack.disabled',
|
||||||
defaultMessage: 'Disabled',
|
defaultMessage: 'Disabled',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.server-modal.name',
|
id: 'instance.server-modal.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
id: 'instance.server-modal.address',
|
id: 'instance.server-modal.address',
|
||||||
defaultMessage: 'Address',
|
defaultMessage: 'Address',
|
||||||
},
|
},
|
||||||
resourcePack: {
|
resourcePack: {
|
||||||
id: 'instance.server-modal.resource-pack',
|
id: 'instance.server-modal.resource-pack',
|
||||||
defaultMessage: 'Resource pack',
|
defaultMessage: 'Resource pack',
|
||||||
},
|
},
|
||||||
placeholderName: {
|
placeholderName: {
|
||||||
id: 'instance.server-modal.placeholder-name',
|
id: 'instance.server-modal.placeholder-name',
|
||||||
defaultMessage: 'Minecraft Server',
|
defaultMessage: 'Minecraft Server',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ resourcePackOptions })
|
defineExpose({ resourcePackOptions })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="w-[450px]">
|
<div class="w-[450px]">
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
||||||
{{ formatMessage(messages.name) }}
|
{{ formatMessage(messages.name) }}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<input
|
||||||
v-model="name"
|
v-model="name"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.placeholderName)"
|
:placeholder="formatMessage(messages.placeholderName)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
||||||
{{ formatMessage(messages.address) }}
|
{{ formatMessage(messages.address) }}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<input
|
||||||
v-model="address"
|
v-model="address"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="example.modrinth.gg"
|
placeholder="example.modrinth.gg"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
||||||
{{ formatMessage(messages.resourcePack) }}
|
{{ formatMessage(messages.resourcePack) }}
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<TeleportDropdownMenu
|
<TeleportDropdownMenu
|
||||||
v-model="resourcePack"
|
v-model="resourcePack"
|
||||||
:options="resourcePackOptions"
|
:options="resourcePackOptions"
|
||||||
name="Server resource pack"
|
name="Server resource pack"
|
||||||
:display-name="
|
:display-name="
|
||||||
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
|
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import cssContent from '@/assets/stylesheets/macFix.css?inline'
|
import cssContent from '@/assets/stylesheets/macFix.css?inline'
|
||||||
|
|
||||||
export async function useCheckDisableMouseover() {
|
export async function useCheckDisableMouseover() {
|
||||||
try {
|
try {
|
||||||
// Fetch the CSS content from the Rust backend
|
// Fetch the CSS content from the Rust backend
|
||||||
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
|
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
|
||||||
|
|
||||||
if (should_disable_mouseover) {
|
if (should_disable_mouseover) {
|
||||||
// Create a style element and set its content
|
// Create a style element and set its content
|
||||||
const styleElement = document.createElement('style')
|
const styleElement = document.createElement('style')
|
||||||
styleElement.innerHTML = cssContent
|
styleElement.innerHTML = cssContent
|
||||||
|
|
||||||
// Append the style element to the document's head
|
// Append the style element to the document's head
|
||||||
document.head.appendChild(styleElement)
|
document.head.appendChild(styleElement)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking OS version from Rust backend', error)
|
console.error('Error checking OS version from Rust backend', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { get_max_memory } from '@/helpers/jre.js'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { get_max_memory } from '@/helpers/jre.js'
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||||
|
|
||||||
const snapPoints = computed(() => {
|
const snapPoints = computed(() => {
|
||||||
let points = []
|
let points = []
|
||||||
let memory = 2048
|
let memory = 2048
|
||||||
|
|
||||||
while (memory <= maxMemory.value) {
|
while (memory <= maxMemory.value) {
|
||||||
points.push(memory)
|
points.push(memory)
|
||||||
memory *= 2
|
memory *= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
return points
|
return points
|
||||||
})
|
})
|
||||||
|
|
||||||
return { maxMemory, snapPoints }
|
return { maxMemory, snapPoints }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export async function init_ads_window(overrideShown = false) {
|
export async function init_ads_window(overrideShown = false) {
|
||||||
return await invoke('plugin:ads|init_ads_window', { overrideShown, dpr: window.devicePixelRatio })
|
return await invoke('plugin:ads|init_ads_window', {
|
||||||
|
overrideShown,
|
||||||
|
dpr: window.devicePixelRatio,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function show_ads_window() {
|
export async function show_ads_window() {
|
||||||
return await invoke('plugin:ads|show_ads_window', { dpr: window.devicePixelRatio })
|
return await invoke('plugin:ads|show_ads_window', { dpr: window.devicePixelRatio })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hide_ads_window(reset) {
|
export async function hide_ads_window(reset) {
|
||||||
return await invoke('plugin:ads|hide_ads_window', { reset })
|
return await invoke('plugin:ads|hide_ads_window', { reset })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function record_ads_click() {
|
export async function record_ads_click() {
|
||||||
return await invoke('plugin:ads|record_ads_click')
|
return await invoke('plugin:ads|record_ads_click')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function open_ads_link(path, origin) {
|
export async function open_ads_link(path, origin) {
|
||||||
return await invoke('plugin:ads|open_link', { path, origin })
|
return await invoke('plugin:ads|open_link', { path, origin })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { posthog } from 'posthog-js'
|
import { posthog } from 'posthog-js'
|
||||||
|
|
||||||
export const initAnalytics = () => {
|
export const initAnalytics = () => {
|
||||||
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
||||||
persistence: 'localStorage',
|
persistence: 'localStorage',
|
||||||
api_host: 'https://posthog.modrinth.com',
|
api_host: 'https://posthog.modrinth.com',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const debugAnalytics = () => {
|
export const debugAnalytics = () => {
|
||||||
posthog.debug()
|
posthog.debug()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const optOutAnalytics = () => {
|
export const optOutAnalytics = () => {
|
||||||
posthog.opt_out_capturing()
|
posthog.opt_out_capturing()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const optInAnalytics = () => {
|
export const optInAnalytics = () => {
|
||||||
posthog.opt_in_capturing()
|
posthog.opt_in_capturing()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const trackEvent = (eventName, properties) => {
|
export const trackEvent = (eventName, properties) => {
|
||||||
posthog.capture(eventName, properties)
|
posthog.capture(eventName, properties)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
* @property {string} user_code - The code to enter on the verification_uri page.
|
* @property {string} user_code - The code to enter on the verification_uri page.
|
||||||
*/
|
*/
|
||||||
export async function login() {
|
export async function login() {
|
||||||
return await invoke('plugin:auth|login')
|
return await invoke('plugin:auth|login')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +30,7 @@ export async function login() {
|
|||||||
* @return {Promise<UUID | undefined>}
|
* @return {Promise<UUID | undefined>}
|
||||||
*/
|
*/
|
||||||
export async function get_default_user() {
|
export async function get_default_user() {
|
||||||
return await invoke('plugin:auth|get_default_user')
|
return await invoke('plugin:auth|get_default_user')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +38,7 @@ export async function get_default_user() {
|
|||||||
* @param {UUID} user
|
* @param {UUID} user
|
||||||
*/
|
*/
|
||||||
export async function set_default_user(user) {
|
export async function set_default_user(user) {
|
||||||
return await invoke('plugin:auth|set_default_user', { user })
|
return await invoke('plugin:auth|set_default_user', { user })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +46,7 @@ export async function set_default_user(user) {
|
|||||||
* @param {UUID} user
|
* @param {UUID} user
|
||||||
*/
|
*/
|
||||||
export async function remove_user(user) {
|
export async function remove_user(user) {
|
||||||
return await invoke('plugin:auth|remove_user', { user })
|
return await invoke('plugin:auth|remove_user', { user })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,5 +54,5 @@ export async function remove_user(user) {
|
|||||||
* @returns {Promise<Credential[]>}
|
* @returns {Promise<Credential[]>}
|
||||||
*/
|
*/
|
||||||
export async function users() {
|
export async function users() {
|
||||||
return await invoke('plugin:auth|get_users')
|
return await invoke('plugin:auth|get_users')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export async function get_project(id, cacheBehaviour) {
|
export async function get_project(id, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
|
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_project_many(ids, cacheBehaviour) {
|
export async function get_project_many(ids, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_project_many', { ids, cacheBehaviour })
|
return await invoke('plugin:cache|get_project_many', { ids, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_version(id, cacheBehaviour) {
|
export async function get_version(id, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_version', { id, cacheBehaviour })
|
return await invoke('plugin:cache|get_version', { id, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_version_many(ids, cacheBehaviour) {
|
export async function get_version_many(ids, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_version_many', { ids, cacheBehaviour })
|
return await invoke('plugin:cache|get_version_many', { ids, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_user(id, cacheBehaviour) {
|
export async function get_user(id, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_user', { id, cacheBehaviour })
|
return await invoke('plugin:cache|get_user', { id, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_user_many(ids, cacheBehaviour) {
|
export async function get_user_many(ids, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_user_many', { ids, cacheBehaviour })
|
return await invoke('plugin:cache|get_user_many', { ids, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_team(id, cacheBehaviour) {
|
export async function get_team(id, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_team', { id, cacheBehaviour })
|
return await invoke('plugin:cache|get_team', { id, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_team_many(ids, cacheBehaviour) {
|
export async function get_team_many(ids, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_team_many', { ids, cacheBehaviour })
|
return await invoke('plugin:cache|get_team_many', { ids, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_organization(id, cacheBehaviour) {
|
export async function get_organization(id, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_organization', { id, cacheBehaviour })
|
return await invoke('plugin:cache|get_organization', { id, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_organization_many(ids, cacheBehaviour) {
|
export async function get_organization_many(ids, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_organization_many', { ids, cacheBehaviour })
|
return await invoke('plugin:cache|get_organization_many', { ids, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_search_results(id, cacheBehaviour) {
|
export async function get_search_results(id, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_search_results', { id, cacheBehaviour })
|
return await invoke('plugin:cache|get_search_results', { id, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_search_results_many(ids, cacheBehaviour) {
|
export async function get_search_results_many(ids, cacheBehaviour) {
|
||||||
return await invoke('plugin:cache|get_search_results_many', { ids, cacheBehaviour })
|
return await invoke('plugin:cache|get_search_results_many', { ids, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function purge_cache_types(cacheTypes) {
|
export async function purge_cache_types(cacheTypes) {
|
||||||
return await invoke('plugin:cache|purge_cache_types', { cacheTypes })
|
return await invoke('plugin:cache|purge_cache_types', { cacheTypes })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { listen } from '@tauri-apps/api/event'
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export async function loading_listener(callback) {
|
export async function loading_listener(callback) {
|
||||||
return await listen('loading', (event) => callback(event.payload))
|
return await listen('loading', (event) => callback(event.payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Payload for the 'process' event
|
/// Payload for the 'process' event
|
||||||
@@ -54,7 +54,7 @@ export async function loading_listener(callback) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export async function process_listener(callback) {
|
export async function process_listener(callback) {
|
||||||
return await listen('process', (event) => callback(event.payload))
|
return await listen('process', (event) => callback(event.payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Payload for the 'profile' event
|
/// Payload for the 'profile' event
|
||||||
@@ -68,7 +68,7 @@ export async function process_listener(callback) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export async function profile_listener(callback) {
|
export async function profile_listener(callback) {
|
||||||
return await listen('profile', (event) => callback(event.payload))
|
return await listen('profile', (event) => callback(event.payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Payload for the 'command' event
|
/// Payload for the 'command' event
|
||||||
@@ -79,9 +79,9 @@ export async function profile_listener(callback) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export async function command_listener(callback) {
|
export async function command_listener(callback) {
|
||||||
return await listen('command', (event) => {
|
return await listen('command', (event) => {
|
||||||
callback(event.payload)
|
callback(event.payload)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Payload for the 'warning' event
|
/// Payload for the 'warning' event
|
||||||
@@ -91,9 +91,9 @@ export async function command_listener(callback) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export async function warning_listener(callback) {
|
export async function warning_listener(callback) {
|
||||||
return await listen('warning', (event) => callback(event.payload))
|
return await listen('warning', (event) => callback(event.payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function friend_listener(callback) {
|
export async function friend_listener(callback) {
|
||||||
return await listen('friend', (event) => callback(event.payload))
|
return await listen('friend', (event) => callback(event.payload))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { getVersion } from '@tauri-apps/api/app'
|
|||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
|
||||||
export const useFetch = async (url, item, isSilent) => {
|
export const useFetch = async (url, item, isSilent) => {
|
||||||
try {
|
try {
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isSilent) {
|
if (!isSilent) {
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
handleError({ message: `Error fetching ${item}` })
|
handleError({ message: `Error fetching ${item}` })
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export async function friends() {
|
export async function friends() {
|
||||||
return await invoke('plugin:friends|friends')
|
return await invoke('plugin:friends|friends')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function friend_statuses() {
|
export async function friend_statuses() {
|
||||||
return await invoke('plugin:friends|friend_statuses')
|
return await invoke('plugin:friends|friend_statuses')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function add_friend(userId) {
|
export async function add_friend(userId) {
|
||||||
return await invoke('plugin:friends|add_friend', { userId })
|
return await invoke('plugin:friends|add_friend', { userId })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove_friend(userId) {
|
export async function remove_friend(userId) {
|
||||||
return await invoke('plugin:friends|remove_friend', { userId })
|
return await invoke('plugin:friends|remove_friend', { userId })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import { create } from './profile'
|
import { create } from './profile'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -27,37 +28,37 @@ import { create } from './profile'
|
|||||||
/// eg: get_importable_instances("MultiMC", "C:/MultiMC")
|
/// eg: get_importable_instances("MultiMC", "C:/MultiMC")
|
||||||
/// returns ["Instance 1", "Instance 2"]
|
/// returns ["Instance 1", "Instance 2"]
|
||||||
export async function get_importable_instances(launcherType, basePath) {
|
export async function get_importable_instances(launcherType, basePath) {
|
||||||
return await invoke('plugin:import|get_importable_instances', { launcherType, basePath })
|
return await invoke('plugin:import|get_importable_instances', { launcherType, basePath })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import an instance from a launcher type and base path
|
/// Import an instance from a launcher type and base path
|
||||||
/// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
|
/// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
|
||||||
export async function import_instance(launcherType, basePath, instanceFolder) {
|
export async function import_instance(launcherType, basePath, instanceFolder) {
|
||||||
// create a basic, empty instance (most properties will be filled in by the import process)
|
// create a basic, empty instance (most properties will be filled in by the import process)
|
||||||
// We do NOT watch the fs for changes to avoid duplicate events during installation
|
// We do NOT watch the fs for changes to avoid duplicate events during installation
|
||||||
// fs watching will be enabled once the instance is imported
|
// fs watching will be enabled once the instance is imported
|
||||||
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
|
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
|
||||||
|
|
||||||
return await invoke('plugin:import|import_instance', {
|
return await invoke('plugin:import|import_instance', {
|
||||||
profilePath,
|
profilePath,
|
||||||
launcherType,
|
launcherType,
|
||||||
basePath,
|
basePath,
|
||||||
instanceFolder,
|
instanceFolder,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if this instance is valid for importing, given a certain launcher type
|
/// Checks if this instance is valid for importing, given a certain launcher type
|
||||||
/// eg: is_valid_importable_instance("C:/MultiMC/Instance 1", "MultiMC")
|
/// eg: is_valid_importable_instance("C:/MultiMC/Instance 1", "MultiMC")
|
||||||
export async function is_valid_importable_instance(instanceFolder, launcherType) {
|
export async function is_valid_importable_instance(instanceFolder, launcherType) {
|
||||||
return await invoke('plugin:import|is_valid_importable_instance', {
|
return await invoke('plugin:import|is_valid_importable_instance', {
|
||||||
instanceFolder,
|
instanceFolder,
|
||||||
launcherType,
|
launcherType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the default path for the given launcher type
|
/// Gets the default path for the given launcher type
|
||||||
/// null if it can't be found or doesn't exist
|
/// null if it can't be found or doesn't exist
|
||||||
/// eg: get_default_launcher_path("MultiMC")
|
/// eg: get_default_launcher_path("MultiMC")
|
||||||
export async function get_default_launcher_path(launcherType) {
|
export async function get_default_launcher_path(launcherType) {
|
||||||
return await invoke('plugin:import|get_default_launcher_path', { launcherType })
|
return await invoke('plugin:import|get_default_launcher_path', { launcherType })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,37 +15,37 @@ JavaVersion {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export async function get_java_versions() {
|
export async function get_java_versions() {
|
||||||
return await invoke('plugin:jre|get_java_versions')
|
return await invoke('plugin:jre|get_java_versions')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function set_java_version(javaVersion) {
|
export async function set_java_version(javaVersion) {
|
||||||
return await invoke('plugin:jre|set_java_version', { javaVersion })
|
return await invoke('plugin:jre|set_java_version', { javaVersion })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds all the installation of Java 7, if it exists
|
// Finds all the installation of Java 7, if it exists
|
||||||
// Returns [JavaVersion]
|
// Returns [JavaVersion]
|
||||||
export async function find_filtered_jres(version) {
|
export async function find_filtered_jres(version) {
|
||||||
return await invoke('plugin:jre|jre_find_filtered_jres', { version })
|
return await invoke('plugin:jre|jre_find_filtered_jres', { version })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets java version from a specific path by trying to run 'java -version' on it.
|
// Gets java version from a specific path by trying to run 'java -version' on it.
|
||||||
// This also validates it, as it returns null if no valid java version is found at the path
|
// This also validates it, as it returns null if no valid java version is found at the path
|
||||||
export async function get_jre(path) {
|
export async function get_jre(path) {
|
||||||
return await invoke('plugin:jre|jre_get_jre', { path })
|
return await invoke('plugin:jre|jre_get_jre', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests JRE version by running 'java -version' on it.
|
// Tests JRE version by running 'java -version' on it.
|
||||||
// Returns true if the version is valid, and matches given (after extraction)
|
// Returns true if the version is valid, and matches given (after extraction)
|
||||||
export async function test_jre(path, majorVersion) {
|
export async function test_jre(path, majorVersion) {
|
||||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically installs specified java version
|
// Automatically installs specified java version
|
||||||
export async function auto_install_java(javaVersion) {
|
export async function auto_install_java(javaVersion) {
|
||||||
return await invoke('plugin:jre|jre_auto_install_java', { javaVersion })
|
return await invoke('plugin:jre|jre_auto_install_java', { javaVersion })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get max memory in KiB
|
// Get max memory in KiB
|
||||||
export async function get_max_memory() {
|
export async function get_max_memory() {
|
||||||
return await invoke('plugin:jre|jre_get_max_memory')
|
return await invoke('plugin:jre|jre_get_max_memory')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,31 +18,35 @@ pub struct Logs {
|
|||||||
/// Get all logs that exist for a given profile
|
/// Get all logs that exist for a given profile
|
||||||
/// This is returned as an array of Log objects, sorted by filename (the folder name, when the log was created)
|
/// This is returned as an array of Log objects, sorted by filename (the folder name, when the log was created)
|
||||||
export async function get_logs(profilePath, clearContents) {
|
export async function get_logs(profilePath, clearContents) {
|
||||||
return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents })
|
return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a profile's log by filename
|
/// Get a profile's log by filename
|
||||||
export async function get_logs_by_filename(profilePath, logType, filename) {
|
export async function get_logs_by_filename(profilePath, logType, filename) {
|
||||||
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename })
|
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a profile's log text only by filename
|
/// Get a profile's log text only by filename
|
||||||
export async function get_output_by_filename(profilePath, logType, filename) {
|
export async function get_output_by_filename(profilePath, logType, filename) {
|
||||||
return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, logType, filename })
|
return await invoke('plugin:logs|logs_get_output_by_filename', {
|
||||||
|
profilePath,
|
||||||
|
logType,
|
||||||
|
filename,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a profile's log by filename
|
/// Delete a profile's log by filename
|
||||||
export async function delete_logs_by_filename(profilePath, logType, filename) {
|
export async function delete_logs_by_filename(profilePath, logType, filename) {
|
||||||
return await invoke('plugin:logs|logs_delete_logs_by_filename', {
|
return await invoke('plugin:logs|logs_delete_logs_by_filename', {
|
||||||
profilePath,
|
profilePath,
|
||||||
logType,
|
logType,
|
||||||
filename,
|
filename,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete all logs for a given profile
|
/// Delete all logs for a given profile
|
||||||
export async function delete_logs(profilePath) {
|
export async function delete_logs(profilePath) {
|
||||||
return await invoke('plugin:logs|logs_delete_logs', { profilePath })
|
return await invoke('plugin:logs|logs_delete_logs', { profilePath })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the latest log for a given profile and cursor (startpoint to read withi nthe file)
|
/// Get the latest log for a given profile and cursor (startpoint to read withi nthe file)
|
||||||
@@ -57,5 +61,5 @@ export async function delete_logs(profilePath) {
|
|||||||
|
|
||||||
// From latest.log directly
|
// From latest.log directly
|
||||||
export async function get_latest_log_cursor(profilePath, cursor) {
|
export async function get_latest_log_cursor(profilePath, cursor) {
|
||||||
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
|
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
/// Gets the game versions from daedalus
|
/// Gets the game versions from daedalus
|
||||||
// Returns a VersionManifest
|
// Returns a VersionManifest
|
||||||
export async function get_game_versions() {
|
export async function get_game_versions() {
|
||||||
return await invoke('plugin:metadata|metadata_get_game_versions')
|
return await invoke('plugin:metadata|metadata_get_game_versions')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the given loader versions from daedalus
|
// Gets the given loader versions from daedalus
|
||||||
// Returns Manifest
|
// Returns Manifest
|
||||||
export async function get_loader_versions(loader) {
|
export async function get_loader_versions(loader) {
|
||||||
return await invoke('plugin:metadata|metadata_get_loader_versions', { loader })
|
return await invoke('plugin:metadata|metadata_get_loader_versions', { loader })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export async function login() {
|
export async function login() {
|
||||||
return await invoke('plugin:mr-auth|modrinth_login')
|
return await invoke('plugin:mr-auth|modrinth_login')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
return await invoke('plugin:mr-auth|logout')
|
return await invoke('plugin:mr-auth|logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get() {
|
export async function get() {
|
||||||
return await invoke('plugin:mr-auth|get')
|
return await invoke('plugin:mr-auth|get')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelLogin() {
|
export async function cancelLogin() {
|
||||||
return await invoke('plugin:mr-auth|cancel_modrinth_login')
|
return await invoke('plugin:mr-auth|cancel_modrinth_login')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,61 +4,62 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import { create } from './profile'
|
import { create } from './profile'
|
||||||
|
|
||||||
// Installs pack from a version ID
|
// Installs pack from a version ID
|
||||||
export async function create_profile_and_install(
|
export async function create_profile_and_install(
|
||||||
projectId,
|
projectId,
|
||||||
versionId,
|
versionId,
|
||||||
packTitle,
|
packTitle,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
createInstanceCallback = () => {},
|
createInstanceCallback = () => {},
|
||||||
) {
|
) {
|
||||||
const location = {
|
const location = {
|
||||||
type: 'fromVersionId',
|
type: 'fromVersionId',
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
version_id: versionId,
|
version_id: versionId,
|
||||||
title: packTitle,
|
title: packTitle,
|
||||||
icon_url: iconUrl,
|
icon_url: iconUrl,
|
||||||
}
|
}
|
||||||
const profile_creator = await invoke('plugin:pack|pack_get_profile_from_pack', { location })
|
const profile_creator = await invoke('plugin:pack|pack_get_profile_from_pack', { location })
|
||||||
const profile = await create(
|
const profile = await create(
|
||||||
profile_creator.name,
|
profile_creator.name,
|
||||||
profile_creator.gameVersion,
|
profile_creator.gameVersion,
|
||||||
profile_creator.modloader,
|
profile_creator.modloader,
|
||||||
profile_creator.loaderVersion,
|
profile_creator.loaderVersion,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
createInstanceCallback(profile)
|
createInstanceCallback(profile)
|
||||||
|
|
||||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function install_to_existing_profile(projectId, versionId, title, profilePath) {
|
export async function install_to_existing_profile(projectId, versionId, title, profilePath) {
|
||||||
const location = {
|
const location = {
|
||||||
type: 'fromVersionId',
|
type: 'fromVersionId',
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
version_id: versionId,
|
version_id: versionId,
|
||||||
title,
|
title,
|
||||||
}
|
}
|
||||||
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
|
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Installs pack from a path
|
// Installs pack from a path
|
||||||
export async function create_profile_and_install_from_file(path) {
|
export async function create_profile_and_install_from_file(path) {
|
||||||
const location = {
|
const location = {
|
||||||
type: 'fromFile',
|
type: 'fromFile',
|
||||||
path: path,
|
path: path,
|
||||||
}
|
}
|
||||||
const profile_creator = await invoke('plugin:pack|pack_get_profile_from_pack', { location })
|
const profile_creator = await invoke('plugin:pack|pack_get_profile_from_pack', { location })
|
||||||
const profile = await create(
|
const profile = await create(
|
||||||
profile_creator.name,
|
profile_creator.name,
|
||||||
profile_creator.gameVersion,
|
profile_creator.gameVersion,
|
||||||
profile_creator.modloader,
|
profile_creator.modloader,
|
||||||
profile_creator.loaderVersion,
|
profile_creator.loaderVersion,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
/// Gets all running process IDs with a given profile path
|
/// Gets all running process IDs with a given profile path
|
||||||
/// Returns [u32]
|
/// Returns [u32]
|
||||||
export async function get_by_profile_path(path) {
|
export async function get_by_profile_path(path) {
|
||||||
return await invoke('plugin:process|process_get_by_profile_path', { path })
|
return await invoke('plugin:process|process_get_by_profile_path', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets all running process IDs with a given profile path
|
/// Gets all running process IDs with a given profile path
|
||||||
/// Returns [u32]
|
/// Returns [u32]
|
||||||
export async function get_all() {
|
export async function get_all() {
|
||||||
return await invoke('plugin:process|process_get_all')
|
return await invoke('plugin:process|process_get_all')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kills a process by UUID
|
/// Kills a process by UUID
|
||||||
export async function kill(uuid) {
|
export async function kill(uuid) {
|
||||||
return await invoke('plugin:process|process_kill', { uuid })
|
return await invoke('plugin:process|process_kill', { uuid })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { install_to_existing_profile } from '@/helpers/pack.js'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
import { install_to_existing_profile } from '@/helpers/pack.js'
|
||||||
|
|
||||||
/// Add instance
|
/// Add instance
|
||||||
/*
|
/*
|
||||||
name: String, // the name of the profile, and relative path to create
|
name: String, // the name of the profile, and relative path to create
|
||||||
@@ -19,142 +20,145 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
|
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
|
||||||
//Trim string name to avoid "Unable to find directory"
|
//Trim string name to avoid "Unable to find directory"
|
||||||
name = name.trim()
|
name = name.trim()
|
||||||
return await invoke('plugin:profile-create|profile_create', {
|
return await invoke('plugin:profile-create|profile_create', {
|
||||||
name,
|
name,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
modloader,
|
modloader,
|
||||||
loaderVersion,
|
loaderVersion,
|
||||||
iconPath,
|
iconPath,
|
||||||
skipInstall,
|
skipInstall,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// duplicate a profile
|
// duplicate a profile
|
||||||
export async function duplicate(path) {
|
export async function duplicate(path) {
|
||||||
return await invoke('plugin:profile-create|profile_duplicate', { path })
|
return await invoke('plugin:profile-create|profile_duplicate', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a profile
|
// Remove a profile
|
||||||
export async function remove(path) {
|
export async function remove(path) {
|
||||||
return await invoke('plugin:profile|profile_remove', { path })
|
return await invoke('plugin:profile|profile_remove', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a profile by path
|
// Get a profile by path
|
||||||
// Returns a Profile
|
// Returns a Profile
|
||||||
export async function get(path) {
|
export async function get(path) {
|
||||||
return await invoke('plugin:profile|profile_get', { path })
|
return await invoke('plugin:profile|profile_get', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_many(paths) {
|
export async function get_many(paths) {
|
||||||
return await invoke('plugin:profile|profile_get_many', { paths })
|
return await invoke('plugin:profile|profile_get_many', { paths })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a profile's projects
|
// Get a profile's projects
|
||||||
// Returns a map of a path to profile file
|
// Returns a map of a path to profile file
|
||||||
export async function get_projects(path, cacheBehaviour) {
|
export async function get_projects(path, cacheBehaviour) {
|
||||||
return await invoke('plugin:profile|profile_get_projects', { path, cacheBehaviour })
|
return await invoke('plugin:profile|profile_get_projects', { path, cacheBehaviour })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a profile's full fs path
|
// Get a profile's full fs path
|
||||||
// Returns a path
|
// Returns a path
|
||||||
export async function get_full_path(path) {
|
export async function get_full_path(path) {
|
||||||
return await invoke('plugin:profile|profile_get_full_path', { path })
|
return await invoke('plugin:profile|profile_get_full_path', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get's a mod's full fs path
|
// Get's a mod's full fs path
|
||||||
// Returns a path
|
// Returns a path
|
||||||
export async function get_mod_full_path(path, projectPath) {
|
export async function get_mod_full_path(path, projectPath) {
|
||||||
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
|
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get optimal java version from profile
|
// Get optimal java version from profile
|
||||||
// Returns a java version
|
// Returns a java version
|
||||||
export async function get_optimal_jre_key(path) {
|
export async function get_optimal_jre_key(path) {
|
||||||
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
|
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a copy of the profile set
|
// Get a copy of the profile set
|
||||||
// Returns hashmap of path -> Profile
|
// Returns hashmap of path -> Profile
|
||||||
export async function list() {
|
export async function list() {
|
||||||
return await invoke('plugin:profile|profile_list')
|
return await invoke('plugin:profile|profile_list')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function check_installed(path, projectId) {
|
export async function check_installed(path, projectId) {
|
||||||
return await invoke('plugin:profile|profile_check_installed', { path, projectId })
|
return await invoke('plugin:profile|profile_check_installed', { path, projectId })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Installs/Repairs a profile
|
// Installs/Repairs a profile
|
||||||
export async function install(path, force) {
|
export async function install(path, force) {
|
||||||
return await invoke('plugin:profile|profile_install', { path, force })
|
return await invoke('plugin:profile|profile_install', { path, force })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates all of a profile's projects
|
// Updates all of a profile's projects
|
||||||
export async function update_all(path) {
|
export async function update_all(path) {
|
||||||
return await invoke('plugin:profile|profile_update_all', { path })
|
return await invoke('plugin:profile|profile_update_all', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates a specified project
|
// Updates a specified project
|
||||||
export async function update_project(path, projectPath) {
|
export async function update_project(path, projectPath) {
|
||||||
return await invoke('plugin:profile|profile_update_project', { path, projectPath })
|
return await invoke('plugin:profile|profile_update_project', { path, projectPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a project to a profile from a version
|
// Add a project to a profile from a version
|
||||||
// Returns a path to the new project file
|
// Returns a path to the new project file
|
||||||
export async function add_project_from_version(path, versionId) {
|
export async function add_project_from_version(path, versionId) {
|
||||||
return await invoke('plugin:profile|profile_add_project_from_version', { path, versionId })
|
return await invoke('plugin:profile|profile_add_project_from_version', { path, versionId })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a project to a profile from a path + project_type
|
// Add a project to a profile from a path + project_type
|
||||||
// Returns a path to the new project file
|
// Returns a path to the new project file
|
||||||
export async function add_project_from_path(path, projectPath, projectType) {
|
export async function add_project_from_path(path, projectPath, projectType) {
|
||||||
return await invoke('plugin:profile|profile_add_project_from_path', {
|
return await invoke('plugin:profile|profile_add_project_from_path', {
|
||||||
path,
|
path,
|
||||||
projectPath,
|
projectPath,
|
||||||
projectType,
|
projectType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle disabling a project
|
// Toggle disabling a project
|
||||||
export async function toggle_disable_project(path, projectPath) {
|
export async function toggle_disable_project(path, projectPath) {
|
||||||
return await invoke('plugin:profile|profile_toggle_disable_project', { path, projectPath })
|
return await invoke('plugin:profile|profile_toggle_disable_project', { path, projectPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a project
|
// Remove a project
|
||||||
export async function remove_project(path, projectPath) {
|
export async function remove_project(path, projectPath) {
|
||||||
return await invoke('plugin:profile|profile_remove_project', { path, projectPath })
|
return await invoke('plugin:profile|profile_remove_project', { path, projectPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update a managed Modrinth profile to a specific version
|
// Update a managed Modrinth profile to a specific version
|
||||||
export async function update_managed_modrinth_version(path, versionId) {
|
export async function update_managed_modrinth_version(path, versionId) {
|
||||||
return await invoke('plugin:profile|profile_update_managed_modrinth_version', { path, versionId })
|
return await invoke('plugin:profile|profile_update_managed_modrinth_version', {
|
||||||
|
path,
|
||||||
|
versionId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repair a managed Modrinth profile
|
// Repair a managed Modrinth profile
|
||||||
export async function update_repair_modrinth(path) {
|
export async function update_repair_modrinth(path) {
|
||||||
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
|
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export a profile to .mrpack
|
// Export a profile to .mrpack
|
||||||
/// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
|
/// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
|
||||||
// Version id is optional (ie: 1.1.5)
|
// Version id is optional (ie: 1.1.5)
|
||||||
export async function export_profile_mrpack(
|
export async function export_profile_mrpack(
|
||||||
path,
|
path,
|
||||||
exportLocation,
|
exportLocation,
|
||||||
includedOverrides,
|
includedOverrides,
|
||||||
versionId,
|
versionId,
|
||||||
description,
|
description,
|
||||||
name,
|
name,
|
||||||
) {
|
) {
|
||||||
return await invoke('plugin:profile|profile_export_mrpack', {
|
return await invoke('plugin:profile|profile_export_mrpack', {
|
||||||
path,
|
path,
|
||||||
exportLocation,
|
exportLocation,
|
||||||
includedOverrides,
|
includedOverrides,
|
||||||
versionId,
|
versionId,
|
||||||
description,
|
description,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a folder path, populate an array of all the subfolders
|
// Given a folder path, populate an array of all the subfolders
|
||||||
@@ -166,40 +170,40 @@ export async function export_profile_mrpack(
|
|||||||
// => [mods, resourcepacks]
|
// => [mods, resourcepacks]
|
||||||
// allows selection for 'included_overrides' in export_profile_mrpack
|
// allows selection for 'included_overrides' in export_profile_mrpack
|
||||||
export async function get_pack_export_candidates(profilePath) {
|
export async function get_pack_export_candidates(profilePath) {
|
||||||
return await invoke('plugin:profile|profile_get_pack_export_candidates', { profilePath })
|
return await invoke('plugin:profile|profile_get_pack_export_candidates', { profilePath })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run Minecraft using a pathed profile
|
// Run Minecraft using a pathed profile
|
||||||
// Returns PID of child
|
// Returns PID of child
|
||||||
export async function run(path) {
|
export async function run(path) {
|
||||||
return await invoke('plugin:profile|profile_run', { path })
|
return await invoke('plugin:profile|profile_run', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function kill(path) {
|
export async function kill(path) {
|
||||||
return await invoke('plugin:profile|profile_kill', { path })
|
return await invoke('plugin:profile|profile_kill', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edits a profile
|
// Edits a profile
|
||||||
export async function edit(path, editProfile) {
|
export async function edit(path, editProfile) {
|
||||||
return await invoke('plugin:profile|profile_edit', { path, editProfile })
|
return await invoke('plugin:profile|profile_edit', { path, editProfile })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edits a profile's icon
|
// Edits a profile's icon
|
||||||
export async function edit_icon(path, iconPath) {
|
export async function edit_icon(path, iconPath) {
|
||||||
return await invoke('plugin:profile|profile_edit_icon', { path, iconPath })
|
return await invoke('plugin:profile|profile_edit_icon', { path, iconPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function finish_install(instance) {
|
export async function finish_install(instance) {
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
if (instance.install_stage !== 'pack_installed') {
|
if (instance.install_stage !== 'pack_installed') {
|
||||||
let linkedData = instance.linked_data
|
let linkedData = instance.linked_data
|
||||||
await install_to_existing_profile(
|
await install_to_existing_profile(
|
||||||
linkedData.project_id,
|
linkedData.project_id,
|
||||||
linkedData.version_id,
|
linkedData.version_id,
|
||||||
instance.name,
|
instance.name,
|
||||||
instance.path,
|
instance.path,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
} else {
|
} else {
|
||||||
await install(instance.path, false).catch(handleError)
|
await install(instance.path, false).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,191 +1,192 @@
|
|||||||
import * as THREE from 'three'
|
|
||||||
import type { Skin, Cape } from '../skins'
|
|
||||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
|
||||||
import { reactive } from 'vue'
|
|
||||||
import {
|
|
||||||
setupSkinModel,
|
|
||||||
disposeCaches,
|
|
||||||
loadTexture,
|
|
||||||
applyCapeTexture,
|
|
||||||
createTransparentTexture,
|
|
||||||
} from '@modrinth/utils'
|
|
||||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
|
||||||
import { headStorage } from '../storage/head-storage'
|
|
||||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
applyCapeTexture,
|
||||||
|
createTransparentTexture,
|
||||||
|
disposeCaches,
|
||||||
|
loadTexture,
|
||||||
|
setupSkinModel,
|
||||||
|
} from '@modrinth/utils'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
import type { Cape, Skin } from '../skins'
|
||||||
|
import { determineModelType, get_normalized_skin_texture } from '../skins'
|
||||||
|
import { headStorage } from '../storage/head-storage'
|
||||||
|
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||||
|
|
||||||
export interface RenderResult {
|
export interface RenderResult {
|
||||||
forwards: string
|
forwards: string
|
||||||
backwards: string
|
backwards: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawRenderResult {
|
export interface RawRenderResult {
|
||||||
forwards: Blob
|
forwards: Blob
|
||||||
backwards: Blob
|
backwards: Blob
|
||||||
}
|
}
|
||||||
|
|
||||||
class BatchSkinRenderer {
|
class BatchSkinRenderer {
|
||||||
private renderer: THREE.WebGLRenderer | null = null
|
private renderer: THREE.WebGLRenderer | null = null
|
||||||
private scene: THREE.Scene | null = null
|
private scene: THREE.Scene | null = null
|
||||||
private camera: THREE.PerspectiveCamera | null = null
|
private camera: THREE.PerspectiveCamera | null = null
|
||||||
private currentModel: THREE.Group | null = null
|
private currentModel: THREE.Group | null = null
|
||||||
private readonly width: number
|
private readonly width: number
|
||||||
private readonly height: number
|
private readonly height: number
|
||||||
|
|
||||||
constructor(width: number = 360, height: number = 504) {
|
constructor(width: number = 360, height: number = 504) {
|
||||||
this.width = width
|
this.width = width
|
||||||
this.height = height
|
this.height = height
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeRenderer(): void {
|
private initializeRenderer(): void {
|
||||||
if (this.renderer) return
|
if (this.renderer) return
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = this.width
|
canvas.width = this.width
|
||||||
canvas.height = this.height
|
canvas.height = this.height
|
||||||
|
|
||||||
this.renderer = new THREE.WebGLRenderer({
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
antialias: true,
|
antialias: true,
|
||||||
alpha: true,
|
alpha: true,
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||||
this.renderer.toneMapping = THREE.NoToneMapping
|
this.renderer.toneMapping = THREE.NoToneMapping
|
||||||
this.renderer.toneMappingExposure = 10.0
|
this.renderer.toneMappingExposure = 10.0
|
||||||
this.renderer.setClearColor(0x000000, 0)
|
this.renderer.setClearColor(0x000000, 0)
|
||||||
this.renderer.setSize(this.width, this.height)
|
this.renderer.setSize(this.width, this.height)
|
||||||
|
|
||||||
this.scene = new THREE.Scene()
|
this.scene = new THREE.Scene()
|
||||||
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
|
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
|
||||||
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
directionalLight.castShadow = true
|
directionalLight.castShadow = true
|
||||||
directionalLight.position.set(2, 4, 3)
|
directionalLight.position.set(2, 4, 3)
|
||||||
this.scene.add(ambientLight)
|
this.scene.add(ambientLight)
|
||||||
this.scene.add(directionalLight)
|
this.scene.add(directionalLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async renderSkin(
|
public async renderSkin(
|
||||||
textureUrl: string,
|
textureUrl: string,
|
||||||
modelUrl: string,
|
modelUrl: string,
|
||||||
capeUrl?: string,
|
capeUrl?: string,
|
||||||
): Promise<RawRenderResult> {
|
): Promise<RawRenderResult> {
|
||||||
this.initializeRenderer()
|
this.initializeRenderer()
|
||||||
|
|
||||||
this.clearScene()
|
this.clearScene()
|
||||||
|
|
||||||
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||||
|
|
||||||
const headPart = this.currentModel!.getObjectByName('Head')
|
const headPart = this.currentModel!.getObjectByName('Head')
|
||||||
let lookAtTarget: [number, number, number]
|
let lookAtTarget: [number, number, number]
|
||||||
|
|
||||||
if (headPart) {
|
if (headPart) {
|
||||||
const headPosition = new THREE.Vector3()
|
const headPosition = new THREE.Vector3()
|
||||||
headPart.getWorldPosition(headPosition)
|
headPart.getWorldPosition(headPosition)
|
||||||
lookAtTarget = [headPosition.x, headPosition.y - 0.3, headPosition.z]
|
lookAtTarget = [headPosition.x, headPosition.y - 0.3, headPosition.z]
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Failed to find 'Head' object in model.")
|
throw new Error("Failed to find 'Head' object in model.")
|
||||||
}
|
}
|
||||||
|
|
||||||
const frontCameraPos: [number, number, number] = [-1.3, 1, 6.3]
|
const frontCameraPos: [number, number, number] = [-1.3, 1, 6.3]
|
||||||
const backCameraPos: [number, number, number] = [-1.3, 1, -2.5]
|
const backCameraPos: [number, number, number] = [-1.3, 1, -2.5]
|
||||||
|
|
||||||
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
|
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
|
||||||
const backwards = await this.renderView(backCameraPos, lookAtTarget)
|
const backwards = await this.renderView(backCameraPos, lookAtTarget)
|
||||||
|
|
||||||
return { forwards, backwards }
|
return { forwards, backwards }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renderView(
|
private async renderView(
|
||||||
cameraPosition: [number, number, number],
|
cameraPosition: [number, number, number],
|
||||||
lookAtPosition: [number, number, number],
|
lookAtPosition: [number, number, number],
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
if (!this.camera || !this.renderer || !this.scene) {
|
if (!this.camera || !this.renderer || !this.scene) {
|
||||||
throw new Error('Renderer not initialized')
|
throw new Error('Renderer not initialized')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.camera.position.set(...cameraPosition)
|
this.camera.position.set(...cameraPosition)
|
||||||
this.camera.lookAt(...lookAtPosition)
|
this.camera.lookAt(...lookAtPosition)
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera)
|
this.renderer.render(this.scene, this.camera)
|
||||||
|
|
||||||
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||||
const response = await fetch(dataUrl)
|
const response = await fetch(dataUrl)
|
||||||
return await response.blob()
|
return await response.blob()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||||
if (!this.scene) {
|
if (!this.scene) {
|
||||||
throw new Error('Renderer not initialized')
|
throw new Error('Renderer not initialized')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
||||||
|
|
||||||
if (capeUrl) {
|
if (capeUrl) {
|
||||||
const capeTexture = await loadTexture(capeUrl)
|
const capeTexture = await loadTexture(capeUrl)
|
||||||
applyCapeTexture(model, capeTexture)
|
applyCapeTexture(model, capeTexture)
|
||||||
} else {
|
} else {
|
||||||
const transparentTexture = createTransparentTexture()
|
const transparentTexture = createTransparentTexture()
|
||||||
applyCapeTexture(model, null, transparentTexture)
|
applyCapeTexture(model, null, transparentTexture)
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
group.add(model)
|
group.add(model)
|
||||||
group.position.set(0, 0.3, 1.95)
|
group.position.set(0, 0.3, 1.95)
|
||||||
group.scale.set(0.8, 0.8, 0.8)
|
group.scale.set(0.8, 0.8, 0.8)
|
||||||
|
|
||||||
this.scene.add(group)
|
this.scene.add(group)
|
||||||
this.currentModel = group
|
this.currentModel = group
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearScene(): void {
|
private clearScene(): void {
|
||||||
if (!this.scene) return
|
if (!this.scene) return
|
||||||
|
|
||||||
while (this.scene.children.length > 0) {
|
while (this.scene.children.length > 0) {
|
||||||
const child = this.scene.children[0]
|
const child = this.scene.children[0]
|
||||||
this.scene.remove(child)
|
this.scene.remove(child)
|
||||||
|
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
if (child.geometry) child.geometry.dispose()
|
if (child.geometry) child.geometry.dispose()
|
||||||
if (child.material) {
|
if (child.material) {
|
||||||
if (Array.isArray(child.material)) {
|
if (Array.isArray(child.material)) {
|
||||||
child.material.forEach((material) => material.dispose())
|
child.material.forEach((material) => material.dispose())
|
||||||
} else {
|
} else {
|
||||||
child.material.dispose()
|
child.material.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
directionalLight.castShadow = true
|
directionalLight.castShadow = true
|
||||||
directionalLight.position.set(2, 4, 3)
|
directionalLight.position.set(2, 4, 3)
|
||||||
this.scene.add(ambientLight)
|
this.scene.add(ambientLight)
|
||||||
this.scene.add(directionalLight)
|
this.scene.add(directionalLight)
|
||||||
|
|
||||||
this.currentModel = null
|
this.currentModel = null
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
if (this.renderer) {
|
if (this.renderer) {
|
||||||
this.renderer.dispose()
|
this.renderer.dispose()
|
||||||
}
|
}
|
||||||
disposeCaches()
|
disposeCaches()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModelUrlForVariant(variant: string): string {
|
function getModelUrlForVariant(variant: string): string {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'SLIM':
|
case 'SLIM':
|
||||||
return SlimPlayerModel
|
return SlimPlayerModel
|
||||||
case 'CLASSIC':
|
case 'CLASSIC':
|
||||||
case 'UNKNOWN':
|
case 'UNKNOWN':
|
||||||
default:
|
default:
|
||||||
return ClassicPlayerModel
|
return ClassicPlayerModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||||
@@ -194,253 +195,253 @@ const DEBUG_MODE = false
|
|||||||
|
|
||||||
let sharedRenderer: BatchSkinRenderer | null = null
|
let sharedRenderer: BatchSkinRenderer | null = null
|
||||||
function getSharedRenderer(): BatchSkinRenderer {
|
function getSharedRenderer(): BatchSkinRenderer {
|
||||||
if (!sharedRenderer) {
|
if (!sharedRenderer) {
|
||||||
sharedRenderer = new BatchSkinRenderer()
|
sharedRenderer = new BatchSkinRenderer()
|
||||||
}
|
}
|
||||||
return sharedRenderer
|
return sharedRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disposeSharedRenderer(): void {
|
export function disposeSharedRenderer(): void {
|
||||||
if (sharedRenderer) {
|
if (sharedRenderer) {
|
||||||
sharedRenderer.dispose()
|
sharedRenderer.dispose()
|
||||||
sharedRenderer = null
|
sharedRenderer = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||||
const validKeys = new Set<string>()
|
const validKeys = new Set<string>()
|
||||||
const validHeadKeys = new Set<string>()
|
const validHeadKeys = new Set<string>()
|
||||||
|
|
||||||
for (const skin of skins) {
|
for (const skin of skins) {
|
||||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||||
const headKey = `${skin.texture_key}-head`
|
const headKey = `${skin.texture_key}-head`
|
||||||
validKeys.add(key)
|
validKeys.add(key)
|
||||||
validHeadKeys.add(headKey)
|
validHeadKeys.add(headKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||||
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to cleanup unused skin previews:', error)
|
console.warn('Failed to cleanup unused skin previews:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64): Promise<Blob> {
|
export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64): Promise<Blob> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.crossOrigin = 'anonymous'
|
img.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
try {
|
try {
|
||||||
const sourceCanvas = document.createElement('canvas')
|
const sourceCanvas = document.createElement('canvas')
|
||||||
const sourceCtx = sourceCanvas.getContext('2d')
|
const sourceCtx = sourceCanvas.getContext('2d')
|
||||||
|
|
||||||
if (!sourceCtx) {
|
if (!sourceCtx) {
|
||||||
throw new Error('Could not get 2D context from source canvas')
|
throw new Error('Could not get 2D context from source canvas')
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCanvas.width = img.width
|
sourceCanvas.width = img.width
|
||||||
sourceCanvas.height = img.height
|
sourceCanvas.height = img.height
|
||||||
|
|
||||||
sourceCtx.drawImage(img, 0, 0)
|
sourceCtx.drawImage(img, 0, 0)
|
||||||
|
|
||||||
const outputCanvas = document.createElement('canvas')
|
const outputCanvas = document.createElement('canvas')
|
||||||
const outputCtx = outputCanvas.getContext('2d')
|
const outputCtx = outputCanvas.getContext('2d')
|
||||||
|
|
||||||
if (!outputCtx) {
|
if (!outputCtx) {
|
||||||
throw new Error('Could not get 2D context from output canvas')
|
throw new Error('Could not get 2D context from output canvas')
|
||||||
}
|
}
|
||||||
|
|
||||||
outputCanvas.width = size
|
outputCanvas.width = size
|
||||||
outputCanvas.height = size
|
outputCanvas.height = size
|
||||||
|
|
||||||
outputCtx.imageSmoothingEnabled = false
|
outputCtx.imageSmoothingEnabled = false
|
||||||
|
|
||||||
const headImageData = sourceCtx.getImageData(8, 8, 8, 8)
|
const headImageData = sourceCtx.getImageData(8, 8, 8, 8)
|
||||||
|
|
||||||
const headCanvas = document.createElement('canvas')
|
const headCanvas = document.createElement('canvas')
|
||||||
const headCtx = headCanvas.getContext('2d')
|
const headCtx = headCanvas.getContext('2d')
|
||||||
|
|
||||||
if (!headCtx) {
|
if (!headCtx) {
|
||||||
throw new Error('Could not get 2D context from head canvas')
|
throw new Error('Could not get 2D context from head canvas')
|
||||||
}
|
}
|
||||||
|
|
||||||
headCanvas.width = 8
|
headCanvas.width = 8
|
||||||
headCanvas.height = 8
|
headCanvas.height = 8
|
||||||
headCtx.putImageData(headImageData, 0, 0)
|
headCtx.putImageData(headImageData, 0, 0)
|
||||||
|
|
||||||
outputCtx.drawImage(headCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
outputCtx.drawImage(headCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||||
|
|
||||||
const hatImageData = sourceCtx.getImageData(40, 8, 8, 8)
|
const hatImageData = sourceCtx.getImageData(40, 8, 8, 8)
|
||||||
|
|
||||||
const hatCanvas = document.createElement('canvas')
|
const hatCanvas = document.createElement('canvas')
|
||||||
const hatCtx = hatCanvas.getContext('2d')
|
const hatCtx = hatCanvas.getContext('2d')
|
||||||
|
|
||||||
if (!hatCtx) {
|
if (!hatCtx) {
|
||||||
throw new Error('Could not get 2D context from hat canvas')
|
throw new Error('Could not get 2D context from hat canvas')
|
||||||
}
|
}
|
||||||
|
|
||||||
hatCanvas.width = 8
|
hatCanvas.width = 8
|
||||||
hatCanvas.height = 8
|
hatCanvas.height = 8
|
||||||
hatCtx.putImageData(hatImageData, 0, 0)
|
hatCtx.putImageData(hatImageData, 0, 0)
|
||||||
|
|
||||||
const hatPixels = hatImageData.data
|
const hatPixels = hatImageData.data
|
||||||
let hasHat = false
|
let hasHat = false
|
||||||
|
|
||||||
for (let i = 3; i < hatPixels.length; i += 4) {
|
for (let i = 3; i < hatPixels.length; i += 4) {
|
||||||
if (hatPixels[i] > 0) {
|
if (hatPixels[i] > 0) {
|
||||||
hasHat = true
|
hasHat = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasHat) {
|
if (hasHat) {
|
||||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputCanvas.toBlob(
|
outputCanvas.toBlob(
|
||||||
(blob) => {
|
(blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
resolve(blob)
|
resolve(blob)
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Failed to create blob from canvas'))
|
reject(new Error('Failed to create blob from canvas'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'image/webp',
|
'image/webp',
|
||||||
0.9,
|
0.9,
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
reject(new Error('Failed to load skin texture image'))
|
reject(new Error('Failed to load skin texture image'))
|
||||||
}
|
}
|
||||||
|
|
||||||
img.src = skinUrl
|
img.src = skinUrl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||||
const headKey = `${skin.texture_key}-head`
|
const headKey = `${skin.texture_key}-head`
|
||||||
|
|
||||||
if (headBlobUrlMap.has(headKey)) {
|
if (headBlobUrlMap.has(headKey)) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
const url = headBlobUrlMap.get(headKey)!
|
const url = headBlobUrlMap.get(headKey)!
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
headBlobUrlMap.delete(headKey)
|
headBlobUrlMap.delete(headKey)
|
||||||
} else {
|
} else {
|
||||||
return headBlobUrlMap.get(headKey)!
|
return headBlobUrlMap.get(headKey)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const skinUrl = await get_normalized_skin_texture(skin)
|
const skinUrl = await get_normalized_skin_texture(skin)
|
||||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||||
const headUrl = URL.createObjectURL(headBlob)
|
const headUrl = URL.createObjectURL(headBlob)
|
||||||
|
|
||||||
headBlobUrlMap.set(headKey, headUrl)
|
headBlobUrlMap.set(headKey, headUrl)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await headStorage.store(headKey, headBlob)
|
await headStorage.store(headKey, headBlob)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to store head render in persistent storage:', error)
|
console.warn('Failed to store head render in persistent storage:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return headUrl
|
return headUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||||
return await generateHeadRender(skin)
|
return await generateHeadRender(skin)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const skinKeys = skins.map(
|
const skinKeys = skins.map(
|
||||||
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||||
)
|
)
|
||||||
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
||||||
|
|
||||||
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
||||||
skinPreviewStorage.batchRetrieve(skinKeys),
|
skinPreviewStorage.batchRetrieve(skinKeys),
|
||||||
headStorage.batchRetrieve(headKeys),
|
headStorage.batchRetrieve(headKeys),
|
||||||
])
|
])
|
||||||
|
|
||||||
for (let i = 0; i < skins.length; i++) {
|
for (let i = 0; i < skins.length; i++) {
|
||||||
const skinKey = skinKeys[i]
|
const skinKey = skinKeys[i]
|
||||||
const headKey = headKeys[i]
|
const headKey = headKeys[i]
|
||||||
|
|
||||||
const rawCached = cachedSkinPreviews[skinKey]
|
const rawCached = cachedSkinPreviews[skinKey]
|
||||||
if (rawCached) {
|
if (rawCached) {
|
||||||
const cached: RenderResult = {
|
const cached: RenderResult = {
|
||||||
forwards: URL.createObjectURL(rawCached.forwards),
|
forwards: URL.createObjectURL(rawCached.forwards),
|
||||||
backwards: URL.createObjectURL(rawCached.backwards),
|
backwards: URL.createObjectURL(rawCached.backwards),
|
||||||
}
|
}
|
||||||
skinBlobUrlMap.set(skinKey, cached)
|
skinBlobUrlMap.set(skinKey, cached)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedHead = cachedHeadPreviews[headKey]
|
const cachedHead = cachedHeadPreviews[headKey]
|
||||||
if (cachedHead) {
|
if (cachedHead) {
|
||||||
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
|
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const skin of skins) {
|
for (const skin of skins) {
|
||||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||||
|
|
||||||
if (skinBlobUrlMap.has(key)) {
|
if (skinBlobUrlMap.has(key)) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
const result = skinBlobUrlMap.get(key)!
|
const result = skinBlobUrlMap.get(key)!
|
||||||
URL.revokeObjectURL(result.forwards)
|
URL.revokeObjectURL(result.forwards)
|
||||||
URL.revokeObjectURL(result.backwards)
|
URL.revokeObjectURL(result.backwards)
|
||||||
skinBlobUrlMap.delete(key)
|
skinBlobUrlMap.delete(key)
|
||||||
} else continue
|
} else continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderer = getSharedRenderer()
|
const renderer = getSharedRenderer()
|
||||||
|
|
||||||
let variant = skin.variant
|
let variant = skin.variant
|
||||||
if (variant === 'UNKNOWN') {
|
if (variant === 'UNKNOWN') {
|
||||||
try {
|
try {
|
||||||
variant = await determineModelType(skin.texture)
|
variant = await determineModelType(skin.texture)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to determine model type for skin ${key}:`, error)
|
console.error(`Failed to determine model type for skin ${key}:`, error)
|
||||||
variant = 'CLASSIC'
|
variant = 'CLASSIC'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelUrl = getModelUrlForVariant(variant)
|
const modelUrl = getModelUrlForVariant(variant)
|
||||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||||
const rawRenderResult = await renderer.renderSkin(
|
const rawRenderResult = await renderer.renderSkin(
|
||||||
await get_normalized_skin_texture(skin),
|
await get_normalized_skin_texture(skin),
|
||||||
modelUrl,
|
modelUrl,
|
||||||
cape?.texture,
|
cape?.texture,
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderResult: RenderResult = {
|
const renderResult: RenderResult = {
|
||||||
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||||
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||||
}
|
}
|
||||||
|
|
||||||
skinBlobUrlMap.set(key, renderResult)
|
skinBlobUrlMap.set(key, renderResult)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await skinPreviewStorage.store(key, rawRenderResult)
|
await skinPreviewStorage.store(key, rawRenderResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const headKey = `${skin.texture_key}-head`
|
const headKey = `${skin.texture_key}-head`
|
||||||
if (!headBlobUrlMap.has(headKey)) {
|
if (!headBlobUrlMap.has(headKey)) {
|
||||||
await generateHeadRender(skin)
|
await generateHeadRender(skin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
disposeSharedRenderer()
|
disposeSharedRenderer()
|
||||||
await cleanupUnusedPreviews(skins)
|
await cleanupUnusedPreviews(skins)
|
||||||
|
|
||||||
await skinPreviewStorage.debugCalculateStorage()
|
await skinPreviewStorage.debugCalculateStorage()
|
||||||
await headStorage.debugCalculateStorage()
|
await headStorage.debugCalculateStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
|
||||||
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
||||||
|
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||||
|
|
||||||
// Settings object
|
// Settings object
|
||||||
/*
|
/*
|
||||||
@@ -31,49 +32,49 @@ Memorysettings {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type AppSettings = {
|
export type AppSettings = {
|
||||||
max_concurrent_downloads: number
|
max_concurrent_downloads: number
|
||||||
max_concurrent_writes: number
|
max_concurrent_writes: number
|
||||||
|
|
||||||
theme: ColorTheme
|
theme: ColorTheme
|
||||||
default_page: 'home' | 'library'
|
default_page: 'home' | 'library'
|
||||||
collapsed_navigation: boolean
|
collapsed_navigation: boolean
|
||||||
hide_nametag_skins_page: boolean
|
hide_nametag_skins_page: boolean
|
||||||
advanced_rendering: boolean
|
advanced_rendering: boolean
|
||||||
native_decorations: boolean
|
native_decorations: boolean
|
||||||
toggle_sidebar: boolean
|
toggle_sidebar: boolean
|
||||||
|
|
||||||
telemetry: boolean
|
telemetry: boolean
|
||||||
discord_rpc: boolean
|
discord_rpc: boolean
|
||||||
personalized_ads: boolean
|
personalized_ads: boolean
|
||||||
|
|
||||||
onboarded: boolean
|
onboarded: boolean
|
||||||
|
|
||||||
extra_launch_args: string[]
|
extra_launch_args: string[]
|
||||||
custom_env_vars: [string, string][]
|
custom_env_vars: [string, string][]
|
||||||
memory: MemorySettings
|
memory: MemorySettings
|
||||||
force_fullscreen: boolean
|
force_fullscreen: boolean
|
||||||
game_resolution: WindowSize
|
game_resolution: WindowSize
|
||||||
hide_on_process_start: boolean
|
hide_on_process_start: boolean
|
||||||
hooks: Hooks
|
hooks: Hooks
|
||||||
|
|
||||||
custom_dir?: string | null
|
custom_dir?: string | null
|
||||||
prev_custom_dir?: string | null
|
prev_custom_dir?: string | null
|
||||||
migrated: boolean
|
migrated: boolean
|
||||||
|
|
||||||
developer_mode: boolean
|
developer_mode: boolean
|
||||||
feature_flags: Record<FeatureFlag, boolean>
|
feature_flags: Record<FeatureFlag, boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get full settings object
|
// Get full settings object
|
||||||
export async function get() {
|
export async function get() {
|
||||||
return (await invoke('plugin:settings|settings_get')) as AppSettings
|
return (await invoke('plugin:settings|settings_get')) as AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set full settings object
|
// Set full settings object
|
||||||
export async function set(settings: AppSettings) {
|
export async function set(settings: AppSettings) {
|
||||||
return await invoke('plugin:settings|settings_set', { settings })
|
return await invoke('plugin:settings|settings_set', { settings })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancel_directory_change(): Promise<void> {
|
export async function cancel_directory_change(): Promise<void> {
|
||||||
return await invoke('plugin:settings|cancel_directory_change')
|
return await invoke('plugin:settings|cancel_directory_change')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,163 +3,163 @@ import { arrayBufferToBase64 } from '@modrinth/utils'
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export interface Cape {
|
export interface Cape {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
texture: string
|
texture: string
|
||||||
is_default: boolean
|
is_default: boolean
|
||||||
is_equipped: boolean
|
is_equipped: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SkinModel = 'CLASSIC' | 'SLIM' | 'UNKNOWN'
|
export type SkinModel = 'CLASSIC' | 'SLIM' | 'UNKNOWN'
|
||||||
export type SkinSource = 'default' | 'custom_external' | 'custom'
|
export type SkinSource = 'default' | 'custom_external' | 'custom'
|
||||||
|
|
||||||
export interface Skin {
|
export interface Skin {
|
||||||
texture_key: string
|
texture_key: string
|
||||||
name?: string
|
name?: string
|
||||||
variant: SkinModel
|
variant: SkinModel
|
||||||
cape_id?: string
|
cape_id?: string
|
||||||
texture: string
|
texture: string
|
||||||
source: SkinSource
|
source: SkinSource
|
||||||
is_equipped: boolean
|
is_equipped: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_MODEL_SORTING = ['Steve', 'Alex'] as string[]
|
export const DEFAULT_MODEL_SORTING = ['Steve', 'Alex'] as string[]
|
||||||
|
|
||||||
export const DEFAULT_MODELS: Record<string, SkinModel> = {
|
export const DEFAULT_MODELS: Record<string, SkinModel> = {
|
||||||
Steve: 'CLASSIC',
|
Steve: 'CLASSIC',
|
||||||
Alex: 'SLIM',
|
Alex: 'SLIM',
|
||||||
Zuri: 'CLASSIC',
|
Zuri: 'CLASSIC',
|
||||||
Sunny: 'CLASSIC',
|
Sunny: 'CLASSIC',
|
||||||
Noor: 'SLIM',
|
Noor: 'SLIM',
|
||||||
Makena: 'SLIM',
|
Makena: 'SLIM',
|
||||||
Kai: 'CLASSIC',
|
Kai: 'CLASSIC',
|
||||||
Efe: 'SLIM',
|
Efe: 'SLIM',
|
||||||
Ari: 'CLASSIC',
|
Ari: 'CLASSIC',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterSavedSkins(list: Skin[]) {
|
export function filterSavedSkins(list: Skin[]) {
|
||||||
const customSkins = list.filter((s) => s.source !== 'default')
|
const customSkins = list.filter((s) => s.source !== 'default')
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
fixUnknownSkins(customSkins).catch(handleError)
|
fixUnknownSkins(customSkins).catch(handleError)
|
||||||
return customSkins
|
return customSkins
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function determineModelType(texture: string): Promise<'SLIM' | 'CLASSIC'> {
|
export async function determineModelType(texture: string): Promise<'SLIM' | 'CLASSIC'> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return reject(new Error('Failed to create canvas rendering context.'))
|
return reject(new Error('Failed to create canvas rendering context.'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = new Image()
|
const image = new Image()
|
||||||
image.crossOrigin = 'anonymous'
|
image.crossOrigin = 'anonymous'
|
||||||
image.src = texture
|
image.src = texture
|
||||||
|
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
canvas.width = image.width
|
canvas.width = image.width
|
||||||
canvas.height = image.height
|
canvas.height = image.height
|
||||||
|
|
||||||
context.drawImage(image, 0, 0)
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
const armX = 54
|
const armX = 54
|
||||||
const armY = 20
|
const armY = 20
|
||||||
const armWidth = 2
|
const armWidth = 2
|
||||||
const armHeight = 12
|
const armHeight = 12
|
||||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||||
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
||||||
if (imageData[alphaIndex] !== 0) {
|
if (imageData[alphaIndex] !== 0) {
|
||||||
resolve('CLASSIC')
|
resolve('CLASSIC')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.remove()
|
canvas.remove()
|
||||||
resolve('SLIM')
|
resolve('SLIM')
|
||||||
}
|
}
|
||||||
|
|
||||||
image.onerror = () => {
|
image.onerror = () => {
|
||||||
canvas.remove()
|
canvas.remove()
|
||||||
reject(new Error('Failed to load the image.'))
|
reject(new Error('Failed to load the image.'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fixUnknownSkins(list: Skin[]) {
|
export async function fixUnknownSkins(list: Skin[]) {
|
||||||
const unknownSkins = list.filter((s) => s.variant === 'UNKNOWN')
|
const unknownSkins = list.filter((s) => s.variant === 'UNKNOWN')
|
||||||
for (const unknownSkin of unknownSkins) {
|
for (const unknownSkin of unknownSkins) {
|
||||||
unknownSkin.variant = await determineModelType(unknownSkin.texture)
|
unknownSkin.variant = await determineModelType(unknownSkin.texture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterDefaultSkins(list: Skin[]) {
|
export function filterDefaultSkins(list: Skin[]) {
|
||||||
return list
|
return list
|
||||||
.filter(
|
.filter(
|
||||||
(s) =>
|
(s) =>
|
||||||
s.source === 'default' &&
|
s.source === 'default' &&
|
||||||
(!s.name || !(s.name in DEFAULT_MODELS) || s.variant === DEFAULT_MODELS[s.name]),
|
(!s.name || !(s.name in DEFAULT_MODELS) || s.variant === DEFAULT_MODELS[s.name]),
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aIndex = a.name ? DEFAULT_MODEL_SORTING.indexOf(a.name) : -1
|
const aIndex = a.name ? DEFAULT_MODEL_SORTING.indexOf(a.name) : -1
|
||||||
const bIndex = b.name ? DEFAULT_MODEL_SORTING.indexOf(b.name) : -1
|
const bIndex = b.name ? DEFAULT_MODEL_SORTING.indexOf(b.name) : -1
|
||||||
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex)
|
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_available_capes(): Promise<Cape[]> {
|
export async function get_available_capes(): Promise<Cape[]> {
|
||||||
return invoke('plugin:minecraft-skins|get_available_capes', {})
|
return invoke('plugin:minecraft-skins|get_available_capes', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_available_skins(): Promise<Skin[]> {
|
export async function get_available_skins(): Promise<Skin[]> {
|
||||||
return invoke('plugin:minecraft-skins|get_available_skins', {})
|
return invoke('plugin:minecraft-skins|get_available_skins', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function add_and_equip_custom_skin(
|
export async function add_and_equip_custom_skin(
|
||||||
textureBlob: Uint8Array,
|
textureBlob: Uint8Array,
|
||||||
variant: SkinModel,
|
variant: SkinModel,
|
||||||
capeOverride?: Cape,
|
capeOverride?: Cape,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
||||||
textureBlob,
|
textureBlob,
|
||||||
variant,
|
variant,
|
||||||
capeOverride,
|
capeOverride,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function set_default_cape(cape?: Cape): Promise<void> {
|
export async function set_default_cape(cape?: Cape): Promise<void> {
|
||||||
await invoke('plugin:minecraft-skins|set_default_cape', {
|
await invoke('plugin:minecraft-skins|set_default_cape', {
|
||||||
cape,
|
cape,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function equip_skin(skin: Skin): Promise<void> {
|
export async function equip_skin(skin: Skin): Promise<void> {
|
||||||
await invoke('plugin:minecraft-skins|equip_skin', {
|
await invoke('plugin:minecraft-skins|equip_skin', {
|
||||||
skin,
|
skin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove_custom_skin(skin: Skin): Promise<void> {
|
export async function remove_custom_skin(skin: Skin): Promise<void> {
|
||||||
await invoke('plugin:minecraft-skins|remove_custom_skin', {
|
await invoke('plugin:minecraft-skins|remove_custom_skin', {
|
||||||
skin,
|
skin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
|
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
|
||||||
const data = await normalize_skin_texture(skin.texture)
|
const data = await normalize_skin_texture(skin.texture)
|
||||||
const base64 = arrayBufferToBase64(data)
|
const base64 = arrayBufferToBase64(data)
|
||||||
return `data:image/png;base64,${base64}`
|
return `data:image/png;base64,${base64}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
|
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
|
||||||
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture })
|
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unequip_skin(): Promise<void> {
|
export async function unequip_skin(): Promise<void> {
|
||||||
await invoke('plugin:minecraft-skins|unequip_skin')
|
await invoke('plugin:minecraft-skins|unequip_skin')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
|
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
|
||||||
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
|
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
|
||||||
return new Uint8Array(data)
|
return new Uint8Array(data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
// Initialize the theseus API state
|
// Initialize the theseus API state
|
||||||
// This should be called during the initializion/opening of the launcher
|
// This should be called during the initializion/opening of the launcher
|
||||||
export async function initialize_state() {
|
export async function initialize_state() {
|
||||||
return await invoke('initialize_state')
|
return await invoke('initialize_state')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets active progress bars
|
// Gets active progress bars
|
||||||
export async function progress_bars_list() {
|
export async function progress_bars_list() {
|
||||||
return await invoke('plugin:utils|progress_bars_list')
|
return await invoke('plugin:utils|progress_bars_list')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get opening command
|
// Get opening command
|
||||||
@@ -21,5 +21,5 @@ export async function progress_bars_list() {
|
|||||||
// This should be called once and only when the app is done booting up and ready to receive a command
|
// This should be called once and only when the app is done booting up and ready to receive a command
|
||||||
// Returns a Command struct- see events.js
|
// Returns a Command struct- see events.js
|
||||||
export async function get_opening_command() {
|
export async function get_opening_command() {
|
||||||
return await invoke('plugin:utils|get_opening_command')
|
return await invoke('plugin:utils|get_opening_command')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,229 +1,229 @@
|
|||||||
interface StoredHead {
|
interface StoredHead {
|
||||||
blob: Blob
|
blob: Blob
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HeadStorage {
|
export class HeadStorage {
|
||||||
private dbName = 'head-storage'
|
private dbName = 'head-storage'
|
||||||
private version = 1
|
private version = 1
|
||||||
private db: IDBDatabase | null = null
|
private db: IDBDatabase | null = null
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(this.dbName, this.version)
|
const request = indexedDB.open(this.dbName, this.version)
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
this.db = request.result
|
this.db = request.result
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
const db = request.result
|
const db = request.result
|
||||||
if (!db.objectStoreNames.contains('heads')) {
|
if (!db.objectStoreNames.contains('heads')) {
|
||||||
db.createObjectStore('heads')
|
db.createObjectStore('heads')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async store(key: string, blob: Blob): Promise<void> {
|
async store(key: string, blob: Blob): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||||
const store = transaction.objectStore('heads')
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
const storedHead: StoredHead = {
|
const storedHead: StoredHead = {
|
||||||
blob,
|
blob,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.put(storedHead, key)
|
const request = store.put(storedHead, key)
|
||||||
|
|
||||||
request.onsuccess = () => resolve()
|
request.onsuccess = () => resolve()
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieve(key: string): Promise<string | null> {
|
async retrieve(key: string): Promise<string | null> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||||
const store = transaction.objectStore('heads')
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.get(key)
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const result = request.result as StoredHead | undefined
|
const result = request.result as StoredHead | undefined
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = URL.createObjectURL(result.blob)
|
const url = URL.createObjectURL(result.blob)
|
||||||
resolve(url)
|
resolve(url)
|
||||||
}
|
}
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||||
const store = transaction.objectStore('heads')
|
const store = transaction.objectStore('heads')
|
||||||
const results: Record<string, Blob | null> = {}
|
const results: Record<string, Blob | null> = {}
|
||||||
|
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
let completedRequests = 0
|
let completedRequests = 0
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
resolve(results)
|
resolve(results)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const request = store.get(key)
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const result = request.result as StoredHead | undefined
|
const result = request.result as StoredHead | undefined
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
results[key] = result.blob
|
results[key] = result.blob
|
||||||
} else {
|
} else {
|
||||||
results[key] = null
|
results[key] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
completedRequests++
|
completedRequests++
|
||||||
if (completedRequests === keys.length) {
|
if (completedRequests === keys.length) {
|
||||||
resolve(results)
|
resolve(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
results[key] = null
|
results[key] = null
|
||||||
completedRequests++
|
completedRequests++
|
||||||
if (completedRequests === keys.length) {
|
if (completedRequests === keys.length) {
|
||||||
resolve(results)
|
resolve(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||||
const store = transaction.objectStore('heads')
|
const store = transaction.objectStore('heads')
|
||||||
let deletedCount = 0
|
let deletedCount = 0
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.openCursor()
|
const request = store.openCursor()
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const key = cursor.primaryKey as string
|
const key = cursor.primaryKey as string
|
||||||
|
|
||||||
if (!validKeys.has(key)) {
|
if (!validKeys.has(key)) {
|
||||||
const deleteRequest = cursor.delete()
|
const deleteRequest = cursor.delete()
|
||||||
deleteRequest.onsuccess = () => {
|
deleteRequest.onsuccess = () => {
|
||||||
deletedCount++
|
deletedCount++
|
||||||
}
|
}
|
||||||
deleteRequest.onerror = () => {
|
deleteRequest.onerror = () => {
|
||||||
console.warn('Failed to delete invalid head entry:', key)
|
console.warn('Failed to delete invalid head entry:', key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.continue()
|
cursor.continue()
|
||||||
} else {
|
} else {
|
||||||
resolve(deletedCount)
|
resolve(deletedCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async debugCalculateStorage(): Promise<void> {
|
async debugCalculateStorage(): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||||
const store = transaction.objectStore('heads')
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
let totalSize = 0
|
let totalSize = 0
|
||||||
let count = 0
|
let count = 0
|
||||||
const entries: Array<{ key: string; size: number }> = []
|
const entries: Array<{ key: string; size: number }> = []
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.openCursor()
|
const request = store.openCursor()
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const key = cursor.primaryKey as string
|
const key = cursor.primaryKey as string
|
||||||
const value = cursor.value as StoredHead
|
const value = cursor.value as StoredHead
|
||||||
|
|
||||||
const entrySize = value.blob.size
|
const entrySize = value.blob.size
|
||||||
totalSize += entrySize
|
totalSize += entrySize
|
||||||
count++
|
count++
|
||||||
|
|
||||||
entries.push({
|
entries.push({
|
||||||
key,
|
key,
|
||||||
size: entrySize,
|
size: entrySize,
|
||||||
})
|
})
|
||||||
|
|
||||||
cursor.continue()
|
cursor.continue()
|
||||||
} else {
|
} else {
|
||||||
console.group('🗄️ Head Storage Debug Info')
|
console.group('🗄️ Head Storage Debug Info')
|
||||||
console.log(`Total entries: ${count}`)
|
console.log(`Total entries: ${count}`)
|
||||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||||
console.log(
|
console.log(
|
||||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||||
console.log(
|
console.log(
|
||||||
'Largest entry:',
|
'Largest entry:',
|
||||||
sortedEntries[0].key,
|
sortedEntries[0].key,
|
||||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||||
)
|
)
|
||||||
console.log(
|
console.log(
|
||||||
'Smallest entry:',
|
'Smallest entry:',
|
||||||
sortedEntries[sortedEntries.length - 1].key,
|
sortedEntries[sortedEntries.length - 1].key,
|
||||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll(): Promise<void> {
|
async clearAll(): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||||
const store = transaction.objectStore('heads')
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.clear()
|
const request = store.clear()
|
||||||
|
|
||||||
request.onsuccess = () => resolve()
|
request.onsuccess = () => resolve()
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const headStorage = new HeadStorage()
|
export const headStorage = new HeadStorage()
|
||||||
|
|||||||
@@ -1,218 +1,218 @@
|
|||||||
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
|
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
|
||||||
|
|
||||||
interface StoredPreview {
|
interface StoredPreview {
|
||||||
forwards: Blob
|
forwards: Blob
|
||||||
backwards: Blob
|
backwards: Blob
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SkinPreviewStorage {
|
export class SkinPreviewStorage {
|
||||||
private dbName = 'skin-previews'
|
private dbName = 'skin-previews'
|
||||||
private version = 1
|
private version = 1
|
||||||
private db: IDBDatabase | null = null
|
private db: IDBDatabase | null = null
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(this.dbName, this.version)
|
const request = indexedDB.open(this.dbName, this.version)
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
this.db = request.result
|
this.db = request.result
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onupgradeneeded = () => {
|
request.onupgradeneeded = () => {
|
||||||
const db = request.result
|
const db = request.result
|
||||||
if (!db.objectStoreNames.contains('previews')) {
|
if (!db.objectStoreNames.contains('previews')) {
|
||||||
db.createObjectStore('previews')
|
db.createObjectStore('previews')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async store(key: string, result: RawRenderResult): Promise<void> {
|
async store(key: string, result: RawRenderResult): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
|
|
||||||
const storedPreview: StoredPreview = {
|
const storedPreview: StoredPreview = {
|
||||||
forwards: result.forwards,
|
forwards: result.forwards,
|
||||||
backwards: result.backwards,
|
backwards: result.backwards,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.put(storedPreview, key)
|
const request = store.put(storedPreview, key)
|
||||||
|
|
||||||
request.onsuccess = () => resolve()
|
request.onsuccess = () => resolve()
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieve(key: string): Promise<RawRenderResult | null> {
|
async retrieve(key: string): Promise<RawRenderResult | null> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.get(key)
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const result = request.result as StoredPreview | undefined
|
const result = request.result as StoredPreview | undefined
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({ forwards: result.forwards, backwards: result.backwards })
|
resolve({ forwards: result.forwards, backwards: result.backwards })
|
||||||
}
|
}
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
|
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
const results: Record<string, RawRenderResult | null> = {}
|
const results: Record<string, RawRenderResult | null> = {}
|
||||||
|
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
let completedRequests = 0
|
let completedRequests = 0
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
resolve(results)
|
resolve(results)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const request = store.get(key)
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const result = request.result as StoredPreview | undefined
|
const result = request.result as StoredPreview | undefined
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
results[key] = { forwards: result.forwards, backwards: result.backwards }
|
results[key] = { forwards: result.forwards, backwards: result.backwards }
|
||||||
} else {
|
} else {
|
||||||
results[key] = null
|
results[key] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
completedRequests++
|
completedRequests++
|
||||||
if (completedRequests === keys.length) {
|
if (completedRequests === keys.length) {
|
||||||
resolve(results)
|
resolve(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
results[key] = null
|
results[key] = null
|
||||||
completedRequests++
|
completedRequests++
|
||||||
if (completedRequests === keys.length) {
|
if (completedRequests === keys.length) {
|
||||||
resolve(results)
|
resolve(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
let deletedCount = 0
|
let deletedCount = 0
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.openCursor()
|
const request = store.openCursor()
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const key = cursor.primaryKey as string
|
const key = cursor.primaryKey as string
|
||||||
|
|
||||||
if (!validKeys.has(key)) {
|
if (!validKeys.has(key)) {
|
||||||
const deleteRequest = cursor.delete()
|
const deleteRequest = cursor.delete()
|
||||||
deleteRequest.onsuccess = () => {
|
deleteRequest.onsuccess = () => {
|
||||||
deletedCount++
|
deletedCount++
|
||||||
}
|
}
|
||||||
deleteRequest.onerror = () => {
|
deleteRequest.onerror = () => {
|
||||||
console.warn('Failed to delete invalid entry:', key)
|
console.warn('Failed to delete invalid entry:', key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.continue()
|
cursor.continue()
|
||||||
} else {
|
} else {
|
||||||
resolve(deletedCount)
|
resolve(deletedCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async debugCalculateStorage(): Promise<void> {
|
async debugCalculateStorage(): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
|
|
||||||
let totalSize = 0
|
let totalSize = 0
|
||||||
let count = 0
|
let count = 0
|
||||||
const entries: Array<{ key: string; size: number }> = []
|
const entries: Array<{ key: string; size: number }> = []
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.openCursor()
|
const request = store.openCursor()
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const key = cursor.primaryKey as string
|
const key = cursor.primaryKey as string
|
||||||
const value = cursor.value as StoredPreview
|
const value = cursor.value as StoredPreview
|
||||||
|
|
||||||
const entrySize = value.forwards.size + value.backwards.size
|
const entrySize = value.forwards.size + value.backwards.size
|
||||||
totalSize += entrySize
|
totalSize += entrySize
|
||||||
count++
|
count++
|
||||||
|
|
||||||
entries.push({
|
entries.push({
|
||||||
key,
|
key,
|
||||||
size: entrySize,
|
size: entrySize,
|
||||||
})
|
})
|
||||||
|
|
||||||
cursor.continue()
|
cursor.continue()
|
||||||
} else {
|
} else {
|
||||||
console.group('🗄️ Skin Preview Storage Debug Info')
|
console.group('🗄️ Skin Preview Storage Debug Info')
|
||||||
console.log(`Total entries: ${count}`)
|
console.log(`Total entries: ${count}`)
|
||||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||||
console.log(
|
console.log(
|
||||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||||
console.log(
|
console.log(
|
||||||
'Largest entry:',
|
'Largest entry:',
|
||||||
sortedEntries[0].key,
|
sortedEntries[0].key,
|
||||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||||
)
|
)
|
||||||
console.log(
|
console.log(
|
||||||
'Smallest entry:',
|
'Smallest entry:',
|
||||||
sortedEntries[sortedEntries.length - 1].key,
|
sortedEntries[sortedEntries.length - 1].key,
|
||||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const skinPreviewStorage = new SkinPreviewStorage()
|
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
|
|
||||||
// Gets cached category tags
|
// Gets cached category tags
|
||||||
export async function get_categories() {
|
export async function get_categories() {
|
||||||
return await invoke('plugin:tags|tags_get_categories')
|
return await invoke('plugin:tags|tags_get_categories')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets cached loaders tags
|
// Gets cached loaders tags
|
||||||
export async function get_loaders() {
|
export async function get_loaders() {
|
||||||
return await invoke('plugin:tags|tags_get_loaders')
|
return await invoke('plugin:tags|tags_get_loaders')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets cached game_versions tags
|
// Gets cached game_versions tags
|
||||||
export async function get_game_versions() {
|
export async function get_game_versions() {
|
||||||
return await invoke('plugin:tags|tags_get_game_versions')
|
return await invoke('plugin:tags|tags_get_game_versions')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets cached donation_platforms tags
|
// Gets cached donation_platforms tags
|
||||||
export async function get_donation_platforms() {
|
export async function get_donation_platforms() {
|
||||||
return await invoke('plugin:tags|tags_get_donation_platforms')
|
return await invoke('plugin:tags|tags_get_donation_platforms')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets cached licenses tags
|
// Gets cached licenses tags
|
||||||
export async function get_report_types() {
|
export async function get_report_types() {
|
||||||
return await invoke('plugin:tags|tags_get_report_types')
|
return await invoke('plugin:tags|tags_get_report_types')
|
||||||
}
|
}
|
||||||
|
|||||||
166
apps/app-frontend/src/helpers/types.d.ts
vendored
166
apps/app-frontend/src/helpers/types.d.ts
vendored
@@ -1,142 +1,142 @@
|
|||||||
import type { ModrinthId } from '@modrinth/utils'
|
import type { ModrinthId } from '@modrinth/utils'
|
||||||
|
|
||||||
type GameInstance = {
|
type GameInstance = {
|
||||||
path: string
|
path: string
|
||||||
install_stage: InstallStage
|
install_stage: InstallStage
|
||||||
|
|
||||||
name: string
|
name: string
|
||||||
icon_path?: string
|
icon_path?: string
|
||||||
|
|
||||||
game_version: string
|
game_version: string
|
||||||
loader: InstanceLoader
|
loader: InstanceLoader
|
||||||
loader_version?: string
|
loader_version?: string
|
||||||
|
|
||||||
groups: string[]
|
groups: string[]
|
||||||
|
|
||||||
linked_data?: LinkedData
|
linked_data?: LinkedData
|
||||||
|
|
||||||
created: Date
|
created: Date
|
||||||
modified: Date
|
modified: Date
|
||||||
last_played?: Date
|
last_played?: Date
|
||||||
|
|
||||||
submitted_time_played: number
|
submitted_time_played: number
|
||||||
recent_time_played: number
|
recent_time_played: number
|
||||||
|
|
||||||
java_path?: string
|
java_path?: string
|
||||||
extra_launch_args?: string[]
|
extra_launch_args?: string[]
|
||||||
custom_env_vars?: [string, string][]
|
custom_env_vars?: [string, string][]
|
||||||
|
|
||||||
memory?: MemorySettings
|
memory?: MemorySettings
|
||||||
force_fullscreen?: boolean
|
force_fullscreen?: boolean
|
||||||
game_resolution?: [number, number]
|
game_resolution?: [number, number]
|
||||||
hooks: Hooks
|
hooks: Hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstallStage =
|
type InstallStage =
|
||||||
| 'installed'
|
| 'installed'
|
||||||
| 'minecraft_installing'
|
| 'minecraft_installing'
|
||||||
| 'pack_installed'
|
| 'pack_installed'
|
||||||
| 'pack_installing'
|
| 'pack_installing'
|
||||||
| 'not_installed'
|
| 'not_installed'
|
||||||
|
|
||||||
type LinkedData = {
|
type LinkedData = {
|
||||||
project_id: ModrinthId
|
project_id: ModrinthId
|
||||||
version_id: ModrinthId
|
version_id: ModrinthId
|
||||||
|
|
||||||
locked: boolean
|
locked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||||
|
|
||||||
type ContentFile = {
|
type ContentFile = {
|
||||||
hash: string
|
hash: string
|
||||||
file_name: string
|
file_name: string
|
||||||
size: number
|
size: number
|
||||||
metadata?: FileMetadata
|
metadata?: FileMetadata
|
||||||
update_version_id?: string
|
update_version_id?: string
|
||||||
project_type: ContentFileProjectType
|
project_type: ContentFileProjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileMetadata = {
|
type FileMetadata = {
|
||||||
project_id: string
|
project_id: string
|
||||||
version_id: string
|
version_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
|
type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
|
||||||
|
|
||||||
type CacheBehaviour =
|
type CacheBehaviour =
|
||||||
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
|
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
|
||||||
| 'stale_while_revalidate_skip_offline'
|
| 'stale_while_revalidate_skip_offline'
|
||||||
// Serve expired data, revalidate in background
|
// Serve expired data, revalidate in background
|
||||||
| 'stale_while_revalidate'
|
| 'stale_while_revalidate'
|
||||||
// Must revalidate if data is expired
|
// Must revalidate if data is expired
|
||||||
| 'must_revalidate'
|
| 'must_revalidate'
|
||||||
// Ignore cache- always fetch updated data from origin
|
// Ignore cache- always fetch updated data from origin
|
||||||
| 'bypass'
|
| 'bypass'
|
||||||
|
|
||||||
type MemorySettings = {
|
type MemorySettings = {
|
||||||
maximum: number
|
maximum: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type WindowSize = {
|
type WindowSize = {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hooks = {
|
type Hooks = {
|
||||||
pre_launch?: string
|
pre_launch?: string
|
||||||
wrapper?: string
|
wrapper?: string
|
||||||
post_exit?: string
|
post_exit?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manifest = {
|
type Manifest = {
|
||||||
gameVersions: ManifestGameVersion[]
|
gameVersions: ManifestGameVersion[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManifestGameVersion = {
|
type ManifestGameVersion = {
|
||||||
id: string
|
id: string
|
||||||
stable: boolean
|
stable: boolean
|
||||||
loaders: ManifestLoaderVersion[]
|
loaders: ManifestLoaderVersion[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManifestLoaderVersion = {
|
type ManifestLoaderVersion = {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
stable: boolean
|
stable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppSettings = {
|
type AppSettings = {
|
||||||
max_concurrent_downloads: number
|
max_concurrent_downloads: number
|
||||||
max_concurrent_writes: number
|
max_concurrent_writes: number
|
||||||
|
|
||||||
theme: 'dark' | 'light' | 'oled'
|
theme: 'dark' | 'light' | 'oled'
|
||||||
default_page: 'Home' | 'Library'
|
default_page: 'Home' | 'Library'
|
||||||
collapsed_navigation: boolean
|
collapsed_navigation: boolean
|
||||||
advanced_rendering: boolean
|
advanced_rendering: boolean
|
||||||
native_decorations: boolean
|
native_decorations: boolean
|
||||||
worlds_in_home: boolean
|
worlds_in_home: boolean
|
||||||
|
|
||||||
telemetry: boolean
|
telemetry: boolean
|
||||||
discord_rpc: boolean
|
discord_rpc: boolean
|
||||||
developer_mode: boolean
|
developer_mode: boolean
|
||||||
personalized_ads: boolean
|
personalized_ads: boolean
|
||||||
|
|
||||||
onboarded: boolean
|
onboarded: boolean
|
||||||
|
|
||||||
extra_launch_args: string[]
|
extra_launch_args: string[]
|
||||||
custom_env_vars: [string, string][]
|
custom_env_vars: [string, string][]
|
||||||
memory: MemorySettings
|
memory: MemorySettings
|
||||||
force_fullscreen: boolean
|
force_fullscreen: boolean
|
||||||
game_resolution: [number, number]
|
game_resolution: [number, number]
|
||||||
hide_on_process_start: boolean
|
hide_on_process_start: boolean
|
||||||
hooks: Hooks
|
hooks: Hooks
|
||||||
|
|
||||||
custom_dir?: string
|
custom_dir?: string
|
||||||
prev_custom_dir?: string
|
prev_custom_dir?: string
|
||||||
migrated: boolean
|
migrated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceSettingsTabProps = {
|
export type InstanceSettingsTabProps = {
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
offline?: boolean
|
offline?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,63 @@
|
|||||||
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
|
||||||
|
|
||||||
export async function isDev() {
|
export async function isDev() {
|
||||||
return await invoke('is_dev')
|
return await invoke('is_dev')
|
||||||
}
|
}
|
||||||
|
|
||||||
// One of 'Windows', 'Linux', 'MacOS'
|
// One of 'Windows', 'Linux', 'MacOS'
|
||||||
export async function getOS() {
|
export async function getOS() {
|
||||||
return await invoke('plugin:utils|get_os')
|
return await invoke('plugin:utils|get_os')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPath(path) {
|
export async function openPath(path) {
|
||||||
return await invoke('plugin:utils|open_path', { path })
|
return await invoke('plugin:utils|open_path', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function highlightInFolder(path) {
|
export async function highlightInFolder(path) {
|
||||||
return await invoke('plugin:utils|highlight_in_folder', { path })
|
return await invoke('plugin:utils|highlight_in_folder', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showLauncherLogsFolder() {
|
export async function showLauncherLogsFolder() {
|
||||||
return await invoke('plugin:utils|show_launcher_logs_folder', {})
|
return await invoke('plugin:utils|show_launcher_logs_folder', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens a profile's folder in the OS file explorer
|
// Opens a profile's folder in the OS file explorer
|
||||||
export async function showProfileInFolder(path) {
|
export async function showProfileInFolder(path) {
|
||||||
const fullPath = await get_full_path(path)
|
const fullPath = await get_full_path(path)
|
||||||
return await openPath(fullPath)
|
return await openPath(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function highlightModInProfile(profilePath, projectPath) {
|
export async function highlightModInProfile(profilePath, projectPath) {
|
||||||
const fullPath = await get_mod_full_path(profilePath, projectPath)
|
const fullPath = await get_mod_full_path(profilePath, projectPath)
|
||||||
return await highlightInFolder(fullPath)
|
return await highlightInFolder(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restartApp() {
|
export async function restartApp() {
|
||||||
return await invoke('restart_app')
|
return await invoke('restart_app')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated This method is no longer needed, and just returns its parameter
|
* @deprecated This method is no longer needed, and just returns its parameter
|
||||||
*/
|
*/
|
||||||
export function sanitizePotentialFileUrl(url) {
|
export function sanitizePotentialFileUrl(url) {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
export const releaseColor = (releaseType) => {
|
export const releaseColor = (releaseType) => {
|
||||||
switch (releaseType) {
|
switch (releaseType) {
|
||||||
case 'release':
|
case 'release':
|
||||||
return 'green'
|
return 'green'
|
||||||
case 'beta':
|
case 'beta':
|
||||||
return 'orange'
|
return 'orange'
|
||||||
case 'alpha':
|
case 'alpha':
|
||||||
return 'red'
|
return 'red'
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyToClipboard(text) {
|
export async function copyToClipboard(text) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,350 +1,351 @@
|
|||||||
|
import { autoToHTML } from '@geometrically/minecraft-motd-parser'
|
||||||
|
import type { GameVersion } from '@modrinth/ui'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { get_full_path } from '@/helpers/profile'
|
import { get_full_path } from '@/helpers/profile'
|
||||||
import { openPath } from '@/helpers/utils'
|
import { openPath } from '@/helpers/utils'
|
||||||
import { autoToHTML } from '@geometrically/minecraft-motd-parser'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { GameVersion } from '@modrinth/ui'
|
|
||||||
|
|
||||||
type BaseWorld = {
|
type BaseWorld = {
|
||||||
name: string
|
name: string
|
||||||
last_played?: string
|
last_played?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
display_status: DisplayStatus
|
display_status: DisplayStatus
|
||||||
type: WorldType
|
type: WorldType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorldType = 'singleplayer' | 'server'
|
export type WorldType = 'singleplayer' | 'server'
|
||||||
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
|
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
|
||||||
|
|
||||||
export type SingleplayerWorld = BaseWorld & {
|
export type SingleplayerWorld = BaseWorld & {
|
||||||
type: 'singleplayer'
|
type: 'singleplayer'
|
||||||
path: string
|
path: string
|
||||||
game_mode: SingleplayerGameMode
|
game_mode: SingleplayerGameMode
|
||||||
hardcore: boolean
|
hardcore: boolean
|
||||||
locked: boolean
|
locked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerWorld = BaseWorld & {
|
export type ServerWorld = BaseWorld & {
|
||||||
type: 'server'
|
type: 'server'
|
||||||
index: number
|
index: number
|
||||||
address: string
|
address: string
|
||||||
pack_status: ServerPackStatus
|
pack_status: ServerPackStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export type World = SingleplayerWorld | ServerWorld
|
export type World = SingleplayerWorld | ServerWorld
|
||||||
|
|
||||||
export type WorldWithProfile = {
|
export type WorldWithProfile = {
|
||||||
profile: string
|
profile: string
|
||||||
} & World
|
} & World
|
||||||
|
|
||||||
export type SingleplayerGameMode = 'survival' | 'creative' | 'adventure' | 'spectator'
|
export type SingleplayerGameMode = 'survival' | 'creative' | 'adventure' | 'spectator'
|
||||||
export type ServerPackStatus = 'enabled' | 'disabled' | 'prompt'
|
export type ServerPackStatus = 'enabled' | 'disabled' | 'prompt'
|
||||||
|
|
||||||
export type ServerStatus = {
|
export type ServerStatus = {
|
||||||
// https://minecraft.wiki/w/Text_component_format
|
// https://minecraft.wiki/w/Text_component_format
|
||||||
description?: string | Chat
|
description?: string | Chat
|
||||||
players?: {
|
players?: {
|
||||||
max: number
|
max: number
|
||||||
online: number
|
online: number
|
||||||
sample: { name: string; id: string }[]
|
sample: { name: string; id: string }[]
|
||||||
}
|
}
|
||||||
version?: {
|
version?: {
|
||||||
name: string
|
name: string
|
||||||
protocol: number
|
protocol: number
|
||||||
legacy: boolean
|
legacy: boolean
|
||||||
}
|
}
|
||||||
favicon?: string
|
favicon?: string
|
||||||
enforces_secure_chat: boolean
|
enforces_secure_chat: boolean
|
||||||
ping?: number
|
ping?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
text: string
|
text: string
|
||||||
bold: boolean
|
bold: boolean
|
||||||
italic: boolean
|
italic: boolean
|
||||||
underlined: boolean
|
underlined: boolean
|
||||||
strikethrough: boolean
|
strikethrough: boolean
|
||||||
obfuscated: boolean
|
obfuscated: boolean
|
||||||
color?: string
|
color?: string
|
||||||
extra: Chat[]
|
extra: Chat[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerData = {
|
export type ServerData = {
|
||||||
refreshing: boolean
|
refreshing: boolean
|
||||||
lastSuccessfulRefresh?: number
|
lastSuccessfulRefresh?: number
|
||||||
status?: ServerStatus
|
status?: ServerStatus
|
||||||
rawMotd?: string | Chat
|
rawMotd?: string | Chat
|
||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProtocolVersion = {
|
export type ProtocolVersion = {
|
||||||
version: number
|
version: number
|
||||||
legacy: boolean
|
legacy: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_recent_worlds(
|
export async function get_recent_worlds(
|
||||||
limit: number,
|
limit: number,
|
||||||
displayStatuses?: DisplayStatus[],
|
displayStatuses?: DisplayStatus[],
|
||||||
): Promise<WorldWithProfile[]> {
|
): Promise<WorldWithProfile[]> {
|
||||||
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
|
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_profile_worlds(path: string): Promise<World[]> {
|
export async function get_profile_worlds(path: string): Promise<World[]> {
|
||||||
return await invoke('plugin:worlds|get_profile_worlds', { path })
|
return await invoke('plugin:worlds|get_profile_worlds', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_singleplayer_world(
|
export async function get_singleplayer_world(
|
||||||
instance: string,
|
instance: string,
|
||||||
world: string,
|
world: string,
|
||||||
): Promise<SingleplayerWorld> {
|
): Promise<SingleplayerWorld> {
|
||||||
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
|
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function set_world_display_status(
|
export async function set_world_display_status(
|
||||||
instance: string,
|
instance: string,
|
||||||
worldType: WorldType,
|
worldType: WorldType,
|
||||||
worldId: string,
|
worldId: string,
|
||||||
displayStatus: DisplayStatus,
|
displayStatus: DisplayStatus,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await invoke('plugin:worlds|set_world_display_status', {
|
return await invoke('plugin:worlds|set_world_display_status', {
|
||||||
instance,
|
instance,
|
||||||
worldType,
|
worldType,
|
||||||
worldId,
|
worldId,
|
||||||
displayStatus,
|
displayStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rename_world(
|
export async function rename_world(
|
||||||
instance: string,
|
instance: string,
|
||||||
world: string,
|
world: string,
|
||||||
newName: string,
|
newName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await invoke('plugin:worlds|rename_world', { instance, world, newName })
|
return await invoke('plugin:worlds|rename_world', { instance, world, newName })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reset_world_icon(instance: string, world: string): Promise<void> {
|
export async function reset_world_icon(instance: string, world: string): Promise<void> {
|
||||||
return await invoke('plugin:worlds|reset_world_icon', { instance, world })
|
return await invoke('plugin:worlds|reset_world_icon', { instance, world })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function backup_world(instance: string, world: string): Promise<number> {
|
export async function backup_world(instance: string, world: string): Promise<number> {
|
||||||
return await invoke('plugin:worlds|backup_world', { instance, world })
|
return await invoke('plugin:worlds|backup_world', { instance, world })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delete_world(instance: string, world: string): Promise<void> {
|
export async function delete_world(instance: string, world: string): Promise<void> {
|
||||||
return await invoke('plugin:worlds|delete_world', { instance, world })
|
return await invoke('plugin:worlds|delete_world', { instance, world })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function add_server_to_profile(
|
export async function add_server_to_profile(
|
||||||
path: string,
|
path: string,
|
||||||
name: string,
|
name: string,
|
||||||
address: string,
|
address: string,
|
||||||
packStatus: ServerPackStatus,
|
packStatus: ServerPackStatus,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
return await invoke('plugin:worlds|add_server_to_profile', { path, name, address, packStatus })
|
return await invoke('plugin:worlds|add_server_to_profile', { path, name, address, packStatus })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function edit_server_in_profile(
|
export async function edit_server_in_profile(
|
||||||
path: string,
|
path: string,
|
||||||
index: number,
|
index: number,
|
||||||
name: string,
|
name: string,
|
||||||
address: string,
|
address: string,
|
||||||
packStatus: ServerPackStatus,
|
packStatus: ServerPackStatus,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await invoke('plugin:worlds|edit_server_in_profile', {
|
return await invoke('plugin:worlds|edit_server_in_profile', {
|
||||||
path,
|
path,
|
||||||
index,
|
index,
|
||||||
name,
|
name,
|
||||||
address,
|
address,
|
||||||
packStatus,
|
packStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove_server_from_profile(path: string, index: number): Promise<void> {
|
export async function remove_server_from_profile(path: string, index: number): Promise<void> {
|
||||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
||||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_server_status(
|
export async function get_server_status(
|
||||||
address: string,
|
address: string,
|
||||||
protocolVersion: ProtocolVersion | null = null,
|
protocolVersion: ProtocolVersion | null = null,
|
||||||
): Promise<ServerStatus> {
|
): Promise<ServerStatus> {
|
||||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function start_join_singleplayer_world(path: string, world: string): Promise<unknown> {
|
export async function start_join_singleplayer_world(path: string, world: string): Promise<unknown> {
|
||||||
return await invoke('plugin:worlds|start_join_singleplayer_world', { path, world })
|
return await invoke('plugin:worlds|start_join_singleplayer_world', { path, world })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function start_join_server(path: string, address: string): Promise<unknown> {
|
export async function start_join_server(path: string, address: string): Promise<unknown> {
|
||||||
return await invoke('plugin:worlds|start_join_server', { path, address })
|
return await invoke('plugin:worlds|start_join_server', { path, address })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showWorldInFolder(instancePath: string, worldPath: string) {
|
export async function showWorldInFolder(instancePath: string, worldPath: string) {
|
||||||
const fullPath = await get_full_path(instancePath)
|
const fullPath = await get_full_path(instancePath)
|
||||||
return await openPath(fullPath + '/saves/' + worldPath)
|
return await openPath(fullPath + '/saves/' + worldPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorldIdentifier(world: World) {
|
export function getWorldIdentifier(world: World) {
|
||||||
return world.type === 'singleplayer' ? world.path : world.address
|
return world.type === 'singleplayer' ? world.path : world.address
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortWorlds(worlds: World[]) {
|
export function sortWorlds(worlds: World[]) {
|
||||||
worlds.sort((a, b) => {
|
worlds.sort((a, b) => {
|
||||||
if (!a.last_played) {
|
if (!a.last_played) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if (!b.last_played) {
|
if (!b.last_played) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return dayjs(b.last_played).diff(dayjs(a.last_played))
|
return dayjs(b.last_played).diff(dayjs(a.last_played))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSingleplayerWorld(world: World): world is SingleplayerWorld {
|
export function isSingleplayerWorld(world: World): world is SingleplayerWorld {
|
||||||
return world.type === 'singleplayer'
|
return world.type === 'singleplayer'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isServerWorld(world: World): world is ServerWorld {
|
export function isServerWorld(world: World): world is ServerWorld {
|
||||||
return world.type === 'server'
|
return world.type === 'server'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshServerData(
|
export async function refreshServerData(
|
||||||
serverData: ServerData,
|
serverData: ServerData,
|
||||||
protocolVersion: ProtocolVersion | null,
|
protocolVersion: ProtocolVersion | null,
|
||||||
address: string,
|
address: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refreshTime = Date.now()
|
const refreshTime = Date.now()
|
||||||
serverData.refreshing = true
|
serverData.refreshing = true
|
||||||
await get_server_status(address, protocolVersion)
|
await get_server_status(address, protocolVersion)
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||||
// Don't update if there was a more recent successful refresh
|
// Don't update if there was a more recent successful refresh
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverData.lastSuccessfulRefresh = Date.now()
|
serverData.lastSuccessfulRefresh = Date.now()
|
||||||
serverData.status = status
|
serverData.status = status
|
||||||
if (status.description) {
|
if (status.description) {
|
||||||
serverData.rawMotd = status.description
|
serverData.rawMotd = status.description
|
||||||
serverData.renderedMotd = autoToHTML(status.description)
|
serverData.renderedMotd = autoToHTML(status.description)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
serverData.refreshing = false
|
serverData.refreshing = false
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||||
if (!protocolVersion?.legacy) {
|
if (!protocolVersion?.legacy) {
|
||||||
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshServers(
|
export function refreshServers(
|
||||||
worlds: World[],
|
worlds: World[],
|
||||||
serverData: Record<string, ServerData>,
|
serverData: Record<string, ServerData>,
|
||||||
protocolVersion: ProtocolVersion | null,
|
protocolVersion: ProtocolVersion | null,
|
||||||
) {
|
) {
|
||||||
const servers = worlds.filter(isServerWorld)
|
const servers = worlds.filter(isServerWorld)
|
||||||
servers.forEach((server) => {
|
servers.forEach((server) => {
|
||||||
if (!serverData[server.address]) {
|
if (!serverData[server.address]) {
|
||||||
serverData[server.address] = {
|
serverData[server.address] = {
|
||||||
refreshing: true,
|
refreshing: true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
serverData[server.address].refreshing = true
|
serverData[server.address].refreshing = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||||
Object.keys(serverData).forEach((address) =>
|
Object.keys(serverData).forEach((address) =>
|
||||||
refreshServerData(serverData[address], protocolVersion, address),
|
refreshServerData(serverData[address], protocolVersion, address),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
||||||
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
||||||
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
worlds[index] = newWorld
|
worlds[index] = newWorld
|
||||||
} else {
|
} else {
|
||||||
console.info(`Adding new world at path: ${worldPath}.`)
|
console.info(`Adding new world at path: ${worldPath}.`)
|
||||||
worlds.push(newWorld)
|
worlds.push(newWorld)
|
||||||
}
|
}
|
||||||
sortWorlds(worlds)
|
sortWorlds(worlds)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDefaultProfileUpdateEvent(
|
export async function handleDefaultProfileUpdateEvent(
|
||||||
worlds: World[],
|
worlds: World[],
|
||||||
instancePath: string,
|
instancePath: string,
|
||||||
e: ProfileEvent,
|
e: ProfileEvent,
|
||||||
) {
|
) {
|
||||||
if (e.event === 'world_updated') {
|
if (e.event === 'world_updated') {
|
||||||
await refreshWorld(worlds, instancePath, e.world)
|
await refreshWorld(worlds, instancePath, e.world)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.event === 'server_joined') {
|
if (e.event === 'server_joined') {
|
||||||
const world = worlds.find(
|
const world = worlds.find(
|
||||||
(w) =>
|
(w) =>
|
||||||
w.type === 'server' &&
|
w.type === 'server' &&
|
||||||
(w.address === `${e.host}:${e.port}` || (e.port == 25565 && w.address == e.host)),
|
(w.address === `${e.host}:${e.port}` || (e.port == 25565 && w.address == e.host)),
|
||||||
)
|
)
|
||||||
if (world) {
|
if (world) {
|
||||||
world.last_played = e.timestamp
|
world.last_played = e.timestamp
|
||||||
sortWorlds(worlds)
|
sortWorlds(worlds)
|
||||||
} else {
|
} else {
|
||||||
console.error(`Could not find world for server join event: ${e.host}:${e.port}`)
|
console.error(`Could not find world for server join event: ${e.host}:${e.port}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
||||||
const worlds = await get_profile_worlds(instancePath).catch((err) => {
|
const worlds = await get_profile_worlds(instancePath).catch((err) => {
|
||||||
console.error(`Error refreshing worlds for instance: ${instancePath}`, err)
|
console.error(`Error refreshing worlds for instance: ${instancePath}`, err)
|
||||||
})
|
})
|
||||||
if (worlds) {
|
if (worlds) {
|
||||||
sortWorlds(worlds)
|
sortWorlds(worlds)
|
||||||
}
|
}
|
||||||
|
|
||||||
return worlds ?? []
|
return worlds ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasServerQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
export function hasServerQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
if (!gameVersions.length) {
|
if (!gameVersions.length) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
const targetIndex = gameVersions.findIndex((v) => v.version === 'a1.0.5_01')
|
const targetIndex = gameVersions.findIndex((v) => v.version === 'a1.0.5_01')
|
||||||
|
|
||||||
return versionIndex === -1 || targetIndex === -1 || versionIndex <= targetIndex
|
return versionIndex === -1 || targetIndex === -1 || versionIndex <= targetIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
if (!gameVersions.length) {
|
if (!gameVersions.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
const targetIndex = gameVersions.findIndex((v) => v.version === '23w14a')
|
const targetIndex = gameVersions.findIndex((v) => v.version === '23w14a')
|
||||||
|
|
||||||
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProfileEvent = { profile_path_id: string } & (
|
export type ProfileEvent = { profile_path_id: string } & (
|
||||||
| {
|
| {
|
||||||
event: 'servers_updated'
|
event: 'servers_updated'
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
event: 'world_updated'
|
event: 'world_updated'
|
||||||
world: string
|
world: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
event: 'server_joined'
|
event: 'server_joined'
|
||||||
host: string
|
host: string
|
||||||
port: number
|
port: number
|
||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import App from '@/App.vue'
|
import 'floating-vue/dist/style.css'
|
||||||
import router from '@/routes'
|
|
||||||
import * as Sentry from '@sentry/vue'
|
import * as Sentry from '@sentry/vue'
|
||||||
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
||||||
import { createPlugin } from '@vintl/vintl/plugin'
|
import { createPlugin } from '@vintl/vintl/plugin'
|
||||||
import FloatingVue from 'floating-vue'
|
import FloatingVue from 'floating-vue'
|
||||||
import 'floating-vue/dist/style.css'
|
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
import App from '@/App.vue'
|
||||||
|
import router from '@/routes'
|
||||||
|
|
||||||
const VIntlPlugin = createPlugin({
|
const VIntlPlugin = createPlugin({
|
||||||
controllerOpts: {
|
controllerOpts: {
|
||||||
defaultLocale: 'en-US',
|
defaultLocale: 'en-US',
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
locales: [
|
locales: [
|
||||||
{
|
{
|
||||||
tag: 'en-US',
|
tag: 'en-US',
|
||||||
meta: {
|
meta: {
|
||||||
displayName: 'American English',
|
displayName: 'American English',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
globalMixin: true,
|
globalMixin: true,
|
||||||
injectInto: [],
|
injectInto: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const vueScan = new VueScanPlugin({
|
const vueScan = new VueScanPlugin({
|
||||||
enabled: false, // Enable or disable the tracker
|
enabled: false, // Enable or disable the tracker
|
||||||
showOverlay: true, // Show overlay to visualize renders
|
showOverlay: true, // Show overlay to visualize renders
|
||||||
log: false, // Log render events to the console
|
log: false, // Log render events to the console
|
||||||
playSound: false, // Play sound on each render
|
playSound: false, // Play sound on each render
|
||||||
})
|
})
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@@ -37,24 +39,24 @@ const pinia = createPinia()
|
|||||||
let app = createApp(App)
|
let app = createApp(App)
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
app,
|
app,
|
||||||
dsn: 'https://9508775ee5034536bc70433f5f531dd4@o485889.ingest.us.sentry.io/4504579615227904',
|
dsn: 'https://9508775ee5034536bc70433f5f531dd4@o485889.ingest.us.sentry.io/4504579615227904',
|
||||||
integrations: [Sentry.browserTracingIntegration({ router })],
|
integrations: [Sentry.browserTracingIntegration({ router })],
|
||||||
tracesSampleRate: 0.1,
|
tracesSampleRate: 0.1,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(vueScan)
|
app.use(vueScan)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(FloatingVue, {
|
app.use(FloatingVue, {
|
||||||
themes: {
|
themes: {
|
||||||
'ribbit-popout': {
|
'ribbit-popout': {
|
||||||
$extend: 'dropdown',
|
$extend: 'dropdown',
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
instantMove: true,
|
instantMove: true,
|
||||||
distance: 8,
|
distance: 8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app.use(VIntlPlugin)
|
app.use(VIntlPlugin)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
DropdownSelect,
|
||||||
|
injectNotificationManager,
|
||||||
|
LoadingIndicator,
|
||||||
|
Pagination,
|
||||||
|
SearchFilterControl,
|
||||||
|
SearchSidebarFilter,
|
||||||
|
useSearch,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { computed, nextTick, 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/ContextMenu.vue'
|
||||||
import type Instance from '@/components/ui/Instance.vue'
|
import type Instance from '@/components/ui/Instance.vue'
|
||||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
@@ -8,25 +28,6 @@ import { get_search_results } from '@/helpers/cache.js'
|
|||||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
||||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
DropdownSelect,
|
|
||||||
injectNotificationManager,
|
|
||||||
LoadingIndicator,
|
|
||||||
Pagination,
|
|
||||||
SearchFilterControl,
|
|
||||||
SearchSidebarFilter,
|
|
||||||
useSearch,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import type { Ref } from 'vue'
|
|
||||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
|
||||||
import type { LocationQuery } from 'vue-router'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -35,34 +36,34 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const projectTypes = computed(() => {
|
const projectTypes = computed(() => {
|
||||||
return [route.params.projectType as ProjectType]
|
return [route.params.projectType as ProjectType]
|
||||||
})
|
})
|
||||||
|
|
||||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||||
get_categories().catch(handleError).then(ref),
|
get_categories().catch(handleError).then(ref),
|
||||||
get_loaders().catch(handleError).then(ref),
|
get_loaders().catch(handleError).then(ref),
|
||||||
get_game_versions().catch(handleError).then(ref),
|
get_game_versions().catch(handleError).then(ref),
|
||||||
])
|
])
|
||||||
|
|
||||||
const tags: Ref<Tags> = computed(() => ({
|
const tags: Ref<Tags> = computed(() => ({
|
||||||
gameVersions: availableGameVersions.value as GameVersion[],
|
gameVersions: availableGameVersions.value as GameVersion[],
|
||||||
loaders: loaders.value as Platform[],
|
loaders: loaders.value as Platform[],
|
||||||
categories: categories.value as Category[],
|
categories: categories.value as Category[],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
game_version: string
|
game_version: string
|
||||||
loader: string
|
loader: string
|
||||||
path: string
|
path: string
|
||||||
install_stage: string
|
install_stage: string
|
||||||
icon_path?: string
|
icon_path?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceProject = {
|
type InstanceProject = {
|
||||||
metadata: {
|
metadata: {
|
||||||
project_id: string
|
project_id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance: Ref<Instance | null> = ref(null)
|
const instance: Ref<Instance | null> = ref(null)
|
||||||
@@ -75,98 +76,98 @@ const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
|
|||||||
await updateInstanceContext()
|
await updateInstanceContext()
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
updateInstanceContext()
|
updateInstanceContext()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function updateInstanceContext() {
|
async function updateInstanceContext() {
|
||||||
if (route.query.i) {
|
if (route.query.i) {
|
||||||
;[instance.value, instanceProjects.value] = await Promise.all([
|
;[instance.value, instanceProjects.value] = await Promise.all([
|
||||||
getInstance(route.query.i).catch(handleError),
|
getInstance(route.query.i).catch(handleError),
|
||||||
getInstanceProjects(route.query.i).catch(handleError),
|
getInstanceProjects(route.query.i).catch(handleError),
|
||||||
])
|
])
|
||||||
newlyInstalled.value = []
|
newlyInstalled.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
|
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
|
||||||
instanceHideInstalled.value = route.query.ai === 'true'
|
instanceHideInstalled.value = route.query.ai === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
|
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
|
||||||
instance.value = null
|
instance.value = null
|
||||||
instanceHideInstalled.value = false
|
instanceHideInstalled.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceFilters = computed(() => {
|
const instanceFilters = computed(() => {
|
||||||
const filters = []
|
const filters = []
|
||||||
|
|
||||||
if (instance.value) {
|
if (instance.value) {
|
||||||
const gameVersion = instance.value.game_version
|
const gameVersion = instance.value.game_version
|
||||||
if (gameVersion) {
|
if (gameVersion) {
|
||||||
filters.push({
|
filters.push({
|
||||||
type: 'game_version',
|
type: 'game_version',
|
||||||
option: gameVersion,
|
option: gameVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const platform = instance.value.loader
|
const platform = instance.value.loader
|
||||||
|
|
||||||
const supportedModLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
const supportedModLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
||||||
|
|
||||||
if (platform && projectTypes.value.includes('mod') && supportedModLoaders.includes(platform)) {
|
if (platform && projectTypes.value.includes('mod') && supportedModLoaders.includes(platform)) {
|
||||||
filters.push({
|
filters.push({
|
||||||
type: 'mod_loader',
|
type: 'mod_loader',
|
||||||
option: platform,
|
option: platform,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceHideInstalled.value && instanceProjects.value) {
|
if (instanceHideInstalled.value && instanceProjects.value) {
|
||||||
const installedMods = Object.values(instanceProjects.value)
|
const installedMods = Object.values(instanceProjects.value)
|
||||||
.filter((x) => x.metadata)
|
.filter((x) => x.metadata)
|
||||||
.map((x) => x.metadata.project_id)
|
.map((x) => x.metadata.project_id)
|
||||||
|
|
||||||
installedMods.push(...newlyInstalled.value)
|
installedMods.push(...newlyInstalled.value)
|
||||||
|
|
||||||
installedMods
|
installedMods
|
||||||
?.map((x) => ({
|
?.map((x) => ({
|
||||||
type: 'project_id',
|
type: 'project_id',
|
||||||
option: `project_id:${x}`,
|
option: `project_id:${x}`,
|
||||||
negative: true,
|
negative: true,
|
||||||
}))
|
}))
|
||||||
.forEach((x) => filters.push(x))
|
.forEach((x) => filters.push(x))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// Selections
|
// Selections
|
||||||
query,
|
query,
|
||||||
currentSortType,
|
currentSortType,
|
||||||
currentFilters,
|
currentFilters,
|
||||||
toggledGroups,
|
toggledGroups,
|
||||||
maxResults,
|
maxResults,
|
||||||
currentPage,
|
currentPage,
|
||||||
overriddenProvidedFilterTypes,
|
overriddenProvidedFilterTypes,
|
||||||
|
|
||||||
// Lists
|
// Lists
|
||||||
filters,
|
filters,
|
||||||
sortTypes,
|
sortTypes,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
requestParams,
|
requestParams,
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
createPageParams,
|
createPageParams,
|
||||||
} = useSearch(projectTypes, tags, instanceFilters)
|
} = useSearch(projectTypes, tags, instanceFilters)
|
||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
const offline = ref(!navigator.onLine)
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
offline.value = true
|
offline.value = true
|
||||||
})
|
})
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
offline.value = false
|
offline.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
@@ -177,350 +178,350 @@ const loading = ref(true)
|
|||||||
const projectType = ref(route.params.projectType)
|
const projectType = ref(route.params.projectType)
|
||||||
|
|
||||||
watch(projectType, () => {
|
watch(projectType, () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
type SearchResult = {
|
type SearchResult = {
|
||||||
project_id: string
|
project_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResults = {
|
type SearchResults = {
|
||||||
total_hits: number
|
total_hits: number
|
||||||
limit: number
|
limit: number
|
||||||
hits: SearchResult[]
|
hits: SearchResult[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: Ref<SearchResults | null> = shallowRef(null)
|
const results: Ref<SearchResults | null> = shallowRef(null)
|
||||||
const pageCount = computed(() =>
|
const pageCount = computed(() =>
|
||||||
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
|
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(requestParams, () => {
|
watch(requestParams, () => {
|
||||||
if (!route.params.projectType) return
|
if (!route.params.projectType) return
|
||||||
refreshSearch()
|
refreshSearch()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function refreshSearch() {
|
async function refreshSearch() {
|
||||||
let rawResults = await get_search_results(requestParams.value)
|
let rawResults = await get_search_results(requestParams.value)
|
||||||
if (!rawResults) {
|
if (!rawResults) {
|
||||||
rawResults = {
|
rawResults = {
|
||||||
result: {
|
result: {
|
||||||
hits: [],
|
hits: [],
|
||||||
total_hits: 0,
|
total_hits: 0,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (instance.value) {
|
if (instance.value) {
|
||||||
for (const val of rawResults.result.hits) {
|
for (const val of rawResults.result.hits) {
|
||||||
val.installed =
|
val.installed =
|
||||||
newlyInstalled.value.includes(val.project_id) ||
|
newlyInstalled.value.includes(val.project_id) ||
|
||||||
Object.values(instanceProjects.value).some(
|
Object.values(instanceProjects.value).some(
|
||||||
(x) => x.metadata && x.metadata.project_id === val.project_id,
|
(x) => x.metadata && x.metadata.project_id === val.project_id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.value = rawResults.result
|
results.value = rawResults.result
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
|
||||||
const persistentParams: LocationQuery = {}
|
const persistentParams: LocationQuery = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(route.query)) {
|
for (const [key, value] of Object.entries(route.query)) {
|
||||||
if (PERSISTENT_QUERY_PARAMS.includes(key)) {
|
if (PERSISTENT_QUERY_PARAMS.includes(key)) {
|
||||||
persistentParams[key] = value
|
persistentParams[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceHideInstalled.value) {
|
if (instanceHideInstalled.value) {
|
||||||
persistentParams.ai = 'true'
|
persistentParams.ai = 'true'
|
||||||
} else {
|
} else {
|
||||||
delete persistentParams.ai
|
delete persistentParams.ai
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
...persistentParams,
|
...persistentParams,
|
||||||
...createPageParams(),
|
...createPageParams(),
|
||||||
}
|
}
|
||||||
|
|
||||||
breadcrumbs.setContext({
|
breadcrumbs.setContext({
|
||||||
name: 'Discover content',
|
name: 'Discover content',
|
||||||
link: `/browse/${projectType.value}`,
|
link: `/browse/${projectType.value}`,
|
||||||
query: params,
|
query: params,
|
||||||
})
|
})
|
||||||
await router.replace({ path: route.path, query: params })
|
await router.replace({ path: route.path, query: params })
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setPage(newPageNumber: number) {
|
async function setPage(newPageNumber: number) {
|
||||||
currentPage.value = newPageNumber
|
currentPage.value = newPageNumber
|
||||||
|
|
||||||
await onSearchChangeToTop()
|
await onSearchChangeToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchWrapper: Ref<HTMLElement | null> = ref(null)
|
const searchWrapper: Ref<HTMLElement | null> = ref(null)
|
||||||
|
|
||||||
async function onSearchChangeToTop() {
|
async function onSearchChangeToTop() {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
query.value = ''
|
query.value = ''
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.projectType,
|
() => route.params.projectType,
|
||||||
async (newType) => {
|
async (newType) => {
|
||||||
// Check if the newType is not the same as the current value
|
// Check if the newType is not the same as the current value
|
||||||
if (!newType || newType === projectType.value) return
|
if (!newType || newType === projectType.value) return
|
||||||
|
|
||||||
projectType.value = newType
|
projectType.value = newType
|
||||||
|
|
||||||
currentSortType.value = { display: 'Relevance', name: 'relevance' }
|
currentSortType.value = { display: 'Relevance', name: 'relevance' }
|
||||||
query.value = ''
|
query.value = ''
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectableProjectTypes = computed(() => {
|
const selectableProjectTypes = computed(() => {
|
||||||
let dataPacks = false,
|
let dataPacks = false,
|
||||||
mods = false,
|
mods = false,
|
||||||
modpacks = false
|
modpacks = false
|
||||||
|
|
||||||
if (instance.value) {
|
if (instance.value) {
|
||||||
if (
|
if (
|
||||||
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
|
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
|
||||||
availableGameVersions.value.findIndex((x) => x.version === '1.13')
|
availableGameVersions.value.findIndex((x) => x.version === '1.13')
|
||||||
) {
|
) {
|
||||||
dataPacks = true
|
dataPacks = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.value.loader !== 'vanilla') {
|
if (instance.value.loader !== 'vanilla') {
|
||||||
mods = true
|
mods = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dataPacks = true
|
dataPacks = true
|
||||||
mods = true
|
mods = true
|
||||||
modpacks = true
|
modpacks = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: LocationQuery = {}
|
const params: LocationQuery = {}
|
||||||
|
|
||||||
if (route.query.i) {
|
if (route.query.i) {
|
||||||
params.i = route.query.i
|
params.i = route.query.i
|
||||||
}
|
}
|
||||||
if (route.query.ai) {
|
if (route.query.ai) {
|
||||||
params.ai = route.query.ai
|
params.ai = route.query.ai
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ label: 'Modpacks', href: `/browse/modpack`, shown: modpacks },
|
{ label: 'Modpacks', href: `/browse/modpack`, shown: modpacks },
|
||||||
{ label: 'Mods', href: `/browse/mod`, shown: mods },
|
{ label: 'Mods', href: `/browse/mod`, shown: mods },
|
||||||
{ label: 'Resource Packs', href: `/browse/resourcepack` },
|
{ label: 'Resource Packs', href: `/browse/resourcepack` },
|
||||||
{ label: 'Data Packs', href: `/browse/datapack`, shown: dataPacks },
|
{ label: 'Data Packs', href: `/browse/datapack`, shown: dataPacks },
|
||||||
{ label: 'Shaders', href: `/browse/shader` },
|
{ label: 'Shaders', href: `/browse/shader` },
|
||||||
]
|
]
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
return links.map((link) => {
|
return links.map((link) => {
|
||||||
return {
|
return {
|
||||||
...link,
|
...link,
|
||||||
href: {
|
href: {
|
||||||
path: link.href,
|
path: link.href,
|
||||||
query: params,
|
query: params,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return links
|
return links
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
gameVersionProvidedByInstance: {
|
gameVersionProvidedByInstance: {
|
||||||
id: 'search.filter.locked.instance-game-version.title',
|
id: 'search.filter.locked.instance-game-version.title',
|
||||||
defaultMessage: 'Game version is provided by the instance',
|
defaultMessage: 'Game version is provided by the instance',
|
||||||
},
|
},
|
||||||
modLoaderProvidedByInstance: {
|
modLoaderProvidedByInstance: {
|
||||||
id: 'search.filter.locked.instance-loader.title',
|
id: 'search.filter.locked.instance-loader.title',
|
||||||
defaultMessage: 'Loader is provided by the instance',
|
defaultMessage: 'Loader is provided by the instance',
|
||||||
},
|
},
|
||||||
providedByInstance: {
|
providedByInstance: {
|
||||||
id: 'search.filter.locked.instance',
|
id: 'search.filter.locked.instance',
|
||||||
defaultMessage: 'Provided by the instance',
|
defaultMessage: 'Provided by the instance',
|
||||||
},
|
},
|
||||||
syncFilterButton: {
|
syncFilterButton: {
|
||||||
id: 'search.filter.locked.instance.sync',
|
id: 'search.filter.locked.instance.sync',
|
||||||
defaultMessage: 'Sync with instance',
|
defaultMessage: 'Sync with instance',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = ref(null)
|
const options = ref(null)
|
||||||
const handleRightClick = (event, result) => {
|
const handleRightClick = (event, result) => {
|
||||||
options.value.showMenu(event, result, [
|
options.value.showMenu(event, result, [
|
||||||
{
|
{
|
||||||
name: 'open_link',
|
name: 'open_link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'copy_link',
|
name: 'copy_link',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
const handleOptionsClick = (args) => {
|
const handleOptionsClick = (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'open_link':
|
case 'open_link':
|
||||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||||
break
|
break
|
||||||
case 'copy_link':
|
case 'copy_link':
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshSearch()
|
await refreshSearch()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport v-if="filters" to="#sidebar-teleport-target">
|
<Teleport v-if="filters" to="#sidebar-teleport-target">
|
||||||
<div
|
<div
|
||||||
v-if="instance"
|
v-if="instance"
|
||||||
class="border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
class="border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="instanceHideInstalled"
|
v-model="instanceHideInstalled"
|
||||||
label="Hide installed content"
|
label="Hide installed content"
|
||||||
class="filter-checkbox"
|
class="filter-checkbox"
|
||||||
@update:model-value="onSearchChangeToTop()"
|
@update:model-value="onSearchChangeToTop()"
|
||||||
@click.prevent.stop
|
@click.prevent.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SearchSidebarFilter
|
<SearchSidebarFilter
|
||||||
v-for="filter in filters.filter((f) => f.display !== 'none')"
|
v-for="filter in filters.filter((f) => f.display !== 'none')"
|
||||||
:key="`filter-${filter.id}`"
|
:key="`filter-${filter.id}`"
|
||||||
v-model:selected-filters="currentFilters"
|
v-model:selected-filters="currentFilters"
|
||||||
v-model:toggled-groups="toggledGroups"
|
v-model:toggled-groups="toggledGroups"
|
||||||
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||||
:provided-filters="instanceFilters"
|
:provided-filters="instanceFilters"
|
||||||
:filter-type="filter"
|
:filter-type="filter"
|
||||||
class="border-0 border-b-[1px] [&:first-child>button]:pt-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
class="border-0 border-b-[1px] [&:first-child>button]:pt-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
|
||||||
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
|
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
|
||||||
content-class="mb-3"
|
content-class="mb-3"
|
||||||
inner-panel-class="ml-2 mr-3"
|
inner-panel-class="ml-2 mr-3"
|
||||||
:open-by-default="
|
:open-by-default="
|
||||||
filter.id.startsWith('category') || filter.id === 'environment' || filter.id === 'license'
|
filter.id.startsWith('category') || filter.id === 'environment' || filter.id === 'license'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3 class="text-base m-0">{{ filter.formatted_name }}</h3>
|
<h3 class="text-base m-0">{{ filter.formatted_name }}</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #locked-game_version>
|
<template #locked-game_version>
|
||||||
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
|
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #locked-mod_loader>
|
<template #locked-mod_loader>
|
||||||
{{ formatMessage(messages.modLoaderProvidedByInstance) }}
|
{{ formatMessage(messages.modLoaderProvidedByInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }} </template>
|
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }} </template>
|
||||||
</SearchSidebarFilter>
|
</SearchSidebarFilter>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<div ref="searchWrapper" class="flex flex-col gap-3 p-6">
|
<div ref="searchWrapper" class="flex flex-col gap-3 p-6">
|
||||||
<template v-if="instance">
|
<template v-if="instance">
|
||||||
<InstanceIndicator :instance="instance" />
|
<InstanceIndicator :instance="instance" />
|
||||||
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
||||||
</template>
|
</template>
|
||||||
<NavTabs :links="selectableProjectTypes" />
|
<NavTabs :links="selectableProjectTypes" />
|
||||||
<div class="iconified-input">
|
<div class="iconified-input">
|
||||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||||
<input
|
<input
|
||||||
v-model="query"
|
v-model="query"
|
||||||
class="h-12 card-shadow"
|
class="h-12 card-shadow"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="`Search ${projectType}s...`"
|
:placeholder="`Search ${projectType}s...`"
|
||||||
/>
|
/>
|
||||||
<Button v-if="query" class="r-btn" @click="() => clearSearch()">
|
<Button v-if="query" class="r-btn" @click="() => clearSearch()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="currentSortType"
|
v-model="currentSortType"
|
||||||
class="max-w-[16rem]"
|
class="max-w-[16rem]"
|
||||||
name="Sort by"
|
name="Sort by"
|
||||||
:options="sortTypes as any"
|
:options="sortTypes as any"
|
||||||
:display-name="(option: SortType | undefined) => option?.display"
|
:display-name="(option: SortType | undefined) => option?.display"
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">Sort by: </span>
|
<span class="font-semibold text-primary">Sort by: </span>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</DropdownSelect>
|
</DropdownSelect>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="maxResults"
|
v-model="maxResults"
|
||||||
name="Max results"
|
name="Max results"
|
||||||
:options="[5, 10, 15, 20, 50, 100]"
|
:options="[5, 10, 15, 20, 50, 100]"
|
||||||
class="max-w-[9rem]"
|
class="max-w-[9rem]"
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">View: </span>
|
<span class="font-semibold text-primary">View: </span>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</DropdownSelect>
|
</DropdownSelect>
|
||||||
<Pagination :page="currentPage" :count="pageCount" class="ml-auto" @switch-page="setPage" />
|
<Pagination :page="currentPage" :count="pageCount" class="ml-auto" @switch-page="setPage" />
|
||||||
</div>
|
</div>
|
||||||
<SearchFilterControl
|
<SearchFilterControl
|
||||||
v-model:selected-filters="currentFilters"
|
v-model:selected-filters="currentFilters"
|
||||||
:filters="filters.filter((f) => f.display !== 'none')"
|
:filters="filters.filter((f) => f.display !== 'none')"
|
||||||
:provided-filters="instanceFilters"
|
:provided-filters="instanceFilters"
|
||||||
:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||||
:provided-message="messages.providedByInstance"
|
:provided-message="messages.providedByInstance"
|
||||||
/>
|
/>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<section v-if="loading" class="offline">
|
<section v-if="loading" class="offline">
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</section>
|
</section>
|
||||||
<section v-else-if="offline && results.total_hits === 0" class="offline">
|
<section v-else-if="offline && results.total_hits === 0" class="offline">
|
||||||
You are currently offline. Connect to the internet to browse Modrinth!
|
You are currently offline. Connect to the internet to browse Modrinth!
|
||||||
</section>
|
</section>
|
||||||
<section v-else class="project-list display-mode--list instance-results" role="list">
|
<section v-else class="project-list display-mode--list instance-results" role="list">
|
||||||
<SearchCard
|
<SearchCard
|
||||||
v-for="result in results.hits"
|
v-for="result in results.hits"
|
||||||
:key="result?.project_id"
|
:key="result?.project_id"
|
||||||
:project="result"
|
:project="result"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
:categories="[
|
:categories="[
|
||||||
...categories.filter(
|
...categories.filter(
|
||||||
(cat) =>
|
(cat) =>
|
||||||
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
|
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
|
||||||
),
|
),
|
||||||
...loaders.filter(
|
...loaders.filter(
|
||||||
(loader) =>
|
(loader) =>
|
||||||
result?.display_categories.includes(loader.name) &&
|
result?.display_categories.includes(loader.name) &&
|
||||||
loader.supported_project_types?.includes(projectType),
|
loader.supported_project_types?.includes(projectType),
|
||||||
),
|
),
|
||||||
]"
|
]"
|
||||||
:installed="result.installed || newlyInstalled.includes(result.project_id)"
|
:installed="result.installed || newlyInstalled.includes(result.project_id)"
|
||||||
@install="
|
@install="
|
||||||
(id) => {
|
(id) => {
|
||||||
newlyInstalled.push(id)
|
newlyInstalled.push(id)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
|
||||||
/>
|
/>
|
||||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</section>
|
</section>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<pagination
|
<pagination
|
||||||
:page="currentPage"
|
:page="currentPage"
|
||||||
:count="pageCount"
|
:count="pageCount"
|
||||||
class="pagination-after"
|
class="pagination-after"
|
||||||
@switch-page="setPage"
|
@switch-page="setPage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import type { SearchResult } from '@modrinth/utils'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { computed, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import RowDisplay from '@/components/RowDisplay.vue'
|
import RowDisplay from '@/components/RowDisplay.vue'
|
||||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||||
import { get_search_results } from '@/helpers/cache.js'
|
import { get_search_results } from '@/helpers/cache.js'
|
||||||
@@ -6,11 +12,6 @@ import { profile_listener } from '@/helpers/events'
|
|||||||
import { list } from '@/helpers/profile.js'
|
import { list } from '@/helpers/profile.js'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import type { SearchResult } from '@modrinth/utils'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { computed, onUnmounted, ref } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -25,102 +26,102 @@ const featuredMods = ref<SearchResult[]>([])
|
|||||||
const installedModpacksFilter = ref('')
|
const installedModpacksFilter = ref('')
|
||||||
|
|
||||||
const recentInstances = computed(() =>
|
const recentInstances = computed(() =>
|
||||||
instances.value
|
instances.value
|
||||||
.filter((x) => x.last_played)
|
.filter((x) => x.last_played)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played))),
|
.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played))),
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasFeaturedProjects = computed(
|
const hasFeaturedProjects = computed(
|
||||||
() => (featuredModpacks.value?.length ?? 0) + (featuredMods.value?.length ?? 0) > 0,
|
() => (featuredModpacks.value?.length ?? 0) + (featuredMods.value?.length ?? 0) > 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
const offline = ref<boolean>(!navigator.onLine)
|
const offline = ref<boolean>(!navigator.onLine)
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
offline.value = true
|
offline.value = true
|
||||||
})
|
})
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
offline.value = false
|
offline.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchInstances() {
|
async function fetchInstances() {
|
||||||
instances.value = await list().catch(handleError)
|
instances.value = await list().catch(handleError)
|
||||||
|
|
||||||
const filters = []
|
const filters = []
|
||||||
for (const instance of instances.value) {
|
for (const instance of instances.value) {
|
||||||
if (instance.linked_data && instance.linked_data.project_id) {
|
if (instance.linked_data && instance.linked_data.project_id) {
|
||||||
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
|
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installedModpacksFilter.value = filters.join(' AND ')
|
installedModpacksFilter.value = filters.join(' AND ')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFeaturedModpacks() {
|
async function fetchFeaturedModpacks() {
|
||||||
const response = await get_search_results(
|
const response = await get_search_results(
|
||||||
`?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${installedModpacksFilter.value}`,
|
`?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${installedModpacksFilter.value}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
featuredModpacks.value = response.result.hits
|
featuredModpacks.value = response.result.hits
|
||||||
} else {
|
} else {
|
||||||
featuredModpacks.value = []
|
featuredModpacks.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFeaturedMods() {
|
async function fetchFeaturedMods() {
|
||||||
const response = await get_search_results('?facets=[["project_type:mod"]]&limit=10&index=follows')
|
const response = await get_search_results('?facets=[["project_type:mod"]]&limit=10&index=follows')
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
featuredMods.value = response.result.hits
|
featuredMods.value = response.result.hits
|
||||||
} else {
|
} else {
|
||||||
featuredModpacks.value = []
|
featuredModpacks.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshFeaturedProjects() {
|
async function refreshFeaturedProjects() {
|
||||||
await Promise.all([fetchFeaturedModpacks(), fetchFeaturedMods()])
|
await Promise.all([fetchFeaturedModpacks(), fetchFeaturedMods()])
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchInstances()
|
await fetchInstances()
|
||||||
await refreshFeaturedProjects()
|
await refreshFeaturedProjects()
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(
|
const unlistenProfile = await profile_listener(
|
||||||
async (e: { event: string; profile_path_id: string }) => {
|
async (e: { event: string; profile_path_id: string }) => {
|
||||||
await fetchInstances()
|
await fetchInstances()
|
||||||
|
|
||||||
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
||||||
await refreshFeaturedProjects()
|
await refreshFeaturedProjects()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProfile()
|
unlistenProfile()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 flex flex-col gap-2">
|
<div class="p-6 flex flex-col gap-2">
|
||||||
<h1 v-if="recentInstances?.length > 0" class="m-0 text-2xl font-extrabold">Welcome back!</h1>
|
<h1 v-if="recentInstances?.length > 0" class="m-0 text-2xl font-extrabold">Welcome back!</h1>
|
||||||
<h1 v-else class="m-0 text-2xl font-extrabold">Welcome to Modrinth App!</h1>
|
<h1 v-else class="m-0 text-2xl font-extrabold">Welcome to Modrinth App!</h1>
|
||||||
<RecentWorldsList :recent-instances="recentInstances" />
|
<RecentWorldsList :recent-instances="recentInstances" />
|
||||||
<RowDisplay
|
<RowDisplay
|
||||||
v-if="hasFeaturedProjects"
|
v-if="hasFeaturedProjects"
|
||||||
:instances="[
|
:instances="[
|
||||||
{
|
{
|
||||||
label: 'Discover a modpack',
|
label: 'Discover a modpack',
|
||||||
route: '/browse/modpack',
|
route: '/browse/modpack',
|
||||||
instances: featuredModpacks,
|
instances: featuredModpacks,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Discover mods',
|
label: 'Discover mods',
|
||||||
route: '/browse/mod',
|
route: '/browse/mod',
|
||||||
instances: featuredMods,
|
instances: featuredMods,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:can-paginate="true"
|
:can-paginate="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user