Compare commits

..
1 Commits
Author SHA1 Message Date
Prospector e7d096e768 Add plus theme, settings appearance redesign 2024-08-27 21:08:28 -07:00
197 changed files with 4482 additions and 18151 deletions

No files matched your search

-3
View File
@@ -13,6 +13,3 @@ max_line_length = 100
[*.md] [*.md]
max_line_length = off max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.rs]
indent_size = 4
+28 -55
View File
@@ -6,7 +6,6 @@ on:
tags: tags:
- 'v*' - 'v*'
paths: paths:
- .github/workflows/app-release.yml
- 'apps/app/**' - 'apps/app/**'
- 'apps/app-frontend/**' - 'apps/app-frontend/**'
- 'packages/app-lib/**' - 'packages/app-lib/**'
@@ -21,12 +20,12 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [macos-latest, windows-latest, ubuntu-22.04] platform: [macos-latest, windows-latest, ubuntu-20.04]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Rust setup (mac) - name: Rust setup (mac)
if: startsWith(matrix.platform, 'macos') if: startsWith(matrix.platform, 'macos')
@@ -44,34 +43,13 @@ jobs:
- 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-
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 20
@@ -88,7 +66,7 @@ jobs:
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache - name: Setup pnpm cache
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -99,13 +77,14 @@ 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 libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libselinux1
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install run: pnpm install
- name: build app (macos) - name: build app (macos)
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config "tauri-release.conf.json" uses: tauri-apps/tauri-action@v0
id: build_os_mac
if: startsWith(matrix.platform, 'macos') if: startsWith(matrix.platform, 'macos')
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -116,40 +95,34 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: "--target universal-apple-darwin --config ./apps/app/tauri-release.conf.json"
working-directory: ./apps/app
- name: build app - name: build app
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json" uses: tauri-apps/tauri-action@v0
id: build_os id: build_os
if: "!startsWith(matrix.platform, 'macos')" if: "!startsWith(matrix.platform, 'macos')"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: "--config ./apps/app/tauri-release.conf.json"
working-directory: ./apps/app
- name: upload ${{ matrix.platform }} - name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
if: startsWith(matrix.platform, 'macos')
with: with:
name: ${{ matrix.platform }} name: ${{ matrix.platform }}
path: | path: "${{ join(fromJSON(steps.build_os_mac.outputs.artifactPaths), '\n') }}"
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 - name: upload ${{ matrix.platform }}
target/release/bundle/*/*.AppImage.tar.gz uses: actions/upload-artifact@v3
target/release/bundle/*/*.AppImage.tar.gz.sig if: "!startsWith(matrix.platform, 'macos')"
target/release/bundle/*/*.deb with:
target/release/bundle/*/*.rpm name: ${{ matrix.platform }}
path: "${{ join(fromJSON(steps.build_os.outputs.artifactPaths), '\n') }}"
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
+2 -2
View File
@@ -11,7 +11,7 @@ on:
jobs: jobs:
build: build:
name: Build, Test, and Lint name: Build, Test, and Lint
runs-on: ubuntu-22.04 runs-on: ubuntu-20.04
steps: steps:
- name: Check out code - name: Check out code
@@ -30,7 +30,7 @@ jobs:
- name: Install build dependencies - name: Install build dependencies
run: | run: |
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 libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libselinux1
- name: Setup Node.JS environment - name: Setup Node.JS environment
uses: actions/setup-node@v4 uses: actions/setup-node@v4
Generated
+1135 -1642
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -16,6 +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 = "27fb16b" }
+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" />
+8 -18
View File
@@ -1,12 +1,11 @@
{ {
"name": "@modrinth/app-frontend", "name": "@modrinth/app-frontend",
"private": true, "private": true,
"version": "0.8.9", "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,19 +13,14 @@
"@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": "^1.6.0",
"@tauri-apps/api": "^2.0.0-rc.3",
"@tauri-apps/plugin-dialog": "^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",
"@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",
"mixpanel-browser": "^2.49.0",
"ofetch": "^1.3.4", "ofetch": "^1.3.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"posthog-js": "^1.158.2", "tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
"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",
@@ -34,21 +28,17 @@
"vue-virtual-scroller": "v2.0.0-beta.8" "vue-virtual-scroller": "v2.0.0-beta.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.1.1", "@tauri-apps/cli": "^1.6.0",
"@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"
} }
+38 -92
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'
@@ -23,26 +15,28 @@ import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
import { handleError, useNotifications } from '@/store/notifications.js' import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js' import { command_listener, warning_listener } from '@/helpers/events.js'
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons' import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
import { type } from '@tauri-apps/plugin-os' import { type } from '@tauri-apps/api/os'
import { isDev, getOS, restartApp } from '@/helpers/utils.js' import { appWindow } from '@tauri-apps/api/window'
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics' import { isDev, getOS } from '@/helpers/utils.js'
import { getCurrentWindow } from '@tauri-apps/api/window' import {
mixpanel_track,
mixpanel_init,
mixpanel_opt_out_tracking,
mixpanel_is_loaded,
} from '@/helpers/mixpanel'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { window as TauriWindow } from '@tauri-apps/api'
import { TauriEvent } from '@tauri-apps/api/event'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue' import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { install_from_file } from './helpers/pack' import { install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js' import { useError } from '@/store/error.js'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue' import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue' import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue' import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import { useInstall } from '@/store/install.js' import { useInstall } from '@/store/install.js'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
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,12 +57,6 @@ const os = ref('')
const stateInitialized = ref(false) const stateInitialized = ref(false)
const criticalErrorMessage = ref()
onMounted(async () => {
await useCheckDisableMouseover()
})
async function setupApp() { async function setupApp() {
stateInitialized.value = true stateInitialized.value = true
const { const {
@@ -91,23 +79,21 @@ async function setupApp() {
showOnboarding.value = !onboarded showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations) if (os.value !== 'MacOS') await appWindow.setDecorations(native_decorations)
themeStore.setThemeState(theme) themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering themeStore.advancedRendering = advanced_rendering
initAnalytics() mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' })
if (!telemetry) { if (!telemetry) {
optOutAnalytics() mixpanel_opt_out_tracking()
} }
if (dev) debugAnalytics() mixpanel_track('Launched', { version, dev, onboarded })
trackEvent('Launched', { version, dev, onboarded })
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault()) if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
const osType = await type() if ((await type()) === 'Darwin') {
if (osType === 'macos') {
document.getElementsByTagName('html')[0].classList.add('mac') document.getElementsByTagName('html')[0].classList.add('mac')
} else { } else {
document.getElementsByTagName('html')[0].classList.add('windows') document.getElementsByTagName('html')[0].classList.add('windows')
@@ -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)
@@ -152,12 +127,18 @@ initialize_state()
const handleClose = async () => { const handleClose = async () => {
await saveWindowState(StateFlags.ALL) await saveWindowState(StateFlags.ALL)
await getCurrentWindow().close() await TauriWindow.getCurrent().close()
} }
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
const router = useRouter() const router = useRouter()
router.afterEach((to, from, failure) => { router.afterEach((to, from, failure) => {
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure }) if (mixpanel_is_loaded()) {
mixpanel_track('PageView', { path: to.path, fromPath: from.path, failed: failure })
}
}) })
const route = useRoute() const route = useRoute()
const isOnBrowse = computed(() => route.path.startsWith('/browse')) const isOnBrowse = computed(() => route.path.startsWith('/browse'))
@@ -197,10 +178,15 @@ 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) window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: target.href,
},
})
} }
e.preventDefault() e.preventDefault()
break break
@@ -233,7 +219,7 @@ async function handleCommand(e) {
// RunMRPack should directly install a local mrpack given a path // RunMRPack should directly install a local mrpack given a path
if (e.path.endsWith('.mrpack')) { if (e.path.endsWith('.mrpack')) {
await install_from_file(e.path).catch(handleError) await install_from_file(e.path).catch(handleError)
trackEvent('InstanceCreate', { mixpanel_track('InstanceCreate', {
source: 'CreationModalFileDrop', source: 'CreationModalFileDrop',
}) })
} }
@@ -242,20 +228,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 +261,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 +276,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">
@@ -328,14 +288,10 @@ async function checkUpdates() {
</section> </section>
</div> </div>
<section v-if="!nativeDecorations" class="window-controls"> <section v-if="!nativeDecorations" class="window-controls">
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()"> <Button class="titlebar-button" icon-only @click="() => appWindow.minimize()">
<MinimizeIcon /> <MinimizeIcon />
</Button> </Button>
<Button <Button class="titlebar-button" icon-only @click="() => appWindow.toggleMaximize()">
class="titlebar-button"
icon-only
@click="() => getCurrentWindow().toggleMaximize()"
>
<MaximizeIcon /> <MaximizeIcon />
</Button> </Button>
<Button class="titlebar-button close" icon-only @click="handleClose"> <Button class="titlebar-button close" icon-only @click="handleClose">
@@ -428,16 +384,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,7 +22,8 @@ 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 { trackEvent } from '@/helpers/analytics' import { useTheming } from '@/store/state.js'
import { mixpanel_track } from '@/helpers/mixpanel'
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() {
@@ -123,14 +125,14 @@ const handleOptionsClick = async (args) => {
await run(args.item.path).catch((err) => await run(args.item.path).catch((err) =>
handleSevereError(err, { profilePath: args.item.path }), handleSevereError(err, { profilePath: args.item.path }),
) )
trackEvent('InstanceStart', { mixpanel_track('InstanceStart', {
loader: args.item.loader, loader: args.item.loader,
game_version: args.item.game_version, game_version: args.item.game_version,
}) })
break break
case 'stop': case 'stop':
await kill(args.item.path).catch(handleError) await kill(args.item.path).catch(handleError)
trackEvent('InstanceStop', { mixpanel_track('InstanceStop', {
loader: args.item.loader, loader: args.item.loader,
game_version: args.item.game_version, game_version: args.item.game_version,
}) })
@@ -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'"
@@ -70,10 +70,9 @@ import {
get_default_user, get_default_user,
} from '@/helpers/auth' } from '@/helpers/auth'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics' import { mixpanel_track } from '@/helpers/mixpanel'
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: {
@@ -119,7 +118,7 @@ async function login() {
await refreshValues() await refreshValues()
} }
trackEvent('AccountLogIn') mixpanel_track('AccountLogIn')
} }
const logout = async (id) => { const logout = async (id) => {
@@ -131,12 +130,12 @@ const logout = async (id) => {
} else { } else {
emit('change') emit('change')
} }
trackEvent('AccountLogOut') mixpanel_track('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
} }
} }
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets' import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets'
import { Button, OverflowMenu } from '@modrinth/ui' import { Button, OverflowMenu } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/api/dialog'
import { add_project_from_path } from '@/helpers/profile.js' import { add_project_from_path } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -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,14 +1,14 @@
<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'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser'
import { handleSevereError } from '@/store/error.js' 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 ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref() const errorModal = ref()
const error = ref() const error = ref()
@@ -85,7 +85,7 @@ async function loginMinecraft() {
await set_default_user(loggedIn.id).catch(handleError) await set_default_user(loggedIn.id).catch(handleError)
} }
await trackEvent('AccountLogIn', { source: 'ErrorModal' }) await mixpanel.track('AccountLogIn')
loadingMinecraft.value = false loadingMinecraft.value = false
errorModal.value.hide() errorModal.value.hide()
} catch (err) { } catch (err) {
@@ -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'">
@@ -230,7 +230,7 @@ async function repairInstance() {
</p> </p>
<p>You may be able to fix it through one of the following ways:</p> <p>You may be able to fix it through one of the following ways:</p>
<ul> <ul>
<li>Ensuring you are connected to the internet, then try restarting the app.</li> <li>Ennsuring you are connected to the internet, then try restarting the app.</li>
<li>Redownloading the app.</li> <li>Redownloading the app.</li>
</ul> </ul>
</template> </template>
@@ -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/api/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">
@@ -3,14 +3,14 @@ import { onUnmounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { StopCircleIcon, PlayIcon } from '@modrinth/assets' import { StopCircleIcon, PlayIcon } from '@modrinth/assets'
import { Card, Avatar, AnimatedLogo } from '@modrinth/ui' import { Card, Avatar, AnimatedLogo } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { kill, run } from '@/helpers/profile' import { kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process' import { get_by_profile_path } from '@/helpers/process'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { trackEvent } from '@/helpers/analytics'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -45,7 +45,7 @@ const play = async (e, context) => {
) )
modLoading.value = false modLoading.value = false
trackEvent('InstancePlay', { mixpanel_track('InstancePlay', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
source: context, source: context,
@@ -58,7 +58,7 @@ const stop = async (e, context) => {
await kill(props.instance.path).catch(handleError) await kill(props.instance.path).catch(handleError)
trackEvent('InstanceStop', { mixpanel_track('InstanceStop', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
source: context, source: context,
@@ -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,16 +207,18 @@ 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'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/api/dialog'
import { convertFileSrc } from '@tauri-apps/api/core' import { tauri } from '@tauri-apps/api'
import { get_game_versions, get_loader_versions } from '@/helpers/metadata' 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 { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/state.js'
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 {
get_default_launcher_path, get_default_launcher_path,
@@ -225,7 +226,8 @@ import {
import_instance, import_instance,
} from '@/helpers/import.js' } from '@/helpers/import.js'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview'
const themeStore = useTheming()
const profile_name = ref('') const profile_name = ref('')
const game_version = ref('') const game_version = ref('')
@@ -255,22 +257,20 @@ defineExpose({
isShowing.value = true isShowing.value = true
modal.value.show() modal.value.show()
unlistener.value = await getCurrentWebview().onDragDropEvent(async (event) => { unlistener.value = await listen('tauri://file-drop', async (event) => {
// Only if modal is showing // Only if modal is showing
if (!isShowing.value) return if (!isShowing.value) return
if (event.payload.type !== 'drop') return
if (creationType.value !== 'from file') return if (creationType.value !== 'from file') return
hide() hide()
const { paths } = event.payload if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) { await install_from_file(event.payload[0]).catch(handleError)
await install_from_file(paths[0]).catch(handleError) mixpanel_track('InstanceCreate', {
trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop', source: 'CreationModalFileDrop',
}) })
} }
}) })
trackEvent('InstanceCreateStart', { source: 'CreationModal' }) mixpanel_track('InstanceCreateStart', { source: 'CreationModal' })
}, },
}) })
@@ -360,7 +360,7 @@ const create_instance = async () => {
icon.value, icon.value,
).catch(handleError) ).catch(handleError)
trackEvent('InstanceCreate', { mixpanel_track('InstanceCreate', {
profile_name: profile_name.value, profile_name: profile_name.value,
game_version: game_version.value, game_version: game_version.value,
loader: loader.value, loader: loader.value,
@@ -371,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: [
{ {
@@ -381,10 +381,8 @@ 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 = tauri.convertFileSrc(icon.value)
} }
const reset_icon = () => { const reset_icon = () => {
@@ -419,9 +417,9 @@ 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', { mixpanel_track('InstanceCreate', {
source: 'CreationModalFileOpen', source: 'CreationModalFileOpen',
}) })
} }
@@ -464,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 { trackEvent } from '@/helpers/analytics' import { mixpanel_track } from '@/helpers/mixpanel'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const chosenInstallOptions = ref([]) const chosenInstallOptions = ref([])
const detectJavaModal = ref(null) const detectJavaModal = ref(null)
@@ -65,7 +67,7 @@ const emit = defineEmits(['submit'])
function setJavaInstall(javaInstall) { function setJavaInstall(javaInstall) {
emit('submit', javaInstall) emit('submit', javaInstall)
detectJavaModal.value.hide() detectJavaModal.value.hide()
trackEvent('JavaAutoDetect', { mixpanel_track('JavaAutoDetect', {
path: javaInstall.path, path: javaInstall.path,
version: javaInstall.version, version: javaInstall.version,
}) })
@@ -63,10 +63,10 @@ import {
import { Button } from '@modrinth/ui' import { Button } from '@modrinth/ui'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js' import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
import { ref } from 'vue' import { ref } from 'vue'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/api/dialog'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue' import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
const props = defineProps({ const props = defineProps({
version: { version: {
@@ -113,7 +113,7 @@ async function testJava() {
) )
testingJava.value = false testingJava.value = false
trackEvent('JavaTest', { mixpanel_track('JavaTest', {
path: props.modelValue ? props.modelValue.path : '', path: props.modelValue ? props.modelValue.path : '',
success: testingJavaSuccess.value, success: testingJavaSuccess.value,
}) })
@@ -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', { mixpanel_track('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])
} }
@@ -169,7 +170,7 @@ async function reinstallJava() {
} }
} }
trackEvent('JavaReInstall', { mixpanel_track('JavaReInstall', {
path: path, path: path,
version: props.version, version: props.version,
}) })
@@ -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, open_ads_link, record_ads_click } 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,103 +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)
}
}
async function openPlusLink() {
await record_ads_click()
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
}
const unlisten = await listen('ads-scroll', (event) => {
if (adsWrapper.value) {
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
updateAdPosition()
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
if (intersectionObserver) {
intersectionObserver.disconnect()
}
if (mutationObserver) {
mutationObserver.disconnect()
}
if (scrollHandler) {
window.removeEventListener('scroll', scrollHandler)
}
unlisten()
})
</script> </script>
<template> <template>
<div <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">75% of ad revenue goes to creators</p>
<button
class="mt-auto items-center gap-1 text-purple hover:underline bg-transparent border-none text-left cursor-pointer outline-none"
@click="openPlusLink"
>
<span>
Support creators and Modrinth ad-free with
<span class="font-bold">Modrinth+</span>
</span>
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
</button>
</div>
</div>
</template> </template>
@@ -117,9 +117,9 @@ import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js' import { progress_bars_list } from '@/helpers/state.js'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { ChatIcon } from '@/assets/icons' import { ChatIcon } from '@/assets/icons'
import { get_many } from '@/helpers/profile.js' import { get_many } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
const router = useRouter() const router = useRouter()
const card = ref(null) const card = ref(null)
@@ -164,7 +164,7 @@ const stop = async (process) => {
try { try {
await killProcess(process.uuid).catch(handleError) await killProcess(process.uuid).catch(handleError)
trackEvent('InstanceStop', { mixpanel_track('InstanceStop', {
loader: process.profile.loader, loader: process.profile.loader,
game_version: process.profile.game_version, game_version: process.profile.game_version,
source: 'AppBar', source: 'AppBar',
@@ -1,10 +1,10 @@
<template> <template>
<div v-if="!hidden" class="splash-screen dark" :class="{ 'fade-out': doneLoading }"> <div v-if="!hidden" class="splash-screen dark" :class="{ 'fade-out': doneLoading }">
<div v-if="os !== 'MacOS'" class="app-buttons"> <div v-if="os !== 'MacOS'" class="app-buttons">
<button class="btn icon-only transparent" icon-only @click="() => getCurrent().minimize()"> <button class="btn icon-only transparent" icon-only @click="() => appWindow.minimize()">
<MinimizeIcon /> <MinimizeIcon />
</button> </button>
<button class="btn icon-only transparent" @click="() => getCurrent().toggleMaximize()"> <button class="btn icon-only transparent" @click="() => appWindow.toggleMaximize()">
<MaximizeIcon /> <MaximizeIcon />
</button> </button>
<button class="btn icon-only transparent" @click="handleClose"> <button class="btn icon-only transparent" @click="handleClose">
@@ -85,9 +85,12 @@
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import { loading_listener } from '@/helpers/events.js' import { loading_listener } from '@/helpers/events.js'
import { getCurrentWindow } from '@tauri-apps/api/window' import { appWindow } 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 { window as TauriWindow } from '@tauri-apps/api'
import { TauriEvent } from '@tauri-apps/api/event'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
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,22 +131,20 @@ 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...'
} }
}) })
const handleClose = async () => { const handleClose = async () => {
await getCurrentWindow().close() await saveWindowState(StateFlags.ALL)
await TauriWindow.getCurrent().close()
} }
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -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,18 +51,19 @@
</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 { mixpanel_track } from '@/helpers/mixpanel'
const themeStore = useTheming()
const instance = ref(null) const instance = ref(null)
const project = ref(null) const project = 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) => {
@@ -81,7 +87,7 @@ defineExpose({
incompatibleModal.value.show() incompatibleModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' }) mixpanel_track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
}, },
}) })
@@ -92,7 +98,7 @@ const install = async () => {
onInstall.value(selectedVersion.value.id) onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide() incompatibleModal.value.hide()
trackEvent('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.value.loader, loader: instance.value.loader,
game_version: instance.value.game_version, game_version: instance.value.game_version,
id: project.value, id: project.value,
@@ -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 { mixpanel_track } from '@/helpers/mixpanel'
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) => {
@@ -23,7 +25,7 @@ defineExpose({
onInstall.value = callback onInstall.value = callback
trackEvent('PackInstallStart') mixpanel_track('PackInstallStart')
}, },
}) })
@@ -37,7 +39,7 @@ async function install() {
project.value.title, project.value.title,
project.value.icon_url, project.value.icon_url,
).catch(handleError) ).catch(handleError)
trackEvent('PackInstall', { mixpanel_track('PackInstall', {
id: project.value.id, id: project.value.id,
version_id: versionId.value, version_id: versionId.value,
title: project.value.title, title: project.value.title,
@@ -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,
@@ -16,14 +16,15 @@ import {
list, list,
create, create,
} from '@/helpers/profile' } from '@/helpers/profile'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/api/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 { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { convertFileSrc } from '@tauri-apps/api/core' import { tauri } from '@tauri-apps/api'
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,
@@ -87,7 +88,7 @@ defineExpose({
installModal.value.show() installModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' }) mixpanel_track('ProjectInstallStart', { source: 'ProjectInstallModal' })
}, },
}) })
@@ -114,7 +115,7 @@ async function install(instance) {
instance.installedMod = true instance.installedMod = true
instance.installing = false instance.installing = false
trackEvent('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.loader, loader: instance.loader,
game_version: instance.game_version, game_version: instance.game_version,
id: project.value.id, id: project.value.id,
@@ -136,12 +137,12 @@ const toggleCreation = () => {
loader.value = null loader.value = null
if (showCreation.value) { if (showCreation.value) {
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' }) mixpanel_track('InstanceCreateStart', { source: 'ProjectInstallModal' })
} }
} }
const upload_icon = async () => { const upload_icon = async () => {
const res = await open({ icon.value = await open({
multiple: false, multiple: false,
filters: [ filters: [
{ {
@@ -150,10 +151,9 @@ 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 = tauri.convertFileSrc(icon.value)
} }
const reset_icon = () => { const reset_icon = () => {
@@ -186,7 +186,7 @@ const createInstance = async () => {
const instance = await get(id, true) const instance = await get(id, true)
await installVersionDependencies(instance, versions.value[0]) await installVersionDependencies(instance, versions.value[0])
trackEvent('InstanceCreate', { mixpanel_track('InstanceCreate', {
profile_name: name.value, profile_name: name.value,
game_version: versions.value[0].game_versions[0], game_version: versions.value[0].game_versions[0],
loader: loader, loader: loader,
@@ -195,7 +195,7 @@ const createInstance = async () => {
source: 'ProjectInstallModal', source: 'ProjectInstallModal',
}) })
trackEvent('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: loader, loader: loader,
game_version: versions.value[0].game_versions[0], game_version: versions.value[0].game_versions[0],
id: project.value, id: project.value,
@@ -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"
@@ -230,7 +235,7 @@ const createInstance = async () => {
@click="installModal.hide()" @click="installModal.hide()"
> >
<Avatar <Avatar
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null" :src="profile.icon_path ? tauri.convertFileSrc(profile.icon_path) : null"
class="profile-image" class="profile-image"
/> />
{{ profile.name }} {{ profile.name }}
@@ -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">
@@ -1,20 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
import cssContent from '@/assets/stylesheets/macFix.css?inline'
export async function useCheckDisableMouseover() {
try {
// Fetch the CSS content from the Rust backend
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
if (should_disable_mouseover) {
// Create a style element and set its content
const styleElement = document.createElement('style')
styleElement.innerHTML = cssContent
// Append the style element to the document's head
document.head.appendChild(styleElement)
}
} catch (error) {
console.error('Error checking OS version from Rust backend', error)
}
}
-21
View File
@@ -1,21 +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 })
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
export async function open_ads_link(path, origin) {
return await invoke('plugin:ads|open_link', { path, origin })
}
@@ -1,23 +0,0 @@
import { posthog } from 'posthog-js'
export const initAnalytics = () => {
posthog.init('phc_hm2ihMpTAoE86xIm7XzsCB8RPiTRKivViK5biiHedm', {
persistence: 'localStorage',
})
}
export const debugAnalytics = () => {
posthog.debug()
}
export const optOutAnalytics = () => {
posthog.opt_out_capturing()
}
export const optInAnalytics = () => {
posthog.opt_in_capturing()
}
export const trackEvent = (eventName, properties) => {
posthog.capture(eventName, properties)
}
+19 -30
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
// Example function: // Example function:
// User goes to auth_url to complete flow, and when completed, authenticate_await_completion() returns the credentials // User goes to auth_url to complete flow, and when completed, authenticate_await_completion() returns the credentials
@@ -13,46 +13,35 @@ import { invoke } from '@tauri-apps/api/core'
// await authenticate_await_completion() // await authenticate_await_completion()
// } // }
/** /// Authenticate a user with Hydra - part 1
* Authenticate a user with Hydra - part 1. /// This begins the authentication flow quasi-synchronously
* This begins the authentication flow quasi-synchronously. /// This returns a DeviceLoginSuccess object, with two relevant fields:
* /// - verification_uri: the URL to go to to complete the flow
* @returns {Promise<DeviceLoginSuccess>} A DeviceLoginSuccess object with two relevant fields: /// - user_code: the code to enter on the verification_uri page
* @property {string} verification_uri - The URL to go to complete the flow.
* @property {string} user_code - The code to enter on the verification_uri page.
*/
export async function login() { export async function login() {
return await invoke('plugin:auth|login') return await invoke('auth_login')
} }
/** /// Retrieves the default user
* Retrieves the default user /// user is UUID
* @return {Promise<UUID | undefined>}
*/
export async function get_default_user() { export async function get_default_user() {
return await invoke('plugin:auth|get_default_user') return await invoke('plugin:auth|auth_get_default_user')
} }
/** /// Updates the default user
* Updates the default user /// user is UUID
* @param {UUID} user
*/
export async function set_default_user(user) { export async function set_default_user(user) {
return await invoke('plugin:auth|set_default_user', { user }) return await invoke('plugin:auth|auth_set_default_user', { user })
} }
/** /// Remove a user account from the database
* Remove a user account from the database /// user is UUID
* @param {UUID} user
*/
export async function remove_user(user) { export async function remove_user(user) {
return await invoke('plugin:auth|remove_user', { user }) return await invoke('plugin:auth|auth_remove_user', { user })
} }
/** /// Returns a list of users
* Returns a list of users /// Returns an Array of Credentials
* @returns {Promise<Credential[]>}
*/
export async function users() { export async function users() {
return await invoke('plugin:auth|get_users') return await invoke('plugin:auth|auth_users')
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
export async function get_project(id, cacheBehaviour) { export async function get_project(id, cacheBehaviour) {
return await invoke('plugin:cache|get_project', { id, cacheBehaviour }) return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
+5 -5
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
import { create } from './profile' import { create } from './profile'
/* /*
@@ -27,7 +27,7 @@ import { create } from './profile'
/// eg: get_importable_instances("MultiMC", "C:/MultiMC") /// eg: get_importable_instances("MultiMC", "C:/MultiMC")
/// returns ["Instance 1", "Instance 2"] /// returns ["Instance 1", "Instance 2"]
export async function get_importable_instances(launcherType, basePath) { export async function get_importable_instances(launcherType, basePath) {
return await invoke('plugin:import|get_importable_instances', { launcherType, basePath }) return await invoke('plugin:import|import_get_importable_instances', { launcherType, basePath })
} }
/// Import an instance from a launcher type and base path /// Import an instance from a launcher type and base path
@@ -38,7 +38,7 @@ export async function import_instance(launcherType, basePath, instanceFolder) {
// fs watching will be enabled once the instance is imported // fs watching will be enabled once the instance is imported
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true) const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
return await invoke('plugin:import|import_instance', { return await invoke('plugin:import|import_import_instance', {
profilePath, profilePath,
launcherType, launcherType,
basePath, basePath,
@@ -49,7 +49,7 @@ export async function import_instance(launcherType, basePath, instanceFolder) {
/// Checks if this instance is valid for importing, given a certain launcher type /// Checks if this instance is valid for importing, given a certain launcher type
/// eg: is_valid_importable_instance("C:/MultiMC/Instance 1", "MultiMC") /// eg: is_valid_importable_instance("C:/MultiMC/Instance 1", "MultiMC")
export async function is_valid_importable_instance(instanceFolder, launcherType) { export async function is_valid_importable_instance(instanceFolder, launcherType) {
return await invoke('plugin:import|is_valid_importable_instance', { return await invoke('plugin:import|import_is_valid_importable_instance', {
instanceFolder, instanceFolder,
launcherType, launcherType,
}) })
@@ -59,5 +59,5 @@ export async function is_valid_importable_instance(instanceFolder, launcherType)
/// null if it can't be found or doesn't exist /// null if it can't be found or doesn't exist
/// eg: get_default_launcher_path("MultiMC") /// eg: get_default_launcher_path("MultiMC")
export async function get_default_launcher_path(launcherType) { export async function get_default_launcher_path(launcherType) {
return await invoke('plugin:import|get_default_launcher_path', { launcherType }) return await invoke('plugin:import|import_get_default_launcher_path', { launcherType })
} }
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
/* /*
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
/* /*
A log is a struct containing the filename string, stdout, and stderr, as follows: A log is a struct containing the filename string, stdout, and stderr, as follows:
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
/// Gets the game versions from daedalus /// Gets the game versions from daedalus
// Returns a VersionManifest // Returns a VersionManifest
+57
View File
@@ -0,0 +1,57 @@
import mixpanel from 'mixpanel-browser'
// mixpanel_track
function trackWrapper(originalTrack) {
return function (event_name, properties = {}) {
try {
originalTrack(event_name, properties)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_track = trackWrapper(mixpanel.track.bind(mixpanel))
// mixpanel_opt_out_tracking()
function optOutTrackingWrapper(originalOptOutTracking) {
return function () {
try {
originalOptOutTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_out_tracking = optOutTrackingWrapper(
mixpanel.opt_out_tracking.bind(mixpanel),
)
// mixpanel_opt_in_tracking()
function optInTrackingWrapper(originalOptInTracking) {
return function () {
try {
originalOptInTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_in_tracking = optInTrackingWrapper(
mixpanel.opt_in_tracking.bind(mixpanel),
)
// mixpanel_init
function initWrapper(originalInit) {
return function (token, config = {}) {
try {
originalInit(token, config)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_init = initWrapper(mixpanel.init.bind(mixpanel))
export const mixpanel_is_loaded = () => {
return mixpanel.__loaded
}
+6 -6
View File
@@ -3,22 +3,22 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
export async function login(provider) { export async function login(provider) {
return await invoke('modrinth_auth_login', { provider }) return await invoke('modrinth_auth_login', { provider })
} }
export async function login_pass(username, password, challenge) { export async function login_pass(username, password, challenge) {
return await invoke('plugin:mr-auth|login_pass', { username, password, challenge }) return await invoke('plugin:mr_auth|login_pass', { username, password, challenge })
} }
export async function login_2fa(code, flow) { export async function login_2fa(code, flow) {
return await invoke('plugin:mr-auth|login_2fa', { code, flow }) return await invoke('plugin:mr_auth|login_2fa', { code, flow })
} }
export async function create_account(username, email, password, challenge, signUpNewsletter) { export async function create_account(username, email, password, challenge, signUpNewsletter) {
return await invoke('plugin:mr-auth|create_account', { return await invoke('plugin:mr_auth|create_account', {
username, username,
email, email,
password, password,
@@ -28,9 +28,9 @@ export async function create_account(username, email, password, challenge, signU
} }
export async function logout() { export async function logout() {
return await invoke('plugin:mr-auth|logout') return await invoke('plugin:mr_auth|logout')
} }
export async function get() { export async function get() {
return await invoke('plugin:mr-auth|get') return await invoke('plugin:mr_auth|get')
} }
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
import { create } from './profile' import { create } from './profile'
// Installs pack from a version ID // Installs pack from a version ID
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
/// Gets all running process IDs with a given profile path /// Gets all running process IDs with a given profile path
/// Returns [u32] /// Returns [u32]
+3 -3
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
/// Add instance /// Add instance
/* /*
@@ -19,7 +19,7 @@ import { invoke } from '@tauri-apps/api/core'
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) { export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
//Trim string name to avoid "Unable to find directory" //Trim string name to avoid "Unable to find directory"
name = name.trim() name = name.trim()
return await invoke('plugin:profile-create|profile_create', { return await invoke('plugin:profile_create|profile_create', {
name, name,
gameVersion, gameVersion,
modloader, modloader,
@@ -31,7 +31,7 @@ export async function create(name, gameVersion, modloader, loaderVersion, iconPa
// duplicate a profile // duplicate a profile
export async function duplicate(path) { export async function duplicate(path) {
return await invoke('plugin:profile-create|profile_duplicate', { path }) return await invoke('plugin:profile_create|profile_duplicate', { path })
} }
// Remove a profile // Remove a profile
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
// Settings object // Settings object
/* /*
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
// Initialize the theseus API state // Initialize the theseus API state
// This should be called during the initializion/opening of the launcher // This should be called during the initializion/opening of the launcher
+1 -1
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
// Gets cached category tags // Gets cached category tags
export async function get_categories() { export async function get_categories() {
+2 -6
View File
@@ -1,5 +1,5 @@
import { get_full_path, get_mod_full_path } from '@/helpers/profile' import { get_full_path, get_mod_full_path } from '@/helpers/profile'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/tauri'
export async function isDev() { export async function isDev() {
return await invoke('is_dev') return await invoke('is_dev')
@@ -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
+2 -9
View File
@@ -4,8 +4,8 @@ import App from '@/App.vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import FloatingVue from 'floating-vue' import FloatingVue from 'floating-vue'
import 'floating-vue/dist/style.css' import 'floating-vue/dist/style.css'
import loadCssMixin from './mixins/macCssFix.js'
import { createPlugin } from '@vintl/vintl/plugin' import { createPlugin } from '@vintl/vintl/plugin'
import * as Sentry from '@sentry/vue'
const VIntlPlugin = createPlugin({ const VIntlPlugin = createPlugin({
controllerOpts: { controllerOpts: {
@@ -27,17 +27,10 @@ const VIntlPlugin = createPlugin({
const pinia = createPinia() const pinia = createPinia()
let app = createApp(App) let app = createApp(App)
Sentry.init({
app,
dsn: 'https://9508775ee5034536bc70433f5f531dd4@o485889.ingest.us.sentry.io/4504579615227904',
integrations: [Sentry.browserTracingIntegration({ router })],
tracesSampleRate: 0.1,
})
app.use(router) app.use(router)
app.use(pinia) app.use(pinia)
app.use(FloatingVue) app.use(FloatingVue)
app.mixin(loadCssMixin)
app.use(VIntlPlugin) app.use(VIntlPlugin)
app.mount('#app') app.mount('#app')
+27
View File
@@ -0,0 +1,27 @@
import { invoke } from '@tauri-apps/api/tauri'
import cssContent from '@/assets/stylesheets/macFix.css?inline'
export default {
async mounted() {
await this.checkDisableMouseover()
},
methods: {
async checkDisableMouseover() {
try {
// Fetch the CSS content from the Rust backend
const should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
if (should_disable_mouseover) {
// Create a style element and set its content
const styleElement = document.createElement('style')
styleElement.innerHTML = cssContent
// Append the style element to the document's head
document.head.appendChild(styleElement)
}
} catch (error) {
console.error('Error checking OS version from Rust backend', error)
}
},
},
}
+13 -13
View File
@@ -19,7 +19,7 @@ import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import SearchCard from '@/components/ui/SearchCard.vue' import SearchCard from '@/components/ui/SearchCard.vue'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js' import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { get_search_results } from '@/helpers/cache.js' import { get_search_results } from '@/helpers/cache.js'
import { debounce } from '@/helpers/utils.js' import { debounce } from '@/helpers/utils.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
@@ -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()
+14 -38
View File
@@ -1,24 +1,18 @@
<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'
import { get as getCreds, logout } from '@/helpers/mr_auth.js' import { get as getCreds, logout } from '@/helpers/mr_auth.js'
import JavaSelector from '@/components/ui/JavaSelector.vue' import JavaSelector from '@/components/ui/JavaSelector.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue' import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
import { optOutAnalytics, optInAnalytics } from '@/helpers/analytics' import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/api/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']
@@ -51,9 +45,9 @@ watch(
const setSettings = JSON.parse(JSON.stringify(newSettings)) const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.telemetry) { if (setSettings.telemetry) {
optInAnalytics() mixpanel_opt_out_tracking()
} else { } else {
optOutAnalytics() mixpanel_opt_in_tracking()
} }
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean) setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
@@ -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">
@@ -363,25 +358,6 @@ async function purgeCache() {
<span class="label__title size-card-header">Privacy</span> <span class="label__title size-card-header">Privacy</span>
</h3> </h3>
</div> </div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Personalized ads</span>
<span class="label__description">
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
option, you opt out and ads will no longer be shown based on your interests.
</span>
</label>
<Toggle
id="opt-out-analytics"
:model-value="settings.personalized_ads"
:checked="settings.personalized_ads"
@update:model-value="
(e) => {
settings.personalized_ads = e
}
"
/>
</div>
<div class="adjacent-input"> <div class="adjacent-input">
<label for="opt-out-analytics"> <label for="opt-out-analytics">
<span class="label__title">Telemetry</span> <span class="label__title">Telemetry</span>
@@ -425,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>
+19 -15
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()">
@@ -131,8 +131,9 @@ import { ref, onUnmounted } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state' import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import { trackEvent } from '@/helpers/analytics' import { mixpanel_track } from '@/helpers/mixpanel'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useFetch } from '@/helpers/fetch'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js' import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -182,7 +183,7 @@ const startInstance = async (context) => {
} }
loading.value = false loading.value = false
trackEvent('InstanceStart', { mixpanel_track('InstanceStart', {
loader: instance.value.loader, loader: instance.value.loader,
game_version: instance.value.game_version, game_version: instance.value.game_version,
source: context, source: context,
@@ -219,7 +220,7 @@ const stopInstance = async (context) => {
playing.value = false playing.value = false
await kill(route.params.id).catch(handleError) await kill(route.params.id).catch(handleError)
trackEvent('InstanceStop', { mixpanel_track('InstanceStop', {
loader: instance.value.loader, loader: instance.value.loader,
game_version: instance.value.game_version, game_version: instance.value.game_version,
source: context, source: context,
@@ -311,6 +312,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
width: 17rem;
} }
Button { Button {
@@ -324,13 +326,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 +375,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 +452,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,
+17 -20
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,
@@ -377,7 +379,8 @@ import {
update_project, update_project,
} from '@/helpers/profile.js' } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { trackEvent } from '@/helpers/analytics' import { mixpanel_track } from '@/helpers/mixpanel'
import { listen } from '@tauri-apps/api/event'
import { highlightModInProfile } from '@/helpers/utils.js' import { highlightModInProfile } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons' import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue' import ExportModal from '@/components/ui/ExportModal.vue'
@@ -390,9 +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'
import { getCurrentWebview } from '@tauri-apps/api/webview'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -682,7 +682,7 @@ const updateAll = async () => {
projects.value[project].updating = false projects.value[project].updating = false
} }
trackEvent('InstanceUpdateAll', { mixpanel_track('InstanceUpdateAll', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
count: setProjects.length, count: setProjects.length,
@@ -708,7 +708,7 @@ const updateProject = async (mod) => {
mod.version = mod.updateVersion.version_number mod.version = mod.updateVersion.version_number
mod.updateVersion = null mod.updateVersion = null
trackEvent('InstanceProjectUpdate', { mixpanel_track('InstanceProjectUpdate', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
id: mod.id, id: mod.id,
@@ -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
@@ -735,7 +735,7 @@ const toggleDisableMod = async (mod) => {
.then((newPath) => { .then((newPath) => {
mod.path = newPath mod.path = newPath
mod.disabled = !mod.disabled mod.disabled = !mod.disabled
trackEvent('InstanceProjectDisable', { mixpanel_track('InstanceProjectDisable', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
id: mod.id, id: mod.id,
@@ -756,7 +756,7 @@ const removeMod = async (mod) => {
await remove_project(props.instance.path, mod.path).catch(handleError) await remove_project(props.instance.path, mod.path).catch(handleError)
projects.value = projects.value.filter((x) => mod.path !== x.path) projects.value = projects.value.filter((x) => mod.path !== x.path)
trackEvent('InstanceProjectRemove', { mixpanel_track('InstanceProjectRemove', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
id: mod.id, id: mod.id,
@@ -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'))
} }
@@ -879,10 +878,8 @@ async function refreshProjects() {
refreshingProjects.value = false refreshingProjects.value = false
} }
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => { const unlisten = await listen('tauri://file-drop', async (event) => {
if (event.payload.type !== 'drop') return for (const file of event.payload) {
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError) await add_project_from_path(props.instance.path, file).catch(handleError)
} }
@@ -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'
@@ -517,16 +541,15 @@ import { computed, readonly, ref, shallowRef, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre.js' import { get_max_memory } from '@/helpers/jre.js'
import { get } from '@/helpers/settings.js' import { get } from '@/helpers/settings.js'
import JavaSelector from '@/components/ui/JavaSelector.vue' import JavaSelector from '@/components/ui/JavaSelector.vue'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/api/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 { mixpanel_track } from '@/helpers/mixpanel'
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 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)
@@ -565,7 +590,7 @@ const availableGroups = ref([
async function resetIcon() { async function resetIcon() {
icon.value = null icon.value = null
await edit_icon(props.instance.path, null).catch(handleError) await edit_icon(props.instance.path, null).catch(handleError)
trackEvent('InstanceRemoveIcon') mixpanel_track('InstanceRemoveIcon')
} }
async function setIcon() { async function setIcon() {
@@ -581,10 +606,10 @@ 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') mixpanel_track('InstanceSetIcon')
} }
const globalSettings = await get().catch(handleError) const globalSettings = await get().catch(handleError)
@@ -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) {
@@ -725,7 +754,7 @@ const repairing = ref(false)
async function duplicateProfile() { async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError) await duplicate(props.instance.path).catch(handleError)
trackEvent('InstanceDuplicate', { mixpanel_track('InstanceDuplicate', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
}) })
@@ -736,7 +765,7 @@ async function repairProfile(force) {
await install(props.instance.path, force).catch(handleError) await install(props.instance.path, force).catch(handleError)
repairing.value = false repairing.value = false
trackEvent('InstanceRepair', { mixpanel_track('InstanceRepair', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
}) })
@@ -767,7 +796,7 @@ async function repairModpack() {
await update_repair_modrinth(props.instance.path).catch(handleError) await update_repair_modrinth(props.instance.path).catch(handleError)
inProgress.value = false inProgress.value = false
trackEvent('InstanceRepair', { mixpanel_track('InstanceRepair', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
}) })
@@ -779,7 +808,7 @@ async function removeProfile() {
await remove(props.instance.path).catch(handleError) await remove(props.instance.path).catch(handleError)
removing.value = false removing.value = false
trackEvent('InstanceRemove', { mixpanel_track('InstanceRemove', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
}) })
@@ -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
+13 -34
View File
@@ -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'
" "
> >
@@ -93,8 +93,7 @@ import {
} from '@modrinth/assets' } from '@modrinth/assets'
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 { mixpanel_track } from '@/helpers/mixpanel'
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++
@@ -118,7 +112,7 @@ const nextImage = () => {
expandedGalleryIndex.value = 0 expandedGalleryIndex.value = 0
} }
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value] expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
trackEvent('GalleryImageNext', { mixpanel_track('GalleryImageNext', {
project_id: props.project.id, project_id: props.project.id,
url: expandedGalleryItem.value.url, url: expandedGalleryItem.value.url,
}) })
@@ -130,37 +124,22 @@ const previousImage = () => {
expandedGalleryIndex.value = props.project.gallery.length - 1 expandedGalleryIndex.value = props.project.gallery.length - 1
} }
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value] expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
trackEvent('GalleryImagePrevious', { mixpanel_track('GalleryImagePrevious', {
project_id: props.project.id, project_id: props.project.id,
url: expandedGalleryItem.value, url: expandedGalleryItem.value,
}) })
} }
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
trackEvent('GalleryImageExpand', { mixpanel_track('GalleryImageExpand', {
project_id: props.project.id, project_id: props.project.id,
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">
+17 -12
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,
@@ -250,10 +257,10 @@ import { useRoute } from 'vue-router'
import { ref, shallowRef, watch } from 'vue' import { ref, shallowRef, watch } from 'vue'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/tauri'
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 {
+1 -1
View File
@@ -139,7 +139,7 @@ export default new createRouter({
linkExactActiveClass: 'router-link-exact-active', linkExactActiveClass: 'router-link-exact-active',
scrollBehavior() { scrollBehavior() {
// Sometimes Vue's scroll behavior is not working as expected, so we need to manually scroll to top (especially on Linux) // Sometimes Vue's scroll behavior is not working as expected, so we need to manually scroll to top (especially on Linux)
document.querySelector('.router-view')?.scrollTo(0, 0) document.querySelector('.router-view').scrollTop = 0
return { return {
el: '.router-view', el: '.router-view',
top: 0, top: 0,
+3 -3
View File
@@ -10,7 +10,7 @@ import {
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_project, get_version_many } from '@/helpers/cache.js' import { get_project, get_version_many } from '@/helpers/cache.js'
import { install as packInstall } from '@/helpers/pack.js' import { install as packInstall } from '@/helpers/pack.js'
import { trackEvent } from '@/helpers/analytics.js' import { mixpanel_track } from '@/helpers/mixpanel.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
export const useInstall = defineStore('installStore', { export const useInstall = defineStore('installStore', {
@@ -51,7 +51,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) { if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) {
await packInstall(project.id, version, project.title, project.icon_url).catch(handleError) await packInstall(project.id, version, project.title, project.icon_url).catch(handleError)
trackEvent('PackInstall', { mixpanel_track('PackInstall', {
id: project.id, id: project.id,
version_id: version, version_id: version,
title: project.title, title: project.title,
@@ -107,7 +107,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
await add_project_from_version(instance.path, version.id).catch(handleError) await add_project_from_version(instance.path, version.id).catch(handleError)
await installVersionDependencies(instance, version) await installVersionDependencies(instance, version)
trackEvent('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.loader, loader: instance.loader,
game_version: instance.game_version, game_version: instance.game_version,
id: project.id, id: project.id,
@@ -23,3 +23,7 @@ export const handleError = (err) => {
}) })
console.error(err) console.error(err)
} }
export const handleMixpanelError = (err) => {
console.error(err)
}
-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
+2
View File
@@ -10,12 +10,14 @@ 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 = { version = "1.7.1", features = ["shell-open"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
thiserror = "1.0" thiserror = "1.0"
url = "2.2" url = "2.2"
webbrowser = "0.8.13" webbrowser = "0.8.13"
dunce = "1.0.3" dunce = "1.0.3"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3" futures = "0.3"
uuid = { version = "1.1", features = ["serde", "v4"] } uuid = { version = "1.1", features = ["serde", "v4"] }
+13 -18
View File
@@ -1,14 +1,17 @@
[package] [package]
name = "theseus_gui" name = "theseus_gui"
version = "0.8.9" version = "0.8.3-1"
description = "The Modrinth App is a desktop application for managing your Minecraft mods" description = "A Tauri App"
license = "GPL-3.0-only" authors = ["you"]
repository = "https://github.com/modrinth/code/apps/app/" license = ""
repository = ""
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-rc", features = ["codegen"] } tauri-build = { version = "1.5.3", features = [] }
[dependencies] [dependencies]
theseus = { path = "../../packages/app-lib", features = ["tauri"] } theseus = { path = "../../packages/app-lib", features = ["tauri"] }
@@ -16,17 +19,14 @@ 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 = { version = "2.0.0-rc", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] } tauri = { version = "1.7.1", features = [ "app-all", "devtools", "dialog", "dialog-confirm", "dialog-open", "macos-private-api", "os-all", "protocol-asset", "shell-open", "window-close", "window-create", "window-hide", "window-maximize", "window-minimize", "window-set-decorations", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-window-state = "2.0.0-rc" tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-deep-link = "2.0.0-rc" tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-os = "2.0.0-rc" tauri-plugin-deep-link = "0.1.2"
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-updater = { version = "2.0.0-rc" }
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"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3" futures = "0.3"
daedalus = "0.2.3" daedalus = "0.2.3"
chrono = "0.4.26" chrono = "0.4.26"
@@ -56,10 +56,6 @@ window-shadows = "0.2.1"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0" cocoa = "0.25.0"
objc = "0.2.7" objc = "0.2.7"
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
@@ -68,4 +64,3 @@ 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 = []
+51 -5
View File
@@ -6,17 +6,63 @@
<array> <array>
<dict> <dict>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<!-- Obviously needs to be replaced with your app's bundle identifier -->
<string>ModrinthApp</string> <string>ModrinthApp</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<!-- register the myapp:// and myscheme:// schemes -->
<string>modrinth</string> <string>modrinth</string>
<string>modrinthscheme</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>NSCameraUsageDescription</key> <!-- Declare file types your app can open -->
<string>A Minecraft mod wants to access your camera.</string> <key>CFBundleDocumentTypes</key>
<key>NSMicrophoneUsageDescription</key> <array>
<string>A Minecraft mod wants to access your microphone.</string> <dict>
<key>CFBundleTypeName</key>
<string>Modrinth type</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.modrinth.theseus-type</string>
</array>
<key>NSDocumentClass</key>
<string>NSDocument</string>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Modrinth File</string>
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>com.modrinth.theseus-type</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mrpack</string>
</array>
<key>public.mime-type</key>
<array>
<string>application/x-mrpack</string>
</array>
</dict>
</dict>
</array>
<key>NSCameraUsageDescription</key>
<string>A Minecraft mod wants to access your camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod wants to access your microphone.</string>
</dict> </dict>
</plist> </plist>
+1 -1
View File
@@ -15,7 +15,7 @@ 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
+2 -237
View File
@@ -1,239 +1,4 @@
use tauri_build::{DefaultPermissionRule, InlinedPlugin};
fn main() { fn main() {
// Sadly, there is no better way to do it right now // Build the Tauri app
// You could try parsing source code here and detecting #[tauri::command] tauri_build::build();
// But I think it's not worth it
// https://github.com/tauri-apps/tauri/issues/10075
tauri_build::try_build(
tauri_build::Attributes::new()
.codegen(tauri_build::CodegenContext::new())
.plugin(
"auth",
InlinedPlugin::new()
.commands(&[
"login",
"remove_user",
"get_default_user",
"set_default_user",
"get_users",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"cache",
InlinedPlugin::new()
.commands(&[
"get_project",
"get_project_many",
"get_version",
"get_version_many",
"get_user",
"get_user_many",
"get_team",
"get_team_many",
"get_organization",
"get_organization_many",
"get_search_results",
"get_search_results_many",
"purge_cache_types",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"import",
InlinedPlugin::new()
.commands(&[
"get_importable_instances",
"import_instance",
"is_valid_importable_instance",
"get_default_launcher_path",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"jre",
InlinedPlugin::new()
.commands(&[
"get_java_versions",
"set_java_version",
"jre_find_filtered_jres",
"jre_get_jre",
"jre_test_jre",
"jre_auto_install_java",
"jre_get_max_memory",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"logs",
InlinedPlugin::new()
.commands(&[
"logs_get_logs",
"logs_get_logs_by_filename",
"logs_get_output_by_filename",
"logs_delete_logs",
"logs_delete_logs_by_filename",
"logs_get_latest_log_cursor",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"metadata",
InlinedPlugin::new()
.commands(&[
"metadata_get_game_versions",
"metadata_get_loader_versions",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"mr-auth",
InlinedPlugin::new()
.commands(&[
"login_pass",
"login_2fa",
"create_account",
"logout",
"get",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"pack",
InlinedPlugin::new()
.commands(&["pack_install", "pack_get_profile_from_pack"])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"process",
InlinedPlugin::new()
.commands(&[
"process_get_all",
"process_get_by_profile_path",
"process_kill",
"process_wait_for",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"profile",
InlinedPlugin::new()
.commands(&[
"profile_remove",
"profile_get",
"profile_get_many",
"profile_get_projects",
"profile_get_optimal_jre_key",
"profile_get_full_path",
"profile_get_mod_full_path",
"profile_list",
"profile_check_installed",
"profile_install",
"profile_update_all",
"profile_update_project",
"profile_add_project_from_version",
"profile_add_project_from_path",
"profile_toggle_disable_project",
"profile_remove_project",
"profile_update_managed_modrinth_version",
"profile_repair_managed_modrinth",
"profile_run",
"profile_run_credentials",
"profile_kill",
"profile_edit",
"profile_edit_icon",
"profile_export_mrpack",
"profile_get_pack_export_candidates",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"profile-create",
InlinedPlugin::new()
.commands(&["profile_create", "profile_duplicate"])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"settings",
InlinedPlugin::new()
.commands(&[
"settings_get",
"settings_set",
"cancel_directory_change",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"tags",
InlinedPlugin::new()
.commands(&[
"tags_get_categories",
"tags_get_report_types",
"tags_get_loaders",
"tags_get_game_versions",
"tags_get_donation_platforms",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"utils",
InlinedPlugin::new()
.commands(&[
"get_os",
"should_disable_mouseover",
"highlight_in_folder",
"open_path",
"show_launcher_logs_folder",
"progress_bars_list",
"get_opening_command",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"ads",
InlinedPlugin::new()
.commands(&[
"init_ads_window",
"hide_ads_window",
"scroll_ads_window",
"show_ads_window",
"record_ads_click",
"open_link",
"get_ads_personalization",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
),
)
.expect("Failed to run tauri-build");
} }
-14
View File
@@ -1,14 +0,0 @@
{
"identifier": "ads",
"description": "",
"local": false,
"remote": {
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
},
"webviews": [
"ads-window"
],
"permissions": [
"ads:default"
]
}
-30
View File
@@ -1,30 +0,0 @@
{
"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"
]
}
-41
View File
@@ -1,41 +0,0 @@
{
"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"
]
}
-11
View File
@@ -1,11 +0,0 @@
{
"identifier": "updater",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"updater:default"
]
}
-1
View File
@@ -1 +0,0 @@
{"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","allow-record-ads-click","allow-open-link","allow-get-ads-personalization"]},"permissions":{"allow-get-ads-personalization":{"identifier":"allow-get-ads-personalization","description":"Enables the get_ads_personalization command without any pre-configured scope.","commands":{"allow":["get_ads_personalization"],"deny":[]}},"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-open-link":{"identifier":"allow-open-link","description":"Enables the open_link command without any pre-configured scope.","commands":{"allow":["open_link"],"deny":[]}},"allow-record-ads-click":{"identifier":"allow-record-ads-click","description":"Enables the record_ads_click command without any pre-configured scope.","commands":{"allow":["record_ads_click"],"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-get-ads-personalization":{"identifier":"deny-get-ads-personalization","description":"Denies the get_ads_personalization command without any pre-configured scope.","commands":{"allow":[],"deny":["get_ads_personalization"]}},"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-open-link":{"identifier":"deny-open-link","description":"Denies the open_link command without any pre-configured scope.","commands":{"allow":[],"deny":["open_link"]}},"deny-record-ads-click":{"identifier":"deny-record-ads-click","description":"Denies the record_ads_click command without any pre-configured scope.","commands":{"allow":[],"deny":["record_ads_click"]}},"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.","commLine truncated
-1
View File
@@ -1 +0,0 @@
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["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"]}}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+346
View File
@@ -0,0 +1,346 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<Property Id="WixShellExecTarget" Value="[!Path]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<!-- THESEUS -->
<ProgId Id="theseus.mrpack.Document" Description="Modrinth File">
<Extension Id="mrpack" ContentType="application/mrpack">
<!-- no flags on argument, so we can hijack deep link library-->
<Verb Id="open" Command="Open" TargetFile="Path" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
<!-- /THESEUS -->
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
<!-- THESEUS -->
<Component Id="FileTypeAssociationsReg" Guid="*">
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationDescription" Value="theseus" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationIcon" Value="[INSTALLDIR]theseus,0" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationName" Value="theseus" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\DefaultIcon" Value="[INSTALLDIR]theseus,1" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\FileAssociations" Name=".mrpack" Value="theseus.mrpack.Document" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\MIMEAssociations" Name="application/mrpack" Value="theseus.mrpack.Document" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\shell\Open\command" Value="&quot;[INSTALLDIR]theseus&quot; -e &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\RegisteredApplications" Name="theseus" Value="SOFTWARE\modrinth\theseus\Capabilities" Type="string" KeyPath="yes" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\theseus.mrpack.Document" Name="MRPACK File" Value="Modrinth Modpack Installer" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack" Name="Content Type" Value="application/mrpack" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithList\theseus" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithProgids" Name="theseus.mrpack.Document" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\SupportedTypes" Name=".mrpack" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\shell\open" Name="FriendlyAppName" Value="theseus" Type="string" />
</Component>
<!-- /THESEUS -->
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
<!-- THESEUS -->
<ComponentRef Id="FileTypeAssociationsReg" />
<!-- /THESEUS -->
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>
-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
+3 -4
View File
@@ -2,17 +2,16 @@
"name": "@modrinth/app", "name": "@modrinth/app",
"scripts": { "scripts": {
"build": "tauri build", "build": "tauri build",
"tauri": "tauri",
"dev": "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": { "devDependencies": {
"@tauri-apps/cli": "2.0.0-rc.16" "@tauri-apps/cli": "^1.6.0"
}, },
"dependencies": { "dependencies": {
"@modrinth/app-frontend": "workspace:*", "@modrinth/app-lib": "workspace:*",
"@modrinth/app-lib": "workspace:*" "@modrinth/app-frontend": "workspace:*"
} }
} }
-23
View File
@@ -1,23 +0,0 @@
document.addEventListener(
'click',
function (e) {
window.top.postMessage({ modrinthAdClick: true }, 'https://modrinth.com')
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
}
},
true,
)
window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
}
-216
View File
@@ -1,216 +0,0 @@
use serde::Serialize;
use std::collections::HashSet;
use std::time::{Duration, Instant};
use tauri::plugin::TauriPlugin;
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
use tauri_plugin_shell::ShellExt;
use theseus::settings;
use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub size: Option<LogicalSize<f32>>,
pub position: Option<LogicalPosition<f32>>,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
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,
last_click: None,
malicious_origins: HashSet::new(),
}));
// 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(AD_LINK.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,
record_ads_click,
open_link,
get_ads_personalization,
])
.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(
AD_LINK.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(())
}
#[tauri::command]
pub async fn record_ads_click<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.last_click = Some(Instant::now());
Ok(())
}
#[tauri::command]
pub async fn open_link<R: Runtime>(
app: tauri::AppHandle<R>,
path: String,
origin: String,
) -> crate::api::Result<()> {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if url::Url::parse(&path).is_ok()
&& !state.malicious_origins.contains(&origin)
{
if let Some(last_click) = state.last_click {
if last_click.elapsed() < Duration::from_millis(100) {
let _ = app.shell().open(&path, None);
state.last_click = None;
return Ok(());
}
}
}
tracing::info!("Malicious click: {path} origin {origin}");
state.malicious_origins.insert(origin);
Ok(())
}
#[tauri::command]
pub async fn get_ads_personalization() -> crate::api::Result<bool> {
let res = settings::get().await?;
Ok(res.personalized_ads)
}
+18 -20
View File
@@ -1,17 +1,16 @@
use crate::api::Result; use crate::api::Result;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin; use tauri::plugin::TauriPlugin;
use tauri::{Manager, Runtime, UserAttentionType}; use tauri::{Manager, UserAttentionType};
use theseus::prelude::*; use theseus::prelude::*;
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("auth") tauri::plugin::Builder::new("auth")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
login, auth_get_default_user,
remove_user, auth_set_default_user,
get_default_user, auth_remove_user,
set_default_user, auth_users,
get_users,
]) ])
.build() .build()
} }
@@ -19,21 +18,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
/// Authenticate a user with Hydra - part 1 /// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at) /// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
#[tauri::command] #[tauri::command]
pub async fn login<R: Runtime>( pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
app: tauri::AppHandle<R>,
) -> Result<Option<Credentials>> {
let flow = minecraft_auth::begin_login().await?; let flow = minecraft_auth::begin_login().await?;
let start = Utc::now(); let start = Utc::now();
if let Some(window) = app.get_webview_window("signin") { if let Some(window) = app.get_window("signin") {
window.close()?; window.close()?;
} }
let window = tauri::WebviewWindowBuilder::new( let window = tauri::WindowBuilder::new(
&app, &app,
"signin", "signin",
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err( tauri::WindowUrl::External(flow.redirect_uri.parse().map_err(
|_| { |_| {
theseus::ErrorKind::OtherError( theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(), "Error parsing auth redirect URL".to_string(),
@@ -56,12 +53,12 @@ pub async fn login<R: Runtime>(
} }
if window if window
.url()? .url()
.as_str() .as_str()
.starts_with("https://login.live.com/oauth20_desktop.srf") .starts_with("https://login.live.com/oauth20_desktop.srf")
{ {
if let Some((_, code)) = if let Some((_, code)) =
window.url()?.query_pairs().find(|x| x.0 == "code") window.url().query_pairs().find(|x| x.0 == "code")
{ {
window.close()?; window.close()?;
let val = let val =
@@ -78,22 +75,23 @@ pub async fn login<R: Runtime>(
Ok(None) Ok(None)
} }
#[tauri::command] #[tauri::command]
pub async fn remove_user(user: uuid::Uuid) -> Result<()> { pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::remove_user(user).await?) Ok(minecraft_auth::remove_user(user).await?)
} }
#[tauri::command] #[tauri::command]
pub async fn get_default_user() -> Result<Option<uuid::Uuid>> { pub async fn auth_get_default_user() -> Result<Option<uuid::Uuid>> {
Ok(minecraft_auth::get_default_user().await?) Ok(minecraft_auth::get_default_user().await?)
} }
#[tauri::command] #[tauri::command]
pub async fn set_default_user(user: uuid::Uuid) -> Result<()> { pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::set_default_user(user).await?) Ok(minecraft_auth::set_default_user(user).await?)
} }
/// Get a copy of the list of all user credentials /// Get a copy of the list of all user credentials
// invoke('plugin:auth|auth_users',user)
#[tauri::command] #[tauri::command]
pub async fn get_users() -> Result<Vec<Credentials>> { pub async fn auth_users() -> Result<Vec<Credentials>> {
Ok(minecraft_auth::users().await?) Ok(minecraft_auth::users().await?)
} }
+8 -8
View File
@@ -8,10 +8,10 @@ use theseus::pack::import;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("import") tauri::plugin::Builder::new("import")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
get_importable_instances, import_get_importable_instances,
import_instance, import_import_instance,
is_valid_importable_instance, import_is_valid_importable_instance,
get_default_launcher_path, import_get_default_launcher_path,
]) ])
.build() .build()
} }
@@ -20,7 +20,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
/// eg: get_importable_instances(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC")) /// eg: get_importable_instances(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"))
/// returns ["Instance 1", "Instance 2"] /// returns ["Instance 1", "Instance 2"]
#[tauri::command] #[tauri::command]
pub async fn get_importable_instances( pub async fn import_get_importable_instances(
launcher_type: ImportLauncherType, launcher_type: ImportLauncherType,
base_path: PathBuf, base_path: PathBuf,
) -> Result<Vec<String>> { ) -> Result<Vec<String>> {
@@ -31,7 +31,7 @@ pub async fn get_importable_instances(
/// profile_path should be a blank profile for this purpose- if the function fails, it will be deleted /// profile_path should be a blank profile for this purpose- if the function fails, it will be deleted
/// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1") /// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
#[tauri::command] #[tauri::command]
pub async fn import_instance( pub async fn import_import_instance(
profile_path: &str, profile_path: &str,
launcher_type: ImportLauncherType, launcher_type: ImportLauncherType,
base_path: PathBuf, base_path: PathBuf,
@@ -50,7 +50,7 @@ pub async fn import_instance(
/// Checks if this instance is valid for importing, given a certain launcher type /// Checks if this instance is valid for importing, given a certain launcher type
/// eg: is_valid_importable_instance(PathBuf::from("C:/MultiMC/Instance 1"), ImportLauncherType::MultiMC) /// eg: is_valid_importable_instance(PathBuf::from("C:/MultiMC/Instance 1"), ImportLauncherType::MultiMC)
#[tauri::command] #[tauri::command]
pub async fn is_valid_importable_instance( pub async fn import_is_valid_importable_instance(
instance_folder: PathBuf, instance_folder: PathBuf,
launcher_type: ImportLauncherType, launcher_type: ImportLauncherType,
) -> Result<bool> { ) -> Result<bool> {
@@ -63,7 +63,7 @@ pub async fn is_valid_importable_instance(
/// Returns the default path for the given launcher type /// Returns the default path for the given launcher type
/// None if it can't be found or doesn't exist /// None if it can't be found or doesn't exist
#[tauri::command] #[tauri::command]
pub async fn get_default_launcher_path( pub async fn import_get_default_launcher_path(
launcher_type: ImportLauncherType, launcher_type: ImportLauncherType,
) -> Result<Option<PathBuf>> { ) -> Result<Option<PathBuf>> {
Ok(import::get_default_launcher_path(launcher_type)) Ok(import::get_default_launcher_path(launcher_type))
+6 -7
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>;
@@ -41,9 +40,9 @@ pub enum TheseusSerializableError {
#[error("Tauri error: {0}")] #[error("Tauri error: {0}")]
Tauri(#[from] tauri::Error), Tauri(#[from] tauri::Error),
#[cfg(feature = "updater")] #[cfg(target_os = "macos")]
#[error("Tauri updater error: {0}")] #[error("Callback error: {0}")]
TauriUpdater(#[from] tauri_plugin_updater::Error), Callback(String),
} }
// Generic implementation of From<T> for ErrorTypeA // Generic implementation of From<T> for ErrorTypeA
@@ -91,15 +90,15 @@ macro_rules! impl_serialize {
} }
// Use the macro to implement Serialize for TheseusSerializableError // Use the macro to implement Serialize for TheseusSerializableError
#[cfg(not(feature = "updater"))] #[cfg(target_os = "macos")]
impl_serialize! { impl_serialize! {
IO, IO,
Tauri, Tauri,
Callback
} }
#[cfg(feature = "updater")] #[cfg(not(target_os = "macos"))]
impl_serialize! { impl_serialize! {
IO, IO,
Tauri, Tauri,
TauriUpdater,
} }
+6 -6
View File
@@ -5,7 +5,7 @@ use tauri::{Manager, UserAttentionType};
use theseus::prelude::*; use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("mr-auth") tauri::plugin::Builder::new("mr_auth")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
login_pass, login_pass,
login_2fa, login_2fa,
@@ -25,14 +25,14 @@ pub async fn modrinth_auth_login(
let start = Utc::now(); let start = Utc::now();
if let Some(window) = app.get_webview_window("modrinth-signin") { if let Some(window) = app.get_window("modrinth-signin") {
window.close()?; window.close()?;
} }
let window = tauri::WebviewWindowBuilder::new( let window = tauri::WindowBuilder::new(
&app, &app,
"modrinth-signin", "modrinth-signin",
tauri::WebviewUrl::External(redirect_uri.parse().map_err(|_| { tauri::WindowUrl::External(redirect_uri.parse().map_err(|_| {
theseus::ErrorKind::OtherError( theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(), "Error parsing auth redirect URL".to_string(),
) )
@@ -53,12 +53,12 @@ pub async fn modrinth_auth_login(
} }
if window if window
.url()? .url()
.as_str() .as_str()
.starts_with("https://launcher-files.modrinth.com/detect.txt") .starts_with("https://launcher-files.modrinth.com/detect.txt")
{ {
let query = window let query = window
.url()? .url()
.query_pairs() .query_pairs()
.map(|(key, val)| { .map(|(key, val)| {
( (
+3 -3
View File
@@ -2,7 +2,7 @@ use crate::api::Result;
use theseus::prelude::*; use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("profile-create") tauri::plugin::Builder::new("profile_create")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
profile_create, profile_create,
profile_duplicate profile_duplicate
@@ -11,7 +11,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
} }
// Creates a profile at the given filepath and adds it to the in-memory state // Creates a profile at the given filepath and adds it to the in-memory state
// invoke('plugin:profile-create|profile_add',profile) // invoke('plugin:profile_create|profile_add',profile)
#[tauri::command] #[tauri::command]
pub async fn profile_create( pub async fn profile_create(
name: String, // the name of the profile, and relative path name: String, // the name of the profile, and relative path
@@ -35,7 +35,7 @@ pub async fn profile_create(
} }
// Creates a profile from a duplicate // Creates a profile from a duplicate
// invoke('plugin:profile-create|profile_duplicate',profile) // invoke('plugin:profile_create|profile_duplicate',profile)
#[tauri::command] #[tauri::command]
pub async fn profile_duplicate(path: &str) -> Result<String> { pub async fn profile_duplicate(path: &str) -> Result<String> {
let res = profile::create::profile_create_from_duplicate(path).await?; let res = profile::create::profile_create_from_duplicate(path).await?;
-1
View File
@@ -137,6 +137,5 @@ pub async fn get_opening_command() -> Result<Option<CommandPayload>> {
// helper function called when redirected by a weblink (ie: modrith://do-something) or when redirected by a .mrpack file (in which case its a filepath) // helper function called when redirected by a weblink (ie: modrith://do-something) or when redirected by a .mrpack file (in which case its a filepath)
// We hijack the deep link library (which also contains functionality for instance-checking) // We hijack the deep link library (which also contains functionality for instance-checking)
pub async fn handle_command(command: String) -> Result<()> { pub async fn handle_command(command: String) -> Result<()> {
tracing::info!("handle command: {command}");
Ok(theseus::handler::parse_and_emit_command(&command).await?) Ok(theseus::handler::parse_and_emit_command(&command).await?)
} }
-22
View File
@@ -1,28 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use tauri::{Manager, Runtime};
use tokio::sync::Mutex; use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct InitialPayload { pub struct InitialPayload {
pub payload: Arc<Mutex<Option<String>>>, pub payload: Arc<Mutex<Option<String>>>,
} }
pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
manager: &M,
) -> InitialPayload {
let initial_payload = manager.try_state::<InitialPayload>();
let mtx = if let Some(initial_payload) = initial_payload {
initial_payload.inner().clone()
} else {
tracing::info!("No initial payload found, creating new");
let payload = InitialPayload {
payload: Arc::new(Mutex::new(None)),
};
manager.manage(payload.clone());
payload
};
mtx
}
+98
View File
@@ -0,0 +1,98 @@
use cocoa::{
base::{id, nil},
foundation::NSAutoreleasePool,
};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use once_cell::sync::OnceCell;
use crate::api::TheseusSerializableError;
type Callback = OnceCell<Box<dyn Fn(String) + Send + Sync + 'static>>;
static CALLBACK: Callback = OnceCell::new();
pub struct AppDelegateClass(pub *const Class);
unsafe impl Send for AppDelegateClass {}
unsafe impl Sync for AppDelegateClass {}
// Obj C class for the app delegate
// This inherits from the TaoAppDelegate (used by tauri) so we do not accidentally override any functionality
// The application_open_file method is the only method we override, as it is currently unimplemented in tauri
lazy_static::lazy_static! {
pub static ref THESEUS_APP_DELEGATE_CLASS: AppDelegateClass = unsafe {
let superclass = class!(TaoAppDelegate);
let mut decl = ClassDecl::new("TheseusAppDelegate", superclass).unwrap();
// Add the method to the class
decl.add_method(
sel!(application:openFile:),
application_open_file as extern "C" fn(&Object, Sel, id, id) -> bool,
);
// Other methods are inherited
AppDelegateClass(decl.register())
};
}
extern "C" fn application_open_file(
_: &Object,
_: Sel,
_: id,
file: id,
) -> bool {
let file = nsstring_to_string(file);
callback(file)
}
pub fn callback(file: String) -> bool {
if let Some(callback) = CALLBACK.get() {
callback(file);
true
} else {
false
}
}
pub fn register_open_file<T>(
callback: T,
) -> Result<(), TheseusSerializableError>
where
T: Fn(String) + Send + Sync + 'static,
{
unsafe {
// Modified from tao: https://github.com/tauri-apps/tao
// sets the current app delegate to be the inherited app delegate rather than the default tauri/tao one
let app: id = msg_send![class!(TaoApp), sharedApplication];
let delegate: id = msg_send![THESEUS_APP_DELEGATE_CLASS.0, new];
let pool = NSAutoreleasePool::new(nil);
let _: () = msg_send![app, setDelegate: delegate];
let _: () = msg_send![pool, drain];
}
CALLBACK.set(Box::new(callback)).map_err(|_| {
TheseusSerializableError::Callback("Callback already set".to_string())
})
}
/// Convert an NSString to a Rust `String`
/// From 'fruitbasket' https://github.com/mrmekon/fruitbasket/
#[allow(clippy::cmp_null)]
pub fn nsstring_to_string(nsstring: *mut Object) -> String {
unsafe {
let cstr: *const i8 = msg_send![nsstring, UTF8String];
if cstr != std::ptr::null() {
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
} else {
"".into()
}
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod deep_link; pub mod deep_link;
pub mod delegate;
pub mod window_ext; pub mod window_ext;
+58 -400
View File
@@ -1,412 +1,70 @@
// Stolen from https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9 /// from: https://github.com/tauri-apps/tauri/issues/4789, full credit to haasal
use objc::{msg_send, sel, sel_impl}; #[cfg(target_os = "macos")]
use rand::{distributions::Alphanumeric, Rng}; use tauri::{Runtime, Window};
use tauri::{
plugin::{Builder, TauriPlugin},
Emitter, Runtime, Window,
}; // 0.8
const WINDOW_CONTROL_PAD_X: f64 = 9.0; #[cfg(target_os = "macos")]
const WINDOW_CONTROL_PAD_Y: f64 = 16.0; pub trait WindowExt {
fn set_transparent_titlebar(&self, transparent: bool);
struct UnsafeWindowHandle(*mut std::ffi::c_void); fn position_traffic_lights(&self, x: f64, y: f64);
unsafe impl Send for UnsafeWindowHandle {}
unsafe impl Sync for UnsafeWindowHandle {}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("traffic_light_positioner")
.on_window_ready(|window| {
#[cfg(target_os = "macos")]
setup_traffic_light_positioner(window);
})
.build()
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn position_traffic_lights( impl<R: Runtime> WindowExt for Window<R> {
ns_window_handle: UnsafeWindowHandle, fn set_transparent_titlebar(&self, transparent: bool) {
x: f64, use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};
y: f64, let window = self.ns_window().unwrap() as cocoa::base::id;
) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let ns_window = ns_window_handle.0 as cocoa::base::id;
unsafe {
let close = ns_window
.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = ns_window
.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom =
ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview(); unsafe {
window.setTitleVisibility_(
NSWindowTitleVisibility::NSWindowTitleHidden,
);
let close_rect: NSRect = msg_send![close, frame]; if transparent {
let button_height = close_rect.size.height; window.setTitlebarAppearsTransparent_(cocoa::base::YES);
} else {
window.setTitlebarAppearsTransparent_(cocoa::base::NO);
}
}
}
let title_bar_frame_height = button_height + y; fn position_traffic_lights(&self, x: f64, y: f64) {
let mut title_bar_rect = NSView::frame(title_bar_container_view); use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
title_bar_rect.size.height = title_bar_frame_height; use cocoa::foundation::NSRect;
title_bar_rect.origin.y = use objc::{msg_send, sel, sel_impl};
NSView::frame(ns_window).size.height - title_bar_frame_height;
let _: () =
msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom]; let window = self.ns_window().unwrap() as cocoa::base::id;
let space_between =
NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() { unsafe {
let mut rect: NSRect = NSView::frame(button); let close = window
rect.origin.x = x + (i as f64 * space_between); .standardWindowButton_(NSWindowButton::NSWindowCloseButton);
button.setFrameOrigin(rect.origin); let miniaturize = window.standardWindowButton_(
NSWindowButton::NSWindowMiniaturizeButton,
);
let zoom = window
.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y =
NSView::frame(window).size.height - title_bar_frame_height;
let _: () =
msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x
- NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
} }
} }
} }
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct WindowState<R: Runtime> {
window: Window<R>,
}
#[cfg(target_os = "macos")]
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
use cocoa::appkit::NSWindow;
use cocoa::base::{id, BOOL};
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use std::ffi::c_void;
// Do the initial positioning
position_traffic_lights(
UnsafeWindowHandle(
window.ns_window().expect("Failed to create window handle"),
),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
// Ensure they stay in place while resizing the window.
fn with_window_state<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>(
this: &Object,
func: F,
) {
let ptr = unsafe {
let x: *mut c_void = *this.get_ivar("app_box");
&mut *(x as *mut WindowState<R>)
};
func(ptr);
}
unsafe {
let ns_win = window
.ns_window()
.expect("NS Window should exist to mount traffic light delegate.")
as id;
let current_delegate: id = ns_win.delegate();
extern "C" fn on_window_should_close(
this: &Object,
_cmd: Sel,
sender: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, windowShouldClose: sender]
}
}
extern "C" fn on_window_will_close(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillClose: notification];
}
}
extern "C" fn on_window_did_resize<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
let id = state.window.ns_window().expect(
"NS window should exist on state to handle resize",
) as id;
#[cfg(target_os = "macos")]
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResize: notification];
}
}
extern "C" fn on_window_did_move(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidMove: notification];
}
}
extern "C" fn on_window_did_change_backing_properties(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
}
}
extern "C" fn on_window_did_become_key(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, windowDidBecomeKey: notification];
}
}
extern "C" fn on_window_did_resign_key(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, windowDidResignKey: notification];
}
}
extern "C" fn on_dragging_entered(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, draggingEntered: notification]
}
}
extern "C" fn on_prepare_for_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, prepareForDragOperation: notification]
}
}
extern "C" fn on_perform_drag_operation(
this: &Object,
_cmd: Sel,
sender: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, performDragOperation: sender]
}
}
extern "C" fn on_conclude_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, concludeDragOperation: notification];
}
}
extern "C" fn on_dragging_exited(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, draggingExited: notification];
}
}
extern "C" fn on_window_will_use_full_screen_presentation_options(
this: &Object,
_cmd: Sel,
window: id,
proposed_options: NSUInteger,
) -> NSUInteger {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
}
}
extern "C" fn on_window_did_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
}
}
extern "C" fn on_window_will_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
}
}
extern "C" fn on_window_did_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-exit-fullscreen", ())
.expect("Failed to emit event");
let id =
state.window.ns_window().expect("Failed to emit event")
as id;
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, windowDidExitFullScreen: notification];
}
}
extern "C" fn on_window_will_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-exit-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
}
}
extern "C" fn on_window_did_fail_to_enter_full_screen(
this: &Object,
_cmd: Sel,
window: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
}
}
extern "C" fn on_effective_appearance_did_change(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
}
}
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![
super_del,
effectiveAppearanceDidChangedOnMainThread: notification
];
}
}
// Are we deallocing this properly ? (I miss safe Rust :( )
let window_label = window.label().to_string();
let app_state = WindowState { window };
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
let random_str: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(20)
.map(char::from)
.collect();
// We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate
// delegate with the same name.
let delegate_name =
format!("windowDelegate_{}_{}", window_label, random_str);
ns_win.setDelegate_(delegate!(&delegate_name, {
window: id = ns_win,
app_box: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
}))
}
}
+117 -195
View File
@@ -4,7 +4,8 @@
)] )]
use native_dialog::{MessageDialog, MessageType}; use native_dialog::{MessageDialog, MessageType};
use tauri::{Listener, Manager}; use tauri::{Manager, PhysicalSize};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use theseus::prelude::*; use theseus::prelude::*;
mod api; mod api;
@@ -13,86 +14,16 @@ mod error;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod macos; mod macos;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
// Should be called in launcher initialization // Should be called in launcher initialization
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[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(())
} }
@@ -114,7 +45,16 @@ fn show_window(app: tauri::AppHandle) {
.unwrap(); .unwrap();
panic!("cannot display application window") panic!("cannot display application window")
} else { } else {
let _ = win.restore_state(StateFlags::all());
let _ = win.set_focus(); let _ = win.set_focus();
// fix issue where window shows as extremely small
if let Ok(size) = win.inner_size() {
let width = if size.width < 1100 { 1280 } else { size.width };
let height = if size.height < 700 { 800 } else { size.height };
let _ = win.set_size(PhysicalSize::new(width, height));
}
} }
} }
@@ -135,14 +75,17 @@ async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
Ok(()) Ok(())
} }
#[tauri::command] #[derive(Clone, serde::Serialize)]
fn restart_app(app: tauri::AppHandle) { struct Payload {
app.restart(); args: Vec<String>,
cwd: String,
} }
// 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() {
tauri_plugin_deep_link::prepare("ModrinthApp");
/* /*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE ERROR > WARN > INFO > DEBUG > TRACE
@@ -162,74 +105,73 @@ fn main() {
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
#[cfg(feature = "updater")]
{
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
}
builder = builder builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
if let Some(payload) = args.get(1) { app.emit_all("single-instance", Payload { args: argv, cwd })
tracing::info!("Handling deep link from arg {payload}"); .unwrap();
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_window_state::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_filename("app-window-state.json")
.build(),
)
.setup(|app| { .setup(|app| {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ let res = {
let payload = macos::deep_link::get_or_init_payload(app); use macos::deep_link::InitialPayload;
let mtx = std::sync::Arc::new(tokio::sync::Mutex::new(None));
let mtx_copy = payload.payload.clone(); app.manage(InitialPayload {
app.listen("deep-link://new-url", move |url| { payload: mtx.clone(),
let mtx_copy_copy = mtx_copy.clone(); });
let request = url.payload().to_owned();
let actual_request = let mtx_copy = mtx.clone();
serde_json::from_str::<Vec<String>>(&request) macos::delegate::register_open_file(move |filename| {
.ok() let mtx_copy = mtx_copy.clone();
.map(|mut x| x.remove(0))
.unwrap_or(request);
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
tracing::info!("Handling deep link {actual_request}"); tracing::info!("Handling file open {filename}");
let mut payload = mtx_copy_copy.lock().await; let mut payload = mtx_copy.lock().await;
if payload.is_none() { if payload.is_none() {
*payload = Some(actual_request.clone()); *payload = Some(filename.clone());
} }
let _ = let _ = api::utils::handle_command(filename).await;
api::utils::handle_command(actual_request).await;
}); });
}); })
.unwrap();
let mtx_copy = mtx.clone();
tauri_plugin_deep_link::register(
"modrinth",
move |request: String| {
let mtx_copy = mtx_copy.clone();
tauri::async_runtime::spawn(async move {
tracing::info!("Handling deep link {request}");
let mut payload = mtx_copy.lock().await;
if payload.is_none() {
*payload = Some(request.clone());
}
let _ = api::utils::handle_command(request).await;
});
},
)
}; };
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
app.listen("deep-link://new-url", |url| { let res = tauri_plugin_deep_link::register(
let payload = url.payload().to_owned(); "modrinth",
tracing::info!("Handling deep link {payload}"); |request: String| {
tauri::async_runtime::spawn(api::utils::handle_command( tracing::info!("Handling deep link {request}");
payload, tauri::async_runtime::spawn(api::utils::handle_command(
)); request,
dbg!(url); ));
}); },
);
if let Err(e) = res {
tracing::error!("Error registering deep link handler: {}", e);
}
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_window("main") {
// Hide window to prevent white flash on startup // Hide window to prevent white flash on startup
@@ -237,14 +179,34 @@ fn main() {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
window.set_shadow(true).unwrap(); use window_shadows::set_shadow;
set_shadow(&window, true).unwrap();
}
#[cfg(target_os = "macos")]
{
use macos::window_ext::WindowExt;
window.set_transparent_titlebar(true);
window.position_traffic_lights(9.0, 16.0);
} }
} }
Ok(()) Ok(())
}); });
builder = builder #[cfg(target_os = "macos")]
{
use tauri::WindowEvent;
builder = builder.on_window_event(|e| {
use macos::window_ext::WindowExt;
if let WindowEvent::Resized(..) = e.event() {
let win = e.window();
win.position_traffic_lights(9.0, 16.0);
}
})
}
let builder = builder
.plugin(api::auth::init()) .plugin(api::auth::init())
.plugin(api::mr_auth::init()) .plugin(api::mr_auth::init())
.plugin(api::import::init()) .plugin(api::import::init())
@@ -259,83 +221,43 @@ 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::auth::auth_login,
api::mr_auth::modrinth_auth_login, api::mr_auth::modrinth_auth_login,
show_window, show_window,
restart_app,
]); ]);
#[cfg(target_os = "macos")] if let Err(e) = builder.run(tauri::generate_context!()) {
{ #[cfg(target_os = "windows")]
builder = builder.plugin(macos::window_ext::init()); {
} // tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
if format!("{:?}", e)
let app = builder.build(tauri::generate_context!()); .contains("Runtime(CreateWebview(WebView2Error(WindowsError")
match app {
Ok(app) => {
#[allow(unused_variables)]
app.run(|app, event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
tracing::info!("Handling webview open {urls:?}");
let file = urls
.into_iter()
.filter_map(|url| url.to_file_path().ok())
.next();
if let Some(file) = file {
let payload =
macos::deep_link::get_or_init_payload(app);
let mtx_copy = payload.payload.clone();
let request = file.to_string_lossy().to_string();
tauri::async_runtime::spawn(async move {
let mut payload = mtx_copy.lock().await;
if payload.is_none() {
*payload = Some(request.clone());
}
let _ = api::utils::handle_command(request).await;
});
}
}
});
}
Err(e) => {
#[cfg(target_os = "windows")]
{ {
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution MessageDialog::new()
if format!("{:?}", e).contains( .set_type(MessageType::Error)
"Runtime(CreateWebview(WebView2Error(WindowsError", .set_title("Initialization error")
) { .set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://docs.modrinth.com/faq/app/webview2")
MessageDialog::new() .show_alert()
.set_type(MessageType::Error) .unwrap();
.set_title("Initialization error")
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://docs.modrinth.com/faq/app/webview2")
.show_alert()
.unwrap();
panic!("webview2 initialization failed") panic!("webview2 initialization failed")
}
} }
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text(&format!(
"Cannot initialize application due to an error:\n{:?}",
e
))
.show_alert()
.unwrap();
panic!("{1}: {:?}", e, "error while running tauri application")
} }
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text(&format!(
"Cannot initialize application due to an error:\n{:?}",
e
))
.show_alert()
.unwrap();
panic!("{1}: {:?}", e, "error while running tauri application")
} }
} }
Loaded 100 of 197 files, more files were not shown because too many files have changed in this diff. Show more