App redesign (#2946)
* Start of app redesign * format * continue progress * Content page nearly done * Fix recursion issues with content page * Fix update all alignment * Discover page progress * Settings progress * Removed unlocked-size hack that breaks web * Revamp project page, refactor web project page to share code with app, fixed loading bar, misc UI/UX enhancements, update ko-fi logo, update arrow icons, fix web issues caused by floating-vue migration, fix tooltip issues, update web tooltips, clean up web hydration issues * Ads + run prettier * Begin auth refactor, move common messages to ui lib, add i18n extraction to all apps, begin Library refactor * fix ads not hiding when plus log in * rev lockfile changes/conflicts * Fix sign in page * Add generated * (mostly) Data driven search * Fix search mobile issue * profile fixes * Project versions page, fix typescript on UI lib and misc fixes * Remove unused gallery component * Fix linkfunction err * Search filter controls at top, localization for locked filters * Fix provided filter names * Fix navigating from instance browse to main browse * Friends frontend (#2995) * Friends system frontend * (almost) finish frontend * finish friends, fix lint * Fix lint --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> * Refresh macOS app icon * Update web search UI more * Fix link opens * Fix frontend build --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
parent
6ec1dcf088
commit
c39bb78e38
2
.github/ISSUE_TEMPLATE/1-app-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/1-app-bug.yml
vendored
@ -56,4 +56,4 @@ body:
|
|||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context about the problem here.
|
description: Add any other context about the problem here.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
2
.github/ISSUE_TEMPLATE/4-feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/4-feature-request.yml
vendored
@ -43,4 +43,4 @@ body:
|
|||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context or screenshots about the suggested enhancement here.
|
description: Add any other context or screenshots about the suggested enhancement here.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -11,4 +11,4 @@ contact_links:
|
|||||||
url: https://roadmap.modrinth.com
|
url: https://roadmap.modrinth.com
|
||||||
- name: 📚 Documentation
|
- name: 📚 Documentation
|
||||||
about: Useful documentation about Modrinth's API
|
about: Useful documentation about Modrinth's API
|
||||||
url: https://docs.modrinth.com
|
url: https://docs.modrinth.com
|
||||||
|
10
.github/workflows/daedalus-docker.yml
vendored
10
.github/workflows/daedalus-docker.yml
vendored
@ -8,12 +8,12 @@ on:
|
|||||||
- .github/workflows/daedalus-docker.yml
|
- .github/workflows/daedalus-docker.yml
|
||||||
- 'apps/daedalus_client/**'
|
- 'apps/daedalus_client/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize ]
|
types: [opened, synchronize]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/daedalus-docker.yml
|
- .github/workflows/daedalus-docker.yml
|
||||||
- 'apps/daedalus_client/**'
|
- 'apps/daedalus_client/**'
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [ checks_requested ]
|
types: [checks_requested]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@ -26,15 +26,13 @@ jobs:
|
|||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v3
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/modrinth/daedalus
|
images: ghcr.io/modrinth/daedalus
|
||||||
-
|
- name: Login to GitHub Images
|
||||||
name: Login to GitHub Images
|
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
-
|
- name: Build and push
|
||||||
name: Build and push
|
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
1
.github/workflows/daedalus-run.yml
vendored
1
.github/workflows/daedalus-run.yml
vendored
@ -21,7 +21,6 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
- name: Pull Docker image from GHCR
|
- name: Pull Docker image from GHCR
|
||||||
run: docker pull ghcr.io/modrinth/daedalus:main
|
run: docker pull ghcr.io/modrinth/daedalus:main
|
||||||
|
|
||||||
|
12
.github/workflows/labrinth-docker.yml
vendored
12
.github/workflows/labrinth-docker.yml
vendored
@ -8,12 +8,12 @@ on:
|
|||||||
- .github/workflows/labrinth-docker.yml
|
- .github/workflows/labrinth-docker.yml
|
||||||
- 'apps/labrinth/**'
|
- 'apps/labrinth/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize ]
|
types: [opened, synchronize]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/labrinth-docker.yml
|
- .github/workflows/labrinth-docker.yml
|
||||||
- 'apps/labrinth/**'
|
- 'apps/labrinth/**'
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [ checks_requested ]
|
types: [checks_requested]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@ -29,19 +29,17 @@ jobs:
|
|||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v3
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/modrinth/labrinth
|
images: ghcr.io/modrinth/labrinth
|
||||||
-
|
- name: Login to GitHub Images
|
||||||
name: Login to GitHub Images
|
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
-
|
- name: Build and push
|
||||||
name: Build and push
|
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: ./apps/labrinth
|
context: ./apps/labrinth
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
|
4
.github/workflows/turbo-ci.yml
vendored
4
.github/workflows/turbo-ci.yml
vendored
@ -2,11 +2,11 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [ checks_requested ]
|
types: [checks_requested]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@ -1,12 +1,7 @@
|
|||||||
{
|
{
|
||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.validate": [
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact"
|
|
||||||
],
|
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
"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/components,src/composables,src/helpers,src/pages,src/store}/**/*.{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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
@ -28,12 +29,13 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"posthog-js": "^1.158.2",
|
"posthog-js": "^1.158.2",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "^3.4.21",
|
"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": {
|
||||||
|
"@formatjs/cli": "^6.2.12",
|
||||||
"@eslint/compat": "^1.1.1",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
@ -47,8 +49,9 @@
|
|||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"tsconfig": "workspace:*",
|
"tsconfig": "workspace:*",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.2.8",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, onMounted } from 'vue'
|
import { computed, ref, onMounted, watch, onUnmounted } from 'vue'
|
||||||
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
|
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
|
ArrowBigUpDashIcon,
|
||||||
|
LogInIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
SearchIcon,
|
|
||||||
LibraryIcon,
|
LibraryIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
CompassIcon,
|
||||||
|
MinimizeIcon,
|
||||||
|
MaximizeIcon,
|
||||||
|
RestoreIcon,
|
||||||
|
LogOutIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, Notifications } from '@modrinth/ui'
|
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
|
||||||
import { useLoading, useTheming } from '@/store/state'
|
import { useLoading, useTheming } from '@/store/state'
|
||||||
|
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||||
import { get } from '@/helpers/settings'
|
import { get } from '@/helpers/settings'
|
||||||
@ -19,10 +26,9 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
|||||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||||
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||||
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
|
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
||||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||||
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
|
|
||||||
import { type } from '@tauri-apps/plugin-os'
|
import { type } from '@tauri-apps/plugin-os'
|
||||||
import { isDev, getOS, restartApp } from '@/helpers/utils.js'
|
import { isDev, getOS, restartApp } from '@/helpers/utils.js'
|
||||||
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||||
@ -37,15 +43,51 @@ import IncompatibilityWarningModal from '@/components/ui/install_flow/Incompatib
|
|||||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||||
import { useInstall } from '@/store/install.js'
|
import { useInstall } from '@/store/install.js'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { open } from '@tauri-apps/plugin-shell'
|
|
||||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||||
import { renderString } from '@modrinth/utils'
|
import { renderString } from '@modrinth/utils'
|
||||||
import { useFetch } from '@/helpers/fetch.js'
|
import { useFetch } from '@/helpers/fetch.js'
|
||||||
import { check } from '@tauri-apps/plugin-updater'
|
import { check } from '@tauri-apps/plugin-updater'
|
||||||
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
|
import { get as getCreds, logout, login } from '@/helpers/mr_auth.js'
|
||||||
|
import { get_user } from '@/helpers/cache.js'
|
||||||
|
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||||
|
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||||
|
import { open as openURL } from '@tauri-apps/plugin-shell'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
const news = ref([
|
||||||
|
{
|
||||||
|
title: 'Modrinth App 0.9.0',
|
||||||
|
summary: 'An all-new UI, Modrinth Servers, and support for collections.',
|
||||||
|
thumbnail:
|
||||||
|
'https://media.beehiiv.com/cdn-cgi/image/format=auto,width=800,height=421,fit=scale-down,onerror=redirect/uploads/publication/thumbnail/a49f8e1b-3835-4ea1-a85b-118c6425ebc3/landscape_1667087426098458.png',
|
||||||
|
date: '2024-10-11T00:00:00Z',
|
||||||
|
link: 'https://blog.modrinth.com/p/creator-revenue-update',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Becoming Sustainable',
|
||||||
|
summary: 'Announcing 5x creator revenue and updates to the monetization program.',
|
||||||
|
thumbnail:
|
||||||
|
'https://media.beehiiv.com/cdn-cgi/image/format=auto,width=800,height=421,fit=scale-down,onerror=redirect/uploads/asset/file/c99b9885-8248-4d7a-b19a-3ae2c902fdd5/revenue.png',
|
||||||
|
date: '2024-09-13T00:00:00Z',
|
||||||
|
link: 'https://blog.modrinth.com/p/creator-revenue-update',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Modrinth+ and New Ads',
|
||||||
|
summary:
|
||||||
|
'Introducing a new advertising system, a subscription to remove ads, and a redesign of the website!\n',
|
||||||
|
thumbnail:
|
||||||
|
'https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/38ce85e4-5d93-43eb-b61b-b6296f6b9e66/things.png?t=1724260059',
|
||||||
|
date: '2024-08-21T00:00:00Z',
|
||||||
|
link: 'https://blog.modrinth.com/p/introducing-modrinth-refreshed-site-look-new-advertising-system',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const urlModal = ref(null)
|
const urlModal = ref(null)
|
||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
const offline = ref(!navigator.onLine)
|
||||||
@ -65,8 +107,18 @@ const stateInitialized = ref(false)
|
|||||||
|
|
||||||
const criticalErrorMessage = ref()
|
const criticalErrorMessage = ref()
|
||||||
|
|
||||||
|
const isMaximized = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await useCheckDisableMouseover()
|
await useCheckDisableMouseover()
|
||||||
|
|
||||||
|
document.querySelector('body').addEventListener('click', handleClick)
|
||||||
|
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.querySelector('body').removeEventListener('click', handleClick)
|
||||||
|
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function setupApp() {
|
async function setupApp() {
|
||||||
@ -97,6 +149,12 @@ async function setupApp() {
|
|||||||
themeStore.collapsedNavigation = collapsed_navigation
|
themeStore.collapsedNavigation = collapsed_navigation
|
||||||
themeStore.advancedRendering = advanced_rendering
|
themeStore.advancedRendering = advanced_rendering
|
||||||
|
|
||||||
|
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||||
|
|
||||||
|
await getCurrentWindow().onResized(async () => {
|
||||||
|
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||||
|
})
|
||||||
|
|
||||||
initAnalytics()
|
initAnalytics()
|
||||||
if (!telemetry) {
|
if (!telemetry) {
|
||||||
optOutAnalytics()
|
optOutAnalytics()
|
||||||
@ -160,7 +218,6 @@ router.afterEach((to, from, failure) => {
|
|||||||
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
|
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
|
||||||
})
|
})
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isOnBrowse = computed(() => route.path.startsWith('/browse'))
|
|
||||||
|
|
||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
loading.setEnabled(false)
|
loading.setEnabled(false)
|
||||||
@ -176,6 +233,46 @@ const modInstallModal = ref()
|
|||||||
const installConfirmModal = ref()
|
const installConfirmModal = ref()
|
||||||
const incompatibilityWarningModal = ref()
|
const incompatibilityWarningModal = ref()
|
||||||
|
|
||||||
|
const credentials = ref()
|
||||||
|
|
||||||
|
async function fetchCredentials() {
|
||||||
|
const creds = await getCreds().catch(handleError)
|
||||||
|
if (creds && creds.user_id) {
|
||||||
|
creds.user = await get_user(creds.user_id).catch(handleError)
|
||||||
|
}
|
||||||
|
credentials.value = creds
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signIn() {
|
||||||
|
await login().catch(handleError)
|
||||||
|
await fetchCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logOut() {
|
||||||
|
await logout().catch(handleError)
|
||||||
|
await fetchCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIDAS_BITFLAG = 1 << 0
|
||||||
|
const hasPlus = computed(
|
||||||
|
() =>
|
||||||
|
credentials.value &&
|
||||||
|
credentials.value.user &&
|
||||||
|
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
hasPlus,
|
||||||
|
() => {
|
||||||
|
if (hasPlus.value) {
|
||||||
|
hide_ads_window(true)
|
||||||
|
} else {
|
||||||
|
show_ads_window()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
invoke('show_window')
|
invoke('show_window')
|
||||||
|
|
||||||
@ -186,41 +283,8 @@ onMounted(() => {
|
|||||||
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
||||||
install.setInstallConfirmModal(installConfirmModal)
|
install.setInstallConfirmModal(installConfirmModal)
|
||||||
install.setModInstallModal(modInstallModal)
|
install.setModInstallModal(modInstallModal)
|
||||||
})
|
|
||||||
|
|
||||||
document.querySelector('body').addEventListener('click', function (e) {
|
fetchCredentials()
|
||||||
let target = e.target
|
|
||||||
while (target != null) {
|
|
||||||
if (target.matches('a')) {
|
|
||||||
if (
|
|
||||||
target.href &&
|
|
||||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
|
||||||
!target.classList.contains('router-link-active') &&
|
|
||||||
!target.href.startsWith('http://localhost') &&
|
|
||||||
!target.href.startsWith('https://tauri.localhost') &&
|
|
||||||
!target.href.startsWith('http://tauri.localhost')
|
|
||||||
) {
|
|
||||||
open(target.href)
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
target = target.parentElement
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
document.querySelector('body').addEventListener('auxclick', function (e) {
|
|
||||||
// disables middle click -> new tab
|
|
||||||
if (e.button === 1) {
|
|
||||||
e.preventDefault()
|
|
||||||
// instead do a left click
|
|
||||||
const event = new MouseEvent('click', {
|
|
||||||
view: window,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
})
|
|
||||||
e.target.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const accounts = ref(null)
|
const accounts = ref(null)
|
||||||
@ -246,7 +310,6 @@ async function handleCommand(e) {
|
|||||||
const updateAvailable = ref(false)
|
const updateAvailable = ref(false)
|
||||||
async function checkUpdates() {
|
async function checkUpdates() {
|
||||||
const update = await check()
|
const update = await check()
|
||||||
console.log(update)
|
|
||||||
updateAvailable.value = !!update
|
updateAvailable.value = !!update
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
@ -256,76 +319,129 @@ async function checkUpdates() {
|
|||||||
5 * 1000 * 60,
|
5 * 1000 * 60,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClick(e) {
|
||||||
|
let target = e.target
|
||||||
|
while (target != null) {
|
||||||
|
if (target.matches('a')) {
|
||||||
|
if (
|
||||||
|
target.href &&
|
||||||
|
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
||||||
|
!target.classList.contains('router-link-active') &&
|
||||||
|
!target.href.startsWith('http://localhost') &&
|
||||||
|
!target.href.startsWith('https://tauri.localhost') &&
|
||||||
|
!target.href.startsWith('http://tauri.localhost') &&
|
||||||
|
target.target !== '_blank'
|
||||||
|
) {
|
||||||
|
openURL(target.href)
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
target = target.parentElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAuxClick(e) {
|
||||||
|
// disables middle click -> new tab
|
||||||
|
if (e.button === 1) {
|
||||||
|
e.preventDefault()
|
||||||
|
// instead do a left click
|
||||||
|
const event = new MouseEvent('click', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
})
|
||||||
|
e.target.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
|
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
|
||||||
<div v-if="stateInitialized" class="app-container">
|
<Suspense>
|
||||||
<div class="nav-container">
|
<AppSettingsModal ref="settingsModal" />
|
||||||
<div class="nav-section">
|
</Suspense>
|
||||||
<suspense>
|
<Suspense>
|
||||||
<AccountsCard ref="accounts" mode="small" />
|
<InstanceCreationModal ref="installationModal" />
|
||||||
</suspense>
|
</Suspense>
|
||||||
<div class="pages-list">
|
<div v-if="stateInitialized" class="app-grid-layout relative">
|
||||||
<RouterLink v-tooltip="'Home'" to="/" class="btn icon-only collapsed-button">
|
<div
|
||||||
<HomeIcon />
|
class="app-grid-navbar bg-bg-raised flex flex-col p-[1rem] pt-0 gap-[0.5rem] z-10 w-[--left-bar-width]"
|
||||||
</RouterLink>
|
>
|
||||||
<RouterLink
|
<NavButton to="/">
|
||||||
v-tooltip="'Browse'"
|
<HomeIcon />
|
||||||
to="/browse/modpack"
|
<template #label>Home</template>
|
||||||
class="btn icon-only collapsed-button"
|
</NavButton>
|
||||||
:class="{
|
<NavButton
|
||||||
'router-link-active': isOnBrowse,
|
to="/browse/modpack"
|
||||||
}"
|
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
|
||||||
>
|
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
|
||||||
<SearchIcon />
|
>
|
||||||
</RouterLink>
|
<CompassIcon />
|
||||||
<RouterLink v-tooltip="'Library'" to="/library" class="btn icon-only collapsed-button">
|
<template #label>Discover content</template>
|
||||||
<LibraryIcon />
|
</NavButton>
|
||||||
</RouterLink>
|
<NavButton
|
||||||
<Suspense>
|
to="/library"
|
||||||
<InstanceCreationModal ref="installationModal" />
|
:is-subpage="
|
||||||
</Suspense>
|
() =>
|
||||||
</div>
|
route.path.startsWith('/instance') ||
|
||||||
</div>
|
((route.path.startsWith('/browse') || route.path.startsWith('/project')) &&
|
||||||
<div class="settings pages-list">
|
route.query.i)
|
||||||
<button
|
"
|
||||||
v-if="updateAvailable"
|
>
|
||||||
v-tooltip="'Install update'"
|
<LibraryIcon />
|
||||||
class="btn btn-outline btn-primary icon-only collapsed-button"
|
<template #label>Library</template>
|
||||||
@click="restartApp()"
|
</NavButton>
|
||||||
|
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||||
|
<NavButton :to="() => $refs.installationModal.show()" :disabled="offline">
|
||||||
|
<PlusIcon />
|
||||||
|
<template #label>Create new instance</template>
|
||||||
|
</NavButton>
|
||||||
|
<div class="flex flex-grow"></div>
|
||||||
|
<NavButton v-if="updateAvailable" :to="() => restartApp()">
|
||||||
|
<DownloadIcon />
|
||||||
|
<template #label>Install update</template>
|
||||||
|
</NavButton>
|
||||||
|
<NavButton :to="() => $refs.settingsModal.show()">
|
||||||
|
<SettingsIcon />
|
||||||
|
<template #label>Settings</template>
|
||||||
|
</NavButton>
|
||||||
|
<ButtonStyled v-if="credentials" type="transparent" circular>
|
||||||
|
<OverflowMenu
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'sign-out',
|
||||||
|
action: () => logOut(),
|
||||||
|
color: 'danger',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
direction="left"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<Avatar
|
||||||
</button>
|
:src="credentials.user.avatar_url"
|
||||||
<Button
|
:alt="credentials.user.username"
|
||||||
v-tooltip="'Create profile'"
|
size="32px"
|
||||||
class="sleek-primary collapsed-button"
|
circle
|
||||||
icon-only
|
/>
|
||||||
:disabled="offline"
|
<template #sign-out> <LogOutIcon /> Sign out </template>
|
||||||
@click="() => $refs.installationModal.show()"
|
</OverflowMenu>
|
||||||
>
|
</ButtonStyled>
|
||||||
<PlusIcon />
|
<NavButton v-else :to="() => signIn()">
|
||||||
</Button>
|
<LogInIcon />
|
||||||
<RouterLink v-tooltip="'Settings'" to="/settings" class="btn icon-only collapsed-button">
|
<template #label>Sign in</template>
|
||||||
<SettingsIcon />
|
</NavButton>
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="view">
|
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
||||||
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
|
<div data-tauri-drag-region class="flex p-4">
|
||||||
<h1>{{ criticalErrorMessage.header }}</h1>
|
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
|
||||||
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
|
<Breadcrumbs />
|
||||||
</div>
|
</div>
|
||||||
<div class="appbar-row">
|
<section class="flex ml-auto">
|
||||||
<div data-tauri-drag-region class="appbar">
|
<div class="flex mr-3">
|
||||||
<section class="navigation-controls">
|
<Suspense>
|
||||||
<Breadcrumbs data-tauri-drag-region />
|
<RunningAppBar />
|
||||||
</section>
|
</Suspense>
|
||||||
<section class="mod-stats">
|
|
||||||
<Suspense>
|
|
||||||
<RunningAppBar />
|
|
||||||
</Suspense>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
<section v-if="!nativeDecorations" class="window-controls">
|
<section v-if="!nativeDecorations" class="window-controls">
|
||||||
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
|
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
|
||||||
@ -336,30 +452,121 @@ async function checkUpdates() {
|
|||||||
icon-only
|
icon-only
|
||||||
@click="() => getCurrentWindow().toggleMaximize()"
|
@click="() => getCurrentWindow().toggleMaximize()"
|
||||||
>
|
>
|
||||||
<MaximizeIcon />
|
<RestoreIcon v-if="isMaximized" />
|
||||||
|
<MaximizeIcon v-else />
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="titlebar-button close" icon-only @click="handleClose">
|
<Button class="titlebar-button close" icon-only @click="handleClose">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="stateInitialized" class="app-contents experimental-styles-within">
|
||||||
|
<div class="app-viewport flex-grow router-view">
|
||||||
|
<div
|
||||||
|
class="loading-indicator-container h-8 fixed z-50"
|
||||||
|
:style="{
|
||||||
|
top: 'calc(var(--top-bar-height))',
|
||||||
|
left: 'calc(var(--left-bar-width))',
|
||||||
|
width: 'calc(100% - var(--left-bar-width) - var(--right-bar-width))',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ModrinthLoadingIndicator />
|
||||||
</div>
|
</div>
|
||||||
<div class="router-view">
|
<div
|
||||||
<ModrinthLoadingIndicator
|
v-if="themeStore.featureFlag_pagePath"
|
||||||
offset-height="var(--appbar-height)"
|
class="absolute bottom-0 left-0 m-2 bg-tooltip-bg text-tooltip-text font-semibold rounded-full px-2 py-1 text-xs z-50"
|
||||||
offset-width="var(--sidebar-width)"
|
>
|
||||||
/>
|
{{ route.fullPath }}
|
||||||
<RouterView v-slot="{ Component }">
|
</div>
|
||||||
<template v-if="Component">
|
<div
|
||||||
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
|
id="background-teleport-target"
|
||||||
<component :is="Component"></component>
|
class="absolute h-full -z-10 rounded-tl-[--radius-xl] overflow-hidden"
|
||||||
</Suspense>
|
:style="{
|
||||||
</template>
|
width: 'calc(100% - var(--right-bar-width))',
|
||||||
</RouterView>
|
}"
|
||||||
|
></div>
|
||||||
|
<RouterView v-slot="{ Component }">
|
||||||
|
<template v-if="Component">
|
||||||
|
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
|
||||||
|
<component :is="Component"></component>
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
</RouterView>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid overflow-auto"
|
||||||
|
:class="{ 'has-plus': hasPlus }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="app-sidebar-scrollable flex-grow shrink overflow-y-auto relative"
|
||||||
|
:class="{ 'pb-12': !hasPlus }"
|
||||||
|
>
|
||||||
|
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
||||||
|
<div class="sidebar-default-content">
|
||||||
|
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||||
|
<h3 class="text-lg m-0">Playing as</h3>
|
||||||
|
<suspense>
|
||||||
|
<AccountsCard ref="accounts" mode="small" />
|
||||||
|
</suspense>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||||
|
<suspense>
|
||||||
|
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||||
|
</suspense>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4 flex flex-col">
|
||||||
|
<h3 class="px-4 text-lg m-0">News</h3>
|
||||||
|
<template v-for="(item, index) in news" :key="`news-${index}`">
|
||||||
|
<a
|
||||||
|
:class="`flex flex-col outline-offset-[-4px] hover:bg-[--brand-gradient-border] focus:bg-[--brand-gradient-border] px-4 transition-colors ${index === 0 ? 'pt-2 pb-4' : 'py-4'}`"
|
||||||
|
:href="item.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="external"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="item.thumbnail"
|
||||||
|
alt="News thumbnail"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="w-full aspect-[3/1] object-cover rounded-2xl border-[1px] border-solid border-[--brand-gradient-border]"
|
||||||
|
/>
|
||||||
|
<h4 class="mt-2 mb-0 text-sm leading-none text-contrast font-semibold">
|
||||||
|
{{ item.title }}
|
||||||
|
</h4>
|
||||||
|
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
|
||||||
|
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
|
||||||
|
{{ dayjs(item.date).fromNow() }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<hr
|
||||||
|
v-if="index !== news.length - 1"
|
||||||
|
class="h-px my-[-2px] mx-4 border-0 m-0 bg-[--brand-gradient-border]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="!hasPlus">
|
||||||
|
<a
|
||||||
|
href="https://modrinth.plus?app"
|
||||||
|
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<ArrowBigUpDashIcon class="text-2xl" /> Upgrade to Modrinth+
|
||||||
|
</a>
|
||||||
|
<PromotionWrapper />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="view">
|
||||||
|
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
|
||||||
|
<h1>{{ criticalErrorMessage.header }}</h1>
|
||||||
|
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<URLConfirmModal ref="urlModal" />
|
<URLConfirmModal ref="urlModal" />
|
||||||
<Notifications ref="notificationsWrapper" />
|
<Notifications ref="notificationsWrapper" sidebar />
|
||||||
<ErrorModal ref="errorModal" />
|
<ErrorModal ref="errorModal" />
|
||||||
<ModInstallModal ref="modInstallModal" />
|
<ModInstallModal ref="modInstallModal" />
|
||||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||||
@ -394,31 +601,61 @@ async function checkUpdates() {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all ease-in-out 0.1s;
|
transition: all ease-in-out 0.1s;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: transparent;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
border-radius: 0;
|
height: 100%;
|
||||||
height: 3.25rem;
|
width: 3rem;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
width: 3.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border-radius: 999999px;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
margin-block: auto;
|
||||||
|
position: absolute;
|
||||||
|
background-color: transparent;
|
||||||
|
scale: 0.9;
|
||||||
|
transition: all ease-in-out 0.2s;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
&.close {
|
&.close {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--color-red);
|
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--color-red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--color-button-bg);
|
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--color-button-bg);
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
--appbar-height: 3.25rem;
|
|
||||||
--sidebar-width: 4.5rem;
|
|
||||||
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -546,16 +783,139 @@ async function checkUpdates() {
|
|||||||
padding: var(--gap-sm) 0;
|
padding: var(--gap-sm) 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-grid-layout,
|
||||||
|
.app-contents {
|
||||||
|
--top-bar-height: 3.75rem;
|
||||||
|
--left-bar-width: 5rem;
|
||||||
|
--right-bar-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template: 'status status' 'nav dummy';
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
position: relative;
|
||||||
|
//z-index: 0;
|
||||||
|
background-color: var(--color-raised-bg);
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid-navbar {
|
||||||
|
grid-area: nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid-statusbar {
|
||||||
|
grid-area: status;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-contents {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
left: 5rem;
|
||||||
|
top: 3.75rem;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: calc(100vh - 3.75rem);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-top-left-radius: var(--radius-xl);
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
//grid-template-columns: 1fr 0px;
|
||||||
|
transition: grid-template-columns 0.4s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator-container {
|
||||||
|
border-top-left-radius: var(--radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar {
|
||||||
|
overflow: visible;
|
||||||
|
width: 300px;
|
||||||
|
position: relative;
|
||||||
|
height: calc(100vh - 3.75rem);
|
||||||
|
background: var(--brand-gradient-bg);
|
||||||
|
|
||||||
|
--color-button-bg: var(--brand-gradient-button);
|
||||||
|
--color-button-bg-hover: var(--brand-gradient-border);
|
||||||
|
--color-divider: var(--brand-gradient-border);
|
||||||
|
--color-divider-dark: var(--brand-gradient-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 250px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 5rem;
|
||||||
|
background: var(--brand-gradient-fade-out-color);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar.has-plus::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar::before {
|
||||||
|
content: '';
|
||||||
|
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: -2rem;
|
||||||
|
width: 2rem;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-viewport {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
//::-webkit-scrollbar-track {
|
||||||
|
// background-color: transparent; /* Make it transparent if needed */
|
||||||
|
// margin-block: 5px;
|
||||||
|
// margin-right: 5px;
|
||||||
|
//}
|
||||||
|
|
||||||
|
.app-contents::before {
|
||||||
|
z-index: 1;
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
left: 5rem;
|
||||||
|
top: 3.75rem;
|
||||||
|
right: -5rem;
|
||||||
|
bottom: -5rem;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
//box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
box-shadow:
|
||||||
|
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
|
||||||
|
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-teleport-content {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-default-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-teleport-content:empty + .sidebar-default-content {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
.mac {
|
.mac {
|
||||||
.nav-container {
|
.app-grid-statusbar {
|
||||||
padding-top: calc(var(--gap-md) + 1.75rem);
|
padding-left: 5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.account-card,
|
|
||||||
.card-section {
|
|
||||||
top: calc(var(--gap-md) + 1.75rem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
export { default as ServerIcon } from './server.svg'
|
|
||||||
export { default as MinimizeIcon } from './minimize.svg'
|
|
||||||
export { default as MaximizeIcon } from './maximize.svg'
|
|
||||||
export { default as SwapIcon } from './arrow-left-right.svg'
|
export { default as SwapIcon } from './arrow-left-right.svg'
|
||||||
export { default as ToggleIcon } from './toggle.svg'
|
export { default as ToggleIcon } from './toggle.svg'
|
||||||
export { default as PackageIcon } from './package.svg'
|
export { default as PackageIcon } from './package.svg'
|
||||||
export { default as VersionIcon } from './milestone.svg'
|
export { default as VersionIcon } from './milestone.svg'
|
||||||
export { default as MoreIcon } from './more.svg'
|
|
||||||
export { default as TextInputIcon } from './text-cursor-input.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 NewInstanceImage } from './new-instance.svg'
|
||||||
export { default as MenuIcon } from './menu.svg'
|
export { default as MenuIcon } from './menu.svg'
|
||||||
export { default as BugIcon } from './bug.svg'
|
|
||||||
export { default as ChatIcon } from './messages-square.svg'
|
export { default as ChatIcon } from './messages-square.svg'
|
||||||
|
28
apps/app-frontend/src/assets/modrinth_app.svg
Normal file
28
apps/app-frontend/src/assets/modrinth_app.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
24
apps/app-frontend/src/assets/modrinth_servers.svg
Normal file
24
apps/app-frontend/src/assets/modrinth_servers.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" version="1.1" viewBox="0 0 1793 199">
|
||||||
|
<g>
|
||||||
|
<g id="Layer_1">
|
||||||
|
<g id="green" fill="var(--color-brand)">
|
||||||
|
<path d="M1184.1,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
||||||
|
<path d="M1291.1,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
||||||
|
<path d="M1357.2,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
||||||
|
<path d="M1460,165.3l-40.8-95.1h23.2l35.1,83.9h-11.4l36.3-83.9h21.4l-40.8,95.1h-23Z"/>
|
||||||
|
<path d="M1579.6,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
||||||
|
<path d="M1645.7,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
||||||
|
<path d="M1749.9,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
||||||
|
<g>
|
||||||
|
<path d="M9.8,143l63.4-38.1-5.8-15.3,18.1-18.6,22.9-4.9,6.6,8.2-10.6,10.7-9.2,2.9-6.6,6.8,3.2,9,6.5,6.9,9.2-2.5,6.6-7.2,14.3-4.5,4.3,9.6-14.8,18.1-24.8,7.8-11.1-12.4-63.6,38.2c-3-3.9-6.5-9.4-8.8-14.7ZM192.8,65.4l-50.4,13.6c2.8,7.4,3.7,11.7,4.5,16.5l50.3-13.6c-.8-5.4-2.2-10.8-4.4-16.5Z" fill-rule="evenodd"/>
|
||||||
|
<path d="M17.3,106.5c3.6,42.1,38.9,75.2,82,75.2s60.7-18.9,74-46.3l16.4,5.7c-15.8,34.1-50.3,57.9-90.4,57.9S3.6,158.2,0,106.5h17.3ZM.3,89.4C5.3,39.2,47.8,0,99.3,0s99.5,44.6,99.5,99.5-1.1,17.4-3.3,25.5l-16.3-5.7c1.6-6.5,2.4-13.1,2.4-19.8,0-45.4-36.9-82.3-82.3-82.3S22.6,48.7,17.6,89.4H.3Z" fill-rule="evenodd"/>
|
||||||
|
<path d="M99,51.6c-26.4,0-47.9,21.5-47.9,48s21.5,48,48,48,2.7,0,4-.2l4.8,16.8c-2.9.4-5.8.6-8.8.6-36,0-65.2-29.2-65.2-65.2S63.1,34.4,99,34.4s1.8,0,2.7,0l-2.7,17.1ZM118.6,37.4c26.4,8.3,45.6,33,45.6,62.2s-16.4,50.2-39.8,60l-4.8-16.7c16.2-7.7,27.4-24.2,27.4-43.3s-13-38.1-31.1-44.9l2.7-17.2Z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="black" fill="currentColor">
|
||||||
|
<path d="M354.8,69.2c12,0,21.7,3.4,28.6,10.4,7,7.2,10.6,17.5,10.6,31.5v54.8h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,5-6.8,12.2-6.8,21.3v48.5h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,4.8-6.8,12-6.8,21.3v48.5h-22.4v-95.6h21.3v12.2c3.6-4.3,8.1-7.5,13.4-9.8,5.4-2.3,11.3-3.4,17.9-3.4s13.6,1.3,19.2,3.9c5.5,2.9,9.8,6.8,13.1,12,3.9-5,8.9-8.9,15.2-11.8,6.3-2.7,13.1-4.1,20.6-4.1ZM466,167.2c-9.7,0-18.4-2.1-26.1-6.3-7.6-4-13.8-10.1-18.1-17.5-4.5-7.3-6.6-15.7-6.6-25.2s2.1-17.9,6.6-25.2c4.3-7.4,10.6-13.4,18.1-17.4,7.7-4.1,16.5-6.3,26.1-6.3s18.6,2.1,26.3,6.3c7.7,4.1,13.8,10,18.3,17.4,4.3,7.3,6.4,15.7,6.4,25.2s-2.1,17.9-6.4,25.2c-4.5,7.5-10.6,13.4-18.3,17.5-7.7,4.1-16.5,6.3-26.3,6.3h0ZM466,148c8.2,0,15-2.7,20.4-8.2,5.4-5.5,8.1-12.7,8.1-21.7s-2.7-16.1-8.1-21.7c-5.4-5.5-12.2-8.2-20.4-8.2s-15,2.7-20.2,8.2c-5.4,5.5-8.1,12.7-8.1,21.7s2.7,16.1,8.1,21.7c5.2,5.5,12,8.2,20.2,8.2ZM631.5,33.1v132.8h-21.5v-12.3c-3.7,4.4-8.3,7.9-13.6,10.2-5.5,2.3-11.5,3.4-18.1,3.4s-17.4-2-24.7-6.1c-7.3-4.1-13.2-9.8-17.4-17.4-4.1-7.3-6.3-15.9-6.3-25.6s2.1-18.3,6.3-25.6c4.1-7.3,10-13.1,17.4-17.2,7.3-4.1,15.6-6.1,24.7-6.1s12.2,1.1,17.4,3.2c5.2,2.1,9.8,5.4,13.4,9.7v-49h22.4ZM581.1,148c5.4,0,10.2-1.3,14.5-3.8,4.3-2.3,7.7-5.9,10.2-10.4,2.5-4.5,3.8-9.8,3.8-15.7s-1.3-11.3-3.8-15.7c-2.5-4.5-5.9-8.1-10.2-10.6-4.3-2.3-9.1-3.6-14.5-3.6s-10.2,1.3-14.5,3.6c-4.3,2.5-7.7,6.1-10.2,10.6-2.5,4.5-3.8,9.8-3.8,15.7s1.3,11.3,3.8,15.7c2.5,4.5,5.9,8.1,10.2,10.4,4.3,2.5,9.1,3.8,14.5,3.8ZM681.6,84.3c6.4-10,17.7-15,34-15v21.3c-1.7-.3-3.4-.5-5.2-.5-8.8,0-15.6,2.5-20.4,7.5-4.8,5.2-7.3,12.5-7.3,22v46.4h-22.4v-95.6h21.3v14h0ZM734.1,70.3h22.4v95.6h-22.4v-95.6ZM745.4,54.6c-4.1,0-7.5-1.3-10.2-3.9-2.7-2.4-4.2-5.9-4.1-9.5,0-3.8,1.4-7,4.1-9.7,2.7-2.5,6.1-3.8,10.2-3.8s7.5,1.3,10.2,3.6c2.7,2.5,4.1,5.5,4.1,9.3s-1.3,7.2-3.9,9.8c-2.7,2.7-6.3,4.1-10.4,4.1ZM839.5,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4v-95.6h21.3v12.3c3.8-4.5,8.4-7.7,14-10,5.5-2.3,12-3.4,19-3.4ZM964.8,160.7c-2.8,2.2-6,3.9-9.5,4.8-3.9,1.1-7.9,1.6-12,1.6-10.6,0-18.6-2.7-24.3-8.2-5.7-5.5-8.6-13.4-8.6-24v-46h-15.7v-17.9h15.7v-21.8h22.4v21.8h25.6v17.9h-25.6v45.5c0,4.7,1.1,8.2,3.4,10.6,2.3,2.5,5.5,3.8,9.8,3.8s9.1-1.3,12.5-3.9l6.3,15.9ZM1036.9,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4V33.1h22.4v48.3c3.8-3.9,8.2-7,13.8-9.1,5.4-2,11.5-3,18.1-3Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.0 KiB |
BIN
apps/app-frontend/src/assets/sad-modrinth-bot.webp
Normal file
BIN
apps/app-frontend/src/assets/sad-modrinth-bot.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
@ -80,19 +80,25 @@ input {
|
|||||||
|
|
||||||
/* Chrome, Edge, and Safari */
|
/* Chrome, Edge, and Safari */
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: var(--gap-md);
|
width: 16px;
|
||||||
border: 3px solid var(--color-scrollbar);
|
border: 3px solid transparent;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: var(--color-bg);
|
background: transparent;
|
||||||
border: 3px solid var(--color-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-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: 3px solid var(--color-bg);
|
border: 5px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighted {
|
.highlighted {
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, Card, DropdownSelect } from '@modrinth/ui'
|
import { Button, DropdownSelect } from '@modrinth/ui'
|
||||||
import { formatCategoryHeader } from '@modrinth/utils'
|
import { formatCategoryHeader } from '@modrinth/utils'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@ -121,11 +121,10 @@ const handleOptionsClick = async (args) => {
|
|||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const group = ref('Category')
|
const group = ref('Category')
|
||||||
const filters = ref('All profiles')
|
|
||||||
const sortBy = ref('Name')
|
const sortBy = ref('Name')
|
||||||
|
|
||||||
const filteredResults = computed(() => {
|
const filteredResults = computed(() => {
|
||||||
let 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())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -159,16 +158,6 @@ const filteredResults = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.value === 'Custom instances') {
|
|
||||||
instances = instances.filter((instance) => {
|
|
||||||
return !instance.linked_data
|
|
||||||
})
|
|
||||||
} else if (filters.value === 'Downloaded modpacks') {
|
|
||||||
instances = instances.filter((instance) => {
|
|
||||||
return instance.linked_data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceMap = new Map()
|
const instanceMap = new Map()
|
||||||
|
|
||||||
if (group.value === 'Loader') {
|
if (group.value === 'Loader') {
|
||||||
@ -229,53 +218,37 @@ const filteredResults = computed(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<div class="iconified-input">
|
||||||
ref="confirmModal"
|
<SearchIcon />
|
||||||
title="Are you sure you want to delete this instance?"
|
<input v-model="search" type="text" class="h-12" placeholder="Search" />
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
<Button class="r-btn" @click="() => (search = '')">
|
||||||
:has-to-type="false"
|
<XIcon />
|
||||||
proceed-label="Delete"
|
</Button>
|
||||||
@proceed="deleteProfile"
|
</div>
|
||||||
/>
|
<div class="flex gap-2">
|
||||||
<Card class="header">
|
<DropdownSelect
|
||||||
<div class="iconified-input">
|
v-slot="{ selected }"
|
||||||
<SearchIcon />
|
v-model="sortBy"
|
||||||
<input v-model="search" type="text" placeholder="Search" class="search-input" />
|
name="Sort Dropdown"
|
||||||
<Button class="r-btn" @click="() => (search = '')">
|
class="max-w-[16rem]"
|
||||||
<XIcon />
|
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||||
</Button>
|
placeholder="Select..."
|
||||||
</div>
|
>
|
||||||
<div class="labeled_button">
|
<span class="font-semibold text-primary">Sort by: </span>
|
||||||
<span>Sort by</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
<DropdownSelect
|
</DropdownSelect>
|
||||||
v-model="sortBy"
|
<DropdownSelect
|
||||||
class="sort-dropdown"
|
v-slot="{ selected }"
|
||||||
name="Sort Dropdown"
|
v-model="group"
|
||||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
class="max-w-[16rem]"
|
||||||
placeholder="Select..."
|
name="Group Dropdown"
|
||||||
/>
|
:options="['Category', 'Loader', 'Game version', 'None']"
|
||||||
</div>
|
placeholder="Select..."
|
||||||
<div class="labeled_button">
|
>
|
||||||
<span>Filter by</span>
|
<span class="font-semibold text-primary">Group by: </span>
|
||||||
<DropdownSelect
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
v-model="filters"
|
</DropdownSelect>
|
||||||
class="filter-dropdown"
|
</div>
|
||||||
name="Filter Dropdown"
|
|
||||||
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
|
|
||||||
placeholder="Select..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="labeled_button">
|
|
||||||
<span>Group by</span>
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="group"
|
|
||||||
class="group-dropdown"
|
|
||||||
name="Group Dropdown"
|
|
||||||
:options="['Category', 'Loader', 'Game version', 'None']"
|
|
||||||
placeholder="Select..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<div
|
<div
|
||||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
@ -298,6 +271,14 @@ const filteredResults = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmModalWrapper
|
||||||
|
ref="confirmModal"
|
||||||
|
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."
|
||||||
|
:has-to-type="false"
|
||||||
|
proceed-label="Delete"
|
||||||
|
@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>
|
||||||
@ -315,7 +296,6 @@ const filteredResults = computed(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
141
apps/app-frontend/src/components/LoadingIndicatorBar.vue
Normal file
141
apps/app-frontend/src/components/LoadingIndicatorBar.vue
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import { useLoading } from '@/store/state.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
throttle: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 1000,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'var(--loading-bar-gradient)',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const indicator = useLoadingIndicator({
|
||||||
|
duration: props.duration,
|
||||||
|
throttle: props.throttle,
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => indicator.clear)
|
||||||
|
|
||||||
|
const loading = useLoading()
|
||||||
|
|
||||||
|
watch(loading, (newValue) => {
|
||||||
|
if (newValue.barEnabled) {
|
||||||
|
if (newValue.loading) {
|
||||||
|
indicator.start()
|
||||||
|
} else {
|
||||||
|
indicator.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function useLoadingIndicator(opts) {
|
||||||
|
const progress = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const step = computed(() => 10000 / opts.duration)
|
||||||
|
|
||||||
|
let _timer = null
|
||||||
|
let _throttle = null
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
clear()
|
||||||
|
progress.value = 0
|
||||||
|
if (opts.throttle) {
|
||||||
|
_throttle = setTimeout(() => {
|
||||||
|
isLoading.value = true
|
||||||
|
_startTimer()
|
||||||
|
}, opts.throttle)
|
||||||
|
} else {
|
||||||
|
isLoading.value = true
|
||||||
|
_startTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
progress.value = 100
|
||||||
|
_hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
clearInterval(_timer)
|
||||||
|
clearTimeout(_throttle)
|
||||||
|
_timer = null
|
||||||
|
_throttle = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function _increase(num) {
|
||||||
|
progress.value = Math.min(100, progress.value + num)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hide() {
|
||||||
|
clear()
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
progress.value = 0
|
||||||
|
}, 400)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _startTimer() {
|
||||||
|
_timer = setInterval(() => {
|
||||||
|
_increase(step.value)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { progress, isLoading, start, finish, clear }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="loading-indicator-bar"
|
||||||
|
:style="{
|
||||||
|
'--_width': `${indicator.progress.value}%`,
|
||||||
|
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
||||||
|
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
||||||
|
top: `0`,
|
||||||
|
right: `0`,
|
||||||
|
left: `${props.offsetWidth}`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
width: `var(--_width)`,
|
||||||
|
height: `var(--_height)`,
|
||||||
|
borderRadius: `var(--_height)`,
|
||||||
|
// opacity: `var(--_opacity)`,
|
||||||
|
background: `${props.color}`,
|
||||||
|
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||||
|
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
||||||
|
zIndex: 6,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-indicator-bar::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: var(--_width);
|
||||||
|
bottom: 0;
|
||||||
|
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
||||||
|
opacity: calc(var(--_opacity) * 0.1);
|
||||||
|
z-index: 5;
|
||||||
|
transition:
|
||||||
|
width 0.1s ease-in-out,
|
||||||
|
opacity 0.1s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
@ -260,7 +260,6 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
@ -294,16 +293,16 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-md);
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-contrast);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
height: 1.5rem;
|
height: 1.25rem;
|
||||||
width: 1.5rem;
|
width: 1.25rem;
|
||||||
color: var(--color-contrast);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
|
|
||||||
import { useLoading } from '@/store/state.js'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
throttle: {
|
|
||||||
type: Number,
|
|
||||||
default: 50,
|
|
||||||
},
|
|
||||||
duration: {
|
|
||||||
type: Number,
|
|
||||||
default: 500,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 3,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
default:
|
|
||||||
'repeating-linear-gradient(to right, var(--color-brand) 0%, var(--color-brand) 100%)',
|
|
||||||
},
|
|
||||||
offsetWidth: {
|
|
||||||
type: String,
|
|
||||||
default: '208px',
|
|
||||||
},
|
|
||||||
offsetHeight: {
|
|
||||||
type: String,
|
|
||||||
default: '52px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, { slots }) {
|
|
||||||
const indicator = useLoadingIndicator({
|
|
||||||
duration: props.duration,
|
|
||||||
throttle: props.throttle,
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => indicator.clear)
|
|
||||||
|
|
||||||
const loading = useLoading()
|
|
||||||
|
|
||||||
watch(loading, (newValue) => {
|
|
||||||
if (newValue.barEnabled) {
|
|
||||||
if (newValue.loading) {
|
|
||||||
indicator.start()
|
|
||||||
} else {
|
|
||||||
indicator.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
h(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
position: 'fixed',
|
|
||||||
top: props.offsetHeight,
|
|
||||||
right: 0,
|
|
||||||
left: props.offsetWidth,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
width: `calc((100vw - ${props.offsetWidth}) * ${indicator.progress.value / 100})`,
|
|
||||||
height: `${props.height}px`,
|
|
||||||
opacity: indicator.isLoading.value ? 1 : 0,
|
|
||||||
background: props.color || undefined,
|
|
||||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
|
||||||
transition: 'width 0.1s, height 0.4s, opacity 0.4s',
|
|
||||||
zIndex: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slots,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function useLoadingIndicator(opts) {
|
|
||||||
const progress = ref(0)
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const step = computed(() => 10000 / opts.duration)
|
|
||||||
|
|
||||||
let _timer = null
|
|
||||||
let _throttle = null
|
|
||||||
|
|
||||||
function start() {
|
|
||||||
clear()
|
|
||||||
progress.value = 0
|
|
||||||
if (opts.throttle) {
|
|
||||||
_throttle = setTimeout(() => {
|
|
||||||
isLoading.value = true
|
|
||||||
_startTimer()
|
|
||||||
}, opts.throttle)
|
|
||||||
} else {
|
|
||||||
isLoading.value = true
|
|
||||||
_startTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function finish() {
|
|
||||||
progress.value = 100
|
|
||||||
_hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
clearInterval(_timer)
|
|
||||||
clearTimeout(_throttle)
|
|
||||||
_timer = null
|
|
||||||
_throttle = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function _increase(num) {
|
|
||||||
progress.value = Math.min(100, progress.value + num)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _hide() {
|
|
||||||
clear()
|
|
||||||
setTimeout(() => {
|
|
||||||
isLoading.value = false
|
|
||||||
setTimeout(() => {
|
|
||||||
progress.value = 0
|
|
||||||
}, 400)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _startTimer() {
|
|
||||||
_timer = setInterval(() => {
|
|
||||||
_increase(step.value)
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
progress,
|
|
||||||
isLoading,
|
|
||||||
start,
|
|
||||||
finish,
|
|
||||||
clear,
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,19 +2,23 @@
|
|||||||
<div
|
<div
|
||||||
v-if="mode !== 'isolated'"
|
v-if="mode !== 'isolated'"
|
||||||
ref="button"
|
ref="button"
|
||||||
v-tooltip.right="'Minecraft accounts'"
|
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||||
class="button-base avatar-button"
|
|
||||||
:class="{ expanded: mode === 'expanded' }"
|
:class="{ expanded: mode === 'expanded' }"
|
||||||
@click="toggleMenu"
|
@click="toggleMenu"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:size="mode === 'expanded' ? 'xs' : 'sm'"
|
size="36px"
|
||||||
:src="
|
:src="
|
||||||
selectedAccount
|
selectedAccount
|
||||||
? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
|
? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
|
||||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<span>{{ selectedAccount ? selectedAccount.username : 'Select account' }}</span>
|
||||||
|
<span class="text-secondary text-xs">Minecraft account</span>
|
||||||
|
</div>
|
||||||
|
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<Card
|
<Card
|
||||||
@ -59,7 +63,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, TrashIcon, LogInIcon } from '@modrinth/assets'
|
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon } from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
@ -73,7 +77,6 @@ import { handleError } from '@/store/state.js'
|
|||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
@ -151,13 +154,8 @@ const handleClickOutside = (event) => {
|
|||||||
|
|
||||||
function toggleMenu(override = true) {
|
function toggleMenu(override = true) {
|
||||||
if (showCard.value || !override) {
|
if (showCard.value || !override) {
|
||||||
if (showCard.value) {
|
|
||||||
show_ads_window()
|
|
||||||
}
|
|
||||||
|
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
} else {
|
} else {
|
||||||
hide_ads_window()
|
|
||||||
showCard.value = true
|
showCard.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,11 +207,11 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
left: 5.5rem;
|
right: 2rem;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@ -288,12 +286,17 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.3s ease;
|
transition:
|
||||||
|
opacity 0.25s ease,
|
||||||
|
translate 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;
|
||||||
|
scale: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-button {
|
.avatar-button {
|
||||||
@ -301,9 +304,10 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
background-color: var(--color-raised-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;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets'
|
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
|
||||||
import { Button, OverflowMenu } from '@modrinth/ui'
|
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { add_project_from_path } from '@/helpers/profile.js'
|
import { add_project_from_path } from '@/helpers/profile.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
@ -26,7 +26,7 @@ const handleAddContentFromFile = async () => {
|
|||||||
|
|
||||||
const handleSearchContent = async () => {
|
const handleSearchContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -34,30 +34,27 @@ const handleSearchContent = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="joined-buttons">
|
<div class="joined-buttons">
|
||||||
<Button color="primary" @click="handleSearchContent"><SearchIcon /> Add content </Button>
|
<ButtonStyled>
|
||||||
|
<button @click="handleSearchContent">
|
||||||
<OverflowMenu
|
<PlusIcon />
|
||||||
:options="[
|
Install content
|
||||||
{
|
</button>
|
||||||
id: 'search',
|
</ButtonStyled>
|
||||||
action: handleSearchContent,
|
<ButtonStyled>
|
||||||
},
|
<OverflowMenu
|
||||||
{
|
:options="[
|
||||||
id: 'from_file',
|
{
|
||||||
action: handleAddContentFromFile,
|
id: 'from_file',
|
||||||
},
|
action: handleAddContentFromFile,
|
||||||
]"
|
},
|
||||||
class="btn btn-primary btn-dropdown-animation icon-only"
|
]"
|
||||||
>
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
<template #search>
|
<template #from_file>
|
||||||
<SearchIcon />
|
<FolderOpenIcon />
|
||||||
<span class="no-wrap"> Search </span>
|
<span class="no-wrap"> Add from file </span>
|
||||||
</template>
|
</template>
|
||||||
<template #from_file>
|
</OverflowMenu>
|
||||||
<FolderOpenIcon />
|
</ButtonStyled>
|
||||||
<span class="no-wrap"> Add from file </span>
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="breadcrumbs">
|
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
||||||
<Button 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 class="breadcrumbs__forward transparent" icon-only @click="$router.forward()">
|
<Button
|
||||||
|
v-if="false"
|
||||||
|
class="breadcrumbs__forward transparent"
|
||||||
|
icon-only
|
||||||
|
@click="$router.forward()"
|
||||||
|
>
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||||
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
|
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="breadcrumb.link"
|
v-if="breadcrumb.link"
|
||||||
:to="{
|
:to="{
|
||||||
@ -20,13 +25,18 @@
|
|||||||
: breadcrumb.name
|
: breadcrumb.name
|
||||||
}}
|
}}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-else class="selected">{{
|
<span
|
||||||
breadcrumb.name.charAt(0) === '?'
|
v-else
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
data-tauri-drag-region
|
||||||
: breadcrumb.name
|
class="text-contrast font-semibold cursor-default select-none"
|
||||||
}}</span>
|
>{{
|
||||||
<ChevronRightIcon v-if="breadcrumb.link" class="chevron" />
|
breadcrumb.name.charAt(0) === '?'
|
||||||
</div>
|
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||||
|
: breadcrumb.name
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -50,38 +60,3 @@ const breadcrumbs = computed(() => {
|
|||||||
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.breadcrumbs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.breadcrumbs__item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
vertical-align: center;
|
|
||||||
margin: auto 0;
|
|
||||||
|
|
||||||
.chevron,
|
|
||||||
a {
|
|
||||||
margin: auto 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs__back,
|
|
||||||
.breadcrumbs__forward {
|
|
||||||
margin: auto 0;
|
|
||||||
color: var(--color-base);
|
|
||||||
height: unset;
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs__forward {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
|
||||||
|
|
||||||
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
||||||
|
|
||||||
@ -38,7 +37,6 @@ const shown = ref(false)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showMenu: (event, passedItem, passedOptions) => {
|
showMenu: (event, passedItem, passedOptions) => {
|
||||||
hide_ads_window()
|
|
||||||
item.value = passedItem
|
item.value = passedItem
|
||||||
options.value = passedOptions
|
options.value = passedOptions
|
||||||
|
|
||||||
@ -71,9 +69,6 @@ const isLinkedData = (item) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
if (shown.value) {
|
|
||||||
show_ads_window()
|
|
||||||
}
|
|
||||||
shown.value = false
|
shown.value = false
|
||||||
emit('menu-closed')
|
emit('menu-closed')
|
||||||
}
|
}
|
||||||
|
@ -323,7 +323,6 @@ async function repairInstance() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
padding: var(--gap-lg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
|
@ -211,7 +211,6 @@ const exportPack = async () => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: var(--gap-xl);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
@ -286,6 +285,7 @@ const exportPack = async () => {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-wrapper {
|
.textarea-wrapper {
|
||||||
|
@ -28,7 +28,7 @@ const modLoading = 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 () => {
|
||||||
|
@ -525,8 +525,8 @@ const next = async () => {
|
|||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: var(--gap-lg);
|
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
|
margin-top: var(--gap-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-label {
|
.input-label {
|
||||||
@ -595,7 +595,6 @@ const next = async () => {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--gap-lg);
|
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
apps/app-frontend/src/components/ui/InstanceIndicator.vue
Normal file
53
apps/app-frontend/src/components/ui/InstanceIndicator.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||||
|
|
||||||
|
type Instance = {
|
||||||
|
game_version: string
|
||||||
|
loader: string
|
||||||
|
path: string
|
||||||
|
install_stage: string
|
||||||
|
icon_path?: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
instance: Instance
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
||||||
|
<router-link
|
||||||
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
|
tabindex="-1"
|
||||||
|
class="flex flex-col gap-4 text-primary"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
|
:alt="instance.name"
|
||||||
|
size="48px"
|
||||||
|
/>
|
||||||
|
<span class="flex flex-col gap-2">
|
||||||
|
<span class="font-extrabold bold text-contrast">
|
||||||
|
{{ instance.name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-secondary flex items-center gap-2 font-semibold">
|
||||||
|
<GameIcon class="h-5 w-5 text-secondary" />
|
||||||
|
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
<ButtonStyled>
|
||||||
|
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||||
|
<LeftArrowIcon /> Back to instance
|
||||||
|
</router-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
@ -73,8 +73,6 @@ function setJavaInstall(javaInstall) {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.auto-detect-modal {
|
.auto-detect-modal {
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
.table-row {
|
.table-row {
|
||||||
grid-template-columns: 1fr 4fr min-content;
|
grid-template-columns: 1fr 4fr min-content;
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="props.disabled" @click="autoDetect">
|
<Button :disabled="props.disabled" @click="autoDetect">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
Auto detect
|
Detect
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||||
<FolderSearchIcon />
|
<FolderSearchIcon />
|
||||||
@ -187,6 +187,7 @@ async function reinstallJava() {
|
|||||||
|
|
||||||
.toggle-setting {
|
.toggle-setting {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -173,7 +173,6 @@ const switchVersion = async (versionId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: var(--gap-xl);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
|
109
apps/app-frontend/src/components/ui/NavButton.vue
Normal file
109
apps/app-frontend/src/components/ui/NavButton.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tooltip-parent flex items-center justify-center">
|
||||||
|
<RouterLink
|
||||||
|
v-if="typeof to === 'string'"
|
||||||
|
:to="to"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:class="{
|
||||||
|
'router-link-active': isPrimary && isPrimary(route),
|
||||||
|
'subpage-active': isSubpage && isSubpage(route),
|
||||||
|
}"
|
||||||
|
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
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"
|
||||||
|
@click="to"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
<div class="tooltip-label">
|
||||||
|
<slot name="label" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
to: (() => void) | string
|
||||||
|
isPrimary?: RouteFunction
|
||||||
|
isSubpage?: RouteFunction
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.router-link-active,
|
||||||
|
.subpage-active {
|
||||||
|
svg {
|
||||||
|
filter: drop-shadow(0 0 0.5rem black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.router-link-active {
|
||||||
|
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-active {
|
||||||
|
@apply text-contrast bg-button-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-parent {
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-parent:hover .tooltip-label {
|
||||||
|
opacity: 1;
|
||||||
|
translate: 0 0;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-label:not(:empty) {
|
||||||
|
--_tooltip-bg: black;
|
||||||
|
--_tooltip-color: var(--dark-color-contrast);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--_tooltip-bg);
|
||||||
|
color: var(--_tooltip-color);
|
||||||
|
text-wrap: nowrap;
|
||||||
|
padding: 0.5rem 0.5rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
left: calc(100% + 0.5rem);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
translate: -0.5rem 0;
|
||||||
|
scale: 0.9;
|
||||||
|
transition: all ease-in-out 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-label:not(:empty)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 100%; /* To the left of the tooltip */
|
||||||
|
margin-top: -5px;
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent var(--_tooltip-bg) transparent transparent;
|
||||||
|
}
|
||||||
|
</style>
|
164
apps/app-frontend/src/components/ui/NavTabs.vue
Normal file
164
apps/app-frontend/src/components/ui/NavTabs.vue
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||||
|
>
|
||||||
|
<RouterLink
|
||||||
|
v-for="(link, index) in filteredLinks"
|
||||||
|
v-show="link.shown === undefined ? true : link.shown"
|
||||||
|
:key="index"
|
||||||
|
ref="tabLinkElements"
|
||||||
|
: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'}`"
|
||||||
|
>
|
||||||
|
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
||||||
|
<span class="text-nowrap">{{ link.label }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
<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'}`"
|
||||||
|
:style="{
|
||||||
|
left: sliderLeftPx,
|
||||||
|
top: sliderTopPx,
|
||||||
|
right: sliderRightPx,
|
||||||
|
bottom: sliderBottomPx,
|
||||||
|
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
import { useRoute, RouterLink } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
label: string
|
||||||
|
href: string | RouteLocationRaw
|
||||||
|
shown?: boolean
|
||||||
|
icon?: unknown
|
||||||
|
subpages?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
links: Tab[]
|
||||||
|
query?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sliderLeft = ref(4)
|
||||||
|
const sliderTop = ref(4)
|
||||||
|
const sliderRight = ref(4)
|
||||||
|
const sliderBottom = ref(4)
|
||||||
|
const activeIndex = ref(-1)
|
||||||
|
const oldIndex = ref(-1)
|
||||||
|
const subpageSelected = ref(false)
|
||||||
|
|
||||||
|
const filteredLinks = computed(() =>
|
||||||
|
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||||
|
)
|
||||||
|
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||||
|
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||||
|
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||||
|
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||||
|
|
||||||
|
function pickLink() {
|
||||||
|
let index = -1
|
||||||
|
subpageSelected.value = false
|
||||||
|
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||||
|
const link = filteredLinks.value[i]
|
||||||
|
|
||||||
|
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
||||||
|
index = i
|
||||||
|
subpageSelected.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeIndex.value = index
|
||||||
|
|
||||||
|
if (activeIndex.value !== -1) {
|
||||||
|
startAnimation()
|
||||||
|
} else {
|
||||||
|
oldIndex.value = -1
|
||||||
|
sliderLeft.value = 0
|
||||||
|
sliderRight.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabLinkElements = ref()
|
||||||
|
|
||||||
|
function startAnimation() {
|
||||||
|
const el = tabLinkElements.value[activeIndex.value].$el
|
||||||
|
|
||||||
|
if (!el || !el.offsetParent) return
|
||||||
|
|
||||||
|
const newValues = {
|
||||||
|
left: el.offsetLeft,
|
||||||
|
top: el.offsetTop,
|
||||||
|
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
|
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||||
|
sliderLeft.value = newValues.left
|
||||||
|
sliderRight.value = newValues.right
|
||||||
|
sliderTop.value = newValues.top
|
||||||
|
sliderBottom.value = newValues.bottom
|
||||||
|
} else {
|
||||||
|
const delay = 200
|
||||||
|
|
||||||
|
if (newValues.left < sliderLeft.value) {
|
||||||
|
sliderLeft.value = newValues.left
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderRight.value = newValues.right
|
||||||
|
}, delay)
|
||||||
|
} else {
|
||||||
|
sliderRight.value = newValues.right
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderLeft.value = newValues.left
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValues.top < sliderTop.value) {
|
||||||
|
sliderTop.value = newValues.top
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderBottom.value = newValues.bottom
|
||||||
|
}, delay)
|
||||||
|
} else {
|
||||||
|
sliderBottom.value = newValues.bottom
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderTop.value = newValues.top
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', pickLink)
|
||||||
|
pickLink()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', pickLink)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(route, () => {
|
||||||
|
pickLink()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.navtabs-transition {
|
||||||
|
/* Delay on opacity is to hide any jankiness as the page loads */
|
||||||
|
transition:
|
||||||
|
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||||
|
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,75 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { get as getCreds } from '@/helpers/mr_auth.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_user } from '@/helpers/cache.js'
|
|
||||||
import { ChevronRightIcon } from '@modrinth/assets'
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
|
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
|
||||||
|
|
||||||
const showAd = ref(true)
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
scroll() {
|
|
||||||
updateAdPosition()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const creds = await getCreds().catch(handleError)
|
|
||||||
if (creds && creds.user_id) {
|
|
||||||
const user = await get_user(creds.user_id).catch(handleError)
|
|
||||||
|
|
||||||
const MIDAS_BITFLAG = 1 << 0
|
|
||||||
if (user && (user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG) {
|
|
||||||
showAd.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const adsWrapper = ref(null)
|
const adsWrapper = ref(null)
|
||||||
let resizeObserver
|
|
||||||
let scrollHandler
|
|
||||||
let intersectionObserver
|
|
||||||
let mutationObserver
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (showAd.value) {
|
updateAdPosition()
|
||||||
updateAdPosition(true)
|
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(() => updateAdPosition())
|
window.addEventListener('resize', updateAdPosition)
|
||||||
resizeObserver.observe(adsWrapper.value)
|
|
||||||
|
|
||||||
intersectionObserver = new IntersectionObserver(() => updateAdPosition())
|
|
||||||
intersectionObserver.observe(adsWrapper.value)
|
|
||||||
|
|
||||||
mutationObserver = new MutationObserver(() => updateAdPosition())
|
|
||||||
mutationObserver.observe(adsWrapper.value, { attributes: true, childList: true, subtree: true })
|
|
||||||
|
|
||||||
// Add scroll event listener
|
|
||||||
scrollHandler = () => {
|
|
||||||
requestAnimationFrame(() => updateAdPosition())
|
|
||||||
}
|
|
||||||
window.addEventListener('scroll', scrollHandler, { passive: true })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateAdPosition(overrideShown = false) {
|
function updateAdPosition() {
|
||||||
if (adsWrapper.value) {
|
if (adsWrapper.value) {
|
||||||
const rect = adsWrapper.value.getBoundingClientRect()
|
const rect = adsWrapper.value.getBoundingClientRect()
|
||||||
|
|
||||||
let y = rect.top + window.scrollY
|
const x = rect.left + window.scrollX
|
||||||
let height = rect.bottom - rect.top
|
const y = rect.top + window.scrollY
|
||||||
|
|
||||||
// Prevent ad from overlaying the app bar
|
init_ads_window(x, y, 300, 250, true)
|
||||||
if (y <= 52) {
|
|
||||||
y = 52
|
|
||||||
height = rect.bottom - 52
|
|
||||||
|
|
||||||
if (height < 0) {
|
|
||||||
height = 0
|
|
||||||
y = -1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init_ads_window(rect.left + window.scrollX, y, rect.right - rect.left, height, overrideShown)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,38 +25,10 @@ async function openPlusLink() {
|
|||||||
await record_ads_click()
|
await record_ads_click()
|
||||||
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
|
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlisten = await listen('ads-scroll', (event) => {
|
|
||||||
if (adsWrapper.value) {
|
|
||||||
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
|
|
||||||
updateAdPosition()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (resizeObserver) {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
}
|
|
||||||
if (intersectionObserver) {
|
|
||||||
intersectionObserver.disconnect()
|
|
||||||
}
|
|
||||||
if (mutationObserver) {
|
|
||||||
mutationObserver.disconnect()
|
|
||||||
}
|
|
||||||
if (scrollHandler) {
|
|
||||||
window.removeEventListener('scroll', scrollHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
unlisten()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
||||||
v-if="showAd"
|
|
||||||
ref="adsWrapper"
|
|
||||||
class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised cursor-pointer"
|
|
||||||
>
|
|
||||||
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
||||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||||
<button
|
<button
|
||||||
|
@ -1,74 +1,110 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card
|
<div
|
||||||
class="card button-base"
|
class="button-base p-4 bg-bg-raised rounded-xl flex gap-3 group"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
emits('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">
|
<div class="icon">
|
||||||
<Avatar :src="project.icon_url" size="md" class="search-icon" />
|
<Avatar :src="project.icon_url" size="96px" class="search-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content-wrapper">
|
<div class="flex flex-col gap-2 overflow-hidden">
|
||||||
<div class="title joined-text">
|
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
||||||
<h2>{{ project.title }}</h2>
|
<span class="text-lg font-extrabold text-contrast m-0 leading-none">{{
|
||||||
<span v-if="project.author">by {{ project.author }}</span>
|
project.title
|
||||||
|
}}</span>
|
||||||
|
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="m-0 line-clamp-2">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tags">
|
<div class="mt-auto flex items-center gap-1 no-wrap">
|
||||||
<Categories :categories="categories" :type="project.project_type">
|
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||||
<EnvironmentIndicator
|
<div
|
||||||
:type-only="project.moderation"
|
v-for="tag in categories"
|
||||||
:client-side="project.client_side"
|
:key="tag"
|
||||||
:server-side="project.server_side"
|
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||||
:type="project.project_type"
|
>
|
||||||
:search="true"
|
{{ formatCategory(tag.name) }}
|
||||||
/>
|
</div>
|
||||||
</Categories>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats button-group">
|
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
||||||
<div v-if="featured" class="badge">
|
<div class="flex items-center gap-2">
|
||||||
<StarIcon />
|
<DownloadIcon class="shrink-0" />
|
||||||
Featured
|
<span>
|
||||||
|
{{ formatNumber(project.downloads) }}
|
||||||
|
<span class="text-secondary">downloads</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge">
|
<div class="flex items-center gap-2">
|
||||||
<DownloadIcon />
|
<HeartIcon class="shrink-0" />
|
||||||
{{ formatNumber(project.downloads) }}
|
<span>
|
||||||
|
{{ formatNumber(project.follows ?? project.followers) }}
|
||||||
|
<span class="text-secondary">followers</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge">
|
<div class="mt-auto relative">
|
||||||
<HeartIcon />
|
<div
|
||||||
{{ formatNumber(project.follows ?? project.followers) }}
|
class="flex items-center gap-2 group-hover:-translate-y-3 group-hover:opacity-0 group-focus-within:opacity-0 group-hover:scale-95 group-focus-within:scale-95 transition-all"
|
||||||
</div>
|
>
|
||||||
<div class="badge">
|
<HistoryIcon class="shrink-0" />
|
||||||
<CalendarIcon />
|
<span>
|
||||||
{{ formatCategory(dayjs(project.date_modified ?? project.updated).fromNow()) }}
|
<span class="text-secondary">Updated</span>
|
||||||
|
{{ dayjs(project.date_modified ?? project.updated).fromNow() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="opacity-0 scale-95 translate-y-3 group-hover:translate-y-0 group-hover:scale-100 group-hover:opacity-100 group-focus-within:opacity-100 group-focus-within:scale-100 absolute bottom-0 right-0 transition-all w-fit"
|
||||||
|
>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button
|
||||||
|
:disabled="installed || installing"
|
||||||
|
class="shrink-0 no-wrap"
|
||||||
|
@click.stop="install()"
|
||||||
|
>
|
||||||
|
<template v-if="!installed">
|
||||||
|
<DownloadIcon v-if="modpack || instance" />
|
||||||
|
<PlusIcon v-else />
|
||||||
|
</template>
|
||||||
|
<CheckIcon v-else />
|
||||||
|
{{
|
||||||
|
installing
|
||||||
|
? 'Installing'
|
||||||
|
: installed
|
||||||
|
? 'Installed'
|
||||||
|
: modpack || instance
|
||||||
|
? 'Install'
|
||||||
|
: 'Add to an instance'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="project.author" class="install">
|
</div>
|
||||||
<Button color="primary" :disabled="installed || installing" @click.stop="install()">
|
|
||||||
<DownloadIcon v-if="!installed" />
|
|
||||||
<CheckIcon v-else />
|
|
||||||
{{ installing ? 'Installing' : installed ? 'Installed' : 'Install' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { DownloadIcon, HeartIcon, CalendarIcon, CheckIcon, StarIcon } from '@modrinth/assets'
|
import {
|
||||||
import { Avatar, Card, Categories, EnvironmentIndicator, Button } from '@modrinth/ui'
|
TagsIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
HeartIcon,
|
||||||
|
PlusIcon,
|
||||||
|
CheckIcon,
|
||||||
|
HistoryIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { formatNumber, formatCategory } 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 } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
@ -99,10 +135,9 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emits = defineEmits(['open'])
|
const emit = defineEmits(['open', 'install'])
|
||||||
|
|
||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
const installed = ref(props.installed)
|
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
@ -111,87 +146,12 @@ async function install() {
|
|||||||
null,
|
null,
|
||||||
props.instance ? props.instance.path : null,
|
props.instance ? props.instance.path : null,
|
||||||
'SearchCard',
|
'SearchCard',
|
||||||
(version) => {
|
() => {
|
||||||
installing.value = false
|
installing.value = false
|
||||||
|
emit('install', props.project.project_id)
|
||||||
if (props.instance && version) {
|
|
||||||
installed.value = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modpack = computed(() => props.project.project_type === 'modpack')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.icon {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
align-self: center;
|
|
||||||
height: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
grid-column: 2 / 4;
|
|
||||||
flex-direction: column;
|
|
||||||
grid-row: 1;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
.description {
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
grid-row: 2;
|
|
||||||
justify-self: stretch;
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.install {
|
|
||||||
grid-column: 3 / 4;
|
|
||||||
grid-row: 2;
|
|
||||||
justify-self: end;
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 6rem auto 7rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
&:active:not(&:disabled) {
|
|
||||||
scale: 0.98 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined-text {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
align-items: baseline;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<ProgressBar class="loading-bar" :progress="loadingProgress" />
|
<ProgressBar class="loading-bar" :progress="Math.min(loadingProgress, 100)" />
|
||||||
<span v-if="message">{{ message }}</span>
|
<span v-if="message">{{ message }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gradient-bg" data-tauri-drag-region></div>
|
<div class="gradient-bg" data-tauri-drag-region></div>
|
||||||
@ -86,8 +86,7 @@ import { ref, watch } from 'vue'
|
|||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { loading_listener } from '@/helpers/events.js'
|
import { loading_listener } from '@/helpers/events.js'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { XIcon } from '@modrinth/assets'
|
import { XIcon, MaximizeIcon, MinimizeIcon } from '@modrinth/assets'
|
||||||
import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js'
|
|
||||||
import { getOS } from '@/helpers/utils.js'
|
import { getOS } from '@/helpers/utils.js'
|
||||||
import { useLoading } from '@/store/loading.js'
|
import { useLoading } from '@/store/loading.js'
|
||||||
|
|
||||||
|
@ -71,7 +71,6 @@ async function install() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
padding: var(--gap-lg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
|
359
apps/app-frontend/src/components/ui/friends/FriendsList.vue
Normal file
359
apps/app-frontend/src/components/ui/friends/FriendsList.vue
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Avatar, ButtonStyled, NewModal, OverflowMenu } from '@modrinth/ui'
|
||||||
|
import {
|
||||||
|
UserPlusIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
MailIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
TrashIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { ref, onUnmounted, watch, computed } from 'vue'
|
||||||
|
import { friend_listener } from '@/helpers/events'
|
||||||
|
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
|
||||||
|
import { get_user_many } from '@/helpers/cache'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
credentials: unknown | null
|
||||||
|
signIn: () => void2
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const userCredentials = computed(() => props.credentials)
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
|
const manageFriendsModal = ref()
|
||||||
|
const friendInvitesModal = ref()
|
||||||
|
|
||||||
|
const username = ref('')
|
||||||
|
const addFriendModal = ref()
|
||||||
|
async function addFriendFromModal() {
|
||||||
|
addFriendModal.value.hide()
|
||||||
|
await add_friend(username.value).catch(handleError)
|
||||||
|
username.value = ''
|
||||||
|
await loadFriends()
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendOptions = ref()
|
||||||
|
async function handleFriendOptions(args) {
|
||||||
|
switch (args.option) {
|
||||||
|
case 'remove-friend':
|
||||||
|
await removeFriend(args.item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFriend(friend: Friend) {
|
||||||
|
await add_friend(
|
||||||
|
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
||||||
|
).catch(handleError)
|
||||||
|
await loadFriends()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFriend(friend: Friend) {
|
||||||
|
await remove_friend(
|
||||||
|
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
||||||
|
).catch(handleError)
|
||||||
|
await loadFriends()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Friend = {
|
||||||
|
id: string
|
||||||
|
friend_id: string | null
|
||||||
|
status: string | null
|
||||||
|
last_updated: Dayjs | null
|
||||||
|
created: Dayjs
|
||||||
|
username: string
|
||||||
|
accepted: boolean
|
||||||
|
online: boolean
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const userFriends = ref<Friend[]>([])
|
||||||
|
const acceptedFriends = computed(() =>
|
||||||
|
userFriends.value
|
||||||
|
.filter((x) => x.accepted)
|
||||||
|
.toSorted((a, b) => {
|
||||||
|
if (a.last_updated === null && b.last_updated === null) {
|
||||||
|
return 0 // Both are null, equal in sorting
|
||||||
|
}
|
||||||
|
if (a.last_updated === null) {
|
||||||
|
return 1 // `a` is null, move it after `b`
|
||||||
|
}
|
||||||
|
if (b.last_updated === null) {
|
||||||
|
return -1 // `b` is null, move it after `a`
|
||||||
|
}
|
||||||
|
// Both are non-null, sort by date
|
||||||
|
return b.last_updated.diff(a.last_updated)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const pendingFriends = computed(() =>
|
||||||
|
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
async function loadFriends(timeout = false) {
|
||||||
|
loading.value = timeout
|
||||||
|
|
||||||
|
try {
|
||||||
|
const friendsList = await friends()
|
||||||
|
|
||||||
|
if (friendsList.length === 0) {
|
||||||
|
userFriends.value = []
|
||||||
|
} else {
|
||||||
|
const friendStatuses = await friend_statuses()
|
||||||
|
const users = await get_user_many(
|
||||||
|
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
|
||||||
|
)
|
||||||
|
|
||||||
|
userFriends.value = friendsList.map((friend) => {
|
||||||
|
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
|
||||||
|
const status = friendStatuses.find(
|
||||||
|
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
id: friend.id,
|
||||||
|
friend_id: friend.friend_id,
|
||||||
|
status: status?.profile_name,
|
||||||
|
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
|
||||||
|
created: dayjs(friend.created),
|
||||||
|
avatar: user?.avatar_url,
|
||||||
|
username: user?.username,
|
||||||
|
online: !!status,
|
||||||
|
accepted: friend.accepted,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading friends', e)
|
||||||
|
if (timeout) {
|
||||||
|
setTimeout(() => loadFriends(), 15 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
userCredentials,
|
||||||
|
() => {
|
||||||
|
console.log('watch', userCredentials.value)
|
||||||
|
if (userCredentials.value === undefined) {
|
||||||
|
userFriends.value = []
|
||||||
|
} else if (userCredentials.value === null) {
|
||||||
|
userFriends.value = []
|
||||||
|
loading.value = false
|
||||||
|
} else {
|
||||||
|
loadFriends(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const unlisten = await friend_listener(() => loadFriends())
|
||||||
|
onUnmounted(() => {
|
||||||
|
unlisten()
|
||||||
|
})
|
||||||
|
// TODO: Remove friends menu
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NewModal ref="manageFriendsModal" header="Manage friends">
|
||||||
|
<p v-if="acceptedFriends.length === 0">You have no friends :C</p>
|
||||||
|
<div v-else class="flex flex-col gap-4">
|
||||||
|
<input type="text" placeholder="Search friends..." class="w-full" />
|
||||||
|
<div
|
||||||
|
v-for="friend in acceptedFriends.filter((x) =>
|
||||||
|
x.username.toLowerCase().includes(search.value),
|
||||||
|
)"
|
||||||
|
:key="friend.username"
|
||||||
|
class="flex gap-2 items-center min-w-[20rem]"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
|
<span
|
||||||
|
v-if="friend.online"
|
||||||
|
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>{{ friend.username }}</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="removeFriend(friend)">
|
||||||
|
<XIcon />
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
<NewModal ref="friendInvitesModal" header="View friend requests">
|
||||||
|
<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-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 />
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<p class="m-0">
|
||||||
|
<template v-if="friend.id === userCredentials.user_id">
|
||||||
|
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
<p class="m-0 text-sm text-secondary">{{ friend.created.fromNow() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<template v-if="friend.id === userCredentials.user_id">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="addFriend(friend)">
|
||||||
|
<UserPlusIcon />
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="removeFriend(friend)">
|
||||||
|
<XIcon />
|
||||||
|
Ignore
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="removeFriend(friend)">
|
||||||
|
<XIcon />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
<NewModal ref="addFriendModal" header="Add a friend">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="m-0 text-xl">Username</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
||||||
|
<input v-model="username" class="mt-2" type="text" placeholder="Enter username..." />
|
||||||
|
</div>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button class="ml-auto" :disabled="username.length === 0" @click="addFriendFromModal">
|
||||||
|
<UserPlusIcon />
|
||||||
|
Add friend
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</NewModal>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="text-lg m-0">Friends</h3>
|
||||||
|
<ButtonStyled type="transparent" circular>
|
||||||
|
<OverflowMenu
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'add-friend',
|
||||||
|
action: () => addFriendModal.show(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'manage-friends',
|
||||||
|
action: () => manageFriendsModal.show(),
|
||||||
|
shown: acceptedFriends.length > 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'view-requests',
|
||||||
|
action: () => friendInvitesModal.show(),
|
||||||
|
shown: pendingFriends.length > 0,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
aria-label="More options"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #add-friend>
|
||||||
|
<UserPlusIcon aria-hidden="true" />
|
||||||
|
Add friend
|
||||||
|
</template>
|
||||||
|
<template #manage-friends>
|
||||||
|
<SettingsIcon aria-hidden="true" />
|
||||||
|
Manage friends
|
||||||
|
<div
|
||||||
|
v-if="acceptedFriends.length > 0"
|
||||||
|
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{{ acceptedFriends.length }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #view-requests>
|
||||||
|
<MailIcon aria-hidden="true" />
|
||||||
|
View friend requests
|
||||||
|
<div
|
||||||
|
v-if="pendingFriends.length > 0"
|
||||||
|
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{{ pendingFriends.length }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 mt-2">
|
||||||
|
<template v-if="loading">
|
||||||
|
<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="flex flex-col w-full">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="acceptedFriends.length === 0">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="mb-2">You have no friends :C</div>
|
||||||
|
<div v-if="!userCredentials">
|
||||||
|
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
Why don't you
|
||||||
|
<span class="text-link cursor-pointer" @click="addFriendModal.show()">add one</span>?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
||||||
|
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
||||||
|
</ContextMenu>
|
||||||
|
<div
|
||||||
|
v-for="friend in acceptedFriends.slice(0, 5)"
|
||||||
|
:key="friend.username"
|
||||||
|
class="flex gap-2 items-center"
|
||||||
|
:class="{ grayscale: !friend.online }"
|
||||||
|
@contextmenu.prevent.stop="
|
||||||
|
(event) =>
|
||||||
|
friendOptions.showMenu(event, friend, [
|
||||||
|
{
|
||||||
|
name: 'remove-friend',
|
||||||
|
color: 'danger',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
|
<span
|
||||||
|
v-if="friend.online"
|
||||||
|
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
||||||
|
{{ friend.username }}
|
||||||
|
</span>
|
||||||
|
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -151,7 +151,6 @@ td:first-child {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
:deep(.animated-dropdown .options) {
|
:deep(.animated-dropdown .options) {
|
||||||
max-height: 13.375rem;
|
max-height: 13.375rem;
|
||||||
|
@ -68,6 +68,5 @@ async function install() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -243,7 +243,7 @@ const createInstance = async () => {
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
:disabled="profile.installedMod || profile.installing || profile.linked_data?.locked"
|
: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" />
|
||||||
@ -253,9 +253,7 @@ const createInstance = async () => {
|
|||||||
? 'Installing...'
|
? 'Installing...'
|
||||||
: profile.installedMod
|
: profile.installedMod
|
||||||
? 'Installed'
|
? 'Installed'
|
||||||
: profile.linked_data && profile.linked_data.locked
|
: 'Install'
|
||||||
? 'Paired'
|
|
||||||
: 'Install'
|
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -308,7 +306,6 @@ const createInstance = async () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1rem;
|
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
161
apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
Normal file
161
apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NewModal } from '@modrinth/ui'
|
||||||
|
import {
|
||||||
|
ReportIcon,
|
||||||
|
ModrinthIcon,
|
||||||
|
ShieldIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
GaugeIcon,
|
||||||
|
PaintBrushIcon,
|
||||||
|
GameIcon,
|
||||||
|
CoffeeIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { ref } 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 { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
|
|
||||||
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const selectedTab = ref(0)
|
||||||
|
const devModeCounter = ref(0)
|
||||||
|
|
||||||
|
const developerModeEnabled = defineMessage({
|
||||||
|
id: 'app.settings.developer-mode-enabled',
|
||||||
|
defaultMessage: 'Developer mode enabled.',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'app.settings.tabs.appearance',
|
||||||
|
defaultMessage: 'Appearance',
|
||||||
|
}),
|
||||||
|
icon: PaintBrushIcon,
|
||||||
|
content: AppearanceSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'app.settings.tabs.privacy',
|
||||||
|
defaultMessage: 'Privacy',
|
||||||
|
}),
|
||||||
|
icon: ShieldIcon,
|
||||||
|
content: PrivacySettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'app.settings.tabs.java-versions',
|
||||||
|
defaultMessage: 'Java versions',
|
||||||
|
}),
|
||||||
|
icon: CoffeeIcon,
|
||||||
|
content: JavaSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'app.settings.tabs.default-instance-options',
|
||||||
|
defaultMessage: 'Default instance options',
|
||||||
|
}),
|
||||||
|
icon: GameIcon,
|
||||||
|
content: DefaultInstanceSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'app.settings.tabs.resource-management',
|
||||||
|
defaultMessage: 'Resource management',
|
||||||
|
}),
|
||||||
|
icon: GaugeIcon,
|
||||||
|
content: ResourceManagementSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: defineMessage({
|
||||||
|
id: 'app.settings.tabs.feature-flags',
|
||||||
|
defaultMessage: 'Feature flags',
|
||||||
|
}),
|
||||||
|
icon: ReportIcon,
|
||||||
|
content: FeatureFlagSettings,
|
||||||
|
developerOnly: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
|
||||||
|
const version = await getVersion()
|
||||||
|
const osPlatform = getOsPlatform()
|
||||||
|
const osVersion = getOsVersion()
|
||||||
|
</script>
|
||||||
|
/
|
||||||
|
<template>
|
||||||
|
<NewModal ref="modal">
|
||||||
|
<template #title>
|
||||||
|
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
|
<SettingsIcon /> Settings
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="grid grid-cols-[auto_1fr] gap-4">
|
||||||
|
<div class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider">
|
||||||
|
<button
|
||||||
|
v-for="(tab, index) in tabs.filter((t) => !t.developerOnly || themeStore.devMode)"
|
||||||
|
:key="index"
|
||||||
|
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-transform ${selectedTab === index ? 'bg-highlight text-brand' : 'bg-transparent text-button-text'}`"
|
||||||
|
@click="() => (selectedTab = index)"
|
||||||
|
>
|
||||||
|
<component :is="tab.icon" class="w-4 h-4" />
|
||||||
|
<span>{{ formatMessage(tab.name) }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mt-auto text-secondary text-sm">
|
||||||
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
|
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
devModeCounter++
|
||||||
|
if (devModeCounter > 5) {
|
||||||
|
themeStore.devMode = !themeStore.devMode
|
||||||
|
devModeCounter = 0
|
||||||
|
|
||||||
|
if (!themeStore.devMode && tabs[selectedTab].developerOnly === true) {
|
||||||
|
selectedTab = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ModrinthIcon class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<p class="m-0">Modrinth App {{ version }}</p>
|
||||||
|
<p class="m-0">
|
||||||
|
<span v-if="osPlatform === 'macos'">MacOS</span>
|
||||||
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
|
{{ osVersion }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-[600px] h-[500px] overflow-y-auto">
|
||||||
|
<component :is="tabs[selectedTab].content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Modal } from '@modrinth/ui'
|
import { NewModal as Modal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||||
import { useTheming } from '@/store/theme.js'
|
import { useTheming } from '@/store/theme.js'
|
||||||
|
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownSelect, Toggle, ThemeSelector } from '@modrinth/ui'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
import { get, set } from '@/helpers/settings'
|
||||||
|
import { watch, ref } from 'vue'
|
||||||
|
import { getOS } from '@/helpers/utils'
|
||||||
|
|
||||||
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
const os = ref(await getOS())
|
||||||
|
const settings = ref(await get())
|
||||||
|
|
||||||
|
watch(settings, async () => {
|
||||||
|
await set(settings.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<h2 class="m-0 text-2xl">Color theme</h2>
|
||||||
|
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||||
|
|
||||||
|
<ThemeSelector
|
||||||
|
:update-color-theme="themeStore.setThemeState"
|
||||||
|
:current-theme="themeStore.selectedTheme"
|
||||||
|
:theme-options="themeStore.themeOptions"
|
||||||
|
system-theme-color="system"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 text-2xl">Advanced rendering</h2>
|
||||||
|
<p class="m-0 mt-1">
|
||||||
|
Enables advanced rendering such as blur effects that may cause performance issues without
|
||||||
|
hardware-accelerated rendering.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
id="advanced-rendering"
|
||||||
|
:model-value="themeStore.advancedRendering"
|
||||||
|
:checked="themeStore.advancedRendering"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
themeStore.advancedRendering = e
|
||||||
|
settings.advanced_rendering = themeStore.advancedRendering
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mt-4 text-2xl">Native Decorations</h2>
|
||||||
|
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="native-decorations"
|
||||||
|
:model-value="settings.native_decorations"
|
||||||
|
:checked="settings.native_decorations"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
settings.native_decorations = e
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mt-4 text-2xl">Minimize launcher</h2>
|
||||||
|
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="minimize-launcher"
|
||||||
|
:model-value="settings.hide_on_process_start"
|
||||||
|
:checked="settings.hide_on_process_start"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
settings.hide_on_process_start = e
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 mt-4 text-2xl">Default landing page</h2>
|
||||||
|
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||||
|
</div>
|
||||||
|
<DropdownSelect
|
||||||
|
id="opening-page"
|
||||||
|
name="Opening page dropdown"
|
||||||
|
:options="['Home', 'Library']"
|
||||||
|
:default-value="settings.default_page"
|
||||||
|
:model-value="settings.default_page"
|
||||||
|
class="opening-page"
|
||||||
|
@change="
|
||||||
|
(e) => {
|
||||||
|
settings.default_page = e.option
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,166 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { get, set } from '@/helpers/settings'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { get_max_memory } from '@/helpers/jre'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { Slider, Toggle } from '@modrinth/ui'
|
||||||
|
|
||||||
|
const fetchSettings = await get()
|
||||||
|
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||||
|
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
||||||
|
|
||||||
|
const settings = ref(fetchSettings)
|
||||||
|
|
||||||
|
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||||
|
|
||||||
|
watch(
|
||||||
|
settings,
|
||||||
|
async () => {
|
||||||
|
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||||
|
|
||||||
|
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||||
|
setSettings.custom_env_vars = setSettings.envVars
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
|
|
||||||
|
if (!setSettings.hooks.pre_launch) {
|
||||||
|
setSettings.hooks.pre_launch = null
|
||||||
|
}
|
||||||
|
if (!setSettings.hooks.wrapper) {
|
||||||
|
setSettings.hooks.wrapper = null
|
||||||
|
}
|
||||||
|
if (!setSettings.hooks.post_exit) {
|
||||||
|
setSettings.hooks.post_exit = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setSettings.custom_dir) {
|
||||||
|
setSettings.custom_dir = null
|
||||||
|
}
|
||||||
|
|
||||||
|
await set(setSettings)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h2 class="m-0 text-2xl">Java arguments</h2>
|
||||||
|
<input
|
||||||
|
id="java-args"
|
||||||
|
v-model="settings.launchArgs"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
class="installation-input"
|
||||||
|
placeholder="Enter java arguments..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="mt-4 m-0 text-2xl">Environmental variables</h2>
|
||||||
|
<input
|
||||||
|
id="env-vars"
|
||||||
|
v-model="settings.envVars"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
class="installation-input"
|
||||||
|
placeholder="Enter environmental variables..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="mt-4 m-0 text-2xl">Java memory</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||||
|
<Slider
|
||||||
|
id="max-memory"
|
||||||
|
v-model="settings.memory.maximum"
|
||||||
|
:min="8"
|
||||||
|
:max="maxMemory"
|
||||||
|
:step="64"
|
||||||
|
unit="MB"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="mt-4 m-0 text-2xl">Hooks</h2>
|
||||||
|
|
||||||
|
<h3 class="mt-2 m-0 text-lg">Pre launch</h3>
|
||||||
|
<p class="m-0 mt-1 leading-tight">Ran before the instance is launched.</p>
|
||||||
|
<input
|
||||||
|
id="pre-launch"
|
||||||
|
v-model="settings.hooks.pre_launch"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter pre-launch command..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 class="mt-2 m-0 text-lg">Wrapper</h3>
|
||||||
|
<p class="m-0 mt-1 leading-tight">Wrapper command for launching Minecraft.</p>
|
||||||
|
<input
|
||||||
|
id="wrapper"
|
||||||
|
v-model="settings.hooks.wrapper"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter wrapper command..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 class="mt-2 m-0 text-lg">Post exit</h3>
|
||||||
|
<p class="m-0 mt-1 leading-tight">Ran after the game closes.</p>
|
||||||
|
<input
|
||||||
|
id="post-exit"
|
||||||
|
v-model="settings.hooks.post_exit"
|
||||||
|
autocomplete="off"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter post-exit command..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="mt-4 m-0 text-2xl">Window size</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mt-2 m-0 text-lg">Fullscreen</h3>
|
||||||
|
<p class="m-0 mt-1 leading-tight">
|
||||||
|
Overwrites the options.txt file to start in full screen when launched.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
id="fullscreen"
|
||||||
|
:model-value="settings.force_fullscreen"
|
||||||
|
:checked="settings.force_fullscreen"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
settings.force_fullscreen = e
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mt-2 m-0 text-lg">Width</h3>
|
||||||
|
<p class="m-0 mt-1 leading-tight">The width of the game window when launched.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="width"
|
||||||
|
v-model="settings.game_resolution[0]"
|
||||||
|
:disabled="settings.force_fullscreen"
|
||||||
|
autocomplete="off"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter width..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mt-2 m-0 text-lg">Height</h3>
|
||||||
|
<p class="m-0 mt-1 leading-tight">The height of the game window when launched.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="height"
|
||||||
|
v-model="settings.game_resolution[1]"
|
||||||
|
:disabled="settings.force_fullscreen"
|
||||||
|
autocomplete="off"
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
placeholder="Enter height..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Toggle } from '@modrinth/ui'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
type ThemeStoreKeys = keyof typeof themeStore
|
||||||
|
|
||||||
|
const options: Ref<ThemeStoreKeys[]> = computed(() => {
|
||||||
|
return Object.keys(themeStore).filter((key) => key.startsWith('featureFlag_')) as ThemeStoreKeys[]
|
||||||
|
})
|
||||||
|
|
||||||
|
function getStoreValue<K extends ThemeStoreKeys>(key: K): (typeof themeStore)[K] {
|
||||||
|
return themeStore[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStoreValue<K extends ThemeStoreKeys>(key: K, value: (typeof themeStore)[K]) {
|
||||||
|
themeStore[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFlagName(name: string) {
|
||||||
|
return name.replace('featureFlag_', '')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 text-xl capitalize">{{ formatFlagName(option) }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
id="advanced-rendering"
|
||||||
|
:model-value="getStoreValue(option)"
|
||||||
|
:checked="getStoreValue(option)"
|
||||||
|
@update:model-value="() => setStoreValue(option, !themeStore[option])"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
|
||||||
|
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||||
|
async function updateJavaVersion(version) {
|
||||||
|
if (version?.path === '') {
|
||||||
|
version.path = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version?.path) {
|
||||||
|
version.path = version.path.replace('java.exe', 'javaw.exe')
|
||||||
|
}
|
||||||
|
|
||||||
|
await set_java_version(version).catch(handleError)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||||
|
<h2 class="m-0 text-2xl" :class="{ 'mt-4': index !== 0 }">Java {{ javaVersion }} location</h2>
|
||||||
|
<JavaSelector
|
||||||
|
:id="'java-selector-' + javaVersion"
|
||||||
|
v-model="javaVersions[javaVersion]"
|
||||||
|
:version="javaVersion"
|
||||||
|
@update:model-value="updateJavaVersion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { get, set } from '@/helpers/settings'
|
||||||
|
import { Toggle } from '@modrinth/ui'
|
||||||
|
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||||
|
|
||||||
|
const settings = ref(await get())
|
||||||
|
|
||||||
|
watch(settings, async () => {
|
||||||
|
if (settings.value.telemetry) {
|
||||||
|
optInAnalytics()
|
||||||
|
} else {
|
||||||
|
optOutAnalytics()
|
||||||
|
}
|
||||||
|
|
||||||
|
await set(settings.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 text-2xl">Personalized ads</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="personalized-ads"
|
||||||
|
:model-value="settings.personalized_ads"
|
||||||
|
:checked="settings.personalized_ads"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
settings.personalized_ads = e
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 text-2xl">Telemetry</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">
|
||||||
|
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
|
||||||
|
longer be collected.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="opt-out-analytics"
|
||||||
|
:model-value="settings.telemetry"
|
||||||
|
:checked="settings.telemetry"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
settings.telemetry = e
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 text-2xl">Discord RPC</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">
|
||||||
|
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. This does not disable
|
||||||
|
any instance-specific Discord Rich Presence integrations, such as those added by mods. (app
|
||||||
|
restart required to take effect)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="disable-discord-rpc"
|
||||||
|
v-model="settings.discord_rpc"
|
||||||
|
:checked="settings.discord_rpc"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,114 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Button, Slider } from '@modrinth/ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { get, set } from '@/helpers/settings.js'
|
||||||
|
import { purge_cache_types } from '@/helpers/cache.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
|
||||||
|
const settings = ref(await get())
|
||||||
|
|
||||||
|
watch(settings, async () => {
|
||||||
|
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||||
|
|
||||||
|
if (!setSettings.custom_dir) {
|
||||||
|
setSettings.custom_dir = null
|
||||||
|
}
|
||||||
|
|
||||||
|
await set(setSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function purgeCache() {
|
||||||
|
await purge_cache_types([
|
||||||
|
'project',
|
||||||
|
'version',
|
||||||
|
'user',
|
||||||
|
'team',
|
||||||
|
'organization',
|
||||||
|
'loader_manifest',
|
||||||
|
'minecraft_manifest',
|
||||||
|
'categories',
|
||||||
|
'report_types',
|
||||||
|
'loaders',
|
||||||
|
'game_versions',
|
||||||
|
'donation_platforms',
|
||||||
|
'file_update',
|
||||||
|
'search_results',
|
||||||
|
]).catch(handleError)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findLauncherDir() {
|
||||||
|
const newDir = await open({
|
||||||
|
multiple: false,
|
||||||
|
directory: true,
|
||||||
|
title: 'Select a new app directory',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newDir) {
|
||||||
|
settings.value.custom_dir = newDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h2 class="m-0 text-2xl">App directory</h2>
|
||||||
|
<p class="m-0 mt-1">
|
||||||
|
The directory where the launcher stores all of its files. Changes will be applied after
|
||||||
|
restarting the launcher.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="m-1 mt-2">
|
||||||
|
<div class="iconified-input w-full">
|
||||||
|
<BoxIcon />
|
||||||
|
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||||
|
<Button class="r-btn" @click="findLauncherDir">
|
||||||
|
<FolderSearchIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<ConfirmModalWrapper
|
||||||
|
ref="purgeCacheConfirmModal"
|
||||||
|
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."
|
||||||
|
:has-to-type="false"
|
||||||
|
proceed-label="Purge cache"
|
||||||
|
@proceed="purgeCache"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="m-0 text-2xl">App cache</h2>
|
||||||
|
<p class="m-0 mt-1 leading-tight">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||||
|
<TrashIcon />
|
||||||
|
Purge cache
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="m-0 text-2xl mt-4">Maximum concurrent downloads</h2>
|
||||||
|
<p class="m-0 mt-1">
|
||||||
|
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)
|
||||||
|
</p>
|
||||||
|
<Slider
|
||||||
|
id="max-downloads"
|
||||||
|
v-model="settings.max_concurrent_downloads"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="m-0 mt-4 text-2xl">Maximum concurrent writes</h2>
|
||||||
|
<p class="m-0 mt-1">
|
||||||
|
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)
|
||||||
|
</p>
|
||||||
|
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||||
|
</template>
|
@ -1,368 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
|
|
||||||
import { Button, Card, Checkbox } from '@modrinth/ui'
|
|
||||||
import {
|
|
||||||
DiscordIcon,
|
|
||||||
GithubIcon,
|
|
||||||
MicrosoftIcon,
|
|
||||||
GoogleIcon,
|
|
||||||
SteamIcon,
|
|
||||||
GitLabIcon,
|
|
||||||
} from '@/assets/external'
|
|
||||||
import { login } from '@/helpers/mr_auth.js'
|
|
||||||
import { handleError, useNotifications } from '@/store/state.js'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
callback: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const modal = ref()
|
|
||||||
const turnstileToken = ref()
|
|
||||||
const widgetId = ref()
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
show: () => {
|
|
||||||
modal.value.show()
|
|
||||||
|
|
||||||
if (window.turnstile === null || !window.turnstile) {
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src =
|
|
||||||
'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback'
|
|
||||||
script.async = true
|
|
||||||
script.defer = true
|
|
||||||
document.head.appendChild(script)
|
|
||||||
|
|
||||||
window.onloadTurnstileCallback = loadWidget
|
|
||||||
} else {
|
|
||||||
loadWidget()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function loadWidget() {
|
|
||||||
widgetId.value = window.turnstile.render('#turnstile-container', {
|
|
||||||
sitekey: '0x4AAAAAAAW3guHM6Eunbgwu',
|
|
||||||
callback: (token) => (turnstileToken.value = token),
|
|
||||||
expiredCallback: () => (turnstileToken.value = null),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWidget() {
|
|
||||||
if (widgetId.value) {
|
|
||||||
window.turnstile.remove(widgetId.value)
|
|
||||||
widgetId.value = null
|
|
||||||
turnstileToken.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loggingIn = ref(true)
|
|
||||||
const twoFactorFlow = ref(null)
|
|
||||||
const twoFactorCode = ref('')
|
|
||||||
|
|
||||||
const email = ref('')
|
|
||||||
const username = ref('')
|
|
||||||
const password = ref('')
|
|
||||||
const confirmPassword = ref('')
|
|
||||||
const subscribe = ref(true)
|
|
||||||
|
|
||||||
async function signInOauth() {
|
|
||||||
const creds = await login().catch(handleSevereError)
|
|
||||||
|
|
||||||
if (creds && creds.type === 'two_factor_required') {
|
|
||||||
twoFactorFlow.value = creds.flow
|
|
||||||
} else if (creds && creds.session) {
|
|
||||||
props.callback()
|
|
||||||
modal.value.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function signIn2fa() {
|
|
||||||
const creds = await login_2fa(twoFactorCode.value, twoFactorFlow.value).catch(handleError)
|
|
||||||
|
|
||||||
if (creds && creds.session) {
|
|
||||||
props.callback()
|
|
||||||
modal.value.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function signIn() {
|
|
||||||
const creds = await login_pass(username.value, password.value, turnstileToken.value).catch(
|
|
||||||
handleError,
|
|
||||||
)
|
|
||||||
window.turnstile.reset(widgetId.value)
|
|
||||||
|
|
||||||
if (creds && creds.type === 'two_factor_required') {
|
|
||||||
twoFactorFlow.value = creds.flow
|
|
||||||
} else if (creds && creds.session) {
|
|
||||||
props.callback()
|
|
||||||
modal.value.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createAccount() {
|
|
||||||
if (password.value !== confirmPassword.value) {
|
|
||||||
const notifs = useNotifications()
|
|
||||||
notifs.addNotification({
|
|
||||||
title: 'An error occurred',
|
|
||||||
text: 'Passwords do not match!',
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const creds = await create_account(
|
|
||||||
username.value,
|
|
||||||
email.value,
|
|
||||||
password.value,
|
|
||||||
turnstileToken.value,
|
|
||||||
subscribe.value,
|
|
||||||
).catch(handleError)
|
|
||||||
window.turnstile.reset(widgetId.value)
|
|
||||||
|
|
||||||
if (creds && creds.session) {
|
|
||||||
props.callback()
|
|
||||||
modal.value.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ModalWrapper ref="modal" :on-hide="removeWidget">
|
|
||||||
<Card>
|
|
||||||
<template v-if="twoFactorFlow">
|
|
||||||
<h1>Enter two-factor code</h1>
|
|
||||||
<p>Please enter a two-factor code to proceed.</p>
|
|
||||||
<input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<h1 v-if="loggingIn">Login to Modrinth</h1>
|
|
||||||
<h1 v-else>Create an account</h1>
|
|
||||||
<div class="button-grid">
|
|
||||||
<Button class="discord" large @click="signInOauth('discord')">
|
|
||||||
<DiscordIcon />
|
|
||||||
Discord
|
|
||||||
</Button>
|
|
||||||
<Button class="github" large @click="signInOauth('github')">
|
|
||||||
<GithubIcon />
|
|
||||||
Github
|
|
||||||
</Button>
|
|
||||||
<Button class="white" large @click="signInOauth('microsoft')">
|
|
||||||
<MicrosoftIcon />
|
|
||||||
Microsoft
|
|
||||||
</Button>
|
|
||||||
<Button class="google" large @click="signInOauth('google')">
|
|
||||||
<GoogleIcon />
|
|
||||||
Google
|
|
||||||
</Button>
|
|
||||||
<Button class="white" large @click="signInOauth('steam')">
|
|
||||||
<SteamIcon />
|
|
||||||
Steam
|
|
||||||
</Button>
|
|
||||||
<Button class="gitlab" large @click="signInOauth('gitlab')">
|
|
||||||
<GitLabIcon />
|
|
||||||
GitLab
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="divider">
|
|
||||||
<hr />
|
|
||||||
<p>Or</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="!loggingIn" class="iconified-input username">
|
|
||||||
<MailIcon />
|
|
||||||
<input v-model="email" type="text" placeholder="Email" />
|
|
||||||
</div>
|
|
||||||
<div class="iconified-input username">
|
|
||||||
<UserIcon />
|
|
||||||
<input
|
|
||||||
v-model="username"
|
|
||||||
type="text"
|
|
||||||
:placeholder="loggingIn ? 'Email or username' : 'Username'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="iconified-input" :class="{ username: !loggingIn }">
|
|
||||||
<LockIcon />
|
|
||||||
<input v-model="password" type="password" placeholder="Password" />
|
|
||||||
</div>
|
|
||||||
<div v-if="!loggingIn" class="iconified-input username">
|
|
||||||
<LockIcon />
|
|
||||||
<input v-model="confirmPassword" type="password" placeholder="Confirm password" />
|
|
||||||
</div>
|
|
||||||
<div class="turnstile">
|
|
||||||
<div id="turnstile-container"></div>
|
|
||||||
<div id="turnstile-container-2"></div>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
v-if="!loggingIn"
|
|
||||||
v-model="subscribe"
|
|
||||||
class="subscribe-btn"
|
|
||||||
label="Subscribe to updates about Modrinth"
|
|
||||||
/>
|
|
||||||
<div class="link-row">
|
|
||||||
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
|
|
||||||
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
|
|
||||||
<a class="button-base" href="https://modrinth.com/auth/reset-password">
|
|
||||||
Forgot password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="button-row">
|
|
||||||
<Button class="transparent" large>Close</Button>
|
|
||||||
<Button v-if="twoFactorCode" color="primary" large @click="signIn2fa"> Login </Button>
|
|
||||||
<Button
|
|
||||||
v-else-if="loggingIn"
|
|
||||||
color="primary"
|
|
||||||
large
|
|
||||||
:disabled="!turnstileToken"
|
|
||||||
@click="signIn"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
<Button v-else color="primary" large :disabled="!turnstileToken" @click="createAccount">
|
|
||||||
Create account
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
:deep(.modal-container) {
|
|
||||||
.modal-body {
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: 25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-gap: var(--gap-md);
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord {
|
|
||||||
background-color: #5865f2;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.github {
|
|
||||||
background-color: #8740f1;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.white {
|
|
||||||
background-color: var(--color-contrast);
|
|
||||||
color: var(--color-accent-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.google {
|
|
||||||
background-color: #4285f4;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gitlab {
|
|
||||||
background-color: #fc6d26;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: var(--gap-md) 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
padding: 0 1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 2px solid var(--color-button-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
margin-bottom: var(--gap-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: var(--gap-md) 0;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-blue);
|
|
||||||
text-decoration: underline;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
padding: var(--gap-md) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.checkbox) {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.turnstile {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 2px solid var(--color-button-bg);
|
|
||||||
height: 66px;
|
|
||||||
margin-top: var(--gap-md);
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
margin: -1px;
|
|
||||||
min-width: calc(100% + 2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -93,3 +93,7 @@ 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) {
|
||||||
|
return await listen('friend', (event) => callback(event.payload))
|
||||||
|
}
|
||||||
|
17
apps/app-frontend/src/helpers/friends.js
Normal file
17
apps/app-frontend/src/helpers/friends.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export async function friends() {
|
||||||
|
return await invoke('plugin:friends|friends')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function friend_statuses() {
|
||||||
|
return await invoke('plugin:friends|friend_statuses')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function add_friend(userId) {
|
||||||
|
return await invoke('plugin:friends|add_friend', { userId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove_friend(userId) {
|
||||||
|
return await invoke('plugin:friends|remove_friend', { userId })
|
||||||
|
}
|
38
apps/app-frontend/src/locales/en-US/index.json
Normal file
38
apps/app-frontend/src/locales/en-US/index.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"app.settings.developer-mode-enabled": {
|
||||||
|
"message": "Developer mode enabled."
|
||||||
|
},
|
||||||
|
"app.settings.tabs.appearance": {
|
||||||
|
"message": "Appearance"
|
||||||
|
},
|
||||||
|
"app.settings.tabs.default-instance-options": {
|
||||||
|
"message": "Default instance options"
|
||||||
|
},
|
||||||
|
"app.settings.tabs.feature-flags": {
|
||||||
|
"message": "Feature flags"
|
||||||
|
},
|
||||||
|
"app.settings.tabs.java-versions": {
|
||||||
|
"message": "Java versions"
|
||||||
|
},
|
||||||
|
"app.settings.tabs.privacy": {
|
||||||
|
"message": "Privacy"
|
||||||
|
},
|
||||||
|
"app.settings.tabs.resource-management": {
|
||||||
|
"message": "Resource management"
|
||||||
|
},
|
||||||
|
"instance.filter.updates-available": {
|
||||||
|
"message": "Updates available"
|
||||||
|
},
|
||||||
|
"search.filter.locked.instance": {
|
||||||
|
"message": "Provided by the instance"
|
||||||
|
},
|
||||||
|
"search.filter.locked.instance-game-version.title": {
|
||||||
|
"message": "Game version is provided by the instance"
|
||||||
|
},
|
||||||
|
"search.filter.locked.instance-loader.title": {
|
||||||
|
"message": "Loader is provided by the instance"
|
||||||
|
},
|
||||||
|
"search.filter.locked.instance.sync": {
|
||||||
|
"message": "Sync with instance"
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,17 @@ Sentry.init({
|
|||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(FloatingVue)
|
app.use(FloatingVue, {
|
||||||
|
themes: {
|
||||||
|
'ribbit-popout': {
|
||||||
|
$extend: 'dropdown',
|
||||||
|
placement: 'bottom-end',
|
||||||
|
instantMove: true,
|
||||||
|
distance: 8,
|
||||||
|
triggers: ['click'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
app.use(VIntlPlugin)
|
app.use(VIntlPlugin)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onUnmounted, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import RowDisplay from '@/components/RowDisplay.vue'
|
import RowDisplay from '@/components/RowDisplay.vue'
|
||||||
import { list } from '@/helpers/profile.js'
|
import { list } from '@/helpers/profile.js'
|
||||||
@ -8,11 +8,6 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { get_search_results } from '@/helpers/cache.js'
|
import { get_search_results } from '@/helpers/cache.js'
|
||||||
import { hide_ads_window } from '@/helpers/ads.js'
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
hide_ads_window(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const featuredModpacks = ref({})
|
const featuredModpacks = ref({})
|
||||||
const featuredMods = ref({})
|
const featuredMods = ref({})
|
||||||
@ -104,7 +99,8 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="p-6 flex flex-col gap-2">
|
||||||
|
<h1 class="m-0 text-2xl">Welcome back!</h1>
|
||||||
<RowDisplay
|
<RowDisplay
|
||||||
v-if="total > 0"
|
v-if="total > 0"
|
||||||
:instances="[
|
:instances="[
|
||||||
@ -132,13 +128,3 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.page-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,628 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, watch, onMounted } from 'vue'
|
|
||||||
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
|
||||||
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui'
|
|
||||||
import { handleError, useTheming } from '@/store/state'
|
|
||||||
import { get, set } from '@/helpers/settings'
|
|
||||||
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
|
|
||||||
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
|
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
|
||||||
import { optOutAnalytics, optInAnalytics } from '@/helpers/analytics'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { getOS } from '@/helpers/utils.js'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
|
||||||
import { get_user, purge_cache_types } from '@/helpers/cache.js'
|
|
||||||
import { hide_ads_window } from '@/helpers/ads.js'
|
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
hide_ads_window(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const pageOptions = ['Home', 'Library']
|
|
||||||
|
|
||||||
const themeStore = useTheming()
|
|
||||||
|
|
||||||
const version = await getVersion()
|
|
||||||
|
|
||||||
const accessSettings = async () => {
|
|
||||||
const settings = await get()
|
|
||||||
|
|
||||||
settings.launchArgs = settings.extra_launch_args.join(' ')
|
|
||||||
settings.envVars = settings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
|
||||||
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchSettings = await accessSettings().catch(handleError)
|
|
||||||
|
|
||||||
const settings = ref(fetchSettings)
|
|
||||||
|
|
||||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
|
||||||
|
|
||||||
watch(
|
|
||||||
settings,
|
|
||||||
async (oldSettings, newSettings) => {
|
|
||||||
if (oldSettings.loaded_config_dir !== newSettings.loaded_config_dir) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSettings = JSON.parse(JSON.stringify(newSettings))
|
|
||||||
|
|
||||||
if (setSettings.telemetry) {
|
|
||||||
optInAnalytics()
|
|
||||||
} else {
|
|
||||||
optOutAnalytics()
|
|
||||||
}
|
|
||||||
|
|
||||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
|
||||||
setSettings.custom_env_vars = setSettings.envVars
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
|
||||||
|
|
||||||
if (!setSettings.hooks.pre_launch) {
|
|
||||||
setSettings.hooks.pre_launch = null
|
|
||||||
}
|
|
||||||
if (!setSettings.hooks.wrapper) {
|
|
||||||
setSettings.hooks.wrapper = null
|
|
||||||
}
|
|
||||||
if (!setSettings.hooks.post_exit) {
|
|
||||||
setSettings.hooks.post_exit = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!setSettings.custom_dir) {
|
|
||||||
setSettings.custom_dir = null
|
|
||||||
}
|
|
||||||
|
|
||||||
await set(setSettings)
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
|
||||||
async function updateJavaVersion(version) {
|
|
||||||
if (version?.path === '') {
|
|
||||||
version.path = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version?.path) {
|
|
||||||
version.path = version.path.replace('java.exe', 'javaw.exe')
|
|
||||||
}
|
|
||||||
|
|
||||||
await set_java_version(version).catch(handleError)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchCredentials() {
|
|
||||||
const creds = await getCreds().catch(handleError)
|
|
||||||
if (creds && creds.user_id) {
|
|
||||||
creds.user = await get_user(creds.user_id).catch(handleError)
|
|
||||||
}
|
|
||||||
credentials.value = creds
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentials = ref()
|
|
||||||
await fetchCredentials()
|
|
||||||
|
|
||||||
const loginScreenModal = ref()
|
|
||||||
|
|
||||||
async function logOut() {
|
|
||||||
await logout().catch(handleError)
|
|
||||||
await fetchCredentials()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function signInAfter() {
|
|
||||||
await fetchCredentials()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findLauncherDir() {
|
|
||||||
const newDir = await open({
|
|
||||||
multiple: false,
|
|
||||||
directory: true,
|
|
||||||
title: 'Select a new app directory',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (newDir) {
|
|
||||||
settings.value.custom_dir = newDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function purgeCache() {
|
|
||||||
await purge_cache_types([
|
|
||||||
'project',
|
|
||||||
'version',
|
|
||||||
'user',
|
|
||||||
'team',
|
|
||||||
'organization',
|
|
||||||
'loader_manifest',
|
|
||||||
'minecraft_manifest',
|
|
||||||
'categories',
|
|
||||||
'report_types',
|
|
||||||
'loaders',
|
|
||||||
'game_versions',
|
|
||||||
'donation_platforms',
|
|
||||||
'file_update',
|
|
||||||
'search_results',
|
|
||||||
]).catch(handleError)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="settings-page">
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">General settings</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<ModrinthLoginScreen ref="loginScreenModal" :callback="signInAfter" />
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="sign-in">
|
|
||||||
<span class="label__title">Manage account</span>
|
|
||||||
<span v-if="credentials" class="label__description">
|
|
||||||
You are currently logged in as {{ credentials.user.username }}.
|
|
||||||
</span>
|
|
||||||
<span v-else> Sign in to your Modrinth account. </span>
|
|
||||||
</label>
|
|
||||||
<button v-if="credentials" id="sign-in" class="btn" @click="logOut">
|
|
||||||
<LogOutIcon />
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
<button v-else id="sign-in" class="btn" @click="$refs.loginScreenModal.show()">
|
|
||||||
<LogInIcon />
|
|
||||||
Sign in
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ConfirmModalWrapper
|
|
||||||
ref="purgeCacheConfirmModal"
|
|
||||||
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."
|
|
||||||
:has-to-type="false"
|
|
||||||
proceed-label="Purge cache"
|
|
||||||
@proceed="purgeCache"
|
|
||||||
/>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="purge-cache">
|
|
||||||
<span class="label__title">App cache</span>
|
|
||||||
<span class="label__description">
|
|
||||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force
|
|
||||||
the app to reload data. <br />
|
|
||||||
This may slow down the app temporarily.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<button id="purge-cache" class="btn" @click="$refs.purgeCacheConfirmModal.show()">
|
|
||||||
<TrashIcon />
|
|
||||||
Purge cache
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label for="appDir">
|
|
||||||
<span class="label__title">App directory</span>
|
|
||||||
<span class="label__description">
|
|
||||||
The directory where the launcher stores all of its files. Changes will be applied after
|
|
||||||
restarting the launcher.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="app-directory">
|
|
||||||
<div class="iconified-input">
|
|
||||||
<BoxIcon />
|
|
||||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
|
||||||
<Button class="r-btn" @click="findLauncherDir">
|
|
||||||
<FolderSearchIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Display</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="theme">
|
|
||||||
<span class="label__title">Color theme</span>
|
|
||||||
<span class="label__description">Change the global launcher color theme.</span>
|
|
||||||
</label>
|
|
||||||
<DropdownSelect
|
|
||||||
id="theme"
|
|
||||||
name="Theme dropdown"
|
|
||||||
:options="themeStore.themeOptions"
|
|
||||||
:default-value="settings.theme"
|
|
||||||
:model-value="settings.theme"
|
|
||||||
class="theme-dropdown"
|
|
||||||
@change="
|
|
||||||
(e) => {
|
|
||||||
themeStore.setThemeState(e.option.toLowerCase())
|
|
||||||
settings.theme = themeStore.selectedTheme
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="advanced-rendering">
|
|
||||||
<span class="label__title">Advanced rendering</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Enables advanced rendering such as blur effects that may cause performance issues
|
|
||||||
without hardware-accelerated rendering.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="advanced-rendering"
|
|
||||||
:model-value="themeStore.advancedRendering"
|
|
||||||
:checked="themeStore.advancedRendering"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
themeStore.advancedRendering = e
|
|
||||||
settings.advanced_rendering = themeStore.advancedRendering
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="minimize-launcher">
|
|
||||||
<span class="label__title">Minimize launcher</span>
|
|
||||||
<span class="label__description"
|
|
||||||
>Minimize the launcher when a Minecraft process starts.</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="minimize-launcher"
|
|
||||||
:model-value="settings.hide_on_process_start"
|
|
||||||
:checked="settings.hide_on_process_start"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
settings.hide_on_process_start = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="getOS() != 'MacOS'" class="adjacent-input">
|
|
||||||
<label for="native-decorations">
|
|
||||||
<span class="label__title">Native decorations</span>
|
|
||||||
<span class="label__description">Use system window frame (app restart required).</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="native-decorations"
|
|
||||||
:model-value="settings.native_decorations"
|
|
||||||
:checked="settings.native_decorations"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
settings.native_decorations = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="opening-page">
|
|
||||||
<span class="label__title">Default landing page</span>
|
|
||||||
<span class="label__description">Change the page to which the launcher opens on.</span>
|
|
||||||
</label>
|
|
||||||
<DropdownSelect
|
|
||||||
id="opening-page"
|
|
||||||
name="Opening page dropdown"
|
|
||||||
:options="pageOptions"
|
|
||||||
:default-value="settings.default_page"
|
|
||||||
:model-value="settings.default_page"
|
|
||||||
class="opening-page"
|
|
||||||
@change="
|
|
||||||
(e) => {
|
|
||||||
settings.default_page = e.option
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Resource management</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="max-downloads">
|
|
||||||
<span class="label__title">Maximum concurrent downloads</span>
|
|
||||||
<span class="label__description">
|
|
||||||
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)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Slider
|
|
||||||
id="max-downloads"
|
|
||||||
v-model="settings.max_concurrent_downloads"
|
|
||||||
:min="1"
|
|
||||||
:max="10"
|
|
||||||
:step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="max-writes">
|
|
||||||
<span class="label__title">Maximum concurrent writes</span>
|
|
||||||
<span class="label__description">
|
|
||||||
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)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Slider
|
|
||||||
id="max-writes"
|
|
||||||
v-model="settings.max_concurrent_writes"
|
|
||||||
:min="1"
|
|
||||||
:max="50"
|
|
||||||
:step="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Privacy</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="opt-out-analytics">
|
|
||||||
<span class="label__title">Personalized ads</span>
|
|
||||||
<span class="label__description">
|
|
||||||
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.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="opt-out-analytics"
|
|
||||||
:model-value="settings.personalized_ads"
|
|
||||||
:checked="settings.personalized_ads"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
settings.personalized_ads = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="opt-out-analytics">
|
|
||||||
<span class="label__title">Telemetry</span>
|
|
||||||
<span class="label__description">
|
|
||||||
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
|
|
||||||
longer be collected.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="opt-out-analytics"
|
|
||||||
:model-value="settings.telemetry"
|
|
||||||
:checked="settings.telemetry"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
settings.telemetry = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="disable-discord-rpc">
|
|
||||||
<span class="label__title">Discord RPC</span>
|
|
||||||
<span class="label__description">
|
|
||||||
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. This does not
|
|
||||||
disable any instance-specific Discord Rich Presence integrations, such as those added by
|
|
||||||
mods. (app restart required to take effect)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="disable-discord-rpc"
|
|
||||||
v-model="settings.discord_rpc"
|
|
||||||
:checked="settings.discord_rpc"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Java settings</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
|
|
||||||
<label :for="'java-' + javaVersion">
|
|
||||||
<span class="label__title">Java {{ javaVersion }} location</span>
|
|
||||||
</label>
|
|
||||||
<JavaSelector
|
|
||||||
:id="'java-selector-' + javaVersion"
|
|
||||||
v-model="javaVersions[javaVersion]"
|
|
||||||
:version="javaVersion"
|
|
||||||
@update:model-value="updateJavaVersion"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<label for="java-args">
|
|
||||||
<span class="label__title">Java arguments</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="java-args"
|
|
||||||
v-model="settings.launchArgs"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter java arguments..."
|
|
||||||
/>
|
|
||||||
<label for="env-vars">
|
|
||||||
<span class="label__title">Environmental variables</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="env-vars"
|
|
||||||
v-model="settings.envVars"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter environmental variables..."
|
|
||||||
/>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="max-memory">
|
|
||||||
<span class="label__title">Java memory</span>
|
|
||||||
<span class="label__description">
|
|
||||||
The memory allocated to each instance when it is ran.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Slider
|
|
||||||
id="max-memory"
|
|
||||||
v-model="settings.memory.maximum"
|
|
||||||
:min="8"
|
|
||||||
:max="maxMemory"
|
|
||||||
:step="64"
|
|
||||||
unit="MB"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Hooks</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="pre-launch">
|
|
||||||
<span class="label__title">Pre launch</span>
|
|
||||||
<span class="label__description"> Ran before the instance is launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="pre-launch"
|
|
||||||
v-model="settings.hooks.pre_launch"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter pre-launch command..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="wrapper">
|
|
||||||
<span class="label__title">Wrapper</span>
|
|
||||||
<span class="label__description"> Wrapper command for launching Minecraft. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="wrapper"
|
|
||||||
v-model="settings.hooks.wrapper"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter wrapper command..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="post-exit">
|
|
||||||
<span class="label__title">Post exit</span>
|
|
||||||
<span class="label__description"> Ran after the game closes. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="post-exit"
|
|
||||||
v-model="settings.hooks.post_exit"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter post-exit command..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Window size</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="fullscreen">
|
|
||||||
<span class="label__title">Fullscreen</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Overwrites the options.txt file to start in full screen when launched.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="fullscreen"
|
|
||||||
:model-value="settings.force_fullscreen"
|
|
||||||
:checked="settings.force_fullscreen"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
settings.force_fullscreen = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="width">
|
|
||||||
<span class="label__title">Width</span>
|
|
||||||
<span class="label__description"> The width of the game window when launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="width"
|
|
||||||
v-model="settings.game_resolution[0]"
|
|
||||||
:disabled="settings.force_fullscreen"
|
|
||||||
autocomplete="off"
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter width..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="height">
|
|
||||||
<span class="label__title">Height</span>
|
|
||||||
<span class="label__description"> The height of the game window when launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="height"
|
|
||||||
v-model="settings.game_resolution[1]"
|
|
||||||
:disabled="settings.force_fullscreen"
|
|
||||||
autocomplete="off"
|
|
||||||
type="number"
|
|
||||||
class="input"
|
|
||||||
placeholder="Enter height..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">About</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<span class="label__title">App version</span>
|
|
||||||
<span class="label__description">Modrinth App v{{ version }} </span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.settings-page {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.installation-input {
|
|
||||||
width: 100% !important;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dropdown {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-divider {
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-directory {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
input {
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,6 +1,4 @@
|
|||||||
import Index from './Index.vue'
|
import Index from './Index.vue'
|
||||||
import Browse from './Browse.vue'
|
import Browse from './Browse.vue'
|
||||||
import Library from './Library.vue'
|
|
||||||
import Settings from './Settings.vue'
|
|
||||||
|
|
||||||
export { Index, Browse, Library, Settings }
|
export { Index, Browse }
|
||||||
|
@ -1,85 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="instance-container">
|
<div
|
||||||
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()">
|
class="p-6 pr-2 pb-4"
|
||||||
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||||
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
|
>
|
||||||
<div class="instance-info">
|
<ExportModal ref="exportModal" :instance="instance" />
|
||||||
<h2 class="name">{{ instance.name }}</h2>
|
<ContentPageHeader>
|
||||||
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
|
<template #icon>
|
||||||
|
<Avatar :src="icon" :alt="instance.name" size="96px" />
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
{{ instance.name }}
|
||||||
|
</template>
|
||||||
|
<template #summary> </template>
|
||||||
|
<template #stats>
|
||||||
|
<div class="flex items-center gap-2 font-semibold transform capitalize">
|
||||||
|
<GameIcon class="h-6 w-6 text-secondary" />
|
||||||
|
{{ instance.loader }} {{ instance.game_version }}
|
||||||
</div>
|
</div>
|
||||||
<span class="button-group">
|
</template>
|
||||||
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
|
<template #actions>
|
||||||
Installing...
|
<ButtonStyled v-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||||
</Button>
|
<button disabled>Installing...</button>
|
||||||
<Button
|
</ButtonStyled>
|
||||||
v-else-if="playing === true"
|
<template v-else>
|
||||||
color="danger"
|
<div class="flex gap-2">
|
||||||
class="instance-button"
|
<ButtonStyled v-if="playing === true" color="red" size="large">
|
||||||
@click="stopInstance('InstancePage')"
|
<button @click="stopInstance('InstancePage')">
|
||||||
>
|
<StopCircleIcon />
|
||||||
<StopCircleIcon />
|
Stop
|
||||||
Stop
|
</button>
|
||||||
</Button>
|
</ButtonStyled>
|
||||||
<Button
|
<ButtonStyled
|
||||||
v-else-if="playing === false && loading === false"
|
v-else-if="playing === false && loading === false"
|
||||||
color="primary"
|
color="brand"
|
||||||
class="instance-button"
|
size="large"
|
||||||
@click="startInstance('InstancePage')"
|
>
|
||||||
>
|
<button @click="startInstance('InstancePage')">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
Play
|
Play
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
</ButtonStyled>
|
||||||
v-else-if="loading === true && playing === false"
|
<ButtonStyled
|
||||||
disabled
|
v-else-if="loading === true && playing === false"
|
||||||
class="instance-button"
|
color="brand"
|
||||||
>
|
size="large"
|
||||||
Loading...
|
>
|
||||||
</Button>
|
<button disabled>Loading...</button>
|
||||||
<Button
|
</ButtonStyled>
|
||||||
v-tooltip="'Open instance folder'"
|
<ButtonStyled size="large" circular>
|
||||||
class="instance-button"
|
<RouterLink
|
||||||
@click="showProfileInFolder(instance.path)"
|
v-tooltip="'Instance settings'"
|
||||||
>
|
:to="`/instance/${encodeURIComponent(route.params.id)}/options`"
|
||||||
<FolderOpenIcon />
|
>
|
||||||
Folder
|
<SettingsIcon />
|
||||||
</Button>
|
</RouterLink>
|
||||||
</span>
|
</ButtonStyled>
|
||||||
<hr class="card-divider" />
|
<ButtonStyled size="large" type="transparent" circular>
|
||||||
<div class="pages-list">
|
<OverflowMenu
|
||||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/`" class="btn">
|
:options="[
|
||||||
<BoxIcon />
|
{
|
||||||
Content
|
id: 'open-folder',
|
||||||
</RouterLink>
|
action: () => showProfileInFolder(instance.path),
|
||||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/logs`" class="btn">
|
},
|
||||||
<FileIcon />
|
{
|
||||||
Logs
|
id: 'export-mrpack',
|
||||||
</RouterLink>
|
action: () => $refs.exportModal.show(),
|
||||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/options`" class="btn">
|
},
|
||||||
<SettingsIcon />
|
]"
|
||||||
Options
|
>
|
||||||
</RouterLink>
|
<MoreVerticalIcon />
|
||||||
</div>
|
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
||||||
</Card>
|
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||||
<PromotionWrapper ref="promo" class="mt-4" />
|
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||||
</div>
|
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||||
<div class="content">
|
</OverflowMenu>
|
||||||
<RouterView v-slot="{ Component }">
|
</ButtonStyled>
|
||||||
<template v-if="Component">
|
</div>
|
||||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
|
||||||
<component
|
|
||||||
:is="Component"
|
|
||||||
:instance="instance"
|
|
||||||
:options="options"
|
|
||||||
:offline="offline"
|
|
||||||
:playing="playing"
|
|
||||||
:versions="modrinthVersions"
|
|
||||||
:installed="instance.install_stage !== 'installed'"
|
|
||||||
></component>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
</template>
|
||||||
</RouterView>
|
</template>
|
||||||
</div>
|
</ContentPageHeader>
|
||||||
|
</div>
|
||||||
|
<div class="px-6">
|
||||||
|
<NavTabs :links="tabs" />
|
||||||
|
</div>
|
||||||
|
<div class="p-6 pt-4">
|
||||||
|
<RouterView v-slot="{ Component }">
|
||||||
|
<template v-if="Component">
|
||||||
|
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
:instance="instance"
|
||||||
|
:options="options"
|
||||||
|
:offline="offline"
|
||||||
|
:playing="playing"
|
||||||
|
:versions="modrinthVersions"
|
||||||
|
:installed="instance.install_stage !== 'installed'"
|
||||||
|
></component>
|
||||||
|
<template #fallback>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
</RouterView>
|
||||||
</div>
|
</div>
|
||||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #play> <PlayIcon /> Play </template>
|
||||||
@ -104,11 +126,18 @@
|
|||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Avatar, Card } from '@modrinth/ui'
|
|
||||||
import {
|
import {
|
||||||
BoxIcon,
|
Avatar,
|
||||||
|
ContentPageHeader,
|
||||||
|
ButtonStyled,
|
||||||
|
OverflowMenu,
|
||||||
|
LoadingIndicator,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import {
|
||||||
|
UserPlusIcon,
|
||||||
|
ServerIcon,
|
||||||
|
PackageIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
FileIcon,
|
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
@ -122,21 +151,24 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
GameIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { get, kill, run } from '@/helpers/profile'
|
import { get, kill, run } from '@/helpers/profile'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
import { process_listener, profile_listener } from '@/helpers/events'
|
import { process_listener, profile_listener } from '@/helpers/events'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ref, onUnmounted } from 'vue'
|
import { ref, onUnmounted, computed } from 'vue'
|
||||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@ -145,6 +177,17 @@ const breadcrumbs = useBreadcrumbs()
|
|||||||
|
|
||||||
const instance = ref(await get(route.params.id).catch(handleError))
|
const instance = ref(await get(route.params.id).catch(handleError))
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{
|
||||||
|
label: 'Content',
|
||||||
|
href: `/instance/${encodeURIComponent(route.params.id)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Logs',
|
||||||
|
href: `/instance/${encodeURIComponent(route.params.id)}/logs`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
breadcrumbs.setName(
|
breadcrumbs.setName(
|
||||||
'Instance',
|
'Instance',
|
||||||
instance.value.name.length > 40
|
instance.value.name.length > 40
|
||||||
@ -300,6 +343,10 @@ const unlistenProcesses = await process_listener((e) => {
|
|||||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
|
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const icon = computed(() =>
|
||||||
|
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
unlistenProfiles()
|
unlistenProfiles()
|
||||||
|
@ -1,331 +1,227 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card v-if="projects.length > 0" class="mod-card">
|
<template v-if="projects.length > 0">
|
||||||
<div class="dropdown-input">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<DropdownSelect
|
<div class="iconified-input flex-grow">
|
||||||
v-model="selectedProjectType"
|
|
||||||
:options="Object.keys(selectableProjectTypes)"
|
|
||||||
default-value="All"
|
|
||||||
name="project-type-dropdown"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
<div class="iconified-input">
|
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
<input
|
<input
|
||||||
v-model="searchFilter"
|
v-model="searchFilter"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="`Search ${search.length} ${(['All', 'Other'].includes(selectedProjectType)
|
:placeholder="`Search content...`"
|
||||||
? 'projects'
|
class="text-input search-input"
|
||||||
: selectedProjectType.toLowerCase()
|
|
||||||
).slice(0, search.length === 1 ? -1 : 64)}...`"
|
|
||||||
class="text-input"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<Button class="r-btn" @click="() => (searchFilter = '')">
|
<Button class="r-btn" @click="() => (searchFilter = '')">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<AddContentButton :instance="instance" />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div v-if="filterOptions.length > 1" class="flex flex-wrap gap-1 items-center pb-4">
|
||||||
v-tooltip="'Refresh projects'"
|
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||||
icon-only
|
<button
|
||||||
:disabled="refreshingProjects"
|
v-for="filter in filterOptions"
|
||||||
@click="refreshProjects"
|
:key="filter"
|
||||||
>
|
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||||
<UpdatedIcon />
|
@click="toggleArray(selectedFilters, filter.id)"
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="canUpdatePack"
|
|
||||||
:disabled="installing"
|
|
||||||
color="secondary"
|
|
||||||
@click="modpackVersionModal.show()"
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
{{ installing ? 'Updating' : 'Update modpack' }}
|
|
||||||
</Button>
|
|
||||||
<Button v-else-if="!isPackLocked" @click="exportModal.show()">
|
|
||||||
<PackageIcon />
|
|
||||||
Export modpack
|
|
||||||
</Button>
|
|
||||||
<Button v-if="!isPackLocked && projects.some((m) => m.outdated)" @click="updateAll">
|
|
||||||
<DownloadIcon />
|
|
||||||
Update all
|
|
||||||
</Button>
|
|
||||||
<AddContentButton v-if="!isPackLocked" :instance="instance" />
|
|
||||||
</Card>
|
|
||||||
<Pagination
|
|
||||||
v-if="projects.length > 0"
|
|
||||||
:page="currentPage"
|
|
||||||
:count="Math.ceil(search.length / 20)"
|
|
||||||
class="pagination-before"
|
|
||||||
:link-function="(page) => `?page=${page}`"
|
|
||||||
@switch-page="switchPage"
|
|
||||||
/>
|
|
||||||
<Card v-if="projects.length > 0" class="list-card">
|
|
||||||
<div class="table">
|
|
||||||
<div class="table-row table-head" :class="{ 'show-options': selected.length > 0 }">
|
|
||||||
<div class="table-cell table-text">
|
|
||||||
<Checkbox v-model="selectAll" class="select-checkbox" />
|
|
||||||
</div>
|
|
||||||
<div v-if="selected.length === 0" class="table-cell table-text name-cell actions-cell">
|
|
||||||
<Button class="transparent" @click="sortProjects('Name')">
|
|
||||||
Name
|
|
||||||
<DropdownIcon v-if="sortColumn === 'Name'" :class="{ down: ascending }" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-if="selected.length === 0" class="table-cell table-text version">
|
|
||||||
<Button class="transparent" @click="sortProjects('Version')">
|
|
||||||
Version
|
|
||||||
<DropdownIcon v-if="sortColumn === 'Version'" :class="{ down: ascending }" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-if="selected.length === 0" class="table-cell table-text actions-cell">
|
|
||||||
<Button class="transparent" @click="sortProjects('Enabled')">
|
|
||||||
Actions
|
|
||||||
<DropdownIcon v-if="sortColumn === 'Enabled'" :class="{ down: ascending }" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="options table-cell name-cell">
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
class="transparent share"
|
|
||||||
@click="() => (showingOptions = !showingOptions)"
|
|
||||||
@mouseover="selectedOption = 'Share'"
|
|
||||||
>
|
|
||||||
<MenuIcon :class="{ open: showingOptions }" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
class="transparent share"
|
|
||||||
@click="shareNames()"
|
|
||||||
@mouseover="selectedOption = 'Share'"
|
|
||||||
>
|
|
||||||
<ShareIcon />
|
|
||||||
Share
|
|
||||||
</Button>
|
|
||||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to remove mods' : ''">
|
|
||||||
<Button
|
|
||||||
:disabled="isPackLocked"
|
|
||||||
class="transparent trash"
|
|
||||||
@click="deleteWarning.show()"
|
|
||||||
@mouseover="selectedOption = 'Delete'"
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to update mods' : ''">
|
|
||||||
<Button
|
|
||||||
:disabled="isPackLocked || offline"
|
|
||||||
class="transparent update"
|
|
||||||
@click="updateSelected()"
|
|
||||||
@mouseover="selectedOption = 'Update'"
|
|
||||||
>
|
|
||||||
<UpdatedIcon />
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to toggle mods' : ''">
|
|
||||||
<Button
|
|
||||||
:disabled="isPackLocked"
|
|
||||||
class="transparent"
|
|
||||||
@click="toggleSelected()"
|
|
||||||
@mouseover="selectedOption = 'Toggle'"
|
|
||||||
>
|
|
||||||
<ToggleIcon />
|
|
||||||
Toggle
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showingOptions && selected.length > 0" class="more-box">
|
|
||||||
<section v-if="selectedOption === 'Share'" class="options">
|
|
||||||
<Button class="transparent" @click="shareNames()">
|
|
||||||
<TextInputIcon />
|
|
||||||
Share names
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="shareUrls()">
|
|
||||||
<GlobeIcon />
|
|
||||||
Share URLs
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="shareFileNames()">
|
|
||||||
<FileIcon />
|
|
||||||
Share file names
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="shareMarkdown()">
|
|
||||||
<CodeIcon />
|
|
||||||
Share as markdown
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
<section v-if="selectedOption === 'Delete'" class="options">
|
|
||||||
<Button class="transparent" @click="deleteWarning.show()">
|
|
||||||
<TrashIcon />
|
|
||||||
Delete selected
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="deleteDisabledWarning.show()">
|
|
||||||
<ToggleIcon />
|
|
||||||
Delete disabled
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
<section v-if="selectedOption === 'Update'" class="options">
|
|
||||||
<Button class="transparent" :disabled="offline" @click="updateAll()">
|
|
||||||
<UpdatedIcon />
|
|
||||||
Update all
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="selectUpdatable()">
|
|
||||||
<CheckIcon />
|
|
||||||
Select updatable
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
<section v-if="selectedOption === 'Toggle'" class="options">
|
|
||||||
<Button class="transparent" @click="enableAll()">
|
|
||||||
<CheckIcon />
|
|
||||||
Toggle on
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="disableAll()">
|
|
||||||
<XIcon />
|
|
||||||
Toggle off
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="hideShowAll()">
|
|
||||||
<EyeIcon v-if="hideNonSelected" />
|
|
||||||
<EyeOffIcon v-else />
|
|
||||||
{{ hideNonSelected ? 'Show' : 'Hide' }} untoggled
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="mod in search.slice((currentPage - 1) * 20, currentPage * 20)"
|
|
||||||
:key="mod.file_name"
|
|
||||||
class="table-row"
|
|
||||||
@contextmenu.prevent.stop="(c) => handleRightClick(c, mod)"
|
|
||||||
>
|
>
|
||||||
<div class="table-cell table-text checkbox">
|
{{ filter.formattedName }}
|
||||||
<Checkbox
|
</button>
|
||||||
:model-value="selectionMap.get(mod.path)"
|
</div>
|
||||||
class="select-checkbox"
|
<ContentListPanel
|
||||||
@update:model-value="(newValue) => selectionMap.set(mod.path, newValue)"
|
v-model="selectedFiles"
|
||||||
/>
|
:locked="isPackLocked"
|
||||||
</div>
|
:items="
|
||||||
<div class="table-cell table-text name-cell">
|
search.map((x) => {
|
||||||
<router-link
|
const item: ContentItem<any> = {
|
||||||
v-if="mod.slug"
|
path: x.path,
|
||||||
:to="{ path: `/project/${mod.slug}/`, query: { i: props.instance.path } }"
|
disabled: x.disabled,
|
||||||
:disabled="offline"
|
filename: x.file_name,
|
||||||
class="mod-content"
|
icon: x.icon,
|
||||||
|
title: x.name,
|
||||||
|
data: x,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.version) {
|
||||||
|
item.version = x.version
|
||||||
|
item.versionId = x.version
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.id) {
|
||||||
|
item.project = {
|
||||||
|
id: x.id,
|
||||||
|
link: { path: `/project/${x.id}`, query: { i: props.instance.path } },
|
||||||
|
linkProps: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.author) {
|
||||||
|
item.creator = {
|
||||||
|
name: x.author,
|
||||||
|
type: 'user',
|
||||||
|
id: x.author,
|
||||||
|
link: 'https://modrinth.com/user/' + x.author,
|
||||||
|
linkProps: { target: '_blank' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:sort-column="sortColumn"
|
||||||
|
:sort-ascending="ascending"
|
||||||
|
:update-sort="sortProjects"
|
||||||
|
>
|
||||||
|
<template v-if="selectedProjects.length > 0" #headers>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ButtonStyled
|
||||||
|
v-if="!isPackLocked && selectedProjects.some((m) => m.outdated)"
|
||||||
|
color="brand"
|
||||||
|
color-fill="text"
|
||||||
|
hover-color-fill="text"
|
||||||
>
|
>
|
||||||
<Avatar :src="mod.icon" />
|
<button @click="updateSelected()"><DownloadIcon /> Update</button>
|
||||||
<div class="mod-text">
|
</ButtonStyled>
|
||||||
<div class="title">{{ mod.name }}</div>
|
<ButtonStyled>
|
||||||
<span v-if="mod.author" class="no-wrap">by {{ mod.author }}</span>
|
<OverflowMenu
|
||||||
</div>
|
:options="[
|
||||||
</router-link>
|
{
|
||||||
<div v-else class="mod-content">
|
id: 'share-names',
|
||||||
<Avatar :src="mod.icon" />
|
action: () => shareNames(),
|
||||||
<span v-tooltip="`${mod.name}`" class="title">{{ mod.name }}</span>
|
},
|
||||||
</div>
|
{
|
||||||
</div>
|
id: 'share-file-names',
|
||||||
<div class="table-cell table-text version">
|
action: () => shareFileNames(),
|
||||||
<span v-tooltip="`${mod.version}`">{{ mod.version }}</span>
|
},
|
||||||
</div>
|
{
|
||||||
<div class="table-cell table-text manage">
|
id: 'share-urls',
|
||||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to remove mods.' : 'Remove project'">
|
action: () => shareUrls(),
|
||||||
<Button :disabled="isPackLocked" icon-only @click="removeMod(mod)">
|
},
|
||||||
<TrashIcon />
|
{
|
||||||
</Button>
|
id: 'share-markdown',
|
||||||
</div>
|
action: () => shareMarkdown(),
|
||||||
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator" />
|
},
|
||||||
<div
|
]"
|
||||||
v-else
|
|
||||||
v-tooltip="isPackLocked ? 'Unlock this instance to update mods.' : 'Update project'"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
:disabled="!mod.outdated || offline || isPackLocked"
|
|
||||||
icon-only
|
|
||||||
@click="updateProject(mod)"
|
|
||||||
>
|
>
|
||||||
<UpdatedIcon v-if="mod.outdated" />
|
<ShareIcon /> Share <DropdownIcon />
|
||||||
<CheckIcon v-else />
|
<template #share-names> <TextInputIcon /> Project names </template>
|
||||||
</Button>
|
<template #share-file-names> <FileIcon /> File names </template>
|
||||||
</div>
|
<template #share-urls> <LinkIcon /> Project links </template>
|
||||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to toggle mods.' : ''">
|
<template #share-markdown> <CodeIcon /> Markdown links </template>
|
||||||
<input
|
</OverflowMenu>
|
||||||
id="switch-1"
|
</ButtonStyled>
|
||||||
:disabled="isPackLocked"
|
<ButtonStyled v-if="selectedProjects.some((m) => m.disabled)">
|
||||||
autocomplete="off"
|
<button @click="enableAll()"><CheckCircleIcon /> Enable</button>
|
||||||
type="checkbox"
|
</ButtonStyled>
|
||||||
class="switch stylized-toggle"
|
<ButtonStyled v-if="selectedProjects.some((m) => !m.disabled)">
|
||||||
:checked="!mod.disabled"
|
<button @click="disableAll()"><SlashIcon /> Disable</button>
|
||||||
@change="toggleDisableMod(mod)"
|
</ButtonStyled>
|
||||||
/>
|
<ButtonStyled color="red">
|
||||||
</div>
|
<button @click="deleteSelected()"><TrashIcon /> Remove</button>
|
||||||
<Button
|
</ButtonStyled>
|
||||||
v-tooltip="`Show ${mod.file_name}`"
|
|
||||||
icon-only
|
|
||||||
@click="highlightModInProfile(instance.path, mod.path)"
|
|
||||||
>
|
|
||||||
<FolderOpenIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template #header-actions>
|
||||||
</Card>
|
<ButtonStyled type="transparent" color-fill="text" hover-color-fill="text">
|
||||||
<div v-else class="empty-prompt">
|
<button :disabled="refreshingProjects" class="w-max" @click="refreshProjects">
|
||||||
<div class="empty-icon">
|
<UpdatedIcon />
|
||||||
<AddProjectImage />
|
Refresh
|
||||||
</div>
|
</button>
|
||||||
<h3>No projects found</h3>
|
</ButtonStyled>
|
||||||
<p class="empty-subtitle">Add a project to get started</p>
|
<ButtonStyled
|
||||||
<AddContentButton :instance="instance" />
|
v-if="!isPackLocked && projects.some((m) => (m as any).outdated)"
|
||||||
</div>
|
type="transparent"
|
||||||
<Pagination
|
color="brand"
|
||||||
v-if="projects.length > 0"
|
color-fill="text"
|
||||||
:page="currentPage"
|
hover-color-fill="text"
|
||||||
:count="Math.ceil(search.length / 20)"
|
@click="updateAll"
|
||||||
class="pagination-after"
|
>
|
||||||
:link-function="(page) => `?page=${page}`"
|
<button class="w-max"><DownloadIcon /> Update all</button>
|
||||||
@switch-page="switchPage"
|
</ButtonStyled>
|
||||||
/>
|
<ButtonStyled
|
||||||
<ModalWrapper ref="deleteWarning" header="Are you sure?">
|
v-if="canUpdatePack"
|
||||||
<div class="modal-body">
|
type="transparent"
|
||||||
<div class="markdown-body">
|
color="brand"
|
||||||
<p>
|
color-fill="text"
|
||||||
Are you sure you want to remove
|
hover-color-fill="text"
|
||||||
<strong>{{ functionValues.length }} project(s)</strong> from {{ instance.name }}?
|
>
|
||||||
<br />
|
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
|
||||||
This action <strong>cannot</strong> be undone.
|
<DownloadIcon /> Update pack
|
||||||
</p>
|
</button>
|
||||||
</div>
|
</ButtonStyled>
|
||||||
<div class="button-group push-right">
|
</template>
|
||||||
<Button @click="deleteWarning.hide()"> Cancel </Button>
|
<template #actions="{ item }">
|
||||||
<Button color="danger" @click="deleteSelected">
|
<ButtonStyled
|
||||||
<TrashIcon />
|
v-if="!isPackLocked && (item.data as any).outdated"
|
||||||
Remove
|
type="transparent"
|
||||||
</Button>
|
color="brand"
|
||||||
</div>
|
circular
|
||||||
</div>
|
>
|
||||||
</ModalWrapper>
|
<button
|
||||||
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?">
|
v-tooltip="`Update`"
|
||||||
<div class="modal-body">
|
:disabled="(item.data as any).updating"
|
||||||
<div class="markdown-body">
|
@click="updateProject(item.data)"
|
||||||
<p>
|
|
||||||
Are you sure you want to remove
|
|
||||||
<strong
|
|
||||||
>{{ Array.from(projects.values()).filter((x) => x.disabled).length }} disabled
|
|
||||||
project(s)</strong
|
|
||||||
>
|
>
|
||||||
from {{ instance.name }}?
|
<DownloadIcon />
|
||||||
<br />
|
</button>
|
||||||
This action <strong>cannot</strong> be undone.
|
</ButtonStyled>
|
||||||
</p>
|
<div v-else class="w-[36px]"></div>
|
||||||
</div>
|
<ButtonStyled type="transparent" circular>
|
||||||
<div class="button-group push-right">
|
<button
|
||||||
<Button @click="deleteDisabledWarning.hide()"> Cancel </Button>
|
v-tooltip="item.disabled ? `Enable` : `Disable`"
|
||||||
<Button color="danger" @click="deleteDisabled">
|
@click="toggleDisableMod(item.data)"
|
||||||
<TrashIcon />
|
>
|
||||||
Remove
|
<CheckCircleIcon v-if="item.disabled" />
|
||||||
</Button>
|
<SlashIcon v-else />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled type="transparent" circular>
|
||||||
|
<OverflowMenu
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'show-file',
|
||||||
|
action: () => highlightModInProfile(instance.path, item.path),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-link',
|
||||||
|
shown: item.project !== undefined,
|
||||||
|
action: () => toggleDisableMod(item.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove',
|
||||||
|
color: 'red',
|
||||||
|
action: () => removeMod(item),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
direction="left"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
<template #show-file> <ExternalIcon /> Show file </template>
|
||||||
|
<template #copy-link> <ClipboardCopyIcon /> Copy link </template>
|
||||||
|
<template v-if="item.disabled" #toggle> <CheckCircleIcon /> Enable </template>
|
||||||
|
<template v-else #toggle> <SlashIcon /> Disable </template>
|
||||||
|
<template #remove> <TrashIcon /> Remove </template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</ContentListPanel>
|
||||||
|
</template>
|
||||||
|
<div v-else class="w-full flex flex-col items-center justify-center mt-6 max-w-[48rem] mx-auto">
|
||||||
|
<div class="top-box w-full">
|
||||||
|
<div class="flex items-center gap-6 w-[32rem] mx-auto">
|
||||||
|
<img src="@/assets/sad-modrinth-bot.webp" class="h-24" />
|
||||||
|
<span class="text-contrast font-bold text-xl"
|
||||||
|
>You haven't added any content to this instance yet.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
<div class="top-box-divider"></div>
|
||||||
|
<div class="flex items-center gap-6 py-4">
|
||||||
|
<AddContentButton :instance="instance" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ShareModalWrapper
|
<ShareModalWrapper
|
||||||
ref="shareModal"
|
ref="shareModal"
|
||||||
share-title="Sharing modpack content"
|
share-title="Sharing modpack content"
|
||||||
@ -340,34 +236,30 @@
|
|||||||
:versions="props.versions"
|
:versions="props.versions"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
|
ExternalIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
CheckIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
FolderOpenIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
GlobeIcon,
|
|
||||||
FileIcon,
|
FileIcon,
|
||||||
EyeIcon,
|
|
||||||
EyeOffIcon,
|
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
FilterIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
SlashIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import { Button, ButtonStyled, ContentListPanel, OverflowMenu } from '@modrinth/ui'
|
||||||
Pagination,
|
|
||||||
DropdownSelect,
|
|
||||||
Checkbox,
|
|
||||||
AnimatedLogo,
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
|
import type { ComputedRef } from 'vue'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||||
import {
|
import {
|
||||||
add_project_from_path,
|
add_project_from_path,
|
||||||
get_projects,
|
get_projects,
|
||||||
@ -379,7 +271,7 @@ import {
|
|||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { highlightModInProfile } from '@/helpers/utils.js'
|
import { highlightModInProfile } from '@/helpers/utils.js'
|
||||||
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
|
import { TextInputIcon } from '@/assets/icons'
|
||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
import AddContentButton from '@/components/ui/AddContentButton.vue'
|
import AddContentButton from '@/components/ui/AddContentButton.vue'
|
||||||
@ -390,9 +282,9 @@ import {
|
|||||||
get_version_many,
|
get_version_many,
|
||||||
} from '@/helpers/cache.js'
|
} from '@/helpers/cache.js'
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
@ -433,7 +325,6 @@ onUnmounted(() => {
|
|||||||
unlistenProfiles()
|
unlistenProfiles()
|
||||||
})
|
})
|
||||||
|
|
||||||
const showingOptions = ref(false)
|
|
||||||
const isPackLocked = computed(() => {
|
const isPackLocked = computed(() => {
|
||||||
return props.instance.linked_data && props.instance.linked_data.locked
|
return props.instance.linked_data && props.instance.linked_data.locked
|
||||||
})
|
})
|
||||||
@ -444,9 +335,14 @@ const canUpdatePack = computed(() => {
|
|||||||
const exportModal = ref(null)
|
const exportModal = ref(null)
|
||||||
|
|
||||||
const projects = ref([])
|
const projects = ref([])
|
||||||
|
const selectedFiles = ref([])
|
||||||
|
const selectedProjects = computed(() =>
|
||||||
|
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
||||||
|
)
|
||||||
|
|
||||||
const selectionMap = ref(new Map())
|
const selectionMap = ref(new Map())
|
||||||
|
|
||||||
const initProjects = async (cacheBehaviour) => {
|
const initProjects = async (cacheBehaviour?) => {
|
||||||
const newProjects = []
|
const newProjects = []
|
||||||
|
|
||||||
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
|
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
|
||||||
@ -504,6 +400,7 @@ const initProjects = async (cacheBehaviour) => {
|
|||||||
icon: project.icon_url,
|
icon: project.icon_url,
|
||||||
disabled: file.file_name.endsWith('.disabled'),
|
disabled: file.file_name.endsWith('.disabled'),
|
||||||
updateVersion: file.update_version_id,
|
updateVersion: file.update_version_id,
|
||||||
|
updated: dayjs(version.date_published),
|
||||||
outdated: !!file.update_version_id,
|
outdated: !!file.update_version_id,
|
||||||
project_type: project.project_type,
|
project_type: project.project_type,
|
||||||
id: project.id,
|
id: project.id,
|
||||||
@ -545,19 +442,77 @@ await initProjects()
|
|||||||
const modpackVersionModal = ref(null)
|
const modpackVersionModal = ref(null)
|
||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||||
|
|
||||||
|
const vintl = useVIntl()
|
||||||
|
const { formatMessage } = vintl
|
||||||
|
|
||||||
|
type FilterOption = {
|
||||||
|
id: string
|
||||||
|
formattedName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
updatesAvailableFilter: {
|
||||||
|
id: 'instance.filter.updates-available',
|
||||||
|
defaultMessage: 'Updates available',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||||
|
const options: FilterOption[] = []
|
||||||
|
|
||||||
|
const frequency = projects.value.reduce((map, item) => {
|
||||||
|
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||||
|
return map
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const types = Object.keys(frequency).sort((a, b) => frequency[b] - frequency[a])
|
||||||
|
|
||||||
|
types.forEach((type) => {
|
||||||
|
options.push({
|
||||||
|
id: type,
|
||||||
|
formattedName: formatProjectType(type) + 's',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isPackLocked.value && projects.value.some((m) => m.outdated)) {
|
||||||
|
options.push({
|
||||||
|
id: 'updates',
|
||||||
|
formattedName: formatMessage(messages.updatesAvailableFilter),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedFilters = ref([])
|
||||||
|
const filteredProjects = computed(() => {
|
||||||
|
const updatesFilter = selectedFilters.value.includes('updates')
|
||||||
|
|
||||||
|
const typeFilters = selectedFilters.value.filter((filter) => filter !== 'updates')
|
||||||
|
|
||||||
|
return projects.value.filter((project) => {
|
||||||
|
return (
|
||||||
|
(typeFilters.length === 0 || typeFilters.includes(project.project_type)) &&
|
||||||
|
(!updatesFilter || project.outdated)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleArray(array, value) {
|
||||||
|
if (array.includes(value)) {
|
||||||
|
array.splice(array.indexOf(value), 1)
|
||||||
|
} else {
|
||||||
|
array.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const searchFilter = ref('')
|
const searchFilter = ref('')
|
||||||
const selectAll = ref(false)
|
const selectAll = ref(false)
|
||||||
const selectedProjectType = ref('All')
|
const selectedProjectType = ref('All')
|
||||||
const deleteWarning = ref(null)
|
|
||||||
const deleteDisabledWarning = ref(null)
|
|
||||||
const hideNonSelected = ref(false)
|
const hideNonSelected = ref(false)
|
||||||
const selectedOption = ref('Share')
|
|
||||||
const shareModal = ref(null)
|
const shareModal = ref(null)
|
||||||
const ascending = ref(true)
|
const ascending = ref(true)
|
||||||
const sortColumn = ref('Name')
|
const sortColumn = ref('Name')
|
||||||
const currentPage = ref(1)
|
|
||||||
|
|
||||||
watch([searchFilter, selectedProjectType], () => (currentPage.value = 1))
|
|
||||||
|
|
||||||
const selected = computed(() =>
|
const selected = computed(() =>
|
||||||
Array.from(selectionMap.value)
|
Array.from(selectionMap.value)
|
||||||
@ -570,7 +525,7 @@ const selected = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const functionValues = computed(() =>
|
const functionValues = computed(() =>
|
||||||
selected.value.length > 0 ? selected.value : Array.from(projects.value.values()),
|
selectedProjects.value.length > 0 ? selectedProjects.value : Array.from(projects.value.values()),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectableProjectTypes = computed(() => {
|
const selectableProjectTypes = computed(() => {
|
||||||
@ -586,7 +541,7 @@ const selectableProjectTypes = computed(() => {
|
|||||||
|
|
||||||
const search = computed(() => {
|
const search = computed(() => {
|
||||||
const projectType = selectableProjectTypes.value[selectedProjectType.value]
|
const projectType = selectableProjectTypes.value[selectedProjectType.value]
|
||||||
const filtered = projects.value
|
const filtered = filteredProjects.value
|
||||||
.filter((mod) => {
|
.filter((mod) => {
|
||||||
return (
|
return (
|
||||||
mod.name.toLowerCase().includes(searchFilter.value.toLowerCase()) &&
|
mod.name.toLowerCase().includes(searchFilter.value.toLowerCase()) &&
|
||||||
@ -600,43 +555,19 @@ const search = computed(() => {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
return updateSort(filtered)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateSort = (projects) => {
|
|
||||||
switch (sortColumn.value) {
|
switch (sortColumn.value) {
|
||||||
case 'Version':
|
case 'Updated':
|
||||||
return projects.slice().sort((a, b) => {
|
return filtered.slice().sort((a, b) => {
|
||||||
if (a.version < b.version) {
|
if (a.updated < b.updated) {
|
||||||
return ascending.value ? -1 : 1
|
|
||||||
}
|
|
||||||
if (a.version > b.version) {
|
|
||||||
return ascending.value ? 1 : -1
|
return ascending.value ? 1 : -1
|
||||||
}
|
}
|
||||||
return 0
|
if (a.updated > b.updated) {
|
||||||
})
|
|
||||||
case 'Author':
|
|
||||||
return projects.slice().sort((a, b) => {
|
|
||||||
if (a.author < b.author) {
|
|
||||||
return ascending.value ? -1 : 1
|
|
||||||
}
|
|
||||||
if (a.author > b.author) {
|
|
||||||
return ascending.value ? 1 : -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
case 'Enabled':
|
|
||||||
return projects.slice().sort((a, b) => {
|
|
||||||
if (a.disabled && !b.disabled) {
|
|
||||||
return ascending.value ? 1 : -1
|
|
||||||
}
|
|
||||||
if (!a.disabled && b.disabled) {
|
|
||||||
return ascending.value ? -1 : 1
|
return ascending.value ? -1 : 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
return projects.slice().sort((a, b) => {
|
return filtered.slice().sort((a, b) => {
|
||||||
if (a.name < b.name) {
|
if (a.name < b.name) {
|
||||||
return ascending.value ? -1 : 1
|
return ascending.value ? -1 : 1
|
||||||
}
|
}
|
||||||
@ -646,7 +577,7 @@ const updateSort = (projects) => {
|
|||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const sortProjects = (filter) => {
|
const sortProjects = (filter) => {
|
||||||
if (sortColumn.value === filter) {
|
if (sortColumn.value === filter) {
|
||||||
@ -690,14 +621,6 @@ const updateAll = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectUpdatable = () => {
|
|
||||||
for (const project of projects.value) {
|
|
||||||
if (project.outdated) {
|
|
||||||
selectionMap.value.set(project.path, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateProject = async (mod) => {
|
const updateProject = async (mod) => {
|
||||||
mod.updating = true
|
mod.updating = true
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||||
@ -753,6 +676,7 @@ const toggleDisableMod = async (mod) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeMod = async (mod) => {
|
const removeMod = async (mod) => {
|
||||||
|
console.log(mod)
|
||||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||||
|
|
||||||
@ -771,20 +695,9 @@ const deleteSelected = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
projects.value = projects.value.filter((x) => !x.selected)
|
projects.value = projects.value.filter((x) => !x.selected)
|
||||||
deleteWarning.value.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteDisabled = async () => {
|
|
||||||
for (const project of Array.of(projects.value.values().filter((x) => x.disabled))) {
|
|
||||||
await remove_project(props.instance.path, project.path).catch(handleError)
|
|
||||||
}
|
|
||||||
|
|
||||||
projects.value = projects.value.filter((x) => !x.selected)
|
|
||||||
deleteDisabledWarning.value.hide()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareNames = async () => {
|
const shareNames = async () => {
|
||||||
console.log(functionValues.value)
|
|
||||||
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,12 +727,6 @@ const shareMarkdown = async () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSelected = async () => {
|
|
||||||
for (const project of functionValues.value) {
|
|
||||||
await toggleDisableMod(project, !project.disabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSelected = async () => {
|
const updateSelected = async () => {
|
||||||
const promises = []
|
const promises = []
|
||||||
for (const project of functionValues.value) {
|
for (const project of functionValues.value) {
|
||||||
@ -829,35 +736,23 @@ const updateSelected = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const enableAll = async () => {
|
const enableAll = async () => {
|
||||||
|
const promises = []
|
||||||
for (const project of functionValues.value) {
|
for (const project of functionValues.value) {
|
||||||
if (project.disabled) {
|
if (project.disabled) {
|
||||||
await toggleDisableMod(project, false)
|
promises.push(toggleDisableMod(project))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await Promise.all(promises).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableAll = async () => {
|
const disableAll = async () => {
|
||||||
|
const promises = []
|
||||||
for (const project of functionValues.value) {
|
for (const project of functionValues.value) {
|
||||||
if (!project.disabled) {
|
if (!project.disabled) {
|
||||||
await toggleDisableMod(project, false)
|
promises.push(toggleDisableMod(project))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
await Promise.all(promises).catch(handleError)
|
||||||
|
|
||||||
const hideShowAll = async () => {
|
|
||||||
hideNonSelected.value = !hideNonSelected.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRightClick = (event, mod) => {
|
|
||||||
if (mod.slug && mod.project_type) {
|
|
||||||
props.options.showMenu(
|
|
||||||
event,
|
|
||||||
{
|
|
||||||
link: `https://modrinth.com/${mod.project_type}/${mod.slug}`,
|
|
||||||
},
|
|
||||||
[{ name: 'open_link' }, { name: 'copy_link' }],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectAll, () => {
|
watch(selectAll, () => {
|
||||||
@ -868,10 +763,6 @@ watch(selectAll, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const switchPage = (page) => {
|
|
||||||
currentPage.value = page
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshingProjects = ref(false)
|
const refreshingProjects = ref(false)
|
||||||
async function refreshProjects() {
|
async function refreshProjects() {
|
||||||
refreshingProjects.value = true
|
refreshingProjects.value = true
|
||||||
@ -1173,16 +1064,6 @@ onUnmounted(() => {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.updating-indicator {
|
|
||||||
height: 2.25rem !important;
|
|
||||||
width: 2.25rem !important;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: 1.25rem !important;
|
|
||||||
width: 1.25rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-checkbox {
|
.select-checkbox {
|
||||||
button.checkbox {
|
button.checkbox {
|
||||||
border: none;
|
border: none;
|
||||||
@ -1190,13 +1071,23 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-input {
|
.search-input {
|
||||||
.selected {
|
min-height: 2.25rem;
|
||||||
height: 2.5rem;
|
background-color: var(--color-raised-bg);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-after {
|
.top-box {
|
||||||
margin-bottom: 5rem;
|
background-image: radial-gradient(
|
||||||
|
50% 100% at 50% 100%,
|
||||||
|
var(--color-brand-highlight) 10%,
|
||||||
|
#ffffff00 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-box-divider {
|
||||||
|
background-image: linear-gradient(90deg, #ffffff00 0%, var(--color-brand) 50%, #ffffff00 100%);
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -897,7 +897,6 @@ async function saveGvLoaderEdits() {
|
|||||||
.change-versions-modal {
|
.change-versions-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1rem;
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
:deep(.animated-dropdown .options) {
|
:deep(.animated-dropdown .options) {
|
||||||
|
17
apps/app-frontend/src/pages/library/Custom.vue
Normal file
17
apps/app-frontend/src/pages/library/Custom.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import GridDisplay from '@/components/GridDisplay.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
instances: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<GridDisplay
|
||||||
|
v-if="instances.length > 0"
|
||||||
|
label="Instances"
|
||||||
|
:instances="instances.filter((i) => !i.linked_data)"
|
||||||
|
/>
|
||||||
|
</template>
|
17
apps/app-frontend/src/pages/library/Downloaded.vue
Normal file
17
apps/app-frontend/src/pages/library/Downloaded.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import GridDisplay from '@/components/GridDisplay.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
instances: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<GridDisplay
|
||||||
|
v-if="instances.length > 0"
|
||||||
|
label="Instances"
|
||||||
|
:instances="instances.filter((i) => i.linked_data)"
|
||||||
|
/>
|
||||||
|
</template>
|
@ -1,20 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||||
import GridDisplay from '@/components/GridDisplay.vue'
|
|
||||||
import { list } from '@/helpers/profile.js'
|
import { list } from '@/helpers/profile.js'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button } from '@modrinth/ui'
|
||||||
import { PlusIcon } from '@modrinth/assets'
|
import { PlusIcon } from '@modrinth/assets'
|
||||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||||
import { NewInstanceImage } from '@/assets/icons'
|
import { NewInstanceImage } from '@/assets/icons'
|
||||||
import { hide_ads_window } from '@/helpers/ads.js'
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
hide_ads_window(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
@ -40,17 +35,31 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
<div class="p-6 flex flex-col gap-3">
|
||||||
<div v-else class="no-instance">
|
<h1 class="m-0 text-2xl">Library</h1>
|
||||||
<div class="icon">
|
<NavTabs
|
||||||
<NewInstanceImage />
|
:links="[
|
||||||
|
{ label: 'All instances', href: `/library` },
|
||||||
|
{ label: 'Downloaded', href: `/library/downloaded` },
|
||||||
|
{ label: 'Custom', href: `/library/custom` },
|
||||||
|
{ label: 'Shared with me', href: `/library/shared`, shown: false },
|
||||||
|
{ label: 'Saved', href: `/library/saved`, shown: false },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<template v-if="instances.length > 0">
|
||||||
|
<RouterView :instances="instances" />
|
||||||
|
</template>
|
||||||
|
<div v-else class="no-instance">
|
||||||
|
<div class="icon">
|
||||||
|
<NewInstanceImage />
|
||||||
|
</div>
|
||||||
|
<h3>No instances found</h3>
|
||||||
|
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
|
||||||
|
<PlusIcon />
|
||||||
|
Create new instance
|
||||||
|
</Button>
|
||||||
|
<InstanceCreationModal ref="installationModal" />
|
||||||
</div>
|
</div>
|
||||||
<h3>No instances found</h3>
|
|
||||||
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
|
|
||||||
<PlusIcon />
|
|
||||||
Create new instance
|
|
||||||
</Button>
|
|
||||||
<InstanceCreationModal ref="installationModal" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
13
apps/app-frontend/src/pages/library/Overview.vue
Normal file
13
apps/app-frontend/src/pages/library/Overview.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script setup>
|
||||||
|
import GridDisplay from '@/components/GridDisplay.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
instances: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||||
|
</template>
|
6
apps/app-frontend/src/pages/library/index.js
Normal file
6
apps/app-frontend/src/pages/library/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Index from './Index.vue'
|
||||||
|
import Overview from './Overview.vue'
|
||||||
|
import Downloaded from './Downloaded.vue'
|
||||||
|
import Custom from './Custom.vue'
|
||||||
|
|
||||||
|
export { Index, Overview, Downloaded, Custom }
|
@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card>
|
<Card>
|
||||||
<div class="markdown-body" v-html="renderHighlightedString(project?.body ?? '')" />
|
<ProjectPageDescription :description="project.body" />
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { renderHighlightedString } from '@modrinth/utils'
|
import { Card, ProjectPageDescription } from '@modrinth/ui'
|
||||||
import { Card } from '@modrinth/ui'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
project: {
|
project: {
|
||||||
@ -21,22 +20,3 @@ export default {
|
|||||||
name: 'Description',
|
name: 'Description',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.markdown-body {
|
|
||||||
:deep(table) {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(hr),
|
|
||||||
:deep(h1),
|
|
||||||
:deep(h2) {
|
|
||||||
max-width: max(60rem, 90%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(ul),
|
|
||||||
:deep(ol) {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -198,7 +198,7 @@ document.addEventListener('keypress', keyListener)
|
|||||||
|
|
||||||
.expanded-image-modal {
|
.expanded-image-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 11;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -1,248 +1,151 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="root-container">
|
<div>
|
||||||
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()">
|
<Teleport to="#sidebar-teleport-target">
|
||||||
<Card v-if="instance" class="small-instance">
|
<ProjectSidebarCompatibility
|
||||||
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
|
:project="data"
|
||||||
<Avatar
|
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
class="project-sidebar-section"
|
||||||
:alt="instance.name"
|
/>
|
||||||
size="sm"
|
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
|
||||||
/>
|
<ProjectSidebarCreators
|
||||||
<div class="small-instance_info">
|
:organization="null"
|
||||||
<span class="title">{{
|
:members="members"
|
||||||
instance.name.length > 20 ? instance.name.substring(0, 20) + '...' : instance.name
|
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||||
}}</span>
|
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||||
<span>
|
link-target="_blank"
|
||||||
{{ instance.loader.charAt(0).toUpperCase() + instance.loader.slice(1) }}
|
class="project-sidebar-section"
|
||||||
{{ instance.game_version }}
|
/>
|
||||||
</span>
|
<ProjectSidebarDetails
|
||||||
</div>
|
:project="data"
|
||||||
</router-link>
|
:has-versions="versions.length > 0"
|
||||||
</Card>
|
:link-target="`_blank`"
|
||||||
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
|
class="project-sidebar-section"
|
||||||
<Avatar size="md" :src="data.icon_url" />
|
/>
|
||||||
<div class="instance-info">
|
</Teleport>
|
||||||
<h2 class="name">{{ data.title }}</h2>
|
<div class="flex flex-col gap-4 p-6">
|
||||||
{{ data.description }}
|
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||||
</div>
|
<template v-if="data">
|
||||||
<Categories
|
<Teleport v-if="themeStore.featureFlag_projectBackground" to="#background-teleport-target">
|
||||||
class="tags"
|
<ProjectBackgroundGradient :project="data" />
|
||||||
:categories="
|
</Teleport>
|
||||||
categories.filter(
|
<ProjectHeader :project="data">
|
||||||
(cat) => data.categories.includes(cat.name) && cat.project_type === 'mod',
|
<template #actions>
|
||||||
)
|
<ButtonStyled size="large" color="brand">
|
||||||
"
|
<button
|
||||||
type="ignored"
|
v-tooltip="installed ? `This project is already installed` : null"
|
||||||
>
|
:disabled="installed || installing"
|
||||||
<EnvironmentIndicator
|
@click="install(null)"
|
||||||
:client-side="data.client_side"
|
>
|
||||||
:server-side="data.server_side"
|
<DownloadIcon v-if="!installed && !installing" />
|
||||||
:type="data.project_type"
|
<CheckIcon v-else-if="installed" />
|
||||||
/>
|
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||||
</Categories>
|
</button>
|
||||||
<hr class="card-divider" />
|
</ButtonStyled>
|
||||||
<div class="button-group">
|
<ButtonStyled size="large" circular type="transparent">
|
||||||
<Button
|
<OverflowMenu
|
||||||
color="primary"
|
:tooltip="`More options`"
|
||||||
class="instance-button"
|
:options="[
|
||||||
:disabled="installed === true || installing === true"
|
{
|
||||||
@click="install(null)"
|
id: 'follow',
|
||||||
>
|
disabled: true,
|
||||||
<DownloadIcon v-if="!installed && !installing" />
|
tooltip: 'Coming soon',
|
||||||
<CheckIcon v-else-if="installed" />
|
action: () => {},
|
||||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
},
|
||||||
</Button>
|
{
|
||||||
<a
|
id: 'save',
|
||||||
:href="`https://modrinth.com/${data.project_type}/${data.slug}`"
|
disabled: true,
|
||||||
rel="external"
|
tooltip: 'Coming soon',
|
||||||
class="btn"
|
action: () => {},
|
||||||
>
|
},
|
||||||
<ExternalIcon />
|
{
|
||||||
Site
|
id: 'open-in-browser',
|
||||||
</a>
|
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||||
</div>
|
external: true,
|
||||||
</Card>
|
},
|
||||||
<PromotionWrapper ref="promo" />
|
{
|
||||||
<Card class="sidebar-card">
|
divider: true,
|
||||||
<div class="stats">
|
},
|
||||||
<div class="stat">
|
{
|
||||||
<DownloadIcon aria-hidden="true" />
|
id: 'report',
|
||||||
<p>
|
color: 'red',
|
||||||
<strong>{{ formatNumber(data.downloads) }}</strong>
|
hoverFilled: true,
|
||||||
<span class="stat-label"> download<span v-if="data.downloads !== '1'">s</span></span>
|
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||||
</p>
|
},
|
||||||
</div>
|
]"
|
||||||
<div class="stat">
|
aria-label="More options"
|
||||||
<HeartIcon aria-hidden="true" />
|
>
|
||||||
<p>
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<strong>{{ formatNumber(data.followers) }}</strong>
|
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||||
<span class="stat-label"> follower<span v-if="data.followers !== '1'">s</span></span>
|
<template #follow> <HeartIcon /> Follow </template>
|
||||||
</p>
|
<template #save> <BookmarkIcon /> Save </template>
|
||||||
</div>
|
<template #report> <ReportIcon /> Report </template>
|
||||||
<div class="stat date">
|
</OverflowMenu>
|
||||||
<CalendarIcon aria-hidden="true" />
|
</ButtonStyled>
|
||||||
<span
|
</template>
|
||||||
><span class="date-label">Created </span> {{ dayjs(data.published).fromNow() }}</span
|
</ProjectHeader>
|
||||||
>
|
<NavTabs
|
||||||
</div>
|
|
||||||
<div class="stat date">
|
|
||||||
<UpdatedIcon aria-hidden="true" />
|
|
||||||
<span
|
|
||||||
><span class="date-label">Updated </span> {{ dayjs(data.updated).fromNow() }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="button-group">
|
|
||||||
<Button class="instance-button" disabled>
|
|
||||||
<ReportIcon />
|
|
||||||
Report
|
|
||||||
</Button>
|
|
||||||
<Button class="instance-button" disabled>
|
|
||||||
<HeartIcon />
|
|
||||||
Follow
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="links">
|
|
||||||
<a
|
|
||||||
v-if="data.issues_url"
|
|
||||||
:href="data.issues_url"
|
|
||||||
class="title"
|
|
||||||
rel="noopener nofollow ugc external"
|
|
||||||
>
|
|
||||||
<IssuesIcon aria-hidden="true" />
|
|
||||||
<span>Issues</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="data.source_url"
|
|
||||||
:href="data.source_url"
|
|
||||||
class="title"
|
|
||||||
rel="noopener nofollow ugc external"
|
|
||||||
>
|
|
||||||
<CodeIcon aria-hidden="true" />
|
|
||||||
<span>Source</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="data.wiki_url"
|
|
||||||
:href="data.wiki_url"
|
|
||||||
class="title"
|
|
||||||
rel="noopener nofollow ugc external"
|
|
||||||
>
|
|
||||||
<WikiIcon aria-hidden="true" />
|
|
||||||
<span>Wiki</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="data.discord_url"
|
|
||||||
:href="data.discord_url"
|
|
||||||
class="title"
|
|
||||||
rel="noopener nofollow ugc external"
|
|
||||||
>
|
|
||||||
<DiscordIcon aria-hidden="true" />
|
|
||||||
<span>Discord</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-for="(donation, index) in data.donation_urls"
|
|
||||||
:key="index"
|
|
||||||
:href="donation.url"
|
|
||||||
rel="noopener nofollow ugc external"
|
|
||||||
>
|
|
||||||
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
|
|
||||||
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
|
|
||||||
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
|
|
||||||
<PaypalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
|
|
||||||
<OpenCollectiveIcon v-else-if="donation.id === 'open-collective'" aria-hidden="true" />
|
|
||||||
<HeartIcon v-else-if="donation.id === 'github'" />
|
|
||||||
<CoinsIcon v-else />
|
|
||||||
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
|
|
||||||
<span v-else-if="donation.id === 'patreon'">Patreon</span>
|
|
||||||
<span v-else-if="donation.id === 'paypal'">PayPal</span>
|
|
||||||
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
|
|
||||||
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
|
|
||||||
<span v-else>Donate</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div v-if="data" class="content-container">
|
|
||||||
<Card class="tabs">
|
|
||||||
<NavRow
|
|
||||||
v-if="data.gallery.length > 0"
|
|
||||||
:links="[
|
:links="[
|
||||||
{
|
{
|
||||||
label: 'Description',
|
label: 'Description',
|
||||||
href: `/project/${$route.params.id}/`,
|
href: `/project/${$route.params.id}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Versions',
|
label: 'Versions',
|
||||||
href: `/project/${$route.params.id}/versions`,
|
href: `/project/${$route.params.id}/versions`,
|
||||||
|
subpages: ['version'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Gallery',
|
label: 'Gallery',
|
||||||
href: `/project/${$route.params.id}/gallery`,
|
href: `/project/${$route.params.id}/gallery`,
|
||||||
|
shown: data.gallery.length > 0,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<NavRow
|
<RouterView
|
||||||
v-else
|
:project="data"
|
||||||
:links="[
|
:versions="versions"
|
||||||
{
|
:members="members"
|
||||||
label: 'Description',
|
:instance="instance"
|
||||||
href: `/project/${$route.params.id}/`,
|
:install="install"
|
||||||
},
|
:installed="installed"
|
||||||
{
|
:installing="installing"
|
||||||
label: 'Versions',
|
:installed-version="installedVersion"
|
||||||
href: `/project/${$route.params.id}/versions`,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</template>
|
||||||
<RouterView
|
<template v-else> Project data coult not be loaded. </template>
|
||||||
:project="data"
|
|
||||||
:versions="versions"
|
|
||||||
:members="members"
|
|
||||||
:instance="instance"
|
|
||||||
:install="install"
|
|
||||||
:installed="installed"
|
|
||||||
:installing="installing"
|
|
||||||
:installed-version="installedVersion"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||||
|
<template #install> <DownloadIcon /> Install </template>
|
||||||
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||||
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||||
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
|
||||||
<template #install> <DownloadIcon /> Install </template>
|
|
||||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
|
||||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
|
||||||
</ContextMenu>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
|
BookmarkIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
ReportIcon,
|
ReportIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
UpdatedIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
WikiIcon,
|
|
||||||
CoinsIcon,
|
|
||||||
CodeIcon,
|
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui'
|
|
||||||
import { formatNumber } from '@modrinth/utils'
|
|
||||||
import {
|
import {
|
||||||
BuyMeACoffeeIcon,
|
ProjectHeader,
|
||||||
DiscordIcon,
|
ProjectSidebarCompatibility,
|
||||||
PatreonIcon,
|
ButtonStyled,
|
||||||
PaypalIcon,
|
OverflowMenu,
|
||||||
KoFiIcon,
|
ProjectSidebarLinks,
|
||||||
OpenCollectiveIcon,
|
ProjectSidebarCreators,
|
||||||
} from '@/assets/external'
|
ProjectSidebarDetails,
|
||||||
import { get_categories } from '@/helpers/tags'
|
ProjectBackgroundGradient,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
|
||||||
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
@ -250,16 +153,18 @@ import { useRoute } from 'vue-router'
|
|||||||
import { ref, shallowRef, watch } from 'vue'
|
import { ref, shallowRef, watch } from 'vue'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
||||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
|
import { useTheming } from '@/store/state.js'
|
||||||
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const options = ref(null)
|
const options = ref(null)
|
||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
@ -273,6 +178,11 @@ const instanceProjects = ref(null)
|
|||||||
const installed = ref(false)
|
const installed = ref(false)
|
||||||
const installedVersion = ref(null)
|
const installedVersion = ref(null)
|
||||||
|
|
||||||
|
const [allLoaders, allGameVersions] = await Promise.all([
|
||||||
|
get_loaders().catch(handleError).then(ref),
|
||||||
|
get_game_versions().catch(handleError).then(ref),
|
||||||
|
])
|
||||||
|
|
||||||
async function fetchProjectData() {
|
async function fetchProjectData() {
|
||||||
const project = await get_project(route.params.id, 'must_revalidate').catch(handleError)
|
const project = await get_project(route.params.id, 'must_revalidate').catch(handleError)
|
||||||
|
|
||||||
@ -331,15 +241,6 @@ async function install(version) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRightClick = (e) => {
|
|
||||||
options.value.showMenu(e, data.value, [
|
|
||||||
{ name: 'install' },
|
|
||||||
{ type: 'divider' },
|
|
||||||
{ name: 'open_link' },
|
|
||||||
{ name: 'copy_link' },
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOptionsClick = (args) => {
|
const handleOptionsClick = (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'install':
|
case 'install':
|
||||||
@ -520,27 +421,7 @@ const handleOptionsClick = (args) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-instance {
|
.project-sidebar-section {
|
||||||
padding: var(--gap-lg);
|
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-bottom: var(--gap-md);
|
|
||||||
|
|
||||||
.instance {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-instance_info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,165 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="filter-header">
|
<div>
|
||||||
<div class="manage">
|
<ProjectPageVersions
|
||||||
<multiselect
|
:loaders="loaders"
|
||||||
v-model="filterLoader"
|
:game-versions="gameVersions"
|
||||||
:options="
|
:versions="versions"
|
||||||
versions
|
:project="project"
|
||||||
.flatMap((value) => value.loaders)
|
:version-link="(version) => `/project/${project.id}/version/${version.id}`"
|
||||||
.filter((value, index, self) => self.indexOf(value) === index)
|
|
||||||
"
|
|
||||||
:multiple="true"
|
|
||||||
:searchable="true"
|
|
||||||
:show-no-results="false"
|
|
||||||
:close-on-select="false"
|
|
||||||
:clear-search-on-select="false"
|
|
||||||
:show-labels="false"
|
|
||||||
:selectable="() => versions.length <= 6"
|
|
||||||
placeholder="Filter loader..."
|
|
||||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
|
||||||
/>
|
|
||||||
<multiselect
|
|
||||||
v-model="filterGameVersions"
|
|
||||||
:options="
|
|
||||||
versions
|
|
||||||
.flatMap((value) => value.game_versions)
|
|
||||||
.filter((value, index, self) => self.indexOf(value) === index)
|
|
||||||
"
|
|
||||||
:multiple="true"
|
|
||||||
:searchable="true"
|
|
||||||
:show-no-results="false"
|
|
||||||
:close-on-select="false"
|
|
||||||
:clear-search-on-select="false"
|
|
||||||
:show-labels="false"
|
|
||||||
:selectable="() => versions.length <= 6"
|
|
||||||
placeholder="Filter versions..."
|
|
||||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
|
||||||
/>
|
|
||||||
<multiselect
|
|
||||||
v-model="filterVersions"
|
|
||||||
:options="
|
|
||||||
versions
|
|
||||||
.map((value) => value.version_type)
|
|
||||||
.filter((value, index, self) => self.indexOf(value) === index)
|
|
||||||
"
|
|
||||||
:multiple="true"
|
|
||||||
:searchable="true"
|
|
||||||
:show-no-results="false"
|
|
||||||
:close-on-select="false"
|
|
||||||
:clear-search-on-select="false"
|
|
||||||
:show-labels="false"
|
|
||||||
:selectable="() => versions.length <= 6"
|
|
||||||
placeholder="Filter release channel..."
|
|
||||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
class="no-wrap clear-filters"
|
|
||||||
:disabled="
|
|
||||||
filterVersions.length === 0 && filterLoader.length === 0 && filterGameVersions.length === 0
|
|
||||||
"
|
|
||||||
:action="clearFilters"
|
|
||||||
>
|
>
|
||||||
<ClearIcon />
|
<template #actions="{ version }">
|
||||||
Clear filters
|
<ButtonStyled circular type="transparent">
|
||||||
</Button>
|
<button
|
||||||
</Card>
|
v-tooltip="`Install`"
|
||||||
<Pagination
|
:class="{
|
||||||
:page="currentPage"
|
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||||
:count="Math.ceil(filteredVersions.length / 20)"
|
!installed || version.id !== installedVersion,
|
||||||
class="pagination-before"
|
}"
|
||||||
:link-function="(page) => `?page=${page}`"
|
|
||||||
@switch-page="switchPage"
|
|
||||||
/>
|
|
||||||
<Card class="mod-card">
|
|
||||||
<div class="table">
|
|
||||||
<div class="table-row table-head">
|
|
||||||
<div class="table-cell table-text download-cell" />
|
|
||||||
<div class="name-cell table-cell table-text">Name</div>
|
|
||||||
<div class="table-cell table-text">Supports</div>
|
|
||||||
<div class="table-cell table-text">Stats</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
|
||||||
:key="version.id"
|
|
||||||
class="table-row selectable"
|
|
||||||
@click="$router.push(`/project/${$route.params.id}/version/${version.id}`)"
|
|
||||||
>
|
|
||||||
<div class="table-cell table-text">
|
|
||||||
<Button
|
|
||||||
:color="installed && version.id === installedVersion ? '' : 'primary'"
|
|
||||||
icon-only
|
|
||||||
:disabled="installing || (installed && version.id === installedVersion)"
|
:disabled="installing || (installed && version.id === installedVersion)"
|
||||||
@click.stop="() => install(version.id)"
|
@click.stop="() => install(version.id)"
|
||||||
>
|
>
|
||||||
<DownloadIcon v-if="!installed" />
|
<DownloadIcon v-if="!installed" />
|
||||||
<SwapIcon v-else-if="installed && version.id !== installedVersion" />
|
<SwapIcon v-else-if="installed && version.id !== installedVersion" />
|
||||||
<CheckIcon v-else />
|
<CheckIcon v-else />
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</ButtonStyled>
|
||||||
<div class="name-cell table-cell table-text">
|
<ButtonStyled circular type="transparent">
|
||||||
<div class="version-link">
|
<OverflowMenu
|
||||||
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
v-if="false"
|
||||||
<div class="version-badge">
|
class="group-hover:!bg-button-bg"
|
||||||
<div class="channel-indicator">
|
:options="[
|
||||||
<Badge
|
{
|
||||||
:color="releaseColor(version.version_type)"
|
id: 'install-elsewhere',
|
||||||
:type="
|
action: () => {},
|
||||||
version.version_type.charAt(0).toUpperCase() + version.version_type.slice(1)
|
shown: false && !!instance,
|
||||||
"
|
color: 'primary',
|
||||||
/>
|
hoverFilled: true,
|
||||||
</div>
|
},
|
||||||
<div>
|
{
|
||||||
{{ version.version_number }}
|
id: 'open-in-browser',
|
||||||
</div>
|
link: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`,
|
||||||
</div>
|
},
|
||||||
</div>
|
]"
|
||||||
</div>
|
aria-label="More options"
|
||||||
<div class="table-cell table-text stacked-text">
|
>
|
||||||
<span>
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
{{
|
<template #install-elsewhere>
|
||||||
version.loaders.map((str) => str.charAt(0).toUpperCase() + str.slice(1)).join(', ')
|
<DownloadIcon aria-hidden="true" />
|
||||||
}}
|
Add to another instance
|
||||||
</span>
|
</template>
|
||||||
<span>
|
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||||
{{ version.game_versions.join(', ') }}
|
</OverflowMenu>
|
||||||
</span>
|
<a
|
||||||
</div>
|
v-else
|
||||||
<div class="table-cell table-text stacked-text">
|
v-tooltip="`Open in browser`"
|
||||||
<div>
|
class="group-hover:!bg-button-bg"
|
||||||
<span> Published on </span>
|
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||||
<strong>
|
target="_blank"
|
||||||
{{
|
>
|
||||||
new Date(version.date_published).toLocaleDateString('en-US', {
|
<ExternalIcon />
|
||||||
year: 'numeric',
|
</a>
|
||||||
month: 'short',
|
</ButtonStyled>
|
||||||
day: 'numeric',
|
</template>
|
||||||
})
|
</ProjectPageVersions>
|
||||||
}}
|
</div>
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>
|
|
||||||
{{ formatNumber(version.downloads) }}
|
|
||||||
</strong>
|
|
||||||
<span> Downloads </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Card, Button, Pagination, Badge } from '@modrinth/ui'
|
import { ProjectPageVersions, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||||
import { CheckIcon, ClearIcon, DownloadIcon } from '@modrinth/assets'
|
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||||
import { formatNumber } from '@modrinth/utils'
|
import { ref, watch } from 'vue'
|
||||||
import Multiselect from 'vue-multiselect'
|
|
||||||
import { releaseColor } from '@/helpers/utils'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { SwapIcon } from '@/assets/icons/index.js'
|
import { SwapIcon } from '@/assets/icons/index.js'
|
||||||
|
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
project: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@ -186,36 +103,17 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [loaders, gameVersions] = await Promise.all([
|
||||||
|
get_loaders().catch(handleError).then(ref),
|
||||||
|
get_game_versions().catch(handleError).then(ref),
|
||||||
|
])
|
||||||
|
|
||||||
const filterVersions = ref([])
|
const filterVersions = ref([])
|
||||||
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
|
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
|
||||||
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
|
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
|
||||||
|
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
const clearFilters = () => {
|
|
||||||
filterVersions.value = []
|
|
||||||
filterLoader.value = []
|
|
||||||
filterGameVersions.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
|
||||||
return props.versions.filter(
|
|
||||||
(projectVersion) =>
|
|
||||||
(filterGameVersions.value.length === 0 ||
|
|
||||||
filterGameVersions.value.some((gameVersion) =>
|
|
||||||
projectVersion.game_versions.includes(gameVersion),
|
|
||||||
)) &&
|
|
||||||
(filterLoader.value.length === 0 ||
|
|
||||||
filterLoader.value.some((loader) => projectVersion.loaders.includes(loader))) &&
|
|
||||||
(filterVersions.value.length === 0 ||
|
|
||||||
filterVersions.value.includes(projectVersion.version_type)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function switchPage(page) {
|
|
||||||
currentPage.value = page
|
|
||||||
}
|
|
||||||
|
|
||||||
//watch all the filters and if a value changes, reset to page 1
|
//watch all the filters and if a value changes, reset to page 1
|
||||||
watch([filterVersions, filterLoader, filterGameVersions], () => {
|
watch([filterVersions, filterLoader, filterGameVersions], () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import * as Pages from '@/pages'
|
import * as Pages from '@/pages'
|
||||||
import * as Project from '@/pages/project'
|
import * as Project from '@/pages/project'
|
||||||
import * as Instance from '@/pages/instance'
|
import * as Instance from '@/pages/instance'
|
||||||
|
import * as Library from '@/pages/library'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures application routing. Add page to pages/index and then add to route table here.
|
* Configures application routing. Add page to pages/index and then add to route table here.
|
||||||
@ -19,27 +20,36 @@ export default new createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/browse/:projectType',
|
path: '/browse/:projectType',
|
||||||
name: 'Browse',
|
name: 'Discover content',
|
||||||
component: Pages.Browse,
|
component: Pages.Browse,
|
||||||
meta: {
|
meta: {
|
||||||
breadcrumb: [{ name: 'Browse' }],
|
breadcrumb: [{ name: 'Discover content' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/library',
|
path: '/library',
|
||||||
name: 'Library',
|
name: 'Library',
|
||||||
component: Pages.Library,
|
component: Library.Index,
|
||||||
meta: {
|
meta: {
|
||||||
breadcrumb: [{ name: 'Library' }],
|
breadcrumb: [{ name: 'Library' }],
|
||||||
},
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '',
|
||||||
name: 'Settings',
|
name: 'Overview',
|
||||||
component: Pages.Settings,
|
component: Library.Overview,
|
||||||
meta: {
|
},
|
||||||
breadcrumb: [{ name: 'Settings' }],
|
{
|
||||||
},
|
path: 'downloaded',
|
||||||
|
name: 'Downloaded',
|
||||||
|
component: Library.Downloaded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'custom',
|
||||||
|
name: 'Custom',
|
||||||
|
component: Library.Custom,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/project/:id',
|
path: '/project/:id',
|
||||||
|
@ -5,6 +5,10 @@ export const useTheming = defineStore('themeStore', {
|
|||||||
themeOptions: ['dark', 'light', 'oled'],
|
themeOptions: ['dark', 'light', 'oled'],
|
||||||
advancedRendering: true,
|
advancedRendering: true,
|
||||||
selectedTheme: 'dark',
|
selectedTheme: 'dark',
|
||||||
|
|
||||||
|
devMode: false,
|
||||||
|
featureFlag_pagePath: false,
|
||||||
|
featureFlag_projectBackground: false,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setThemeState(newTheme) {
|
setThemeState(newTheme) {
|
||||||
|
@ -5,7 +5,7 @@ export default {
|
|||||||
'./src/layouts/**/*.vue',
|
'./src/layouts/**/*.vue',
|
||||||
'./src/pages/**/*.vue',
|
'./src/pages/**/*.vue',
|
||||||
'./src/plugins/**/*.{js,ts}',
|
'./src/plugins/**/*.{js,ts}',
|
||||||
'./src/app.vue',
|
'./src/App.vue',
|
||||||
'./src/error.vue',
|
'./src/error.vue',
|
||||||
// monorepo - TODO: migrate this to its own package
|
// monorepo - TODO: migrate this to its own package
|
||||||
'../../packages/**/*.{js,vue,ts}',
|
'../../packages/**/*.{js,vue,ts}',
|
||||||
@ -65,6 +65,9 @@ export default {
|
|||||||
textHover: 'var(--color-button-text-hover)',
|
textHover: 'var(--color-button-text-hover)',
|
||||||
bgActive: 'var(--color-button-bg-active)',
|
bgActive: 'var(--color-button-bg-active)',
|
||||||
textActive: 'var(--color-button-text-active)',
|
textActive: 'var(--color-button-text-active)',
|
||||||
|
border: 'var(--color-button-border)',
|
||||||
|
bgSelected: 'var(--color-button-bg-selected)',
|
||||||
|
textSelected: 'var(--color-button-text-selected)',
|
||||||
},
|
},
|
||||||
toggleHandle: 'var(--color-toggle-handle)',
|
toggleHandle: 'var(--color-toggle-handle)',
|
||||||
dropdown: {
|
dropdown: {
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
HTML Testing playground for Theseus:
|
HTML Testing playground for Theseus:
|
||||||
|
|
||||||
<br><br><a href="modrinth://mod/test_id">Install mod 'test_id'</a>
|
<br /><br /><a href="modrinth://mod/test_id">Install mod 'test_id'</a>
|
||||||
|
@ -227,6 +227,19 @@ fn main() {
|
|||||||
.default_permission(
|
.default_permission(
|
||||||
DefaultPermissionRule::AllowAllCommands,
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.plugin(
|
||||||
|
"friends",
|
||||||
|
InlinedPlugin::new()
|
||||||
|
.commands(&[
|
||||||
|
"friends",
|
||||||
|
"friend_statuses",
|
||||||
|
"add_friend",
|
||||||
|
"remove_friend",
|
||||||
|
])
|
||||||
|
.default_permission(
|
||||||
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.expect("Failed to run tauri-build");
|
.expect("Failed to run tauri-build");
|
||||||
|
@ -5,10 +5,6 @@
|
|||||||
"remote": {
|
"remote": {
|
||||||
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
|
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
|
||||||
},
|
},
|
||||||
"webviews": [
|
"webviews": ["ads-window"],
|
||||||
"ads-window"
|
"permissions": ["ads:default"]
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
"ads:default"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
"identifier": "core",
|
"identifier": "core",
|
||||||
"description": "",
|
"description": "",
|
||||||
"local": true,
|
"local": true,
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:path:default",
|
"core:path:default",
|
||||||
@ -27,4 +25,4 @@
|
|||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:webview:allow-set-webview-zoom"
|
"core:webview:allow-set-webview-zoom"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"settings:default",
|
"settings:default",
|
||||||
"tags:default",
|
"tags:default",
|
||||||
"utils:default",
|
"utils:default",
|
||||||
"ads:default"
|
"ads:default",
|
||||||
|
"friends:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,6 @@
|
|||||||
"identifier": "updater",
|
"identifier": "updater",
|
||||||
"description": "",
|
"description": "",
|
||||||
"local": true,
|
"local": true,
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
"permissions": ["updater:default"]
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
"updater:default"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -16,4 +16,4 @@
|
|||||||
"@modrinth/app-lib": "workspace:*",
|
"@modrinth/app-lib": "workspace:*",
|
||||||
"@modrinth/daedalus": "workspace:*"
|
"@modrinth/daedalus": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
|
use tauri::{LogicalPosition, LogicalSize, Manager, Runtime};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
use theseus::settings;
|
use theseus::settings;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
@ -47,7 +46,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
init_ads_window,
|
init_ads_window,
|
||||||
hide_ads_window,
|
hide_ads_window,
|
||||||
scroll_ads_window,
|
|
||||||
show_ads_window,
|
show_ads_window,
|
||||||
record_ads_click,
|
record_ads_click,
|
||||||
open_link,
|
open_link,
|
||||||
@ -154,21 +152,6 @@ pub async fn hide_ads_window<R: Runtime>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
struct ScrollEvent {
|
|
||||||
scroll: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn scroll_ads_window<R: Runtime>(
|
|
||||||
app: tauri::AppHandle<R>,
|
|
||||||
scroll: f32,
|
|
||||||
) -> crate::api::Result<()> {
|
|
||||||
let _ = app.emit("ads-scroll", ScrollEvent { scroll });
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn record_ads_click<R: Runtime>(
|
pub async fn record_ads_click<R: Runtime>(
|
||||||
app: tauri::AppHandle<R>,
|
app: tauri::AppHandle<R>,
|
||||||
|
33
apps/app/src/api/friends.rs
Normal file
33
apps/app/src/api/friends.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use tauri::plugin::TauriPlugin;
|
||||||
|
use theseus::prelude::{UserFriend, UserStatus};
|
||||||
|
|
||||||
|
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||||
|
tauri::plugin::Builder::new("friends")
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
friends,
|
||||||
|
friend_statuses,
|
||||||
|
add_friend,
|
||||||
|
remove_friend
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn friends() -> crate::api::Result<Vec<UserFriend>> {
|
||||||
|
Ok(theseus::friends::friends().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn friend_statuses() -> crate::api::Result<Vec<UserStatus>> {
|
||||||
|
Ok(theseus::friends::friend_statuses().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_friend(user_id: &str) -> crate::api::Result<()> {
|
||||||
|
Ok(theseus::friends::add_friend(user_id).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_friend(user_id: &str) -> crate::api::Result<()> {
|
||||||
|
Ok(theseus::friends::remove_friend(user_id).await?)
|
||||||
|
}
|
@ -18,6 +18,7 @@ pub mod utils;
|
|||||||
|
|
||||||
pub mod ads;
|
pub mod ads;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod friends;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ fn position_traffic_lights(
|
|||||||
let title_bar_container_view = close.superview().superview();
|
let title_bar_container_view = close.superview().superview();
|
||||||
|
|
||||||
let close_rect: NSRect = msg_send![close, frame];
|
let close_rect: NSRect = msg_send![close, frame];
|
||||||
let button_height = close_rect.size.height;
|
let button_height = close_rect.size.height + 12.0;
|
||||||
|
|
||||||
let title_bar_frame_height = button_height + y;
|
let title_bar_frame_height = button_height + y;
|
||||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||||
@ -58,7 +58,7 @@ fn position_traffic_lights(
|
|||||||
|
|
||||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||||
let mut rect: NSRect = NSView::frame(button);
|
let mut rect: NSRect = NSView::frame(button);
|
||||||
rect.origin.x = x + (i as f64 * space_between);
|
rect.origin.x = x + (i as f64 * space_between) + 6.0;
|
||||||
button.setFrameOrigin(rect.origin);
|
button.setFrameOrigin(rect.origin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,19 +169,19 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
// .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||||
if let Some(payload) = args.get(1) {
|
// if let Some(payload) = args.get(1) {
|
||||||
tracing::info!("Handling deep link from arg {payload}");
|
// tracing::info!("Handling deep link from arg {payload}");
|
||||||
let payload = payload.clone();
|
// let payload = payload.clone();
|
||||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
// tauri::async_runtime::spawn(api::utils::handle_command(
|
||||||
payload,
|
// payload,
|
||||||
));
|
// ));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if let Some(win) = app.get_window("main") {
|
// if let Some(win) = app.get_window("main") {
|
||||||
let _ = win.set_focus();
|
// let _ = win.set_focus();
|
||||||
}
|
// }
|
||||||
}))
|
// }))
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
@ -260,6 +260,7 @@ fn main() {
|
|||||||
.plugin(api::utils::init())
|
.plugin(api::utils::init())
|
||||||
.plugin(api::cache::init())
|
.plugin(api::cache::init())
|
||||||
.plugin(api::ads::init())
|
.plugin(api::ads::init())
|
||||||
|
.plugin(api::friends::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
initialize_state,
|
initialize_state,
|
||||||
is_dev,
|
is_dev,
|
||||||
|
@ -11,12 +11,7 @@
|
|||||||
"copyright": "",
|
"copyright": "",
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"externalBin": [],
|
"externalBin": [],
|
||||||
"icon": [
|
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico"
|
|
||||||
],
|
|
||||||
"windows": {
|
"windows": {
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
@ -72,7 +67,7 @@
|
|||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Modrinth App",
|
"title": "Modrinth App",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"minHeight": 750,
|
"minHeight": 700,
|
||||||
"minWidth": 1100,
|
"minWidth": 1100,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"zoomHotkeysEnabled": false,
|
"zoomHotkeysEnabled": false,
|
||||||
@ -81,20 +76,14 @@
|
|||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"scope": [
|
"scope": ["$APPDATA/caches/icons/*", "$APPCONFIG/caches/icons/*", "$CONFIG/caches/icons/*"],
|
||||||
"$APPDATA/caches/icons/*",
|
|
||||||
"$APPCONFIG/caches/icons/*",
|
|
||||||
"$CONFIG/caches/icons/*"
|
|
||||||
],
|
|
||||||
"enable": true
|
"enable": true
|
||||||
},
|
},
|
||||||
"capabilities": ["ads", "core", "plugins"],
|
"capabilities": ["ads", "core", "plugins"],
|
||||||
"csp": {
|
"csp": {
|
||||||
"default-src": "'self' customprotocol: asset:",
|
"default-src": "'self' customprotocol: asset:",
|
||||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||||
"font-src": [
|
"font-src": ["https://cdn-raw.modrinth.com/fonts/inter/"],
|
||||||
"https://cdn-raw.modrinth.com/fonts/inter/"
|
|
||||||
],
|
|
||||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||||
"style-src": "'unsafe-inline' 'self'",
|
"style-src": "'unsafe-inline' 'self'",
|
||||||
"script-src": "https://*.posthog.com 'self'",
|
"script-src": "https://*.posthog.com 'self'",
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
# Daedalus
|
# Daedalus
|
||||||
|
|
||||||
Daedalus is a powerful tool which queries and generates metadata for the Minecraft (and other games in the future!) game
|
Daedalus is a powerful tool which queries and generates metadata for the Minecraft (and other games in the future!) game
|
||||||
and mod loaders for:
|
and mod loaders for:
|
||||||
|
|
||||||
- Performance (Serving static files can be easily cached and is extremely quick)
|
- Performance (Serving static files can be easily cached and is extremely quick)
|
||||||
- Ease for Launcher Devs (Metadata is served in an easy to query and use format)
|
- Ease for Launcher Devs (Metadata is served in an easy to query and use format)
|
||||||
- Reliability (Provides a versioning system which ensures no breakage with updates)
|
- Reliability (Provides a versioning system which ensures no breakage with updates)
|
||||||
|
|
||||||
Daedalus supports the original Minecraft data and reposting for the Forge, Fabric, Quilt, and NeoForge loaders.
|
Daedalus supports the original Minecraft data and reposting for the Forge, Fabric, Quilt, and NeoForge loaders.
|
||||||
|
@ -6,12 +6,12 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- minio-data:/data
|
- minio-data:/data
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- '9000:9000'
|
||||||
- "9001:9001"
|
- '9001:9001'
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: minioadmin
|
MINIO_ROOT_USER: minioadmin
|
||||||
MINIO_ROOT_PASSWORD: miniosecret
|
MINIO_ROOT_PASSWORD: miniosecret
|
||||||
command: server /data --console-address ":9001"
|
command: server /data --console-address ":9001"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
minio-data:
|
minio-data:
|
||||||
|
@ -63,9 +63,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add missing tinyfd to the broken LWJGL 3.2.2 variant",
|
"_comment": "Add missing tinyfd to the broken LWJGL 3.2.2 variant",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.2.2"],
|
||||||
"org.lwjgl:lwjgl:3.2.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -114,9 +112,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add additional library just for osx-arm64. No override needed",
|
"_comment": "Add additional library just for osx-arm64. No override needed",
|
||||||
"match": [
|
"match": ["ca.weblite:java-objc-bridge:1.0.0"],
|
||||||
"ca.weblite:java-objc-bridge:1.0.0"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -140,9 +136,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add additional classifiers for jinput-platform",
|
"_comment": "Add additional classifiers for jinput-platform",
|
||||||
"match": [
|
"match": ["net.java.jinput:jinput-platform:2.0.5"],
|
||||||
"net.java.jinput:jinput-platform:2.0.5"
|
|
||||||
],
|
|
||||||
"override": {
|
"override": {
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"classifiers": {
|
"classifiers": {
|
||||||
@ -1628,9 +1622,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Only allow osx-arm64 for existing java-objc-bridge:1.1",
|
"_comment": "Only allow osx-arm64 for existing java-objc-bridge:1.1",
|
||||||
"match": [
|
"match": ["ca.weblite:java-objc-bridge:1.1"],
|
||||||
"ca.weblite:java-objc-bridge:1.1"
|
|
||||||
],
|
|
||||||
"override": {
|
"override": {
|
||||||
"rules": [
|
"rules": [
|
||||||
{
|
{
|
||||||
@ -1666,9 +1658,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-glfw:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-glfw:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1692,9 +1682,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-jemalloc:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-jemalloc:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1718,9 +1706,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-openal:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-openal:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1744,9 +1730,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-opengl:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-opengl:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1770,9 +1754,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-stb:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-stb:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1796,9 +1778,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-tinyfd:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-tinyfd:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1822,9 +1802,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.3.1"],
|
||||||
"org.lwjgl:lwjgl:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1848,9 +1826,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-glfw:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-glfw:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1874,9 +1850,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-jemalloc:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-jemalloc:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1900,9 +1874,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-openal:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-openal:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1926,9 +1898,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-opengl:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-opengl:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1952,9 +1922,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-stb:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-stb:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -1978,9 +1946,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-tinyfd:3.3.1"],
|
||||||
"org.lwjgl:lwjgl-tinyfd:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2004,9 +1970,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.1",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.3.1"],
|
||||||
"org.lwjgl:lwjgl:3.3.1"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2030,9 +1994,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-freetype:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-freetype:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2056,9 +2018,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-glfw:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-glfw:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2082,9 +2042,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-jemalloc:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-jemalloc:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2108,9 +2066,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-openal:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-openal:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2134,9 +2090,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-opengl:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-opengl:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2160,9 +2114,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-stb:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-stb:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2186,9 +2138,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-tinyfd:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-tinyfd:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2212,9 +2162,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.3.2"],
|
||||||
"org.lwjgl:lwjgl:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2238,9 +2186,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-freetype:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-freetype:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2264,9 +2210,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-glfw:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-glfw:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2290,9 +2234,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-jemalloc:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-jemalloc:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2316,9 +2258,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-openal:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-openal:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2342,9 +2282,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-opengl:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-opengl:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2368,9 +2306,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-stb:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-stb:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2394,9 +2330,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-tinyfd:3.3.2"],
|
||||||
"org.lwjgl:lwjgl-tinyfd:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2420,9 +2354,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.2",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.3.2"],
|
||||||
"org.lwjgl:lwjgl:3.3.2"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2446,9 +2378,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-freetype:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-freetype:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2472,9 +2402,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-glfw:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-glfw:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2498,9 +2426,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-jemalloc:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-jemalloc:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2524,9 +2450,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-openal:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-openal:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2550,9 +2474,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-opengl:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-opengl:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2576,9 +2498,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-stb:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-stb:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2602,9 +2522,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-tinyfd:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-tinyfd:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2628,9 +2546,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm64 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.3.3"],
|
||||||
"org.lwjgl:lwjgl:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2654,9 +2570,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-freetype:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-freetype:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2680,9 +2594,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-glfw:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-glfw:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2706,9 +2618,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-jemalloc:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-jemalloc:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2732,9 +2642,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-openal:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-openal:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2758,9 +2666,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-opengl:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-opengl:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2784,9 +2690,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-stb:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-stb:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2810,9 +2714,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl-tinyfd:3.3.3"],
|
||||||
"org.lwjgl:lwjgl-tinyfd:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@ -2836,9 +2738,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
"_comment": "Add linux-arm32 support for LWJGL 3.3.3",
|
||||||
"match": [
|
"match": ["org.lwjgl:lwjgl:3.3.3"],
|
||||||
"org.lwjgl:lwjgl:3.3.3"
|
|
||||||
],
|
|
||||||
"additionalLibraries": [
|
"additionalLibraries": [
|
||||||
{
|
{
|
||||||
"downloads": {
|
"downloads": {
|
||||||
|
@ -10,4 +10,4 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/daedalus": "workspace:*"
|
"@modrinth/daedalus": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Modrinth Documentation
|
# Modrinth Documentation
|
||||||
|
|
||||||
Welcome to the Modrinth documentation!
|
Welcome to the Modrinth documentation!
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -354,7 +354,7 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: The mod loaders that this version supports. In case of resource packs, use "minecraft"
|
description: The mod loaders that this version supports. In case of resource packs, use "minecraft"
|
||||||
example: ["fabric", "forge", "minecraft"]
|
example: ['fabric', 'forge', 'minecraft']
|
||||||
featured:
|
featured:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Whether the version is featured or not
|
description: Whether the version is featured or not
|
||||||
|
@ -6,19 +6,21 @@
|
|||||||
- title: __Welcome to Modrinth's Discord server!__
|
- title: __Welcome to Modrinth's Discord server!__
|
||||||
url: https://modrinth.com
|
url: https://modrinth.com
|
||||||
color: 0x1bd96a
|
color: 0x1bd96a
|
||||||
description: "Modrinth is the place for Minecraft mods, plugins, data packs, shaders, resource packs, and
|
description:
|
||||||
modpacks. Discover, play, and share Minecraft content through our open-source platform built for the community."
|
'Modrinth is the place for Minecraft mods, plugins, data packs, shaders, resource packs, and
|
||||||
|
modpacks. Discover, play, and share Minecraft content through our open-source platform built for the community.'
|
||||||
|
|
||||||
- type: embed
|
- type: embed
|
||||||
embeds:
|
embeds:
|
||||||
- title: "**:scroll: __Rules__**"
|
- title: '**:scroll: __Rules__**'
|
||||||
color: 0x4f9cff
|
color: 0x4f9cff
|
||||||
description: "Modrinth's rules are easy to follow. Despite this, please keep in mind that this is not an entirely
|
description:
|
||||||
|
"Modrinth's rules are easy to follow. Despite this, please keep in mind that this is not an entirely
|
||||||
open forum. First and foremost, this Discord server is intended to facilitate the development of Modrinth and
|
open forum. First and foremost, this Discord server is intended to facilitate the development of Modrinth and
|
||||||
for communication regarding Modrinth. Ultimately, it is up to the discretion of the moderators whether your
|
for communication regarding Modrinth. Ultimately, it is up to the discretion of the moderators whether your
|
||||||
messages are in violation of our rules.\n\n
|
messages are in violation of our rules.\n\n
|
||||||
Modrinth's rules are split up into two categories: the **__DOs__** and the **__DO NOTs__**."
|
Modrinth's rules are split up into two categories: the **__DOs__** and the **__DO NOTs__**."
|
||||||
- title: ":white_check_mark: Do:"
|
- title: ':white_check_mark: Do:'
|
||||||
color: 0x1bd96a
|
color: 0x1bd96a
|
||||||
description: >-
|
description: >-
|
||||||
1. Treat every user with respect and consider the opinions and viewpoints of others
|
1. Treat every user with respect and consider the opinions and viewpoints of others
|
||||||
@ -33,7 +35,7 @@
|
|||||||
4. Contact the moderators at any time via the <@&895382919772766219> ping
|
4. Contact the moderators at any time via the <@&895382919772766219> ping
|
||||||
|
|
||||||
5. Respect the use of accessibility and self-identity tools such as [PluralKit](https://pluralkit.me)
|
5. Respect the use of accessibility and self-identity tools such as [PluralKit](https://pluralkit.me)
|
||||||
- title: ":no_entry: Do not:"
|
- title: ':no_entry: Do not:'
|
||||||
color: 0xff496e
|
color: 0xff496e
|
||||||
description: >-
|
description: >-
|
||||||
6. Harass, bother, provoke, or insult anyone, including by sending unsolicited DMs or friend requests
|
6. Harass, bother, provoke, or insult anyone, including by sending unsolicited DMs or friend requests
|
||||||
@ -46,7 +48,7 @@
|
|||||||
9. Report Modrinth content in the Discord (use the Report button on the website)
|
9. Report Modrinth content in the Discord (use the Report button on the website)
|
||||||
|
|
||||||
10. Assume staff member's opinions reflect those of Modrinth
|
10. Assume staff member's opinions reflect those of Modrinth
|
||||||
- title: ":pencil2: Nickname policy:"
|
- title: ':pencil2: Nickname policy:'
|
||||||
color: 0xffa347
|
color: 0xffa347
|
||||||
description: >-
|
description: >-
|
||||||
We want to keep this server clean and therefore require that display names of all members on the server are
|
We want to keep this server clean and therefore require that display names of all members on the server are
|
||||||
@ -60,7 +62,7 @@
|
|||||||
from the server. We will also permanently remove any users whose profiles contain inappropriate content.
|
from the server. We will also permanently remove any users whose profiles contain inappropriate content.
|
||||||
- type: links
|
- type: links
|
||||||
color: 0x4f9cff
|
color: 0x4f9cff
|
||||||
title: "**:link: __Links__**"
|
title: '**:link: __Links__**'
|
||||||
links:
|
links:
|
||||||
Website: https://modrinth.com
|
Website: https://modrinth.com
|
||||||
Support: https://support.modrinth.com
|
Support: https://support.modrinth.com
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineCollection } from 'astro:content';
|
import { defineCollection } from 'astro:content'
|
||||||
import { docsSchema } from '@astrojs/starlight/schema';
|
import { docsSchema } from '@astrojs/starlight/schema'
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
docs: defineCollection({ schema: docsSchema() }),
|
docs: defineCollection({ schema: docsSchema() }),
|
||||||
};
|
}
|
||||||
|
@ -7,4 +7,4 @@ description: Guide for contributing to Modrinth's gradle plugin
|
|||||||
|
|
||||||
Minotaur contains two test environments within it - one with ForgeGradle and one with Fabric Loom. You may tweak with these environments to test whatever you may be trying; just make sure that the `modrinth` task within each still functions properly. GitHub Actions will validate this if you're making a pull request, so you may want to use [`act pull_request`](https://github.com/nektos/act) to test them locally.
|
Minotaur contains two test environments within it - one with ForgeGradle and one with Fabric Loom. You may tweak with these environments to test whatever you may be trying; just make sure that the `modrinth` task within each still functions properly. GitHub Actions will validate this if you're making a pull request, so you may want to use [`act pull_request`](https://github.com/nektos/act) to test them locally.
|
||||||
|
|
||||||
[minotaur]: https://github.com/modrinth/minotaur
|
[minotaur]: https://github.com/modrinth/minotaur
|
||||||
|
@ -12,4 +12,4 @@ hero:
|
|||||||
link: https://support.modrinth.com
|
link: https://support.modrinth.com
|
||||||
icon: external
|
icon: external
|
||||||
variant: minimal
|
variant: minimal
|
||||||
---
|
---
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
:root,
|
:root,
|
||||||
::backdrop,
|
::backdrop,
|
||||||
:root[data-theme='light'],
|
:root[data-theme='light'],
|
||||||
[data-theme='light'] ::backdrop{
|
[data-theme='light'] ::backdrop {
|
||||||
--sl-font-system: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
--sl-font-system: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
|
||||||
--sl-color-white: var(--color-contrast); /* “white” */
|
--sl-color-white: var(--color-contrast); /* “white” */
|
||||||
--sl-color-gray-1: var(--color-base);
|
--sl-color-gray-1: var(--color-base);
|
||||||
@ -49,6 +49,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme='light'],
|
:root[data-theme='light'],
|
||||||
[data-theme='light'] ::backdrop{
|
[data-theme='light'] ::backdrop {
|
||||||
--sl-color-bg: var(--color-raised-bg);
|
--sl-color-bg: var(--color-raised-bg);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict"
|
"extends": "astro/tsconfigs/strict"
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"glob": "^10.2.7",
|
"glob": "^10.2.7",
|
||||||
"nuxt": "^3.12.3",
|
"nuxt": "^3.14.1592",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"sass": "^1.58.0",
|
"sass": "^1.58.0",
|
||||||
@ -62,5 +62,6 @@
|
|||||||
"vue3-ace-editor": "^2.2.4",
|
"vue3-ace-editor": "^2.2.4",
|
||||||
"vue3-apexcharts": "^1.5.2",
|
"vue3-apexcharts": "^1.5.2",
|
||||||
"xss": "^1.0.14"
|
"xss": "^1.0.14"
|
||||||
}
|
},
|
||||||
|
"web-types": "../../web-types.json"
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user