Compare commits

..
Author SHA1 Message Date
Jakob 75f9fa5daf Added confirm for checking discussion threads
Signed-off-by: Jakob <minenash@protonmail.com>
2024-08-28 01:17:31 -04:00
Jakob c9d34feab7 fixed tag in web bug report
Signed-off-by: Jakob <minenash@protonmail.com>
2024-08-28 01:12:55 -04:00
102 changed files with 1654 additions and 3663 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
name: 🌐 Website bug (modrinth.com) name: 🌐 Website bug (modrinth.com)
description: Report an issue on the Modrinth website. description: Report an issue on the Modrinth website.
labels: [bug, frontend] labels: [bug, web]
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
@@ -9,6 +9,8 @@ body:
options: options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests - label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests
required: true required: true
- label: I checked the [existing discussions](https://github.com/orgs/modrinth/discussions) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com) - label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true required: true
- type: dropdown - type: dropdown
+5 -34
View File
@@ -41,34 +41,10 @@ jobs:
with: with:
components: rustfmt, clippy components: rustfmt, clippy
- name: Install Tauri CLI
run: cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
- name: Setup rust cache - name: Setup rust cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: target/**
target/**
!target/*/release/bundle/*/*.dmg
!target/*/release/bundle/*/*.app.tar.gz
!target/*/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.dmg
!target/release/bundle/*/*.app.tar.gz
!target/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.AppImage
!target/release/bundle/*/*.AppImage.tar.gz
!target/release/bundle/*/*.AppImage.tar.gz.sig
!target/release/bundle/*/*.deb
!target/release/bundle/*/*.rpm
!target/release/bundle/msi/*.msi
!target/release/bundle/msi/*.msi.zip
!target/release/bundle/msi/*.msi.zip.sig
!target/release/bundle/nsis/*.exe
!target/release/bundle/nsis/*.nsis.zip
!target/release/bundle/nsis/*.nsis.zip.sig
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-rust-target- ${{ runner.os }}-rust-target-
@@ -102,7 +78,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu') if: startsWith(matrix.platform, 'ubuntu')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev sqlite3
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install run: pnpm install
@@ -121,7 +97,6 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
V1_COMPATIBLE_BIN_NAME: true
- name: build app - name: build app
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json" run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
@@ -131,7 +106,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
V1_COMPATIBLE_BIN_NAME: true
- name: upload ${{ matrix.platform }} - name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -151,10 +125,7 @@ jobs:
target/release/bundle/*/*.deb target/release/bundle/*/*.deb
target/release/bundle/*/*.rpm target/release/bundle/*/*.rpm
target/release/bundle/msi/*.msi target/release/bundle/*/*.msi
target/release/bundle/msi/*.msi.zip target/release/bundle/*/*.msi.zip
target/release/bundle/msi/*.msi.zip.sig target/release/bundle/*/*.msi.zip.sig
target/release/bundle/nsis/*.exe
target/release/bundle/nsis/*.nsis.zip
target/release/bundle/nsis/*.nsis.zip.sig
-3
View File
@@ -32,9 +32,6 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Tauri CLI
run: cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
- name: Setup Node.JS environment - name: Setup Node.JS environment
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
Generated
+269 -483
View File
File diff suppressed because it is too large. Load diff
-4
View File
@@ -16,7 +16,3 @@ strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros] [profile.dev.package.sqlx-macros]
opt-level = 3 opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev = "5840108" }
tauri = { git = "https://github.com/modrinth/tauri", rev = "5e29428" }
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['custom/vue'],
}
-22
View File
@@ -1,22 +0,0 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
import { fixupPluginRules } from '@eslint/compat'
import turboPlugin from 'eslint-plugin-turbo'
export default createConfigForNuxt().append([
{
name: 'turbo',
plugins: {
turbo: fixupPluginRules(turboPlugin),
},
rules: {
'turbo/no-undeclared-env-vars': 'error',
},
},
{
name: 'modrinth',
rules: {
'vue/html-self-closing': 'off',
'vue/multi-word-component-names': 'off',
},
},
])
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modrinth App</title> <title>Modrinth App</title>
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" /> <link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
+9 -15
View File
@@ -1,12 +1,11 @@
{ {
"name": "@modrinth/app-frontend", "name": "@modrinth/app-frontend",
"private": true, "private": true,
"version": "0.8.7", "version": "0.8.3-1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vite build",
"tsc:check": "vue-tsc --noEmit",
"lint": "eslint . && prettier --check .", "lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write ." "fix": "eslint . --fix && prettier --write ."
}, },
@@ -14,41 +13,36 @@
"@modrinth/assets": "workspace:*", "@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*", "@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*", "@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@tauri-apps/api": "^2.0.0-rc.3", "@tauri-apps/api": "^2.0.0-rc.3",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0", "@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0", "@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/plugin-updater": "^2.0.0-rc.0",
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0", "@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@vintl/vintl": "^4.4.1", "@vintl/vintl": "^4.4.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"ofetch": "^1.3.4", "ofetch": "^1.3.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"posthog-js": "^1.158.2",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",
"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",
"posthog-js": "^1.158.2",
"@sentry/vue": "^8.27.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.1.1", "@tauri-apps/cli": "^2.0.0-rc",
"@nuxt/eslint-config": "^0.5.6",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^9.9.1", "eslint": "^8.57.0",
"eslint-config-custom": "workspace:*", "eslint-config-custom": "workspace:*",
"eslint-plugin-turbo": "^2.1.1",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"sass": "^1.74.1", "sass": "^1.74.1",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"tsconfig": "workspace:*", "tsconfig": "workspace:*",
"typescript": "^5.5.4", "vite": "^5.2.8"
"vite": "^5.2.8",
"vue-tsc": "^2.1.6"
}, },
"packageManager": "pnpm@9.4.0" "packageManager": "pnpm@9.4.0"
} }
+3 -66
View File
@@ -1,15 +1,7 @@
<script setup> <script setup>
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router' import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import { import { HomeIcon, SearchIcon, LibraryIcon, PlusIcon, SettingsIcon, XIcon } from '@modrinth/assets'
HomeIcon,
SearchIcon,
LibraryIcon,
PlusIcon,
SettingsIcon,
XIcon,
DownloadIcon,
} from '@modrinth/assets'
import { Button, Notifications } from '@modrinth/ui' import { Button, Notifications } from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state' import { useLoading, useTheming } from '@/store/state'
import AccountsCard from '@/components/ui/AccountsCard.vue' import AccountsCard from '@/components/ui/AccountsCard.vue'
@@ -24,7 +16,7 @@ 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 { 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 } from '@/helpers/utils.js'
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics' import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
@@ -39,10 +31,6 @@ 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 { 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 { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater'
const themeStore = useTheming() const themeStore = useTheming()
@@ -63,8 +51,6 @@ const os = ref('')
const stateInitialized = ref(false) const stateInitialized = ref(false)
const criticalErrorMessage = ref()
onMounted(async () => { onMounted(async () => {
await useCheckDisableMouseover() await useCheckDisableMouseover()
}) })
@@ -121,18 +107,7 @@ async function setupApp() {
}), }),
) )
useFetch(
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
'criticalAnnouncements',
true,
).then((res) => {
if (res && res.header && res.body) {
criticalErrorMessage.value = res
}
})
get_opening_command().then(handleCommand) get_opening_command().then(handleCommand)
checkUpdates()
} }
const stateFailed = ref(false) const stateFailed = ref(false)
@@ -151,7 +126,6 @@ initialize_state()
}) })
const handleClose = async () => { const handleClose = async () => {
await saveWindowState(StateFlags.ALL)
await getCurrentWindow().close() await getCurrentWindow().close()
} }
@@ -197,8 +171,7 @@ document.querySelector('body').addEventListener('click', function (e) {
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) && ['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
!target.classList.contains('router-link-active') && !target.classList.contains('router-link-active') &&
!target.href.startsWith('http://localhost') && !target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost') && !target.href.startsWith('https://tauri.localhost')
!target.href.startsWith('http://tauri.localhost')
) { ) {
open(target.href) open(target.href)
} }
@@ -242,20 +215,6 @@ async function handleCommand(e) {
urlModal.value.show(e) urlModal.value.show(e)
} }
} }
const updateAvailable = ref(false)
async function checkUpdates() {
const update = await check()
console.log(update)
updateAvailable.value = !!update
setTimeout(
() => {
checkUpdates()
},
5 * 1000 * 60,
)
}
</script> </script>
<template> <template>
@@ -289,14 +248,6 @@ async function checkUpdates() {
</div> </div>
</div> </div>
<div class="settings pages-list"> <div class="settings pages-list">
<button
v-if="updateAvailable"
v-tooltip="'Install update'"
class="btn btn-outline btn-primary icon-only collapsed-button"
@click="restartApp()"
>
<DownloadIcon />
</button>
<Button <Button
v-tooltip="'Create profile'" v-tooltip="'Create profile'"
class="sleek-primary collapsed-button" class="sleek-primary collapsed-button"
@@ -312,10 +263,6 @@ async function checkUpdates() {
</div> </div>
</div> </div>
<div class="view"> <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 class="appbar-row"> <div class="appbar-row">
<div data-tauri-drag-region class="appbar"> <div data-tauri-drag-region class="appbar">
<section class="navigation-controls"> <section class="navigation-controls">
@@ -428,16 +375,6 @@ async function checkUpdates() {
width: calc(100% - var(--sidebar-width)); width: calc(100% - var(--sidebar-width));
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
.critical-error-banner {
margin-top: -1.25rem;
padding: 1rem;
background-color: rgba(203, 34, 69, 0.1);
border-left: 2px solid var(--color-red);
border-bottom: 2px solid var(--color-red);
border-right: 2px solid var(--color-red);
border-radius: 1rem;
}
.appbar { .appbar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -12,13 +12,13 @@ import {
SearchIcon, SearchIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, Card, DropdownSelect } from '@modrinth/ui' import { ConfirmModal, Button, Card, 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'
import { useTheming } from '@/store/theme.js'
import { duplicate, remove } from '@/helpers/profile.js' import { duplicate, remove } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const props = defineProps({ const props = defineProps({
instances: { instances: {
@@ -35,6 +35,7 @@ const props = defineProps({
const instanceOptions = ref(null) const instanceOptions = ref(null)
const instanceComponents = ref(null) const instanceComponents = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null) const currentDeleteInstance = ref(null)
const confirmModal = ref(null) const confirmModal = ref(null)
@@ -229,12 +230,13 @@ const filteredResults = computed(() => {
}) })
</script> </script>
<template> <template>
<ConfirmModalWrapper <ConfirmModal
ref="confirmModal" ref="confirmModal"
title="Are you sure you want to delete this instance?" title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it." description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false" :has-to-type="false"
proceed-label="Delete" proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile" @proceed="deleteProfile"
/> />
<Card class="header"> <Card class="header">
@@ -12,7 +12,7 @@ import {
EyeIcon, EyeIcon,
ChevronRightIcon, ChevronRightIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue' import { ConfirmModal } from '@modrinth/ui'
import Instance from '@/components/ui/Instance.vue' import Instance from '@/components/ui/Instance.vue'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
@@ -22,6 +22,7 @@ import { handleError } from '@/store/notifications.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js' import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js' import { install as installVersion } from '@/store/install.js'
@@ -52,6 +53,7 @@ const instanceComponents = ref(null)
const rows = ref(null) const rows = ref(null)
const deleteConfirmModal = ref(null) const deleteConfirmModal = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null) const currentDeleteInstance = ref(null)
async function deleteProfile() { async function deleteProfile() {
@@ -205,12 +207,13 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<ConfirmModalWrapper <ConfirmModal
ref="deleteConfirmModal" ref="deleteConfirmModal"
title="Are you sure you want to delete this instance?" title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it." description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false" :has-to-type="false"
proceed-label="Delete" proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile" @proceed="deleteProfile"
/> />
<div class="content"> <div class="content">
@@ -5,7 +5,7 @@
v-tooltip.right="'Minecraft accounts'" v-tooltip.right="'Minecraft accounts'"
class="button-base avatar-button" class="button-base avatar-button"
:class="{ expanded: mode === 'expanded' }" :class="{ expanded: mode === 'expanded' }"
@click="toggleMenu" @click="showCard = !showCard"
> >
<Avatar <Avatar
:size="mode === 'expanded' ? 'xs' : 'sm'" :size="mode === 'expanded' ? 'xs' : 'sm'"
@@ -73,7 +73,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: {
@@ -134,9 +133,9 @@ const logout = async (id) => {
trackEvent('AccountLogOut') trackEvent('AccountLogOut')
} }
const showCard = ref(false) let showCard = ref(false)
const card = ref(null) let card = ref(null)
const button = ref(null) let button = ref(null)
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY) const elements = document.elementsFromPoint(event.clientX, event.clientY)
if ( if (
@@ -145,20 +144,7 @@ const handleClickOutside = (event) => {
!elements.includes(card.value.$el) && !elements.includes(card.value.$el) &&
!button.value.contains(event.target) !button.value.contains(event.target)
) { ) {
toggleMenu(false)
}
}
function toggleMenu(override = true) {
if (showCard.value || !override) {
if (showCard.value) {
show_ads_window()
}
showCard.value = false showCard.value = false
} else {
hide_ads_window()
showCard.value = true
} }
} }
@@ -20,7 +20,7 @@ const handleAddContentFromFile = async () => {
if (!newProject) return if (!newProject) return
for (const project of newProject) { for (const project of newProject) {
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError) await add_project_from_path(props.instance.path, project).catch(handleError)
} }
} }
@@ -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')
} }
@@ -1,5 +1,6 @@
<script setup> <script setup>
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets' import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
import { Modal } from '@modrinth/ui'
import { ChatIcon } from '@/assets/icons' import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue' import { ref } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js' import { login as login_flow, set_default_user } from '@/helpers/auth.js'
@@ -8,7 +9,6 @@ import { handleSevereError } from '@/store/error.js'
import { cancel_directory_change } from '@/helpers/settings.js' import { cancel_directory_change } from '@/helpers/settings.js'
import { install } from '@/helpers/profile.js' import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref() const errorModal = ref()
const error = ref() const error = ref()
@@ -121,7 +121,7 @@ async function repairInstance() {
</script> </script>
<template> <template>
<ModalWrapper ref="errorModal" :header="title" :closable="closable"> <Modal ref="errorModal" :header="title" :closable="closable">
<div class="modal-body"> <div class="modal-body">
<div class="markdown-body"> <div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'"> <template v-if="errorType === 'minecraft_auth'">
@@ -272,7 +272,7 @@ async function repairInstance() {
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button> <button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style> <style>
@@ -1,12 +1,12 @@
<script setup> <script setup>
import { XIcon, PlusIcon } from '@modrinth/assets' import { XIcon, PlusIcon } from '@modrinth/assets'
import { Button, Checkbox } from '@modrinth/ui' import { Button, Checkbox, Modal } from '@modrinth/ui'
import { PackageIcon, VersionIcon } from '@/assets/icons' import { PackageIcon, VersionIcon } from '@/assets/icons'
import { ref } from 'vue' import { ref } from 'vue'
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js' import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { useTheming } from '@/store/theme'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -30,6 +30,8 @@ const files = ref([])
const folders = ref([]) const folders = ref([])
const showingFiles = ref(false) const showingFiles = ref(false)
const themeStore = useTheming()
const initFiles = async () => { const initFiles = async () => {
const newFolders = new Map() const newFolders = new Map()
const sep = '/' const sep = '/'
@@ -104,7 +106,7 @@ const exportPack = async () => {
</script> </script>
<template> <template>
<ModalWrapper ref="exportModal" header="Export modpack"> <Modal ref="exportModal" header="Export modpack" :noblur="!themeStore.advancedRendering">
<div class="modal-body"> <div class="modal-body">
<div class="labeled_input"> <div class="labeled_input">
<p>Modpack Name</p> <p>Modpack Name</p>
@@ -206,7 +208,7 @@ const exportPack = async () => {
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,5 +1,5 @@
<template> <template>
<ModalWrapper ref="modal" header="Create instance"> <Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
<div class="modal-header"> <div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" /> <Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
</div> </div>
@@ -193,11 +193,10 @@
/> />
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { import {
PlusIcon, PlusIcon,
UploadIcon, UploadIcon,
@@ -208,7 +207,7 @@ import {
FolderSearchIcon, FolderSearchIcon,
UpdatedIcon, UpdatedIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui' import { Avatar, Button, Chips, Modal, Checkbox } from '@modrinth/ui'
import { computed, onUnmounted, ref, shallowRef } from 'vue' import { computed, onUnmounted, ref, shallowRef } from 'vue'
import { get_loaders } from '@/helpers/tags' import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile' import { create } from '@/helpers/profile'
@@ -218,6 +217,7 @@ import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { install_from_file } from '@/helpers/pack.js' import { install_from_file } from '@/helpers/pack.js'
import { import {
@@ -227,6 +227,8 @@ import {
} from '@/helpers/import.js' } from '@/helpers/import.js'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
const themeStore = useTheming()
const profile_name = ref('') const profile_name = ref('')
const game_version = ref('') const game_version = ref('')
const loader = ref('vanilla') const loader = ref('vanilla')
@@ -369,7 +371,7 @@ const create_instance = async () => {
} }
const upload_icon = async () => { const upload_icon = async () => {
const res = await open({ icon.value = await open({
multiple: false, multiple: false,
filters: [ filters: [
{ {
@@ -379,8 +381,6 @@ const upload_icon = async () => {
], ],
}) })
icon.value = res.path ?? res
if (!icon.value) return if (!icon.value) return
display_icon.value = convertFileSrc(icon.value) display_icon.value = convertFileSrc(icon.value)
} }
@@ -417,7 +417,7 @@ const openFile = async () => {
const newProject = await open({ multiple: false }) const newProject = await open({ multiple: false })
if (!newProject) return if (!newProject) return
hide() hide()
await install_from_file(newProject.path ?? newProject).catch(handleError) await install_from_file(newProject).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
source: 'CreationModalFileOpen', source: 'CreationModalFileOpen',
@@ -462,7 +462,7 @@ const promises = profileOptions.value.map(async (option) => {
option.name, option.name,
instances.map((name) => ({ name, selected: false })), instances.map((name) => ({ name, selected: false })),
) )
} catch { } catch (error) {
// Allow failure silently // Allow failure silently
} }
}) })
@@ -1,5 +1,5 @@
<template> <template>
<ModalWrapper ref="detectJavaModal" header="Select java version"> <Modal ref="detectJavaModal" header="Select java version" :noblur="!themeStore.advancedRendering">
<div class="auto-detect-modal"> <div class="auto-detect-modal">
<div class="table"> <div class="table">
<div class="table-row table-head"> <div class="table-row table-head">
@@ -32,16 +32,18 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<script setup> <script setup>
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets' import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui' import { Modal, Button } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { find_filtered_jres } from '@/helpers/jre.js' import { find_filtered_jres } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const chosenInstallOptions = ref([]) const chosenInstallOptions = ref([])
const detectJavaModal = ref(null) const detectJavaModal = ref(null)
@@ -124,19 +124,20 @@ async function testJava() {
} }
async function handleJavaFileInput() { async function handleJavaFileInput() {
const filePath = await open() let filePath = await open()
if (filePath) { if (filePath) {
let result = await get_jre(filePath.path ?? filePath) let result = await get_jre(filePath)
if (!result) { if (!result) {
result = { result = {
path: filePath.path ?? filePath, path: filePath,
version: props.version.toString(), version: props.version.toString(),
architecture: 'x86', architecture: 'x86',
} }
} }
trackEvent('JavaManualSelect', { trackEvent('JavaManualSelect', {
path: filePath,
version: props.version, version: props.version,
}) })
@@ -149,7 +150,7 @@ async function autoDetect() {
if (!props.compact) { if (!props.compact) {
detectJavaModal.value.show(props.version, props.modelValue) detectJavaModal.value.show(props.version, props.modelValue)
} else { } else {
const versions = await find_filtered_jres(props.version).catch(handleError) let versions = await find_filtered_jres(props.version).catch(handleError)
if (versions.length > 0) { if (versions.length > 0) {
emit('update:modelValue', versions[0]) emit('update:modelValue', versions[0])
} }
@@ -1,11 +1,11 @@
<script setup> <script setup>
import { CheckIcon } from '@modrinth/assets' import { CheckIcon } from '@modrinth/assets'
import { Button, Badge } from '@modrinth/ui' import { Button, Modal, Badge } from '@modrinth/ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useTheming } from '@/store/theme'
import { update_managed_modrinth_version } from '@/helpers/profile' import { update_managed_modrinth_version } from '@/helpers/profile'
import { releaseColor } from '@/helpers/utils' import { releaseColor } from '@/helpers/utils'
import { SwapIcon } from '@/assets/icons/index.js' import { SwapIcon } from '@/assets/icons/index.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({ const props = defineProps({
versions: { versions: {
@@ -33,6 +33,8 @@ const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installing = computed(() => props.instance.install_stage !== 'installed') const installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false) const inProgress = ref(false)
const themeStore = useTheming()
const switchVersion = async (versionId) => { const switchVersion = async (versionId) => {
inProgress.value = true inProgress.value = true
await update_managed_modrinth_version(props.instance.path, versionId) await update_managed_modrinth_version(props.instance.path, versionId)
@@ -41,10 +43,11 @@ const switchVersion = async (versionId) => {
</script> </script>
<template> <template>
<ModalWrapper <Modal
ref="modpackVersionModal" ref="modpackVersionModal"
class="modpack-version-modal" class="modpack-version-modal"
header="Change modpack version" header="Change modpack version"
:noblur="!themeStore.advancedRendering"
> >
<div class="modal-body"> <div class="modal-body">
<Card v-if="instance.linked_data" class="mod-card"> <Card v-if="instance.linked_data" class="mod-card">
@@ -108,7 +111,7 @@ const switchVersion = async (versionId) => {
</div> </div>
</Card> </Card>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,20 +1,12 @@
<script setup> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref } from 'vue'
import { Promotion } from '@modrinth/ui'
import { get as getCreds } from '@/helpers/mr_auth.js' import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js' import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true) const showAd = ref(true)
defineExpose({
scroll() {
updateAdPosition()
},
})
const creds = await getCreds().catch(handleError) const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) { if (creds && creds.user_id) {
const user = await get_user(creds.user_id).catch(handleError) const user = await get_user(creds.user_id).catch(handleError)
@@ -24,98 +16,8 @@ if (creds && creds.user_id) {
showAd.value = false showAd.value = false
} }
} }
const adsWrapper = ref(null)
let resizeObserver
let scrollHandler
let intersectionObserver
let mutationObserver
onMounted(() => {
if (showAd.value) {
updateAdPosition(true)
resizeObserver = new ResizeObserver(() => 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) {
if (adsWrapper.value) {
const rect = adsWrapper.value.getBoundingClientRect()
let y = rect.top + window.scrollY
let height = rect.bottom - rect.top
// Prevent ad from overlaying the app bar
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)
}
}
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 <Promotion v-if="showAd" :external="false" query-param="?r=launcher" />
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">
<p class="m-0 text-2xl font-bold text-contrast">90% of ad revenue goes to creators</p>
<a
href="https://modrinth.com/plus"
class="mt-auto items-center gap-1 text-purple hover:underline"
>
<span>
Support creators and Modrinth ad-free with
<span class="font-bold">Modrinth+</span>
</span>
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
</a>
</div>
</div>
</template> </template>
@@ -88,6 +88,8 @@ 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 } from '@modrinth/assets'
import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js' import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js'
import { TauriEvent } from '@tauri-apps/api/event'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { getOS } from '@/helpers/utils.js' import { getOS } from '@/helpers/utils.js'
import { useLoading } from '@/store/loading.js' import { useLoading } from '@/store/loading.js'
@@ -128,16 +130,9 @@ const os = ref('')
getOS().then((x) => (os.value = x)) getOS().then((x) => (os.value = x))
loading_listener(async (e) => { loading_listener(async (e) => {
console.log(e)
if (e.event.type === 'directory_move') { if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1) loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating app directory...' message.value = 'Updating app directory...'
} else if (e.event.type === 'launcher_update') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating Modrinth App...'
} else if (e.event.type === 'checking_for_updates') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Checking for updates...'
} }
}) })
@@ -1,12 +1,11 @@
<script setup> <script setup>
import { Button } from '@modrinth/ui' import { Modal, Button } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import SearchCard from '@/components/ui/SearchCard.vue' import SearchCard from '@/components/ui/SearchCard.vue'
import { get_categories } from '@/helpers/tags.js' import { get_categories } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_version, get_project } from '@/helpers/cache.js' import { get_version, get_project } from '@/helpers/cache.js'
import { install as installVersion } from '@/store/install.js' import { install as installVersion } from '@/store/install.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const confirmModal = ref(null) const confirmModal = ref(null)
const project = ref(null) const project = ref(null)
@@ -42,7 +41,7 @@ async function install() {
</script> </script>
<template> <template>
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`"> <Modal ref="confirmModal" :header="`Install ${project?.title}`">
<div class="modal-body"> <div class="modal-body">
<SearchCard <SearchCard
:project="project" :project="project"
@@ -61,7 +60,7 @@ async function install() {
</div> </div>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,5 +1,10 @@
<template> <template>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall"> <Modal
ref="incompatibleModal"
header="Incompatibility warning"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body"> <div class="modal-body">
<p> <p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
@@ -46,19 +51,20 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { XIcon, DownloadIcon } from '@modrinth/assets' import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button, DropdownSelect } from '@modrinth/ui' import { Button, Modal, DropdownSelect } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils' import { formatCategory } from '@modrinth/utils'
import { add_project_from_version as installMod } from '@/helpers/profile' import { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue' import { ref } from 'vue'
import { handleError } from '@/store/state.js' import { handleError, useTheming } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
const themeStore = useTheming()
const instance = ref(null) const instance = ref(null)
const project = ref(null) const project = ref(null)
const versions = ref(null) const versions = ref(null)
@@ -66,7 +72,7 @@ const selectedVersion = ref(null)
const incompatibleModal = ref(null) const incompatibleModal = ref(null)
const installing = ref(false) const installing = ref(false)
const onInstall = ref(() => {}) let onInstall = ref(() => {})
defineExpose({ defineExpose({
show: (instanceVal, projectVal, projectVersions, callback) => { show: (instanceVal, projectVal, projectVersions, callback) => {
@@ -1,18 +1,20 @@
<script setup> <script setup>
import { XIcon, DownloadIcon } from '@modrinth/assets' import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui' import { Button, Modal } from '@modrinth/ui'
import { install as pack_install } from '@/helpers/pack' import { install as pack_install } from '@/helpers/pack'
import { ref } from 'vue' import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { useTheming } from '@/store/theme.js'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const versionId = ref() const versionId = ref()
const project = ref() const project = ref()
const confirmModal = ref(null) const confirmModal = ref(null)
const installing = ref(false) const installing = ref(false)
const onInstall = ref(() => {}) let onInstall = ref(() => {})
defineExpose({ defineExpose({
show: (projectVal, versionIdVal, callback) => { show: (projectVal, versionIdVal, callback) => {
@@ -50,7 +52,12 @@ async function install() {
</script> </script>
<template> <template>
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall"> <Modal
ref="confirmModal"
header="Are you sure?"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body"> <div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p> <p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right"> <div class="input-group push-right">
@@ -60,7 +67,7 @@ async function install() {
> >
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -7,7 +7,7 @@ import {
RightArrowIcon, RightArrowIcon,
CheckIcon, CheckIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Card } from '@modrinth/ui' import { Avatar, Modal, Button, Card } from '@modrinth/ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { import {
add_project_from_version as installMod, add_project_from_version as installMod,
@@ -19,11 +19,12 @@ import {
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { installVersionDependencies } from '@/store/install.js' import { installVersionDependencies } from '@/store/install.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const router = useRouter() const router = useRouter()
const versions = ref() const versions = ref()
@@ -48,7 +49,7 @@ const shownProfiles = computed(() =>
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase()) return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
}) })
.filter((profile) => { .filter((profile) => {
const loaders = versions.value.flatMap((v) => v.loaders) let loaders = versions.value.flatMap((v) => v.loaders)
return ( return (
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) && versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
@@ -59,7 +60,7 @@ const shownProfiles = computed(() =>
}), }),
) )
const onInstall = ref(() => {}) let onInstall = ref(() => {})
defineExpose({ defineExpose({
show: async (projectVal, versionsVal, callback) => { show: async (projectVal, versionsVal, callback) => {
@@ -77,7 +78,7 @@ defineExpose({
onInstall.value = callback onInstall.value = callback
const profilesVal = await list().catch(handleError) const profilesVal = await list().catch(handleError)
for (const profile of profilesVal) { for (let profile of profilesVal) {
profile.installing = false profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value.id).catch( profile.installedMod = await check_installed(profile.path, project.value.id).catch(
handleError, handleError,
@@ -141,7 +142,7 @@ const toggleCreation = () => {
} }
const upload_icon = async () => { const upload_icon = async () => {
const res = await open({ icon.value = await open({
multiple: false, multiple: false,
filters: [ filters: [
{ {
@@ -150,7 +151,6 @@ const upload_icon = async () => {
}, },
], ],
}) })
icon.value = res.path ?? res
if (!icon.value) return if (!icon.value) return
display_icon.value = convertFileSrc(icon.value) display_icon.value = convertFileSrc(icon.value)
@@ -213,7 +213,12 @@ const createInstance = async () => {
</script> </script>
<template> <template>
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall"> <Modal
ref="installModal"
header="Install project to instance"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body"> <div class="modal-body">
<input <input
v-model="searchFilter" v-model="searchFilter"
@@ -299,7 +304,7 @@ const createInstance = async () => {
<Button @click="installModal.hide()">Cancel</Button> <Button @click="installModal.hide()">Cancel</Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,69 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ConfirmModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
confirmationText: {
type: String,
default: '',
},
hasToType: {
type: Boolean,
default: false,
},
title: {
type: String,
default: 'No title defined',
required: true,
},
description: {
type: String,
default: 'No description defined',
required: true,
},
proceedLabel: {
type: String,
default: 'Proceed',
},
})
const emit = defineEmits(['proceed'])
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
function proceed() {
emit('proceed')
}
</script>
<template>
<ConfirmModal
ref="modal"
:confirmation-text="confirmationText"
:has-to-type="hasToType"
:title="title"
:description="description"
:proceed-label="proceedLabel"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
@proceed="proceed"
/>
</template>
@@ -1,49 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Modal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const props = defineProps({
header: {
type: String,
default: null,
},
closable: {
type: Boolean,
default: true,
},
onHide: {
type: Function,
default() {
return () => {}
},
},
})
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
props.onHide()
}
</script>
<template>
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
<slot />
</Modal>
</template>
@@ -1,61 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ShareModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
header: {
type: String,
default: 'Share',
},
shareTitle: {
type: String,
default: 'Modrinth',
},
shareText: {
type: String,
default: null,
},
link: {
type: Boolean,
default: false,
},
openInNewTab: {
type: Boolean,
default: true,
},
})
const modal = ref(null)
defineExpose({
show: (passedContent) => {
hide_ads_window()
modal.value.show(passedContent)
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
</script>
<template>
<ShareModal
ref="modal"
:header="header"
:share-title="shareTitle"
:share-text="shareText"
:link="link"
:open-in-new-tab="openInNewTab"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
/>
</template>
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets' import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
import { Button, Card, Checkbox } from '@modrinth/ui' import { Button, Card, Checkbox, Modal } from '@modrinth/ui'
import { import {
DiscordIcon, DiscordIcon,
GithubIcon, GithubIcon,
@@ -13,7 +13,6 @@ import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.
import { handleError, useNotifications } from '@/store/state.js' import { handleError, useNotifications } from '@/store/state.js'
import { ref } from 'vue' import { ref } from 'vue'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({ const props = defineProps({
callback: { callback: {
@@ -133,7 +132,7 @@ async function createAccount() {
</script> </script>
<template> <template>
<ModalWrapper ref="modal" :on-hide="removeWidget"> <Modal ref="modal" :on-hide="removeWidget">
<Card> <Card>
<template v-if="twoFactorFlow"> <template v-if="twoFactorFlow">
<h1>Enter two-factor code</h1> <h1>Enter two-factor code</h1>
@@ -218,17 +217,17 @@ async function createAccount() {
v-else-if="loggingIn" v-else-if="loggingIn"
color="primary" color="primary"
large large
:disabled="!turnstileToken"
@click="signIn" @click="signIn"
:disabled="!turnstileToken"
> >
Login Login
</Button> </Button>
<Button v-else color="primary" large :disabled="!turnstileToken" @click="createAccount"> <Button v-else color="primary" large @click="createAccount" :disabled="!turnstileToken">
Create account Create account
</Button> </Button>
</div> </div>
</Card> </Card>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
-13
View File
@@ -1,13 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
export async function init_ads_window(x, y, width, height, overrideShown = false) {
return await invoke('plugin:ads|init_ads_window', { x, y, width, height, overrideShown })
}
export async function show_ads_window() {
return await invoke('plugin:ads|show_ads_window')
}
export async function hide_ads_window(reset) {
return await invoke('plugin:ads|hide_ads_window', { reset })
}
+1 -5
View File
@@ -33,10 +33,6 @@ export async function highlightModInProfile(profilePath, projectPath) {
return await highlightInFolder(fullPath) return await highlightInFolder(fullPath)
} }
export async function restartApp() {
return await invoke('restart_app')
}
export const releaseColor = (releaseType) => { export const releaseColor = (releaseType) => {
switch (releaseType) { switch (releaseType) {
case 'release': case 'release':
@@ -56,7 +52,7 @@ export function debounce(fn, wait) {
if (timer) { if (timer) {
clearTimeout(timer) // clear any pre-existing timer clearTimeout(timer) // clear any pre-existing timer
} }
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this // get the current context const context = this // get the current context
timer = setTimeout(() => { timer = setTimeout(() => {
fn.apply(context, args) // call the function if time expires fn.apply(context, args) // call the function if time expires
+12 -12
View File
@@ -381,20 +381,20 @@ const sortedCategories = computed(() => {
// identifier[0], then if it ties, identifier[1], etc // identifier[0], then if it ties, identifier[1], etc
async function sortByNameOrNumber(sortable, identifiers) { async function sortByNameOrNumber(sortable, identifiers) {
sortable.sort((a, b) => { sortable.sort((a, b) => {
for (const identifier of identifiers) { for (let identifier of identifiers) {
const aNum = parseFloat(a[identifier]) let aNum = parseFloat(a[identifier])
const bNum = parseFloat(b[identifier]) let bNum = parseFloat(b[identifier])
if (isNaN(aNum) && isNaN(bNum)) { if (isNaN(aNum) && isNaN(bNum)) {
// Both are strings, sort alphabetically // Both are strings, sort alphabetically
const stringComp = a[identifier].localeCompare(b[identifier]) let stringComp = a[identifier].localeCompare(b[identifier])
if (stringComp != 0) return stringComp if (stringComp != 0) return stringComp
} else if (!isNaN(aNum) && !isNaN(bNum)) { } else if (!isNaN(aNum) && !isNaN(bNum)) {
// Both are numbers, sort numerically // Both are numbers, sort numerically
const numComp = aNum - bNum let numComp = aNum - bNum
if (numComp != 0) return numComp if (numComp != 0) return numComp
} else { } else {
// One is a number and one is a string, numbers go first // One is a number and one is a string, numbers go first
const numStringComp = isNaN(aNum) ? 1 : -1 let numStringComp = isNaN(aNum) ? 1 : -1
if (numStringComp != 0) return numStringComp if (numStringComp != 0) return numStringComp
} }
} }
@@ -528,8 +528,7 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
<template> <template>
<div ref="searchWrapper" class="search-container"> <div ref="searchWrapper" class="search-container">
<aside class="filter-panel" @scroll="$refs.promo.scroll()"> <aside class="filter-panel">
<PromotionWrapper ref="promo" />
<Card v-if="instanceContext" class="small-instance"> <Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance"> <router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar <Avatar
@@ -676,7 +675,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
</Card> </Card>
</aside> </aside>
<div class="search"> <div class="search">
<Card class="project-type-container mt-4"> <PromotionWrapper class="mt-4" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" /> <NavRow :links="selectableProjectTypes" />
</Card> </Card>
<Card class="search-panel-container"> <Card class="search-panel-container">
@@ -878,13 +878,13 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
.filter-panel { .filter-panel {
position: fixed; position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem; padding: 1rem 0.5rem 1rem 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: fit-content; height: fit-content;
min-height: calc(100vh - 3.25rem); min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem); max-height: calc(100vh - 3.25rem);
width: 20rem;
overflow-y: auto; overflow-y: auto;
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
@@ -903,8 +903,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
} }
.search { .search {
margin: 0 1rem 0.5rem calc(20rem + 1rem); margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - calc(20rem + 1rem)); width: calc(100% - 20.5rem);
.offline { .offline {
margin: 1rem; margin: 1rem;
+2 -7
View File
@@ -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({})
@@ -47,7 +42,7 @@ const getInstances = async () => {
return dateB - dateA return dateB - dateA
}) })
const filters = [] let filters = []
for (const instance of recentInstances.value) { for (const instance of recentInstances.value) {
if (instance.linked_data && instance.linked_data.project_id) { if (instance.linked_data && instance.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`) filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
+1 -6
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, onUnmounted, ref, shallowRef } from 'vue' import { onUnmounted, ref, shallowRef } from 'vue'
import GridDisplay from '@/components/GridDisplay.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'
@@ -10,11 +10,6 @@ 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'
onMounted(() => {
hide_ads_window(true)
})
const route = useRoute() const route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
+10 -15
View File
@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, watch, onMounted } from 'vue' import { ref, watch } from 'vue'
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets' import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui' import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state' import { handleError, useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings'
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre' import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
@@ -13,12 +13,6 @@ import { open } from '@tauri-apps/plugin-dialog'
import { getOS } from '@/helpers/utils.js' import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { get_user, purge_cache_types } from '@/helpers/cache.js' 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 pageOptions = ['Home', 'Library']
@@ -175,12 +169,13 @@ async function purgeCache() {
Sign in Sign in
</button> </button>
</div> </div>
<ConfirmModalWrapper <ConfirmModal
ref="purgeCacheConfirmModal" ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?" title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily." description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false" :has-to-type="false"
proceed-label="Purge cache" proceed-label="Purge cache"
:noblur="!themeStore.advancedRendering"
@proceed="purgeCache" @proceed="purgeCache"
/> />
<div class="adjacent-input"> <div class="adjacent-input">
@@ -406,14 +401,14 @@ async function purgeCache() {
<span class="label__title size-card-header">Java settings</span> <span class="label__title size-card-header">Java settings</span>
</h3> </h3>
</div> </div>
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`"> <template v-for="version in [21, 17, 8]">
<label :for="'java-' + javaVersion"> <label :for="'java-' + version">
<span class="label__title">Java {{ javaVersion }} location</span> <span class="label__title">Java {{ version }} location</span>
</label> </label>
<JavaSelector <JavaSelector
:id="'java-selector-' + javaVersion" :id="'java-selector-' + version"
v-model="javaVersions[javaVersion]" v-model="javaVersions[version]"
:version="javaVersion" :version="version"
@update:model-value="updateJavaVersion" @update:model-value="updateJavaVersion"
/> />
</template> </template>
+14 -11
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="instance-container"> <div class="instance-container">
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()"> <div class="side-cards">
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick"> <Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" /> <Avatar size="lg" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
<div class="instance-info"> <div class="instance-info">
<h2 class="name">{{ instance.name }}</h2> <h2 class="name">{{ instance.name }}</h2>
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span> <span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
@@ -61,9 +61,9 @@
</RouterLink> </RouterLink>
</div> </div>
</Card> </Card>
<PromotionWrapper ref="promo" class="mt-4" />
</div> </div>
<div class="content"> <div class="content">
<PromotionWrapper />
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()"> <Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
@@ -311,6 +311,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
width: 17rem;
} }
Button { Button {
@@ -324,13 +325,12 @@ Button {
} }
.side-cards { .side-cards {
position: fixed; position: absolute;
width: 300px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem;
min-height: calc(100vh - 3.25rem); min-height: calc(100% - 3.25rem);
max-height: calc(100vh - 3.25rem); max-height: calc(100% - 3.25rem);
overflow-y: auto; overflow-y: auto;
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
@@ -374,7 +374,10 @@ Button {
overflow: auto; overflow: auto;
gap: 1rem; gap: 1rem;
min-height: 100%; min-height: 100%;
padding: 1rem; }
.content {
margin-left: 19rem;
} }
.instance-info { .instance-info {
@@ -448,10 +451,10 @@ Button {
} }
.content { .content {
margin: 0 1rem 0.5rem 20rem; width: 100%;
width: calc(100% - 20rem);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem 1rem 0 0;
overflow: auto; overflow: auto;
} }
@@ -76,7 +76,7 @@
</div> </div>
</RecycleScroller> </RecycleScroller>
</div> </div>
<ShareModalWrapper <ShareModal
ref="shareModal" ref="shareModal"
header="Share Log" header="Share Log"
share-title="Instance Log" share-title="Instance Log"
@@ -89,7 +89,7 @@
<script setup> <script setup>
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets' import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
import { Button, Card, Checkbox, DropdownSelect } from '@modrinth/ui' import { Button, Card, ShareModal, Checkbox, DropdownSelect } from '@modrinth/ui'
import { import {
delete_logs_by_filename, delete_logs_by_filename,
get_logs, get_logs,
@@ -107,7 +107,6 @@ import { handleError } from '@/store/notifications.js'
import { ofetch } from 'ofetch' import { ofetch } from 'ofetch'
import { RecycleScroller } from 'vue-virtual-scroller' import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
dayjs.extend(isToday) dayjs.extend(isToday)
dayjs.extend(isYesterday) dayjs.extend(isYesterday)
@@ -296,7 +295,7 @@ if (logs.value.length > 1 && !props.playing) {
const deleteLog = async () => { const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) { if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
const deleteIndex = selectedLogIndex.value let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1 selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_filename( await delete_logs_by_filename(
props.instance.path, props.instance.path,
+9 -10
View File
@@ -284,7 +284,7 @@
:link-function="(page) => `?page=${page}`" :link-function="(page) => `?page=${page}`"
@switch-page="switchPage" @switch-page="switchPage"
/> />
<ModalWrapper ref="deleteWarning" header="Are you sure?"> <Modal ref="deleteWarning" header="Are you sure?">
<div class="modal-body"> <div class="modal-body">
<div class="markdown-body"> <div class="markdown-body">
<p> <p>
@@ -302,8 +302,8 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?"> <Modal ref="deleteDisabledWarning" header="Are you sure?">
<div class="modal-body"> <div class="modal-body">
<div class="markdown-body"> <div class="markdown-body">
<p> <p>
@@ -325,8 +325,8 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ShareModalWrapper <ShareModal
ref="shareModal" ref="shareModal"
share-title="Sharing modpack content" share-title="Sharing modpack content"
share-text="Check out the projects I'm using in my modpack!" share-text="Check out the projects I'm using in my modpack!"
@@ -360,6 +360,8 @@ import {
import { import {
Pagination, Pagination,
DropdownSelect, DropdownSelect,
ShareModal,
Modal,
Checkbox, Checkbox,
AnimatedLogo, AnimatedLogo,
Avatar, Avatar,
@@ -391,8 +393,6 @@ 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'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -717,7 +717,7 @@ const updateProject = async (mod) => {
}) })
} }
const locks = {} let locks = {}
const toggleDisableMod = async (mod) => { const toggleDisableMod = async (mod) => {
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property. // Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
@@ -725,7 +725,7 @@ const toggleDisableMod = async (mod) => {
locks[mod.id] = ref(null) locks[mod.id] = ref(null)
} }
const lock = locks[mod.id] let lock = locks[mod.id]
while (lock.value) { while (lock.value) {
await lock.value await lock.value
@@ -784,7 +784,6 @@ const deleteDisabled = async () => {
} }
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'))
} }
@@ -1,13 +1,18 @@
<template> <template>
<ConfirmModalWrapper <ConfirmModal
ref="modal_confirm" ref="modal_confirm"
title="Are you sure you want to delete this instance?" title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it." description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false" :has-to-type="false"
proceed-label="Delete" proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="removeProfile" @proceed="removeProfile"
/> />
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?"> <Modal
ref="modalConfirmUnlock"
header="Are you sure you want to unlock this instance?"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-delete"> <div class="modal-delete">
<div <div
class="markdown-body" class="markdown-body"
@@ -26,9 +31,13 @@
</button> </button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?"> <Modal
ref="modalConfirmUnpair"
header="Are you sure you want to unpair this instance?"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-delete"> <div class="modal-delete">
<div <div
class="markdown-body" class="markdown-body"
@@ -47,9 +56,13 @@
</button> </button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ModalWrapper ref="changeVersionsModal" header="Change instance versions"> <Modal
ref="changeVersionsModal"
header="Change instance versions"
:noblur="!themeStore.advancedRendering"
>
<div class="change-versions-modal universal-body"> <div class="change-versions-modal universal-body">
<div class="input-row"> <div class="input-row">
<p class="input-label">Loader</p> <p class="input-label">Loader</p>
@@ -93,7 +106,7 @@
</button> </button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<section class="card"> <section class="card">
<div class="label"> <div class="label">
<h3> <h3>
@@ -498,7 +511,18 @@ import {
DownloadIcon, DownloadIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui' import {
Button,
Toggle,
ConfirmModal,
Card,
Slider,
Checkbox,
Avatar,
Modal,
Chips,
DropdownSelect,
} from '@modrinth/ui'
import { SwapIcon } from '@/assets/icons' import { SwapIcon } from '@/assets/icons'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
@@ -522,11 +546,10 @@ import { open } from '@tauri-apps/plugin-dialog'
import { get_loader_versions } from '@/helpers/metadata.js' import { get_loader_versions } from '@/helpers/metadata.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js' import { get_game_versions, get_loaders } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue' import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
@@ -547,6 +570,8 @@ const props = defineProps({
}, },
}) })
const themeStore = useTheming()
const title = ref(props.instance.name) const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path) const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups) const groups = ref(props.instance.groups)
@@ -581,7 +606,7 @@ async function setIcon() {
if (!value) return if (!value) return
icon.value = value.path ?? value icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError) await edit_icon(props.instance.path, icon.value).catch(handleError)
trackEvent('InstanceSetIcon') trackEvent('InstanceSetIcon')
@@ -596,12 +621,12 @@ const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError)) const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path }) const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined) const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
const javaArgs = ref( const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '), (props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
) )
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined) const overrideEnvVars = ref(!!props.instance.custom_env_vars)
const envVars = ref( const envVars = ref(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars) (props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('=')) .map((x) => x.join('='))
@@ -685,15 +710,19 @@ const editProfileObject = computed(() => {
} }
if (overrideJavaArgs.value) { if (overrideJavaArgs.value) {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean) if (javaArgs.value !== '') {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
} }
if (overrideEnvVars.value) { if (overrideEnvVars.value) {
editProfile.custom_env_vars = envVars.value if (envVars.value !== '') {
.trim() editProfile.custom_env_vars = envVars.value
.split(/\s+/) .trim()
.filter(Boolean) .split(/\s+/)
.map((x) => x.split('=').filter(Boolean)) .filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
} }
if (overrideMemorySettings.value) { if (overrideMemorySettings.value) {
@@ -876,7 +905,7 @@ const editing = ref(false)
async function saveGvLoaderEdits() { async function saveGvLoaderEdits() {
editing.value = true editing.value = true
const editProfile = editProfileObject.value let editProfile = editProfileObject.value
editProfile.loader = loader.value editProfile.loader = loader.value
editProfile.game_version = gameVersion.value editProfile.game_version = gameVersion.value
@@ -20,14 +20,14 @@
</span> </span>
</Card> </Card>
</div> </div>
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage"> <div v-if="expandedGalleryItem" class="expanded-image-modal" @click="expandedGalleryItem = null">
<div class="content"> <div class="content">
<img <img
class="image" class="image"
:class="{ 'zoomed-in': zoomedIn }" :class="{ 'zoomed-in': zoomedIn }"
:src=" :src="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'" :alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -45,15 +45,15 @@
</div> </div>
<div class="controls"> <div class="controls">
<div class="buttons"> <div class="buttons">
<Button class="close" icon-only @click="hideImage"> <Button class="close" icon-only @click="expandedGalleryItem = null">
<XIcon aria-hidden="true" /> <XIcon aria-hidden="true" />
</Button> </Button>
<a <a
class="open btn icon-only" class="open btn icon-only"
target="_blank" target="_blank"
:href=" :href="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
> >
@@ -94,7 +94,6 @@ import {
import { Button, Card } from '@modrinth/ui' import { Button, Card } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const props = defineProps({ const props = defineProps({
project: { project: {
@@ -103,14 +102,9 @@ const props = defineProps({
}, },
}) })
const expandedGalleryItem = ref(null) let expandedGalleryItem = ref(null)
const expandedGalleryIndex = ref(0) let expandedGalleryIndex = ref(0)
const zoomedIn = ref(false) let zoomedIn = ref(false)
const hideImage = () => {
expandedGalleryItem.value = null
show_ads_window()
}
const nextImage = () => { const nextImage = () => {
expandedGalleryIndex.value++ expandedGalleryIndex.value++
@@ -137,7 +131,6 @@ const previousImage = () => {
} }
const expandImage = (item, index) => { const expandImage = (item, index) => {
hide_ads_window()
expandedGalleryItem.value = item expandedGalleryItem.value = item
expandedGalleryIndex.value = index expandedGalleryIndex.value = index
zoomedIn.value = false zoomedIn.value = false
@@ -147,20 +140,6 @@ const expandImage = (item, index) => {
url: item.url, url: item.url,
}) })
} }
function keyListener(e) {
if (expandedGalleryItem.value) {
e.preventDefault()
if (e.key === 'Escape') {
hideImage()
} else if (e.key === 'ArrowLeft') {
previousImage()
} else if (e.key === 'ArrowRight') {
nextImage()
}
}
}
document.addEventListener('keypress', keyListener)
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+16 -11
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="root-container"> <div class="root-container">
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()"> <div v-if="data" class="project-sidebar">
<Card v-if="instance" class="small-instance"> <Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`"> <router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar <Avatar
@@ -20,7 +20,7 @@
</router-link> </router-link>
</Card> </Card>
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick"> <Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="md" :src="data.icon_url" /> <Avatar size="lg" :src="data.icon_url" />
<div class="instance-info"> <div class="instance-info">
<h2 class="name">{{ data.title }}</h2> <h2 class="name">{{ data.title }}</h2>
{{ data.description }} {{ data.description }}
@@ -61,9 +61,7 @@
Site Site
</a> </a>
</div> </div>
</Card> <hr class="card-divider" />
<PromotionWrapper ref="promo" />
<Card class="sidebar-card">
<div class="stats"> <div class="stats">
<div class="stat"> <div class="stat">
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />
@@ -165,6 +163,7 @@
</Card> </Card>
</div> </div>
<div v-if="data" class="content-container"> <div v-if="data" class="content-container">
<PromotionWrapper />
<Card class="tabs"> <Card class="tabs">
<NavRow <NavRow
v-if="data.gallery.length > 0" v-if="data.gallery.length > 0"
@@ -232,7 +231,15 @@ import {
GlobeIcon, GlobeIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui' import {
Categories,
EnvironmentIndicator,
Card,
Avatar,
Button,
Promotion,
NavRow,
} from '@modrinth/ui'
import { formatNumber } from '@modrinth/utils' import { formatNumber } from '@modrinth/utils'
import { import {
BuyMeACoffeeIcon, BuyMeACoffeeIcon,
@@ -253,7 +260,7 @@ import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/core' 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_project_many, get_team, get_version_many } from '@/helpers/cache.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
@@ -302,13 +309,11 @@ async function fetchProjectData() {
await fetchProjectData() await fetchProjectData()
const promo = ref(null)
watch( watch(
() => route.params.id, () => route.params.id,
async () => { async () => {
if (route.params.id && route.path.startsWith('/project')) { if (route.params.id && route.path.startsWith('/project')) {
await fetchProjectData() await fetchProjectData()
promo.value.scroll()
} }
}, },
) )
@@ -372,7 +377,7 @@ const handleOptionsClick = (args) => {
.project-sidebar { .project-sidebar {
position: fixed; position: fixed;
width: calc(300px + 1.5rem); width: 20rem;
min-height: calc(100vh - 3.25rem); min-height: calc(100vh - 3.25rem);
height: fit-content; height: fit-content;
max-height: calc(100vh - 3.25rem); max-height: calc(100vh - 3.25rem);
@@ -398,7 +403,7 @@ const handleOptionsClick = (args) => {
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
margin-left: calc(300px + 1rem); margin-left: 19.5rem;
} }
.button-group { .button-group {
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowJs": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"strict": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+10 -2
View File
@@ -1,4 +1,12 @@
{ {
"files": [], "compilerOptions": {
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] "module": "NodeNext",
"moduleResolution": "NodeNext",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ESNext"
},
"exclude": ["node_modules", "dist"]
} }
-17
View File
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+2 -2
View File
@@ -42,8 +42,8 @@ export default defineConfig({
port: 1420, port: 1420,
strictPort: true, strictPort: true,
}, },
// to make use of `TAURI_ENV_DEBUG` and other env variables // to make use of `TAURI_DEBUG` and other env variables
// https://v2.tauri.app/reference/environment-variables/#tauri-cli-hook-commands // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'], envPrefix: ['VITE_', 'TAURI_'],
build: { build: {
// Tauri supports es2021 // Tauri supports es2021
+1
View File
@@ -10,6 +10,7 @@ theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = "2.0.0-rc.4"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
thiserror = "1.0" thiserror = "1.0"
url = "2.2" url = "2.2"
+5 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "theseus_gui" name = "theseus_gui"
version = "0.8.7" version = "0.8.3-1"
description = "The Modrinth App is a desktop application for managing your Minecraft mods" description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/" repository = "https://github.com/modrinth/code/apps/app/"
@@ -8,7 +8,7 @@ edition = "2021"
build = "build.rs" build = "build.rs"
[build-dependencies] [build-dependencies]
tauri-build = { git = "https://github.com/modrinth/tauri", features = ["codegen"], rev = "5e29428" } tauri-build = { version = "2.0.0-rc", features = ["codegen"] }
[dependencies] [dependencies]
theseus = { path = "../../packages/app-lib", features = ["tauri"] } theseus = { path = "../../packages/app-lib", features = ["tauri"] }
@@ -16,14 +16,13 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { git = "https://github.com/modrinth/tauri", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"], rev = "5e29428" } tauri = { version = "2.0.0-rc.6", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
tauri-plugin-window-state = "2.0.0-rc" tauri-plugin-window-state = "2.0.0-rc"
tauri-plugin-deep-link = "2.0.0-rc" tauri-plugin-deep-link = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc" tauri-plugin-os = "2.0.0-rc"
tauri-plugin-shell = "2.0.0-rc" tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc" tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-updater = { version = "2.0.0-rc" } tauri-plugin-updater = { version = "2.0.0-rc.1", optional = true }
tauri-plugin-single-instance = { version = "2.0.0-rc" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
thiserror = "1.0" thiserror = "1.0"
@@ -58,9 +57,6 @@ cocoa = "0.25.0"
objc = "0.2.7" objc = "0.2.7"
rand = "0.8.5" rand = "0.8.5"
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { version = "2.0.0-rc", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
@@ -68,4 +64,4 @@ default = ["custom-protocol"]
# this feature is used for production builds where `devPath` points to the filesystem # this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this # DO NOT remove this
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
updater = [] updater = ["dep:tauri-plugin-updater"]
+1 -2
View File
@@ -15,14 +15,13 @@ Before you begin, ensure you have the following installed on your machine:
- [Node.js](https://nodejs.org/en/) - [Node.js](https://nodejs.org/en/)
- [pnpm](https://pnpm.io/) - [pnpm](https://pnpm.io/)
- [Rust](https://www.rust-lang.org/tools/install) - [Rust](https://www.rust-lang.org/tools/install)
- [Tauri](https://v2.tauri.app/start/prerequisites/) - [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/#installing)
### Setup ### Setup
Follow these steps to set up your development environment: Follow these steps to set up your development environment:
```bash ```bash
cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
pnpm install pnpm install
pnpm app:dev pnpm app:dev
``` ```
+1 -14
View File
@@ -62,7 +62,7 @@ fn main() {
InlinedPlugin::new() InlinedPlugin::new()
.commands(&[ .commands(&[
"get_java_versions", "get_java_versions",
"set_java_version", "set_java_versions",
"jre_find_filtered_jres", "jre_find_filtered_jres",
"jre_get_jre", "jre_get_jre",
"jre_test_jre", "jre_test_jre",
@@ -217,19 +217,6 @@ fn main() {
.default_permission( .default_permission(
DefaultPermissionRule::AllowAllCommands, DefaultPermissionRule::AllowAllCommands,
), ),
)
.plugin(
"ads",
InlinedPlugin::new()
.commands(&[
"init_ads_window",
"hide_ads_window",
"scroll_ads_window",
"show_ads_window",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
), ),
) )
.expect("Failed to run tauri-build"); .expect("Failed to run tauri-build");
-15
View File
@@ -1,15 +0,0 @@
{
"identifier": "ads",
"description": "",
"local": false,
"remote": {
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
},
"webviews": [
"ads-window"
],
"permissions": [
"shell:allow-open",
"ads:default"
]
}
+1 -2
View File
@@ -35,7 +35,6 @@
"cache:default", "cache:default",
"settings:default", "settings:default",
"tags:default", "tags:default",
"utils:default", "utils:default"
"ads:default"
] ]
} }
-11
View File
@@ -1,11 +0,0 @@
{
"identifier": "updater",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"updater:default"
]
}
+1 -1
View File
@@ -1 +1 @@
{"ads":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-init-ads-window","allow-hide-ads-window","allow-scroll-ads-window","allow-show-ads-window"]},"permissions":{"allow-hide-ads-window":{"identifier":"allow-hide-ads-window","description":"Enables the hide_ads_window command without any pre-configured scope.","commands":{"allow":["hide_ads_window"],"deny":[]}},"allow-init-ads-window":{"identifier":"allow-init-ads-window","description":"Enables the init_ads_window command without any pre-configured scope.","commands":{"allow":["init_ads_window"],"deny":[]}},"allow-scroll-ads-window":{"identifier":"allow-scroll-ads-window","description":"Enables the scroll_ads_window command without any pre-configured scope.","commands":{"allow":["scroll_ads_window"],"deny":[]}},"allow-show-ads-window":{"identifier":"allow-show-ads-window","description":"Enables the show_ads_window command without any pre-configured scope.","commands":{"allow":["show_ads_window"],"deny":[]}},"deny-hide-ads-window":{"identifier":"deny-hide-ads-window","description":"Denies the hide_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["hide_ads_window"]}},"deny-init-ads-window":{"identifier":"deny-init-ads-window","description":"Denies the init_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["init_ads_window"]}},"deny-scroll-ads-window":{"identifier":"deny-scroll-ads-window","description":"Denies the scroll_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["scroll_ads_window"]}},"deny-show-ads-window":{"identifier":"deny-show-ads-window","description":"Denies the show_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["show_ads_window"]}}},"permission_sets":{},"global_scope_schema":null},"auth":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-login","allow-remove-user","allow-get-default-user","allow-set-default-user","allow-get-users"]},"permissions":{"allow-get-default-user":{"identifier":"allow-get-default-user","description":"Enables the get_default_user command without any pre-configured scope.","commands":{"allow":["get_default_user"],"deny":[]}},"allow-get-users":{"identifier":"allow-get-users","description":"Enables the get_users command without any pre-configured scope.","commands":{"allow":["get_users"],"deny":[]}},"allow-login":{"identifier":"allow-login","description":"Enables the login command without any pre-configured scope.","commands":{"allow":["login"],"deny":[]}},"allow-remove-user":{"identifier":"allow-remove-user","description":"Enables the remove_user command without any pre-configured scope.","commands":{"allow":["remove_user"],"deny":[]}},"allow-set-default-user":{"identifier":"allow-set-default-user","description":"Enables the set_default_user command without any pre-configured scope.","commands":{"allow":["set_default_user"],"deny":[]}},"deny-get-default-user":{"identifier":"deny-get-default-user","description":"Denies the get_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["get_default_user"]}},"deny-get-users":{"identifier":"deny-get-users","description":"Denies the get_users command without any pre-configured scope.","commands":{"allow":[],"deny":["get_users"]}},"deny-login":{"identifier":"deny-login","description":"Denies the login command without any pre-configured scope.","commands":{"allow":[],"deny":["login"]}},"deny-remove-user":{"identifier":"deny-remove-user","description":"Denies the remove_user command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_user"]}},"deny-set-default-user":{"identifier":"deny-set-default-user","description":"Denies the set_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["set_default_user"]}}},"permission_sets":{},"global_scope_schema":null},"cache":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-get-project","allow-get-project-many","allow-get-version","allow-get-version-many","allow-get-user","allow-get-user-many","allow-get-team","allow-get-team-many","allow-get-organization","allow-get-organization-many","allow-get-search-results","allow-get-search-results-many","allow-purge-cache-types"]},"permissions":{"allow-get-organization":{"identifier":"allow-get-organization","description":"Enables the get_organization command without any pre-configured scope.","commands":{"allow":["get_organization"],"deny":[]}},"allow-get-organization-many":{"identifier":"allow-get-organization-many","description":"Enables the get_organization_many command without any pre-configured scope.","commands":{"allow":["get_organization_many"],"deny":[]}},"allow-get-project":{"identifier":"allow-get-project","description":"Enables the get_project command without any pre-configured scLine truncated {"auth":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-login","allow-remove-user","allow-get-default-user","allow-set-default-user","allow-get-users"]},"permissions":{"allow-get-default-user":{"identifier":"allow-get-default-user","description":"Enables the get_default_user command without any pre-configured scope.","commands":{"allow":["get_default_user"],"deny":[]}},"allow-get-users":{"identifier":"allow-get-users","description":"Enables the get_users command without any pre-configured scope.","commands":{"allow":["get_users"],"deny":[]}},"allow-login":{"identifier":"allow-login","description":"Enables the login command without any pre-configured scope.","commands":{"allow":["login"],"deny":[]}},"allow-remove-user":{"identifier":"allow-remove-user","description":"Enables the remove_user command without any pre-configured scope.","commands":{"allow":["remove_user"],"deny":[]}},"allow-set-default-user":{"identifier":"allow-set-default-user","description":"Enables the set_default_user command without any pre-configured scope.","commands":{"allow":["set_default_user"],"deny":[]}},"deny-get-default-user":{"identifier":"deny-get-default-user","description":"Denies the get_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["get_default_user"]}},"deny-get-users":{"identifier":"deny-get-users","description":"Denies the get_users command without any pre-configured scope.","commands":{"allow":[],"deny":["get_users"]}},"deny-login":{"identifier":"deny-login","description":"Denies the login command without any pre-configured scope.","commands":{"allow":[],"deny":["login"]}},"deny-remove-user":{"identifier":"deny-remove-user","description":"Denies the remove_user command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_user"]}},"deny-set-default-user":{"identifier":"deny-set-default-user","description":"Denies the set_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["set_default_user"]}}},"permission_sets":{},"global_scope_schema":null},"cache":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-get-project","allow-get-project-many","allow-get-version","allow-get-version-many","allow-get-user","allow-get-user-many","allow-get-team","allow-get-team-many","allow-get-organization","allow-get-organization-many","allow-get-search-results","allow-get-search-results-many","allow-purge-cache-types"]},"permissions":{"allow-get-organization":{"identifier":"allow-get-organization","description":"Enables the get_organization command without any pre-configured scope.","commands":{"allow":["get_organization"],"deny":[]}},"allow-get-organization-many":{"identifier":"allow-get-organization-many","description":"Enables the get_organization_many command without any pre-configured scope.","commands":{"allow":["get_organization_many"],"deny":[]}},"allow-get-project":{"identifier":"allow-get-project","description":"Enables the get_project command without any pre-configured scope.","commands":{"allow":["get_project"],"deny":[]}},"allow-get-project-many":{"identifier":"allow-get-project-many","description":"Enables the get_project_many command without any pre-configured scope.","commands":{"allow":["get_project_many"],"deny":[]}},"allow-get-search-results":{"identifier":"allow-get-search-results","description":"Enables the get_search_results command without any pre-configured scope.","commands":{"allow":["get_search_results"],"deny":[]}},"allow-get-search-results-many":{"identifier":"allow-get-search-results-many","description":"Enables the get_search_results_many command without any pre-configured scope.","commands":{"allow":["get_search_results_many"],"deny":[]}},"allow-get-team":{"identifier":"allow-get-team","description":"Enables the get_team command without any pre-configured scope.","commands":{"allow":["get_team"],"deny":[]}},"allow-get-team-many":{"identifier":"allow-get-team-many","description":"Enables the get_team_many command without any pre-configured scope.","commands":{"allow":["get_team_many"],"deny":[]}},"allow-get-user":{"identifier":"allow-get-user","description":"Enables the get_user command without any pre-configured scope.","commands":{"allow":["get_user"],"deny":[]}},"allow-get-user-many":{"identifier":"allow-get-user-many","description":"Enables the get_user_many command without any pre-configured scope.","commands":{"allow":["get_user_many"],"deny":[]}},"allow-get-version":{"identifier":"allow-get-version","description":"Enables the get_version command without any pre-configured scope.","commands":{"allow":["get_version"],"deny":[]}},"allow-get-version-many":{"identifier":"allow-get-version-many","description":"Enables the get_version_many command without any pre-configured scope.","commands":{"allow":["get_version_many"],"deny":[]}},"allow-purge-cache-types":{"identifier":"allow-purge-cache-typLine truncated
+1 -1
View File
@@ -1 +1 @@
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["shell:allow-open","ads:default"]},"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default","ads:default"]},"updater":{"identifier":"updater","description":"","local":true,"windows":["main"],"permissions":["updater:default"]}} {"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default"]}}
+4 -130
View File
@@ -299,69 +299,6 @@
}, },
"Identifier": { "Identifier": {
"oneOf": [ "oneOf": [
{
"description": "ads:default -> Default plugin permissions.",
"type": "string",
"enum": [
"ads:default"
]
},
{
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-hide-ads-window"
]
},
{
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-scroll-ads-window"
]
},
{
"description": "ads:allow-show-ads-window -> Enables the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-show-ads-window"
]
},
{
"description": "ads:deny-hide-ads-window -> Denies the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-hide-ads-window"
]
},
{
"description": "ads:deny-init-ads-window -> Denies the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-init-ads-window"
]
},
{
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-scroll-ads-window"
]
},
{
"description": "ads:deny-show-ads-window -> Denies the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-show-ads-window"
]
},
{ {
"description": "auth:default -> Default plugin permissions.", "description": "auth:default -> Default plugin permissions.",
"type": "string", "type": "string",
@@ -2848,10 +2785,10 @@
] ]
}, },
{ {
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.", "description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:allow-set-java-version" "jre:allow-set-java-versions"
] ]
}, },
{ {
@@ -2897,10 +2834,10 @@
] ]
}, },
{ {
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.", "description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:deny-set-java-version" "jre:deny-set-java-versions"
] ]
}, },
{ {
@@ -3918,69 +3855,6 @@
"tags:deny-tags-get-report-types" "tags:deny-tags-get-report-types"
] ]
}, },
{
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string",
"enum": [
"updater:default"
]
},
{
"description": "updater:allow-check -> Enables the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-check"
]
},
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download-and-install"
]
},
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-check"
]
},
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download-and-install"
]
},
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"description": "utils:default -> Default plugin permissions.", "description": "utils:default -> Default plugin permissions.",
"type": "string", "type": "string",
+4 -67
View File
@@ -299,69 +299,6 @@
}, },
"Identifier": { "Identifier": {
"oneOf": [ "oneOf": [
{
"description": "ads:default -> Default plugin permissions.",
"type": "string",
"enum": [
"ads:default"
]
},
{
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-hide-ads-window"
]
},
{
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-scroll-ads-window"
]
},
{
"description": "ads:allow-show-ads-window -> Enables the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-show-ads-window"
]
},
{
"description": "ads:deny-hide-ads-window -> Denies the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-hide-ads-window"
]
},
{
"description": "ads:deny-init-ads-window -> Denies the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-init-ads-window"
]
},
{
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-scroll-ads-window"
]
},
{
"description": "ads:deny-show-ads-window -> Denies the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-show-ads-window"
]
},
{ {
"description": "auth:default -> Default plugin permissions.", "description": "auth:default -> Default plugin permissions.",
"type": "string", "type": "string",
@@ -2848,10 +2785,10 @@
] ]
}, },
{ {
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.", "description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:allow-set-java-version" "jre:allow-set-java-versions"
] ]
}, },
{ {
@@ -2897,10 +2834,10 @@
] ]
}, },
{ {
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.", "description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:deny-set-java-version" "jre:deny-set-java-versions"
] ]
}, },
{ {
+4 -130
View File
@@ -299,69 +299,6 @@
}, },
"Identifier": { "Identifier": {
"oneOf": [ "oneOf": [
{
"description": "ads:default -> Default plugin permissions.",
"type": "string",
"enum": [
"ads:default"
]
},
{
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-hide-ads-window"
]
},
{
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-scroll-ads-window"
]
},
{
"description": "ads:allow-show-ads-window -> Enables the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-show-ads-window"
]
},
{
"description": "ads:deny-hide-ads-window -> Denies the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-hide-ads-window"
]
},
{
"description": "ads:deny-init-ads-window -> Denies the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-init-ads-window"
]
},
{
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-scroll-ads-window"
]
},
{
"description": "ads:deny-show-ads-window -> Denies the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-show-ads-window"
]
},
{ {
"description": "auth:default -> Default plugin permissions.", "description": "auth:default -> Default plugin permissions.",
"type": "string", "type": "string",
@@ -2848,10 +2785,10 @@
] ]
}, },
{ {
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.", "description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:allow-set-java-version" "jre:allow-set-java-versions"
] ]
}, },
{ {
@@ -2897,10 +2834,10 @@
] ]
}, },
{ {
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.", "description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:deny-set-java-version" "jre:deny-set-java-versions"
] ]
}, },
{ {
@@ -3918,69 +3855,6 @@
"tags:deny-tags-get-report-types" "tags:deny-tags-get-report-types"
] ]
}, },
{
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string",
"enum": [
"updater:default"
]
},
{
"description": "updater:allow-check -> Enables the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-check"
]
},
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download-and-install"
]
},
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-check"
]
},
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download-and-install"
]
},
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"description": "utils:default -> Default plugin permissions.", "description": "utils:default -> Default plugin permissions.",
"type": "string", "type": "string",
-41
View File
@@ -1,41 +0,0 @@
!macro NSIS_HOOK_POSTINSTALL
SetShellVarContext current
IfFileExists "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe" file_found file_not_found
file_found:
Delete "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Delete "$LOCALAPPDATA${PRODUCTNAME}\uninstall.exe"
RMDir "$LOCALAPPDATA${PRODUCTNAME}"
!insertmacro DeleteAppUserModelId
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
!insertmacro IsShortcutTarget "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
RMDir "$SMPROGRAMS$AppStartMenuFolder"
${EndIf}
!insertmacro IsShortcutTarget "$SMPROGRAMS${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS${PRODUCTNAME}.lnk"
${EndIf}
!insertmacro IsShortcutTarget "$DESKTOP${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$DESKTOP${PRODUCTNAME}.lnk"
Delete "$DESKTOP${PRODUCTNAME}.lnk"
${EndIf}
DeleteRegKey HKCU "${UNINSTKEY}"
goto end_of_test ;<== important for not continuing on the else branch
file_not_found:
end_of_test:
!macroend
+6 -3
View File
@@ -1,13 +1,16 @@
{ {
"name": "@modrinth/app", "name": "@modrinth/app",
"scripts": { "scripts": {
"build": "cargo tauri build", "build": "tauri build",
"tauri": "cargo tauri", "tauri": "tauri",
"dev": "cargo tauri dev", "dev": "tauri dev",
"test": "cargo test", "test": "cargo test",
"lint": "cargo fmt --check && cargo clippy -- -D warnings", "lint": "cargo fmt --check && cargo clippy -- -D warnings",
"fix": "cargo fmt && cargo clippy --fix" "fix": "cargo fmt && cargo clippy --fix"
}, },
"devDependencies": {
"@tauri-apps/cli": "2.0.0-rc.5"
},
"dependencies": { "dependencies": {
"@modrinth/app-frontend": "workspace:*", "@modrinth/app-frontend": "workspace:*",
"@modrinth/app-lib": "workspace:*" "@modrinth/app-lib": "workspace:*"
-20
View File
@@ -1,20 +0,0 @@
if (!window.modrinthClickListener) {
window.modrinthClickListener = true
document.addEventListener('click', function (e) {
let target = e.target
while (target != null) {
if (target.matches('a')) {
e.preventDefault()
if (target.href) {
window.top.postMessage({ modrinthOpenUrl: target.href }, 'https://modrinth.com')
}
break
}
target = target.parentElement
}
})
}
window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
}
-161
View File
@@ -1,161 +0,0 @@
use serde::Serialize;
use tauri::plugin::TauriPlugin;
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub size: Option<LogicalSize<f32>>,
pub position: Option<LogicalPosition<f32>>,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("ads")
.setup(|app, _api| {
app.manage(RwLock::new(AdsState {
shown: true,
size: None,
position: None,
}));
// We refresh the ads window every 5 minutes for performance
let app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) = app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(
"https://modrinth.com/wrapper/app-ads-cookie"
.parse()
.unwrap(),
);
}
tokio::time::sleep(std::time::Duration::from_secs(60 * 5))
.await;
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
init_ads_window,
hide_ads_window,
scroll_ads_window,
show_ads_window,
])
.build()
}
#[tauri::command]
#[cfg(not(target_os = "linux"))]
pub async fn init_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
x: f32,
y: f32,
width: f32,
height: f32,
override_shown: bool,
) -> crate::api::Result<()> {
use tauri::WebviewUrl;
const LINK_SCRIPT: &str = include_str!("ads-init.js");
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.size = Some(LogicalSize::new(width, height));
state.position = Some(LogicalPosition::new(x, y));
if override_shown {
state.shown = true;
}
if let Some(webview) = app.webviews().get("ads-window") {
if state.shown {
let _ = webview.set_position(LogicalPosition::new(x, y));
let _ = webview.set_size(LogicalSize::new(width, height));
}
} else if let Some(window) = app.get_window("main") {
let _ = window.add_child(
tauri::webview::WebviewBuilder::new(
"ads-window",
WebviewUrl::External(
"https://modrinth.com/wrapper/app-ads-cookie".parse().unwrap(),
),
)
.initialization_script(LINK_SCRIPT)
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
.zoom_hotkeys_enabled(false)
.transparent(true),
if state.shown {
LogicalPosition::new(x, y)
} else {
LogicalPosition::new(-1000.0, -1000.0)
},
LogicalSize::new(width, height),
);
}
Ok(())
}
// TODO: make ads work on linux
#[tauri::command]
#[cfg(target_os = "linux")]
pub async fn init_ads_window() {}
#[tauri::command]
pub async fn show_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.shown = true;
if let Some(size) = state.size {
let _ = webview.set_size(size);
}
if let Some(position) = state.position {
let _ = webview.set_position(position);
}
}
Ok(())
}
#[tauri::command]
pub async fn hide_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
reset: Option<bool>,
) -> crate::api::Result<()> {
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.shown = false;
if reset.unwrap_or(false) {
state.size = None;
state.position = None;
}
let _ = webview.set_position(LogicalPosition::new(-1000, -1000));
}
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(())
}
-13
View File
@@ -16,7 +16,6 @@ pub mod settings;
pub mod tags; pub mod tags;
pub mod utils; pub mod utils;
pub mod ads;
pub mod cache; pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>; pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
@@ -40,10 +39,6 @@ pub enum TheseusSerializableError {
#[error("Tauri error: {0}")] #[error("Tauri error: {0}")]
Tauri(#[from] tauri::Error), Tauri(#[from] tauri::Error),
#[cfg(feature = "updater")]
#[error("Tauri updater error: {0}")]
TauriUpdater(#[from] tauri_plugin_updater::Error),
} }
// Generic implementation of From<T> for ErrorTypeA // Generic implementation of From<T> for ErrorTypeA
@@ -91,15 +86,7 @@ macro_rules! impl_serialize {
} }
// Use the macro to implement Serialize for TheseusSerializableError // Use the macro to implement Serialize for TheseusSerializableError
#[cfg(not(feature = "updater"))]
impl_serialize! { impl_serialize! {
IO, IO,
Tauri, Tauri,
} }
#[cfg(feature = "updater")]
impl_serialize! {
IO,
Tauri,
TauriUpdater,
}
+2 -84
View File
@@ -26,73 +26,11 @@ extern crate objc;
#[tauri::command] #[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app.clone()).await?; theseus::EventState::init(app.clone()).await?;
State::init().await?;
#[cfg(feature = "updater")]
{
use tauri_plugin_updater::UpdaterExt;
let updater = app.updater_builder().build()?;
let update_fut = updater.check();
State::init().await?;
let check_bar = theseus::init_loading(
theseus::LoadingBarType::CheckingForUpdates,
1.0,
"Checking for updates...",
)
.await?;
let update = update_fut.await;
drop(check_bar);
if let Some(update) = update.ok().flatten() {
tracing::info!("Update found: {:?}", update.download_url);
let loader_bar_id = theseus::init_loading(
theseus::LoadingBarType::LauncherUpdate {
version: update.version.clone(),
current_version: update.current_version.clone(),
},
1.0,
"Updating Modrinth App...",
)
.await?;
// 100 MiB
const DEFAULT_CONTENT_LENGTH: u64 = 1024 * 1024 * 100;
update
.download_and_install(
|chunk_length, content_length| {
let _ = theseus::emit_loading(
&loader_bar_id,
(chunk_length as f64)
/ (content_length
.unwrap_or(DEFAULT_CONTENT_LENGTH)
as f64),
None,
);
},
|| {},
)
.await?;
app.restart();
}
}
#[cfg(not(feature = "updater"))]
{
State::init().await?;
}
let state = State::get().await?; let state = State::get().await?;
app.asset_protocol_scope() app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir(), true)?; .allow_directory(state.directories.caches_dir(), true)?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir().join("icons"), true)?;
Ok(()) Ok(())
} }
@@ -101,7 +39,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[tauri::command] #[tauri::command]
fn show_window(app: tauri::AppHandle) { fn show_window(app: tauri::AppHandle) {
let win = app.get_window("main").unwrap(); let win = app.get_webview_window("main").unwrap();
if let Err(e) = win.show() { if let Err(e) = win.show() {
MessageDialog::new() MessageDialog::new()
.set_type(MessageType::Error) .set_type(MessageType::Error)
@@ -135,11 +73,6 @@ async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
Ok(()) Ok(())
} }
#[tauri::command]
fn restart_app(app: tauri::AppHandle) {
app.restart();
}
// if Tauri app is called with arguments, then those arguments will be treated as commands // if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks // ie: deep links or filepaths for .mrpacks
fn main() { fn main() {
@@ -169,19 +102,6 @@ fn main() {
} }
builder = builder builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if let Some(payload) = args.get(1) {
tracing::info!("Handling deep link from arg {payload}");
let payload = payload.clone();
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
));
}
if let Some(win) = app.get_window("main") {
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())
@@ -259,14 +179,12 @@ fn main() {
.plugin(api::tags::init()) .plugin(api::tags::init())
.plugin(api::utils::init()) .plugin(api::utils::init())
.plugin(api::cache::init()) .plugin(api::cache::init())
.plugin(api::ads::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
initialize_state, initialize_state,
is_dev, is_dev,
toggle_decorations, toggle_decorations,
api::mr_auth::modrinth_auth_login, api::mr_auth::modrinth_auth_login,
show_window, show_window,
restart_app,
]); ]);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
-5
View File
@@ -5,11 +5,6 @@
"build": { "build": {
"features": ["updater"] "features": ["updater"]
}, },
"app": {
"security": {
"capabilities": ["ads", "core", "plugins", "updater"]
}
},
"plugins": { "plugins": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
+4 -19
View File
@@ -23,10 +23,6 @@
"timestampUrl": "http://timestamp.digicert.com", "timestampUrl": "http://timestamp.digicert.com",
"wix": { "wix": {
"template": "./msi/main.wxs" "template": "./msi/main.wxs"
},
"nsis": {
"installMode": "perMachine",
"installerHooks": "./nsis/hooks.nsi"
} }
}, },
"longDescription": "", "longDescription": "",
@@ -52,7 +48,7 @@
] ]
}, },
"productName": "Modrinth App", "productName": "Modrinth App",
"version": "0.8.7", "version": "0.8.3-1",
"identifier": "ModrinthApp", "identifier": "ModrinthApp",
"plugins": { "plugins": {
"deep-link": { "deep-link": {
@@ -74,10 +70,10 @@
"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": true,
"decorations": false "decorations": false
} }
], ],
@@ -90,18 +86,7 @@
], ],
"enable": true "enable": true
}, },
"capabilities": ["ads", "core", "plugins"], "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src unsafe-inline 'self'"
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs https://cmp.inmobi.com",
"font-src": [
"https://cdn-raw.modrinth.com/fonts/inter/"
],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self'",
"script-src": "https://cmp.inmobi.com https://*.cloudflare.com https://*.posthog.com 'self'",
"frame-src": "https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'"
}
} }
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@
"minHeight": 700, "minHeight": 700,
"minWidth": 1100, "minWidth": 1100,
"visible": false, "visible": false,
"zoomHotkeysEnabled": false, "zoomHotkeysEnabled": true,
"decorations": true "decorations": true
} }
] ]
@@ -62,10 +62,6 @@
.normal-page__content { .normal-page__content {
grid-area: content; grid-area: content;
} }
.normal-page__header {
grid-area: header;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -165,8 +161,4 @@
max-width: calc(80rem - 18.75rem - 0.75rem); max-width: calc(80rem - 18.75rem - 0.75rem);
//overflow-x: hidden; //overflow-x: hidden;
} }
.normal-page__header {
grid-area: header;
}
} }
+221 -201
View File
@@ -430,208 +430,220 @@
}" }"
> >
<div class="normal-page__header relative my-4"> <div class="normal-page__header relative my-4">
<ContentPageHeader> <div
<template #icon> class="grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
>
<div class="flex gap-4">
<Avatar :src="project.icon_url" :alt="project.title" size="96px" /> <Avatar :src="project.icon_url" :alt="project.title" size="96px" />
</template> <div class="flex flex-col gap-1">
<template #title> <div class="flex flex-wrap items-center gap-2">
{{ project.title }} <h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
</template> {{ project.title }}
<template #title-suffix> </h1>
<Badge v-if="auth.user && currentMember" :type="project.status" class="status-badge" /> <Badge
</template> v-if="auth.user && currentMember"
<template #summary> :type="project.status"
{{ project.description }} class="status-badge"
</template> />
<template #stats> </div>
<div <p class="m-0 line-clamp-2 max-w-[40rem]">
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold" {{ project.description }}
> </p>
<DownloadIcon class="h-6 w-6 text-secondary" /> <div class="mt-auto flex flex-wrap gap-4">
{{ $formatNumber(project.downloads) }}
</div>
<div
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
>
<HeartIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.followers) }}
</span>
</div>
<div class="hidden items-center gap-2 md:flex">
<TagsIcon class="h-6 w-6 text-secondary" />
<div class="flex flex-wrap gap-2">
<div <div
v-for="(category, index) in project.categories" class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
:key="index"
class="tag-list__item"
> >
{{ formatCategory(category) }} <DownloadIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.downloads) }}
</span>
</div>
<div
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
>
<HeartIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.followers) }}
</span>
</div>
<div class="hidden items-center gap-2 md:flex">
<TagsIcon class="h-6 w-6 text-secondary" />
<div class="flex flex-wrap gap-2">
<div
v-for="(category, index) in project.categories"
:key="index"
class="tag-list__item"
>
{{ formatCategory(category) }}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </div>
<template #actions> <div class="flex flex-col justify-center gap-4">
<div class="hidden sm:contents"> <div class="flex flex-wrap gap-2">
<ButtonStyled <div class="hidden sm:contents">
size="large" <ButtonStyled
:color="route.name === 'type-id-version-version' ? `standard` : `brand`" size="large"
> :color="route.name === 'type-id-version-version' ? `standard` : `brand`"
<button @click="(event) => downloadModal.show(event)"> >
<DownloadIcon aria-hidden="true" /> <button @click="(event) => downloadModal.show(event)">
Download <DownloadIcon aria-hidden="true" />
</button> Download
</ButtonStyled> </button>
</div> </ButtonStyled>
<div class="contents sm:hidden"> </div>
<div class="contents sm:hidden">
<ButtonStyled
size="large"
circular
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
>
<button
aria-label="Download"
class="flex sm:hidden"
@click="(event) => downloadModal.show(event)"
>
<DownloadIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<ButtonStyled <ButtonStyled
size="large" size="large"
circular circular
:color="route.name === 'type-id-version-version' ? `standard` : `brand`" :color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
> >
<button <button
aria-label="Download" v-if="auth.user"
class="flex sm:hidden" v-tooltip="following ? `Unfollow` : `Follow`"
@click="(event) => downloadModal.show(event)" :aria-label="following ? `Unfollow` : `Follow`"
@click="userFollowProject(project)"
> >
<DownloadIcon aria-hidden="true" /> <HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
</button> </button>
<nuxt-link v-else v-tooltip="'Follow'" to="/auth/sign-in" aria-label="Follow">
<HeartIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular>
<PopoutMenu v-if="auth.user" v-tooltip="'Save'" from="top-right" aria-label="Save">
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
placeholder="Search collections..."
class="search-input menu-search"
/>
<div v-if="collections.length > 0" class="collections-list">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">No collections found.</p>
</div>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
>
<PlusIcon aria-hidden="true" />
Create new collection
</button>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
<nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
>
<SettingsIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'analytics',
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
hoverOnly: true,
shown: auth.user && !!currentMember,
},
{
divider: true,
shown: auth.user && !!currentMember,
},
{
id: 'moderation-checklist',
action: () => (showModerationChecklist = true),
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
divider: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
id: 'report',
action: () =>
auth.user ? reportProject(project.id) : navigateTo('/auth/sign-in'),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #analytics>
<ChartIcon aria-hidden="true" />
Analytics
</template>
<template #moderation-checklist>
<ScaleIcon aria-hidden="true" />
Review project
</template>
<template #report>
<ReportIcon aria-hidden="true" />
Report
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
Copy ID
</template>
</OverflowMenu>
</ButtonStyled> </ButtonStyled>
</div> </div>
<ButtonStyled </div>
size="large" </div>
circular
:color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
>
<button
v-if="auth.user"
v-tooltip="following ? `Unfollow` : `Follow`"
:aria-label="following ? `Unfollow` : `Follow`"
@click="userFollowProject(project)"
>
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
</button>
<nuxt-link v-else v-tooltip="'Follow'" to="/auth/sign-in" aria-label="Follow">
<HeartIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular>
<PopoutMenu v-if="auth.user" v-tooltip="'Save'" from="top-right" aria-label="Save">
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
placeholder="Search collections..."
class="search-input menu-search"
/>
<div v-if="collections.length > 0" class="collections-list">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">No collections found.</p>
</div>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
>
<PlusIcon aria-hidden="true" />
Create new collection
</button>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
<nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
>
<SettingsIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'analytics',
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
hoverOnly: true,
shown: auth.user && !!currentMember,
},
{
divider: true,
shown: auth.user && !!currentMember,
},
{
id: 'moderation-checklist',
action: () => (showModerationChecklist = true),
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
divider: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
id: 'report',
action: () =>
auth.user ? reportProject(project.id) : navigateTo('/auth/sign-in'),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #analytics>
<ChartIcon aria-hidden="true" />
Analytics
</template>
<template #moderation-checklist>
<ScaleIcon aria-hidden="true" />
Review project
</template>
<template #report>
<ReportIcon aria-hidden="true" />
Report
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
Copy ID
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
<ProjectMemberHeader <ProjectMemberHeader
v-if="currentMember" v-if="currentMember"
:project="project" :project="project"
@@ -689,13 +701,13 @@
" "
> >
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3> <h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="tag-list"> <div class="status-list">
<div <div
v-if=" v-if="
(project.client_side === 'required' && project.server_side !== 'required') || (project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional') (project.client_side === 'optional' && project.server_side === 'optional')
" "
class="tag-list__item" class="status-list__item"
> >
<ClientIcon aria-hidden="true" /> <ClientIcon aria-hidden="true" />
Client-side Client-side
@@ -705,29 +717,34 @@
(project.server_side === 'required' && project.client_side !== 'required') || (project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional') (project.client_side === 'optional' && project.server_side === 'optional')
" "
class="tag-list__item" class="status-list__item"
> >
<ServerIcon aria-hidden="true" /> <ServerIcon aria-hidden="true" />
Server-side Server-side
</div> </div>
<div v-if="false" class="tag-list__item"> <div v-if="false" class="status-list__item">
<UserIcon aria-hidden="true" /> <UserIcon aria-hidden="true" />
Singleplayer Singleplayer
</div> </div>
<div <div
v-if=" v-if="project.client_side === 'required' && project.server_side === 'required'"
project.project_type !== 'datapack' && class="status-list__item"
((project.client_side === 'required' && project.server_side === 'required') ||
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional'))
"
class="tag-list__item"
> >
<MonitorSmartphoneIcon aria-hidden="true" /> <MonitorSmartphoneIcon aria-hidden="true" />
Client and server Client and server
</div> </div>
<div
v-else-if="
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional')
"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server <span class="text-sm">(optional)</span>
</div>
</div> </div>
</section> </section>
</div> </div>
@@ -1027,7 +1044,6 @@ import {
OverflowMenu, OverflowMenu,
PopoutMenu, PopoutMenu,
ScrollablePanel, ScrollablePanel,
ContentPageHeader,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils"; import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -1718,6 +1734,10 @@ const navLinks = computed(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.normal-page__header {
grid-area: header;
}
.settings-header { .settings-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -139,8 +139,8 @@
class="image" class="image"
:class="{ 'zoomed-in': zoomedIn }" :class="{ 'zoomed-in': zoomedIn }"
:src=" :src="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'" :alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -165,8 +165,8 @@
class="open circle-button" class="open circle-button"
target="_blank" target="_blank"
:href=" :href="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
> >
@@ -567,7 +567,6 @@
:show-labels="false" :show-labels="false"
:limit="6" :limit="6"
:hide-selected="true" :hide-selected="true"
:custom-label="(version) => version"
placeholder="Choose versions..." placeholder="Choose versions..."
/> />
<Checkbox <Checkbox
+209 -250
View File
@@ -1,260 +1,240 @@
<template> <template>
<div <div v-if="organization" class="normal-page">
v-if="organization" <div class="normal-page__sidebar">
class="experimental-styles-within new-page sidebar" <div v-if="routeHasSettings" class="universal-card">
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }" <Breadcrumbs
> current-title="Settings"
<ModalCreation ref="modal_creation" :organization-id="organization.id" /> :link-stack="[
<template v-if="routeHasSettings"> { href: `/dashboard/organizations`, label: 'Organizations' },
<div class="normal-page__sidebar"> {
href: `/organization/${organization.slug}`,
label: organization.name,
allowTrimming: true,
},
]"
/>
<div class="page-header__settings">
<Avatar size="sm" :src="organization.icon_url" />
<div class="title-section">
<h2 class="settings-title">
<nuxt-link :to="`/organization/${organization.slug}/settings`">
{{ organization.name }}
</nuxt-link>
</h2>
<span>
{{ $formatNumber(acceptedMembers?.length || 0) }}
member<template v-if="acceptedMembers?.length !== 1">s</template>
</span>
</div>
</div>
<h2>Organization settings</h2>
<NavStack>
<NavStackItem :link="`/organization/${organization.slug}/settings`" label="Overview">
<SettingsIcon />
</NavStackItem>
<NavStackItem
:link="`/organization/${organization.slug}/settings/members`"
label="Members"
>
<UsersIcon />
</NavStackItem>
<NavStackItem
:link="`/organization/${organization.slug}/settings/projects`"
label="Projects"
>
<BoxIcon />
</NavStackItem>
<NavStackItem
:link="`/organization/${organization.slug}/settings/analytics`"
label="Analytics"
>
<ChartIcon />
</NavStackItem>
</NavStack>
</div>
<template v-else>
<div class="universal-card"> <div class="universal-card">
<Breadcrumbs <div class="page-header__icon">
current-title="Settings" <Avatar size="md" :src="organization.icon_url" />
:link-stack="[
{ href: `/dashboard/organizations`, label: 'Organizations' },
{
href: `/organization/${organization.slug}`,
label: organization.name,
allowTrimming: true,
},
]"
/>
<div class="page-header__settings">
<Avatar size="sm" :src="organization.icon_url" />
<div class="title-section">
<h2 class="settings-title">
<nuxt-link :to="`/organization/${organization.slug}/settings`">
{{ organization.name }}
</nuxt-link>
</h2>
<span>
{{ $formatNumber(acceptedMembers?.length || 0) }}
member<template v-if="acceptedMembers?.length !== 1">s</template>
</span>
</div>
</div> </div>
<h2>Organization settings</h2> <div class="page-header__text">
<h1 class="title">{{ organization.name }}</h1>
<NavStack> <div>
<NavStackItem :link="`/organization/${organization.slug}/settings`" label="Overview"> <span class="organization-label"><OrganizationIcon /> Organization</span>
<SettingsIcon /> </div>
</NavStackItem>
<NavStackItem <div class="organization-description">
:link="`/organization/${organization.slug}/settings/members`" <div class="metadata-item markdown-body collection-description">
label="Members" <p>{{ organization.description }}</p>
> </div>
<UsersIcon />
</NavStackItem> <hr class="card-divider" />
<NavStackItem
:link="`/organization/${organization.slug}/settings/projects`" <div class="primary-stat">
label="Projects" <UserIcon class="primary-stat__icon" aria-hidden="true" />
> <div class="primary-stat__text">
<BoxIcon /> <span class="primary-stat__counter">
</NavStackItem> {{ $formatNumber(acceptedMembers?.length || 0) }}
<NavStackItem </span>
:link="`/organization/${organization.slug}/settings/analytics`" member<template v-if="acceptedMembers?.length !== 1">s</template>
label="Analytics" </div>
> </div>
<ChartIcon />
</NavStackItem> <div class="primary-stat">
</NavStack> <BoxIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ $formatNumber(projects?.length || 0) }}
</span>
project<span v-if="projects?.length !== 1">s</span>
</div>
</div>
<div class="primary-stat no-margin">
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ formatCompactNumber(sumDownloads) }}
</span>
download<span v-if="sumDownloads !== 1">s</span>
</div>
</div>
</div>
</div>
</div> </div>
</div>
<div class="normal-page__content">
<NuxtPage />
</div>
</template>
<template v-else>
<div class="normal-page__header py-4">
<ContentPageHeader>
<template #icon>
<Avatar :src="organization.icon_url" :alt="organization.name" size="96px" />
</template>
<template #title>
{{ organization.name }}
</template>
<template #title-suffix>
<div class="ml-1 flex items-center gap-2 font-semibold">
<OrganizationIcon /> Organization
</div>
</template>
<template #summary>
{{ organization.description }}
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<UsersIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(acceptedMembers?.length || 0) }}
members
</div>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div>
<div class="flex items-center gap-2 font-semibold">
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
downloads
</div>
</template>
<template #actions>
<ButtonStyled v-if="auth.user && currentMember" size="large">
<NuxtLink :to="`/organization/${organization.slug}/settings`">
<SettingsIcon aria-hidden="true" />
Manage
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () =>
navigateTo('/organization/' + organization.slug + '/settings/projects'),
hoverOnly: true,
shown: auth.user && currentMember,
},
{ divider: true, shown: auth.user && currentMember },
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
Manage projects
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</div>
<div class="normal-page__sidebar">
<AdPlaceholder <AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus" v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/> />
<div class="card flex-card"> <div class="creator-list universal-card">
<h2>Members</h2> <div class="title-and-link">
<div class="details-list"> <h3>Members</h3>
<template v-for="member in acceptedMembers" :key="member.user.id">
<nuxt-link
class="details-list__item details-list__item--type-large"
:to="`/user/${member.user.username}`"
>
<Avatar :src="member.user.avatar_url" circle />
<div class="rows">
<span class="flex items-center gap-1">
{{ member.user.username }}
<CrownIcon
v-if="member.is_owner"
v-tooltip="'Organization owner'"
class="text-brand-orange"
/>
</span>
<span class="details-list__item__text--style-secondary">
{{ member.role ? member.role : "Member" }}
</span>
</div>
</nuxt-link>
</template>
</div> </div>
</div>
</div>
<div class="normal-page__content">
<div v-if="isInvited" class="universal-card information invited">
<h2>Invitation to join {{ organization.name }}</h2>
<p>You have been invited to join {{ organization.name }}.</p>
<div class="input-group">
<button class="iconified-button brand-button" @click="onAcceptInvite">
<CheckIcon />Accept
</button>
<button class="iconified-button danger-button" @click="onDeclineInvite">
<XIcon />Decline
</button>
</div>
</div>
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
<NavTabs :links="navLinks" />
</div>
<template v-if="projects?.length > 0">
<div class="project-list display-mode--list">
<ProjectCard
v-for="project in (route.params.projectType !== undefined
? projects.filter((x) =>
x.project_types.includes(
route.params.projectType.substr(0, route.params.projectType.length - 1),
),
)
: projects
)
.slice()
.sort((a, b) => b.downloads - a.downloads)"
:id="project.slug || project.id"
:key="project.id"
:name="project.name"
:display="cosmetics.searchDisplayMode.user"
:featured-image="project.gallery.find((element) => element.featured)?.url"
project-type-url="project"
:description="project.summary"
:created-at="project.published"
:updated-at="project.updated"
:downloads="project.downloads.toString()"
:follows="project.followers.toString()"
:icon-url="project.icon_url"
:categories="project.categories"
:client-side="project.client_side"
:server-side="project.server_side"
:status="
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
? project.status
: null
"
:type="project.project_types[0] ?? 'project'"
:color="project.color"
/>
</div>
</template>
<div v-else-if="true" class="error"> <template v-for="member in acceptedMembers" :key="member.user.id">
<UpToDate class="icon" /><br /> <nuxt-link class="creator button-base" :to="`/user/${member.user.username}`">
<span class="preserve-lines text"> <Avatar :src="member.user.avatar_url" circle />
This organization doesn't have any projects yet. <p class="name">
<template v-if="isPermission(currentMember?.organization_permissions, 1 << 4)"> {{ member.user.username }}
Would you like to <CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
<a class="link" @click="$refs.modal_creation.show()">create one</a>? </p>
</template> <p class="role">{{ member.role }}</p>
</span> </nuxt-link>
</template>
</div>
</template>
</div>
<div v-if="!routeHasSettings" class="normal-page__content">
<ModalCreation ref="modal_creation" :organization-id="organization.id" />
<div v-if="isInvited" class="universal-card information invited">
<h2>Invitation to join {{ organization.name }}</h2>
<p>You have been invited to join {{ organization.name }}.</p>
<div class="input-group">
<button class="iconified-button brand-button" @click="onAcceptInvite">
<CheckIcon />Accept
</button>
<button class="iconified-button danger-button" @click="onDeclineInvite">
<XIcon />Decline
</button>
</div> </div>
</div> </div>
</template> <nav class="navigation-card">
<NavRow
:links="[
{
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.slug}`,
},
...projectTypes.map((x) => {
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.slug}/${x}s`,
};
}),
]"
/>
<div v-if="auth.user && currentMember" class="input-group">
<nuxt-link :to="`/organization/${organization.slug}/settings`" class="iconified-button">
<SettingsIcon /> Manage
</nuxt-link>
</div>
</nav>
<template v-if="projects?.length > 0">
<div class="project-list display-mode--list">
<ProjectCard
v-for="project in (route.params.projectType !== undefined
? projects.filter((x) =>
x.project_types.includes(
route.params.projectType.substr(0, route.params.projectType.length - 1),
),
)
: projects
)
.slice()
.sort((a, b) => b.downloads - a.downloads)"
:id="project.slug || project.id"
:key="project.id"
:name="project.name"
:display="cosmetics.searchDisplayMode.user"
:featured-image="project.gallery.find((element) => element.featured)?.url"
project-type-url="project"
:description="project.summary"
:created-at="project.published"
:updated-at="project.updated"
:downloads="project.downloads.toString()"
:follows="project.followers.toString()"
:icon-url="project.icon_url"
:categories="project.categories"
:client-side="project.client_side"
:server-side="project.server_side"
:status="
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
? project.status
: null
"
:type="project.project_types[0] ?? 'project'"
:color="project.color"
/>
</div>
</template>
<div v-else-if="true" class="error">
<UpToDate class="icon" /><br />
<span class="preserve-lines text">
This organization doesn't have any projects yet.
<template v-if="isPermission(currentMember?.organization_permissions, 1 << 4)">
Would you like to
<a class="link" @click="$refs.modal_creation.show()">create one</a>?
</template>
</span>
</div>
</div>
<NuxtPage />
</div> </div>
</template> </template>
<script setup> <script setup>
import { import {
BoxIcon, BoxIcon,
MoreVerticalIcon, UserIcon,
UsersIcon, UsersIcon,
SettingsIcon, SettingsIcon,
ChartIcon, ChartIcon,
CheckIcon, CheckIcon,
XIcon, XIcon,
ClipboardCopyIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Avatar, ButtonStyled, Breadcrumbs, ContentPageHeader, OverflowMenu } from "@modrinth/ui"; import { Avatar, Breadcrumbs } from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue"; import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue"; import NavStackItem from "~/components/ui/NavStackItem.vue";
import NavRow from "~/components/ui/NavRow.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
@@ -264,7 +244,6 @@ import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component"; import DownloadIcon from "~/assets/images/utils/download.svg?component";
import CrownIcon from "~/assets/images/utils/crown.svg?component"; import CrownIcon from "~/assets/images/utils/crown.svg?component";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
import NavTabs from "~/components/ui/NavTabs.vue";
const vintl = useVIntl(); const vintl = useVIntl();
const { formatMessage } = vintl; const { formatMessage } = vintl;
@@ -472,26 +451,6 @@ useSeoMeta({
ogDescription: organization.value.description, ogDescription: organization.value.description,
ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png", ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png",
}); });
const navLinks = computed(() => [
{
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.value.slug}`,
},
...projectTypes.value
.map((x) => {
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.value.slug}/${x}s`,
};
})
.slice()
.sort((a, b) => a.label.localeCompare(b.label)),
]);
async function copyId() {
await navigator.clipboard.writeText(organization.value.id);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+147 -83
View File
@@ -3,88 +3,75 @@
<ModalCreation ref="modal_creation" /> <ModalCreation ref="modal_creation" />
<CollectionCreateModal ref="modal_collection_creation" /> <CollectionCreateModal ref="modal_collection_creation" />
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }"> <div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<div class="normal-page__header py-4"> <div class="normal-page__header pt-4">
<ContentPageHeader> <div
<template #icon> class="mb-4 grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
>
<div class="flex gap-4">
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle /> <Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
</template> <div class="flex flex-col gap-2">
<template #title> <div class="flex flex-wrap items-center gap-2">
{{ user.username }} <h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
</template> {{ user.username }}
<template #summary> </h1>
{{ </div>
user.bio <p class="m-0 line-clamp-2 max-w-[40rem]">
? user.bio {{
: projects.length === 0 user.bio
? "A Modrinth user." ? user.bio
: "A Modrinth creator." : projects.length === 0
}} ? "A Modrinth user."
</template> : "A Modrinth creator."
<template #stats> }}
<div </p>
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div> </div>
<div </div>
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold" <div class="flex flex-col justify-center gap-4">
> <div class="flex flex-wrap gap-2">
<DownloadIcon class="h-6 w-6 text-secondary" /> <ButtonStyled size="large">
{{ formatCompactNumber(sumDownloads) }} <NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
downloads <EditIcon aria-hidden="true" />
{{ formatMessage(commonMessages.editButton) }}
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () => navigateTo('/dashboard/projects'),
hoverOnly: true,
shown: auth.user && auth.user.id === user.id,
},
{ divider: true, shown: auth.user && auth.user.id === user.id },
{
id: 'report',
action: () => reportUser(user.id),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
{{ formatMessage(messages.profileManageProjectsButton) }}
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</div> </div>
<div class="flex items-center gap-2 font-semibold"> </div>
<CalendarIcon class="h-6 w-6 text-secondary" /> </div>
Joined
{{ formatRelativeTime(user.created) }}
</div>
</template>
<template #actions>
<ButtonStyled size="large">
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
<EditIcon aria-hidden="true" />
{{ formatMessage(commonMessages.editButton) }}
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () => navigateTo('/dashboard/projects'),
hoverOnly: true,
shown: auth.user && auth.user.id === user.id,
},
{ divider: true, shown: auth.user && auth.user.id === user.id },
{
id: 'report',
action: () => reportUser(user.id),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
{{ formatMessage(messages.profileManageProjectsButton) }}
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</div> </div>
<div class="normal-page__content"> <div class="normal-page__content">
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto"> <div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
@@ -207,6 +194,72 @@
</div> </div>
</div> </div>
<div class="normal-page__sidebar"> <div class="normal-page__sidebar">
<div class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
<div class="flex items-center gap-2">
<BoxIcon aria-hidden="true" class="stroke-[3] text-secondary" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileProjectsStats"
:values="{ count: formatCompactNumber(projects.length) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<DownloadIcon aria-hidden="true" class="stroke-[3] text-secondary" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileDownloadsStats"
:values="{ count: formatCompactNumber(sumDownloads) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<HeartIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileProjectsFollowersStats"
:values="{ count: formatCompactNumber(sumFollows) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<CalendarIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileJoinedAt"
:values="{ ago: formatRelativeTime(user.created) }"
>
<template #date="{ children }">
<span class="font-bold text-primary">
<component :is="() => children" />
</span>
</template>
</IntlFormatted>
</div>
</div>
</div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<div v-if="organizations.length > 0" class="card flex-card"> <div v-if="organizations.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2> <h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@@ -235,9 +288,6 @@
</div> </div>
</div> </div>
</div> </div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -254,7 +304,7 @@ import {
ClipboardCopyIcon, ClipboardCopyIcon,
MoreVerticalIcon, MoreVerticalIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { OverflowMenu, ButtonStyled, ContentPageHeader } from "@modrinth/ui"; import { OverflowMenu, ButtonStyled } from "@modrinth/ui";
import NavTabs from "~/components/ui/NavTabs.vue"; import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
import { reportUser } from "~/utils/report-helpers.ts"; import { reportUser } from "~/utils/report-helpers.ts";
@@ -268,6 +318,7 @@ import EarlyAdopterBadge from "~/assets/images/badges/early-adopter.svg?componen
import ReportIcon from "~/assets/images/utils/report.svg?component"; import ReportIcon from "~/assets/images/utils/report.svg?component";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component"; import EditIcon from "~/assets/images/utils/edit.svg?component";
import HeartIcon from "~/assets/images/utils/heart.svg?component";
import WorldIcon from "~/assets/images/utils/world.svg?component"; import WorldIcon from "~/assets/images/utils/world.svg?component";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from "~/components/ui/Avatar.vue"; import Avatar from "~/components/ui/Avatar.vue";
@@ -454,6 +505,15 @@ const sumDownloads = computed(() => {
return sum; return sum;
}); });
const sumFollows = computed(() => {
let sum = 0;
for (const project of projects.value) {
sum += project.followers;
}
return sum;
});
const badges = computed(() => { const badges = computed(() => {
const badges = []; const badges = [];
@@ -593,4 +653,8 @@ export default defineNuxtComponent({
} }
} }
} }
.normal-page__header {
grid-area: header;
}
</style> </style>
-2
View File
@@ -472,5 +472,3 @@ visiblemeasures.com, 1041, DIRECT #yieldmo
iqzone.com,IQ190,RESELLER iqzone.com,IQ190,RESELLER
krushmedia.com, AJxF6R537a9M6CaTvK, RESELLER krushmedia.com, AJxF6R537a9M6CaTvK, RESELLER
consumable.com, 2001460, DIRECT, aefcd3d2f45b5070 consumable.com, 2001460, DIRECT, aefcd3d2f45b5070
google.com, pub-6729046591418183, RESELLER, f08c47fec0942fa0
-90
View File
@@ -1,90 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modrinth App Ad</title>
<script
src="https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js"
async
></script>
<link rel="preload" href="https://www.googletagservices.com/tag/js/gpt.js" as="script" />
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
cursor: pointer;
}
.ads-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
#plus-link {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
#modrinth-rail-1 {
border-radius: 1rem;
position: absolute;
left: 0;
bottom: 0;
z-index: 2;
}
</style>
</head>
<body>
<div class="ads-container">
<a id="plus-link" href="https://modrinth.com/plus" target="_blank"></a>
<div id="modrinth-rail-1" />
</div>
<script>
window.tude = window.tude || { cmd: [] };
tude.cmd.push(function () {
tude.refreshAdsViaDivMappings([
{
divId: "modrinth-rail-1",
baseDivId: "pb-slot-square-2",
},
]);
});
let lastUrl = null
window.addEventListener(
"message",
(event) => {
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__ && lastUrl !== event.data.modrinthOpenUrl) {
lastUrl = event.data.modrinthOpenUrl
window.__TAURI_INTERNALS__.invoke("plugin:shell|open", {
path: event.data.modrinthOpenUrl,
});
setTimeout(() => {
lastUrl = null
}, 500)
}
},
false,
);
window.addEventListener("mousewheel", (event) => {
if (window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|scroll_ads_window", {
scroll: event.deltaY,
});
}
});
document.addEventListener("contextmenu", (event) => event.preventDefault());
</script>
</body>
</html>
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "theseus" name = "theseus"
version = "0.8.7" version = "0.8.3-1"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2021" edition = "2021"
@@ -36,7 +36,7 @@ tracing-error = "0.2.0"
paste = { version = "1.0" } paste = { version = "1.0" }
tauri = { git = "https://github.com/modrinth/tauri", rev = "5e29428", optional = true } tauri = { version = "2.0.0-rc.4", optional = true }
indicatif = { version = "0.17.3", optional = true } indicatif = { version = "0.17.3", optional = true }
async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] } async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] }
+4 -4
View File
@@ -68,7 +68,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
pub name: PathBuf, pub name: PathBuf,
} }
emit_loading(&loading_bar, 0.0, Some("Fetching java version"))?; emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
let packages = fetch_json::<Vec<Package>>( let packages = fetch_json::<Vec<Package>>(
Method::GET, Method::GET,
&format!( &format!(
@@ -80,7 +80,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
&state.fetch_semaphore, &state.fetch_semaphore,
&state.pool, &state.pool,
).await?; ).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version"))?; emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
if let Some(download) = packages.first() { if let Some(download) = packages.first() {
let file = fetch_advanced( let file = fetch_advanced(
@@ -115,13 +115,13 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
} }
} }
emit_loading(&loading_bar, 0.0, Some("Extracting java"))?; emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
archive.extract(&path).map_err(|_| { archive.extract(&path).map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError( crate::Error::from(crate::ErrorKind::InputError(
"Failed to extract java zip".to_string(), "Failed to extract java zip".to_string(),
)) ))
})?; })?;
emit_loading(&loading_bar, 10.0, Some("Done extracting java"))?; emit_loading(&loading_bar, 10.0, Some("Done extracting java")).await?;
let mut base_path = path.join( let mut base_path = path.join(
download download
.name .name
+1 -1
View File
@@ -287,7 +287,7 @@ pub async fn copy_dotminecraft(
fetch::copy(&src_child, &dst_child, io_semaphore).await?; fetch::copy(&src_child, &dst_child, io_semaphore).await?;
emit_loading(&loading_bar, 1.0, None)?; emit_loading(&loading_bar, 1.0, None).await?;
} }
Ok(loading_bar) Ok(loading_bar)
} }
@@ -191,7 +191,7 @@ pub async fn generate_pack_from_version_id(
let state = State::get().await?; let state = State::get().await?;
let loading_bar = if let Some(bar) = initialized_loading_bar { let loading_bar = if let Some(bar) = initialized_loading_bar {
emit_loading(&bar, 0.0, Some("Downloading pack file"))?; emit_loading(&bar, 0.0, Some("Downloading pack file")).await?;
bar bar
} else { } else {
init_loading( init_loading(
@@ -207,7 +207,7 @@ pub async fn generate_pack_from_version_id(
.await? .await?
}; };
emit_loading(&loading_bar, 0.0, Some("Fetching version"))?; emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
let version = CachedEntry::get_version( let version = CachedEntry::get_version(
&version_id, &version_id,
None, None,
@@ -220,7 +220,7 @@ pub async fn generate_pack_from_version_id(
"Invalid version ID specified!".to_string(), "Invalid version ID specified!".to_string(),
) )
})?; })?;
emit_loading(&loading_bar, 10.0, None)?; emit_loading(&loading_bar, 10.0, None).await?;
let (url, hash) = let (url, hash) =
if let Some(file) = version.files.iter().find(|x| x.primary) { if let Some(file) = version.files.iter().find(|x| x.primary) {
@@ -248,7 +248,7 @@ pub async fn generate_pack_from_version_id(
&state.pool, &state.pool,
) )
.await?; .await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata"))?; emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
let project = CachedEntry::get_project( let project = CachedEntry::get_project(
&version.project_id, &version.project_id,
@@ -263,7 +263,7 @@ pub async fn generate_pack_from_version_id(
) )
})?; })?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?; emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = project.icon_url { let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?; let state = State::get().await?;
let icon_bytes = let icon_bytes =
@@ -287,7 +287,7 @@ pub async fn generate_pack_from_version_id(
} else { } else {
None None
}; };
emit_loading(&loading_bar, 10.0, None)?; emit_loading(&loading_bar, 10.0, None).await?;
Ok(CreatePack { Ok(CreatePack {
file, file,
@@ -203,7 +203,7 @@ pub async fn install_zipped_mrpack_files(
) )
.await?; .await?;
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))?; emit_loading(&loading_bar, 0.0, Some("Extracting overrides")).await?;
let mut total_len = 0; let mut total_len = 0;
@@ -270,7 +270,8 @@ pub async fn install_zipped_mrpack_files(
"Extracting override {}/{}", "Extracting override {}/{}",
index, total_len index, total_len
)), )),
)?; )
.await?;
} }
} }
+7 -2
View File
@@ -40,10 +40,15 @@ pub mod update;
#[tracing::instrument] #[tracing::instrument]
pub async fn remove(path: &str) -> crate::Result<()> { pub async fn remove(path: &str) -> crate::Result<()> {
let state = State::get().await?; let state = State::get().await?;
Profile::remove(path, &state.pool).await?;
let mut transaction = state.pool.begin().await?;
Profile::remove(path, &mut transaction).await?;
emit_profile(path, ProfilePayloadType::Removed).await?; emit_profile(path, ProfilePayloadType::Removed).await?;
transaction.commit().await?;
Ok(()) Ok(())
} }
@@ -505,7 +510,7 @@ pub async fn export_mrpack(
// Iterate over every file in the folder // Iterate over every file in the folder
// Every file that is NOT in the config file is added to the zip, in overrides // Every file that is NOT in the config file is added to the zip, in overrides
for path in path_list { for path in path_list {
emit_loading(&loading_bar, 1.0, None)?; emit_loading(&loading_bar, 1.0, None).await?;
let relative_path = pack_get_relative_path(&profile_base_path, &path)?; let relative_path = pack_get_relative_path(&profile_base_path, &path)?;
+16 -19
View File
@@ -9,7 +9,7 @@ use crate::event::{
}; };
use futures::prelude::*; use futures::prelude::*;
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
use tauri::{Emitter, Manager}; use tauri::Emitter;
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@@ -33,7 +33,7 @@ const CLI_PROGRESS_BAR_TOTAL: u64 = 1000;
pub async fn loading_function() -> crate::Result<()> { pub async fn loading_function() -> crate::Result<()> {
let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Loading something long...").await; let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Loading something long...").await;
for i in 0..100 { for i in 0..100 {
emit_loading(&loading_bar, 1.0, None)?; emit_loading(&loading_bar, 1.0, None).await?;
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
} }
@@ -62,7 +62,7 @@ pub async fn init_loading_unsafe(
total: f64, total: f64,
title: &str, title: &str,
) -> crate::Result<LoadingBarId> { ) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
let key = LoadingBarId(Uuid::new_v4()); let key = LoadingBarId(Uuid::new_v4());
event_state.loading_bars.insert( event_state.loading_bars.insert(
@@ -91,7 +91,7 @@ pub async fn init_loading_unsafe(
}, },
); );
// attempt an initial loading_emit event to the frontend // attempt an initial loading_emit event to the frontend
emit_loading(&key, 0.0, None)?; emit_loading(&key, 0.0, None).await?;
Ok(key) Ok(key)
} }
@@ -118,7 +118,7 @@ pub async fn edit_loading(
total: f64, total: f64,
title: &str, title: &str,
) -> crate::Result<()> { ) -> crate::Result<()> {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
if let Some(mut bar) = event_state.loading_bars.get_mut(&id.0) { if let Some(mut bar) = event_state.loading_bars.get_mut(&id.0) {
bar.bar_type = bar_type; bar.bar_type = bar_type;
@@ -132,7 +132,7 @@ pub async fn edit_loading(
} }
}; };
emit_loading(id, 0.0, None)?; emit_loading(id, 0.0, None).await?;
Ok(()) Ok(())
} }
@@ -144,12 +144,12 @@ pub async fn edit_loading(
#[allow(unused_variables)] #[allow(unused_variables)]
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
pub fn emit_loading( pub async fn emit_loading(
key: &LoadingBarId, key: &LoadingBarId,
increment_frac: f64, increment_frac: f64,
message: Option<&str>, message: Option<&str>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
let mut loading_bar = match event_state.loading_bars.get_mut(&key.0) { let mut loading_bar = match event_state.loading_bars.get_mut(&key.0) {
Some(f) => f, Some(f) => f,
@@ -211,7 +211,7 @@ pub fn emit_loading(
pub async fn emit_warning(message: &str) -> crate::Result<()> { pub async fn emit_warning(message: &str) -> crate::Result<()> {
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
{ {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
event_state event_state
.app .app
.emit( .emit(
@@ -228,22 +228,18 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> {
// emit_command(CommandPayload::Something { something }) // emit_command(CommandPayload::Something { something })
// ie: installing a pack, opening an .mrpack, etc // ie: installing a pack, opening an .mrpack, etc
// Generally used for url deep links and file opens that we want to handle in the frontend // Generally used for url deep links and file opens that we we want to handle in the frontend
#[allow(dead_code)] #[allow(dead_code)]
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn emit_command(command: CommandPayload) -> crate::Result<()> { pub async fn emit_command(command: CommandPayload) -> crate::Result<()> {
tracing::debug!("Command: {}", serde_json::to_string(&command)?); tracing::debug!("Command: {}", serde_json::to_string(&command)?);
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
{ {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
event_state event_state
.app .app
.emit("command", command) .emit("command", command)
.map_err(EventError::from)?; .map_err(EventError::from)?;
if let Some(window) = event_state.app.get_window("main") {
let _ = window.set_focus();
}
} }
Ok(()) Ok(())
} }
@@ -258,7 +254,7 @@ pub async fn emit_process(
) -> crate::Result<()> { ) -> crate::Result<()> {
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
{ {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
event_state event_state
.app .app
.emit( .emit(
@@ -283,7 +279,7 @@ pub async fn emit_profile(
) -> crate::Result<()> { ) -> crate::Result<()> {
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
{ {
let event_state = crate::EventState::get()?; let event_state = crate::EventState::get().await?;
event_state event_state
.app .app
.emit( .emit(
@@ -333,7 +329,7 @@ macro_rules! loading_join {
async move { async move {
let res = $task.await; let res = $task.await;
if let Some(key) = key { if let Some(key) = key {
$crate::event::emit::emit_loading(key, increment, message)?; $crate::event::emit::emit_loading(key, increment, message).await?;
} }
res res
} }
@@ -380,7 +376,8 @@ where
async move { async move {
f.await?; f.await?;
if let Some(key) = key { if let Some(key) = key {
emit_loading(key, total / (num_futs as f64), message)?; emit_loading(key, total / (num_futs as f64), message)
.await?;
} }
Ok(()) Ok(())
} }
+11 -9
View File
@@ -45,14 +45,21 @@ impl EventState {
.cloned() .cloned()
} }
pub fn get() -> crate::Result<Arc<Self>> { #[cfg(feature = "tauri")]
pub async fn get() -> crate::Result<Arc<Self>> {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone()) Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
} }
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
}
// Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date // Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date
pub async fn list_progress_bars() -> crate::Result<DashMap<Uuid, LoadingBar>> pub async fn list_progress_bars() -> crate::Result<DashMap<Uuid, LoadingBar>>
{ {
let value = Self::get()?; let value = Self::get().await?;
Ok(value.loading_bars.clone()) Ok(value.loading_bars.clone())
} }
@@ -60,7 +67,7 @@ impl EventState {
pub async fn get_main_window() -> crate::Result<Option<tauri::WebviewWindow>> pub async fn get_main_window() -> crate::Result<Option<tauri::WebviewWindow>>
{ {
use tauri::Manager; use tauri::Manager;
let value = Self::get()?; let value = Self::get().await?;
Ok(value.app.get_webview_window("main")) Ok(value.app.get_webview_window("main"))
} }
} }
@@ -88,7 +95,7 @@ impl Drop for LoadingBarId {
fn drop(&mut self) { fn drop(&mut self) {
let loader_uuid = self.0; let loader_uuid = self.0;
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(event_state) = EventState::get() { if let Ok(event_state) = EventState::get().await {
#[cfg(any(feature = "tauri", feature = "cli"))] #[cfg(any(feature = "tauri", feature = "cli"))]
if let Some((_, bar)) = if let Some((_, bar)) =
event_state.loading_bars.remove(&loader_uuid) event_state.loading_bars.remove(&loader_uuid)
@@ -173,11 +180,6 @@ pub enum LoadingBarType {
import_location: PathBuf, import_location: PathBuf,
profile_name: String, profile_name: String,
}, },
CheckingForUpdates,
LauncherUpdate {
version: String,
current_version: String,
},
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
+3 -3
View File
@@ -111,7 +111,7 @@ pub async fn download_version_info(
}?; }?;
if let Some(loading_bar) = loading_bar { if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 5.0, None)?; emit_loading(loading_bar, 5.0, None).await?;
} }
tracing::debug!("Loaded version info for Minecraft {version_id}"); tracing::debug!("Loaded version info for Minecraft {version_id}");
@@ -154,7 +154,7 @@ pub async fn download_client(
tracing::trace!("Fetched client version {version}"); tracing::trace!("Fetched client version {version}");
} }
if let Some(loading_bar) = loading_bar { if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 9.0, None)?; emit_loading(loading_bar, 9.0, None).await?;
} }
tracing::debug!("Client loaded for version {version}!"); tracing::debug!("Client loaded for version {version}!");
@@ -196,7 +196,7 @@ pub async fn download_assets_index(
}?; }?;
if let Some(loading_bar) = loading_bar { if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 5.0, None)?; emit_loading(loading_bar, 5.0, None).await?;
} }
tracing::debug!("Assets index successfully loaded!"); tracing::debug!("Assets index successfully loaded!");
Ok(res) Ok(res)
+5 -3
View File
@@ -336,7 +336,8 @@ pub async fn install_minecraft(
server => ""; server => "";
} }
emit_loading(&loading_bar, 0.0, Some("Running forge processors"))?; emit_loading(&loading_bar, 0.0, Some("Running forge processors"))
.await?;
let total_length = processors.len(); let total_length = processors.len();
// Forge processors (90-100) // Forge processors (90-100)
@@ -401,7 +402,8 @@ pub async fn install_minecraft(
"Running forge processor {}/{}", "Running forge processor {}/{}",
index, total_length index, total_length
)), )),
)?; )
.await?;
} }
} }
} }
@@ -412,7 +414,7 @@ pub async fn install_minecraft(
async { Ok(()) } async { Ok(()) }
}) })
.await?; .await?;
emit_loading(&loading_bar, 1.0, Some("Finished installing"))?; emit_loading(&loading_bar, 1.0, Some("Finished installing")).await?;
Ok(()) Ok(())
} }
+1 -4
View File
@@ -20,9 +20,6 @@ mod state;
pub use api::*; pub use api::*;
pub use error::*; pub use error::*;
pub use event::{ pub use event::{EventState, LoadingBar, LoadingBarType};
emit::emit_loading, emit::init_loading, EventState, LoadingBar,
LoadingBarType,
};
pub use logger::start_logger; pub use logger::start_logger;
pub use state::State; pub use state::State;
+4 -12
View File
@@ -271,7 +271,6 @@ pub struct License {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GalleryItem { pub struct GalleryItem {
pub url: String, pub url: String,
pub raw_url: String,
pub featured: bool, pub featured: bool,
pub title: Option<String>, pub title: Option<String>,
pub description: Option<String>, pub description: Option<String>,
@@ -704,16 +703,6 @@ impl CachedEntry {
.await?; .await?;
for row in query { for row in query {
let row_exists = row.data.is_some();
let parsed_data = row
.data
.and_then(|x| serde_json::from_value::<CacheValue>(x).ok());
// If data is corrupted/failed to parse ignore it
if row_exists && parsed_data.is_none() {
continue;
}
if row.expires <= Utc::now().timestamp() { if row.expires <= Utc::now().timestamp() {
if cache_behaviour == CacheBehaviour::MustRevalidate { if cache_behaviour == CacheBehaviour::MustRevalidate {
continue; continue;
@@ -738,7 +727,10 @@ impl CachedEntry {
.unwrap_or(false) .unwrap_or(false)
}); });
if let Some(data) = parsed_data { if let Some(data) = row
.data
.and_then(|x| serde_json::from_value::<CacheValue>(x).ok())
{
return_vals.push(Self { return_vals.push(Self {
id: row.id, id: row.id,
alias: row.alias, alias: row.alias,
+8 -4
View File
@@ -330,7 +330,8 @@ impl DirectoryInfo {
&loader_bar_id, &loader_bar_id,
10.0 / (MOVE_DIRS.len() as f64), 10.0 / (MOVE_DIRS.len() as f64),
None, None,
)?; )
.await?;
} }
let paths_len = paths.len(); let paths_len = paths.len();
@@ -379,7 +380,8 @@ impl DirectoryInfo {
&loader_bar_id, &loader_bar_id,
90.0 / paths_len as f64, 90.0 / paths_len as f64,
None, None,
); )
.await;
success_idxs.insert(idx); success_idxs.insert(idx);
@@ -431,7 +433,8 @@ impl DirectoryInfo {
&loader_bar_id, &loader_bar_id,
((x.size as f64) / (total_size as f64)) * 60.0, ((x.size as f64) / (total_size as f64)) * 60.0,
None, None,
); )
.await;
Ok::<(), crate::Error>(()) Ok::<(), crate::Error>(())
} }
@@ -450,7 +453,8 @@ impl DirectoryInfo {
&loader_bar_id, &loader_bar_id,
30.0 / paths_len as f64, 30.0 / paths_len as f64,
None, None,
)?; )
.await?;
Ok::<(), crate::Error>(()) Ok::<(), crate::Error>(())
}; };
+1 -1
View File
@@ -119,7 +119,7 @@ pub(crate) async fn watch_profile(
{ {
let path = profile_path.join(folder); let path = profile_path.join(folder);
if !path.exists() && !path.is_symlink() { if !path.exists() {
crate::util::io::create_dir_all(&path).await?; crate::util::io::create_dir_all(&path).await?;
} }
+7 -7
View File
@@ -501,8 +501,13 @@ impl Profile {
pub async fn remove( pub async fn remove(
profile_path: &str, profile_path: &str,
pool: &SqlitePool, transaction: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> crate::Result<()> { ) -> crate::Result<()> {
if let Ok(path) = crate::api::profile::get_full_path(profile_path).await
{
io::remove_dir_all(&path).await?;
}
sqlx::query!( sqlx::query!(
" "
DELETE FROM profiles DELETE FROM profiles
@@ -510,14 +515,9 @@ impl Profile {
", ",
profile_path profile_path
) )
.execute(pool) .execute(&mut **transaction)
.await?; .await?;
if let Ok(path) = crate::api::profile::get_full_path(profile_path).await
{
io::remove_dir_all(&path).await?;
}
Ok(()) Ok(())
} }
+2 -1
View File
@@ -130,7 +130,8 @@ pub async fn fetch_advanced(
(chunk.len() as f64 / total_size as f64) (chunk.len() as f64 / total_size as f64)
* total, * total,
None, None,
)?; )
.await?;
} }
Ok(bytes::Bytes::from(bytes)) Ok(bytes::Bytes::from(bytes))
+8 -21
View File
@@ -9,21 +9,9 @@ pub trait OsExt {
/// Gets the OS + Arch of the current system /// Gets the OS + Arch of the current system
fn native_arch(java_arch: &str) -> Self; fn native_arch(java_arch: &str) -> Self;
/// Gets the OS from an OS + Arch
fn get_os(&self) -> Self;
} }
impl OsExt for Os { impl OsExt for Os {
fn native() -> Self {
match std::env::consts::OS {
"windows" => Self::Windows,
"macos" => Self::Osx,
"linux" => Self::Linux,
_ => Self::Unknown,
}
}
fn native_arch(java_arch: &str) -> Self { fn native_arch(java_arch: &str) -> Self {
if std::env::consts::OS == "windows" { if std::env::consts::OS == "windows" {
if java_arch == "aarch64" { if java_arch == "aarch64" {
@@ -50,13 +38,12 @@ impl OsExt for Os {
} }
} }
fn get_os(&self) -> Self { fn native() -> Self {
match self { match std::env::consts::OS {
Os::OsxArm64 => Os::Osx, "windows" => Self::Windows,
Os::LinuxArm32 => Os::Linux, "macos" => Self::Osx,
Os::LinuxArm64 => Os::Linux, "linux" => Self::Linux,
Os::WindowsArm64 => Os::Windows, _ => Self::Unknown,
_ => self.clone(),
} }
} }
} }
@@ -85,8 +72,8 @@ pub fn os_rule(
if minecraft_updated if minecraft_updated
&& (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32)
{ {
rule_match &= Os::native() == name.get_os() rule_match &=
|| &Os::native_arch(java_arch) == name; &Os::native() == name || &Os::native_arch(java_arch) == name;
} else { } else {
rule_match &= &Os::native_arch(java_arch) == name; rule_match &= &Os::native_arch(java_arch) == name;
} }
+2 -1
View File
@@ -833,6 +833,7 @@ a,
// MARKDOWN // MARKDOWN
.markdown-body { .markdown-body {
overflow-y: auto;
h1:first-child { h1:first-child {
margin-top: 0; margin-top: 0;
} }
@@ -886,7 +887,7 @@ a,
a { a {
cursor: pointer; cursor: pointer;
color: var(--color-blue); color: var(--color-link);
&:focus-visible, &:focus-visible,
&:hover { &:hover {
@@ -1,26 +0,0 @@
<template>
<div
class="grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
>
<div class="flex gap-4">
<slot name="icon" />
<div class="flex flex-col gap-1">
<div class="flex flex-wrap items-center gap-2">
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
<slot name="title" />
</h1>
<slot name="title-suffix" />
</div>
<p class="m-0 line-clamp-2 max-w-[40rem]">
<slot name="summary" />
</p>
<div class="mt-auto flex flex-wrap gap-4">
<slot name="stats" />
</div>
</div>
</div>
<div class="flex flex-wrap gap-2 items-center">
<slot name="actions" />
</div>
</div>
</template>
@@ -797,7 +797,7 @@ function openVideoModal() {
.markdown-resource-link { .markdown-resource-link {
cursor: pointer; cursor: pointer;
color: var(--color-blue); color: var(--color-link);
&:focus-visible, &:focus-visible,
&:hover { &:hover {
-1
View File
@@ -7,7 +7,6 @@ export { default as Card } from './base/Card.vue'
export { default as Checkbox } from './base/Checkbox.vue' export { default as Checkbox } from './base/Checkbox.vue'
export { default as Chips } from './base/Chips.vue' export { default as Chips } from './base/Chips.vue'
export { default as ConditionalNuxtLink } from './base/ConditionalNuxtLink.vue' export { default as ConditionalNuxtLink } from './base/ConditionalNuxtLink.vue'
export { default as ContentPageHeader } from './base/ContentPageHeader.vue'
export { default as CopyCode } from './base/CopyCode.vue' export { default as CopyCode } from './base/CopyCode.vue'
export { default as DoubleIcon } from './base/DoubleIcon.vue' export { default as DoubleIcon } from './base/DoubleIcon.vue'
export { default as DropArea } from './base/DropArea.vue' export { default as DropArea } from './base/DropArea.vue'
@@ -1,5 +1,5 @@
<template> <template>
<NewModal ref="modal" :noblur="noblur" danger :on-hide="onHide"> <NewModal ref="modal" :noblur="noblur" danger>
<template #title> <template #title>
<slot name="title"> <slot name="title">
<span class="font-extrabold text-contrast text-lg">{{ title }}</span> <span class="font-extrabold text-contrast text-lg">{{ title }}</span>
@@ -76,12 +76,6 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
onHide: {
type: Function,
default() {
return () => {}
},
},
}) })
const emit = defineEmits(['proceed']) const emit = defineEmits(['proceed'])
@@ -58,7 +58,6 @@ const props = withDefaults(
closeOnEsc?: boolean closeOnEsc?: boolean
warnOnClose?: boolean warnOnClose?: boolean
header?: string header?: string
onHide?: () => void
}>(), }>(),
{ {
type: true, type: true,
@@ -66,7 +65,6 @@ const props = withDefaults(
danger: false, danger: false,
closeOnEsc: true, closeOnEsc: true,
warnOnClose: false, warnOnClose: false,
onHide: () => {},
}, },
) )
@@ -89,7 +87,6 @@ function show(event?: MouseEvent) {
} }
function hide() { function hide() {
props.onHide()
visible.value = false visible.value = false
window.removeEventListener('mousedown', updateMousePosition) window.removeEventListener('mousedown', updateMousePosition)
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keydown', handleKeyDown)
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more