Merge branch 'main' into security-txt

This commit is contained in:
Erb3 2024-10-20 18:04:58 +02:00 committed by GitHub
commit 0bc5e954e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
963 changed files with 111437 additions and 5085 deletions

View File

@ -13,3 +13,6 @@ max_line_length = 100
[*.md]
max_line_length = off
trim_trailing_whitespace = false
[*.rs]
indent_size = 4

View File

@ -1,6 +1,9 @@
name: Deploy frontend
name: Clear pages cache
on: push
on:
push:
branches:
- prod
jobs:
wait:
@ -16,7 +19,6 @@ jobs:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: '9ddae624c98677d68d93df6e524a6061'
project: 'frontend'
githubToken: ${{ secrets.GITHUB_TOKEN }}
commitHash: ${{ steps.push-changes.outputs.commit-hash }}
- name: Purge cache
if: github.ref == 'refs/heads/prod'

46
.github/workflows/labrinth-docker.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: docker-build
on:
push:
branches: [ "main" ]
paths:
- .github/workflows/labrinth-docker.yml
- 'apps/labrinth/**'
pull_request:
types: [ opened, synchronize ]
paths:
- .github/workflows/labrinth-docker.yml
- 'apps/labrinth/**'
merge_group:
types: [ checks_requested ]
jobs:
docker:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./apps/labrinth
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Fetch docker metadata
id: docker_meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/modrinth/labrinth
-
name: Login to GitHub Images
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./apps/labrinth
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View File

@ -6,6 +6,7 @@ on:
tags:
- 'v*'
paths:
- .github/workflows/app-release.yml
- 'apps/app/**'
- 'apps/app-frontend/**'
- 'packages/app-lib/**'
@ -20,12 +21,12 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest, ubuntu-20.04]
platform: [macos-latest, windows-latest, ubuntu-22.04]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Rust setup (mac)
if: startsWith(matrix.platform, 'macos')
@ -43,13 +44,34 @@ jobs:
- name: Setup rust cache
uses: actions/cache@v4
with:
path: target/**
path: |
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') }}
restore-keys: |
${{ runner.os }}-rust-target-
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
@ -66,7 +88,7 @@ jobs:
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
@ -77,14 +99,13 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libselinux1
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
- name: Install frontend dependencies
run: pnpm install
- name: build app (macos)
uses: tauri-apps/tauri-action@v0
id: build_os_mac
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config "tauri-release.conf.json"
if: startsWith(matrix.platform, 'macos')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -95,34 +116,40 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: "--target universal-apple-darwin --config ./apps/app/tauri-release.conf.json"
working-directory: ./apps/app
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: build app
uses: tauri-apps/tauri-action@v0
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
id: build_os
if: "!startsWith(matrix.platform, 'macos')"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: "--config ./apps/app/tauri-release.conf.json"
working-directory: ./apps/app
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v3
if: startsWith(matrix.platform, 'macos')
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}
path: "${{ join(fromJSON(steps.build_os_mac.outputs.artifactPaths), '\n') }}"
path: |
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
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v3
if: "!startsWith(matrix.platform, 'macos')"
with:
name: ${{ matrix.platform }}
path: "${{ join(fromJSON(steps.build_os.outputs.artifactPaths), '\n') }}"
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

View File

@ -11,7 +11,7 @@ on:
jobs:
build:
name: Build, Test, and Lint
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Check out code
@ -30,7 +30,7 @@ jobs:
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libselinux1
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Setup Node.JS environment
uses: actions/setup-node@v4
@ -62,9 +62,19 @@ jobs:
- name: Build
run: pnpm build
env:
SQLX_OFFLINE: true
- name: Lint
run: pnpm lint
env:
SQLX_OFFLINE: true
- name: Start docker compose
run: docker compose up -d
- name: Test
run: pnpm test
env:
SQLX_OFFLINE: true
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres

2
.gitignore vendored
View File

@ -55,3 +55,5 @@ generated
# app testing dir
app-playground-data/*
.astro

5776
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,8 @@ resolver = '2'
members = [
'./packages/app-lib',
'./apps/app-playground',
'./apps/app'
'./apps/app',
'./apps/labrinth'
]
# Optimize for speed and reduce size on release builds
@ -16,3 +17,6 @@ strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros]
opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev = "27fb16b" }

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ['custom/vue'],
}

View File

@ -0,0 +1,22 @@
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',
},
},
])

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modrinth App</title>
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />

View File

@ -1,11 +1,12 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.8.3-1",
"version": "0.8.9",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vue-tsc --noEmit && vite build",
"tsc:check": "vue-tsc --noEmit",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write ."
},
@ -13,14 +14,19 @@
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@tauri-apps/api": "^1.6.0",
"@sentry/vue": "^8.27.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",
"dayjs": "^1.11.10",
"floating-vue": "^5.2.2",
"mixpanel-browser": "^2.49.0",
"ofetch": "^1.3.4",
"pinia": "^2.1.7",
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
"posthog-js": "^1.158.2",
"vite-svg-loader": "^5.1.0",
"vue": "^3.4.21",
"vue-multiselect": "3.0.0",
@ -28,17 +34,21 @@
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@tauri-apps/cli": "^1.6.0",
"@eslint/compat": "^1.1.1",
"@nuxt/eslint-config": "^0.5.6",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint": "^9.9.1",
"eslint-config-custom": "workspace:*",
"eslint-plugin-turbo": "^2.1.1",
"postcss": "^8.4.39",
"prettier": "^3.2.5",
"sass": "^1.74.1",
"tailwindcss": "^3.4.4",
"tsconfig": "workspace:*",
"vite": "^5.2.8"
"typescript": "^5.5.4",
"vite": "^5.2.8",
"vue-tsc": "^2.1.6"
},
"packageManager": "pnpm@9.4.0"
}

View File

@ -1,7 +1,15 @@
<script setup>
import { computed, ref, onMounted } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import { HomeIcon, SearchIcon, LibraryIcon, PlusIcon, SettingsIcon, XIcon } from '@modrinth/assets'
import {
HomeIcon,
SearchIcon,
LibraryIcon,
PlusIcon,
SettingsIcon,
XIcon,
DownloadIcon,
} from '@modrinth/assets'
import { Button, Notifications } from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state'
import AccountsCard from '@/components/ui/AccountsCard.vue'
@ -15,28 +23,26 @@ import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
import { type } from '@tauri-apps/api/os'
import { appWindow } from '@tauri-apps/api/window'
import { isDev, getOS } from '@/helpers/utils.js'
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 { type } from '@tauri-apps/plugin-os'
import { isDev, getOS, restartApp } from '@/helpers/utils.js'
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window'
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 { install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import { useInstall } from '@/store/install.js'
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-shell'
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()
@ -57,6 +63,12 @@ const os = ref('')
const stateInitialized = ref(false)
const criticalErrorMessage = ref()
onMounted(async () => {
await useCheckDisableMouseover()
})
async function setupApp() {
stateInitialized.value = true
const {
@ -79,21 +91,23 @@ async function setupApp() {
showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await appWindow.setDecorations(native_decorations)
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering
mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' })
initAnalytics()
if (!telemetry) {
mixpanel_opt_out_tracking()
optOutAnalytics()
}
mixpanel_track('Launched', { version, dev, onboarded })
if (dev) debugAnalytics()
trackEvent('Launched', { version, dev, onboarded })
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
if ((await type()) === 'Darwin') {
const osType = await type()
if (osType === 'macos') {
document.getElementsByTagName('html')[0].classList.add('mac')
} else {
document.getElementsByTagName('html')[0].classList.add('windows')
@ -107,7 +121,18 @@ 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)
checkUpdates()
}
const stateFailed = ref(false)
@ -127,18 +152,12 @@ initialize_state()
const handleClose = async () => {
await saveWindowState(StateFlags.ALL)
await TauriWindow.getCurrent().close()
await getCurrentWindow().close()
}
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
const router = useRouter()
router.afterEach((to, from, failure) => {
if (mixpanel_is_loaded()) {
mixpanel_track('PageView', { path: to.path, fromPath: from.path, failed: failure })
}
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
})
const route = useRoute()
const isOnBrowse = computed(() => route.path.startsWith('/browse'))
@ -178,15 +197,10 @@ document.querySelector('body').addEventListener('click', function (e) {
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
!target.classList.contains('router-link-active') &&
!target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost')
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: target.href,
},
})
open(target.href)
}
e.preventDefault()
break
@ -219,7 +233,7 @@ async function handleCommand(e) {
// RunMRPack should directly install a local mrpack given a path
if (e.path.endsWith('.mrpack')) {
await install_from_file(e.path).catch(handleError)
mixpanel_track('InstanceCreate', {
trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
@ -228,6 +242,20 @@ async function handleCommand(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>
<template>
@ -261,6 +289,14 @@ async function handleCommand(e) {
</div>
</div>
<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
v-tooltip="'Create profile'"
class="sleek-primary collapsed-button"
@ -276,6 +312,10 @@ async function handleCommand(e) {
</div>
</div>
<div class="view">
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
<h1>{{ criticalErrorMessage.header }}</h1>
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
</div>
<div class="appbar-row">
<div data-tauri-drag-region class="appbar">
<section class="navigation-controls">
@ -288,10 +328,14 @@ async function handleCommand(e) {
</section>
</div>
<section v-if="!nativeDecorations" class="window-controls">
<Button class="titlebar-button" icon-only @click="() => appWindow.minimize()">
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
<MinimizeIcon />
</Button>
<Button class="titlebar-button" icon-only @click="() => appWindow.toggleMaximize()">
<Button
class="titlebar-button"
icon-only
@click="() => getCurrentWindow().toggleMaximize()"
>
<MaximizeIcon />
</Button>
<Button class="titlebar-button close" icon-only @click="handleClose">
@ -384,6 +428,16 @@ async function handleCommand(e) {
width: calc(100% - var(--sidebar-width));
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 {
display: flex;
align-items: center;

View File

@ -12,13 +12,13 @@ import {
SearchIcon,
XIcon,
} from '@modrinth/assets'
import { ConfirmModal, Button, Card, DropdownSelect } from '@modrinth/ui'
import { Button, Card, DropdownSelect } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import dayjs from 'dayjs'
import { useTheming } from '@/store/theme.js'
import { duplicate, remove } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const props = defineProps({
instances: {
@ -35,7 +35,6 @@ const props = defineProps({
const instanceOptions = ref(null)
const instanceComponents = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null)
const confirmModal = ref(null)
@ -230,13 +229,12 @@ const filteredResults = computed(() => {
})
</script>
<template>
<ConfirmModal
<ConfirmModalWrapper
ref="confirmModal"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile"
/>
<Card class="header">

View File

@ -12,7 +12,7 @@ import {
EyeIcon,
ChevronRightIcon,
} from '@modrinth/assets'
import { ConfirmModal } from '@modrinth/ui'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import Instance from '@/components/ui/Instance.vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
@ -22,8 +22,7 @@ import { handleError } from '@/store/notifications.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { trackEvent } from '@/helpers/analytics'
import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
@ -53,7 +52,6 @@ const instanceComponents = ref(null)
const rows = ref(null)
const deleteConfirmModal = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null)
async function deleteProfile() {
@ -125,14 +123,14 @@ const handleOptionsClick = async (args) => {
await run(args.item.path).catch((err) =>
handleSevereError(err, { profilePath: args.item.path }),
)
mixpanel_track('InstanceStart', {
trackEvent('InstanceStart', {
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'stop':
await kill(args.item.path).catch(handleError)
mixpanel_track('InstanceStop', {
trackEvent('InstanceStop', {
loader: args.item.loader,
game_version: args.item.game_version,
})
@ -207,13 +205,12 @@ onUnmounted(() => {
</script>
<template>
<ConfirmModal
<ConfirmModalWrapper
ref="deleteConfirmModal"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile"
/>
<div class="content">

View File

@ -5,7 +5,7 @@
v-tooltip.right="'Minecraft accounts'"
class="button-base avatar-button"
:class="{ expanded: mode === 'expanded' }"
@click="showCard = !showCard"
@click="toggleMenu"
>
<Avatar
:size="mode === 'expanded' ? 'xs' : 'sm'"
@ -70,9 +70,10 @@ import {
get_default_user,
} from '@/helpers/auth'
import { handleError } from '@/store/state.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events'
import { handleSevereError } from '@/store/error.js'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
defineProps({
mode: {
@ -118,7 +119,7 @@ async function login() {
await refreshValues()
}
mixpanel_track('AccountLogIn')
trackEvent('AccountLogIn')
}
const logout = async (id) => {
@ -130,12 +131,12 @@ const logout = async (id) => {
} else {
emit('change')
}
mixpanel_track('AccountLogOut')
trackEvent('AccountLogOut')
}
let showCard = ref(false)
let card = ref(null)
let button = ref(null)
const showCard = ref(false)
const card = ref(null)
const button = ref(null)
const handleClickOutside = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY)
if (
@ -144,7 +145,20 @@ const handleClickOutside = (event) => {
!elements.includes(card.value.$el) &&
!button.value.contains(event.target)
) {
toggleMenu(false)
}
}
function toggleMenu(override = true) {
if (showCard.value || !override) {
if (showCard.value) {
show_ads_window()
}
showCard.value = false
} else {
hide_ads_window()
showCard.value = true
}
}

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets'
import { Button, OverflowMenu } from '@modrinth/ui'
import { open } from '@tauri-apps/api/dialog'
import { open } from '@tauri-apps/plugin-dialog'
import { add_project_from_path } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router'
@ -20,7 +20,7 @@ const handleAddContentFromFile = async () => {
if (!newProject) return
for (const project of newProject) {
await add_project_from_path(props.instance.path, project).catch(handleError)
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
}
}

View File

@ -25,6 +25,7 @@
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const emit = defineEmits(['menu-closed', 'option-clicked'])
@ -37,6 +38,7 @@ const shown = ref(false)
defineExpose({
showMenu: (event, passedItem, passedOptions) => {
hide_ads_window()
item.value = passedItem
options.value = passedOptions
@ -69,6 +71,9 @@ const isLinkedData = (item) => {
}
const hideContextMenu = () => {
if (shown.value) {
show_ads_window()
}
shown.value = false
emit('menu-closed')
}

View File

@ -1,14 +1,14 @@
<script setup>
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
import { Modal } from '@modrinth/ui'
import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser'
import { handleSevereError } from '@/store/error.js'
import { cancel_directory_change } from '@/helpers/settings.js'
import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref()
const error = ref()
@ -85,7 +85,7 @@ async function loginMinecraft() {
await set_default_user(loggedIn.id).catch(handleError)
}
await mixpanel.track('AccountLogIn')
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
loadingMinecraft.value = false
errorModal.value.hide()
} catch (err) {
@ -121,7 +121,7 @@ async function repairInstance() {
</script>
<template>
<Modal ref="errorModal" :header="title" :closable="closable">
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
@ -230,7 +230,7 @@ async function repairInstance() {
</p>
<p>You may be able to fix it through one of the following ways:</p>
<ul>
<li>Ennsuring you are connected to the internet, then try restarting the app.</li>
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
<li>Redownloading the app.</li>
</ul>
</template>
@ -272,7 +272,7 @@ async function repairInstance() {
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style>

View File

@ -1,12 +1,12 @@
<script setup>
import { XIcon, PlusIcon } from '@modrinth/assets'
import { Button, Checkbox, Modal } from '@modrinth/ui'
import { Button, Checkbox } from '@modrinth/ui'
import { PackageIcon, VersionIcon } from '@/assets/icons'
import { ref } from 'vue'
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
import { open } from '@tauri-apps/api/dialog'
import { open } from '@tauri-apps/plugin-dialog'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({
instance: {
@ -30,8 +30,6 @@ const files = ref([])
const folders = ref([])
const showingFiles = ref(false)
const themeStore = useTheming()
const initFiles = async () => {
const newFolders = new Map()
const sep = '/'
@ -106,7 +104,7 @@ const exportPack = async () => {
</script>
<template>
<Modal ref="exportModal" header="Export modpack" :noblur="!themeStore.advancedRendering">
<ModalWrapper ref="exportModal" header="Export modpack">
<div class="modal-body">
<div class="labeled_input">
<p>Modpack Name</p>
@ -208,7 +206,7 @@ const exportPack = async () => {
</Button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style scoped lang="scss">

View File

@ -3,14 +3,14 @@ import { onUnmounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { StopCircleIcon, PlayIcon } from '@modrinth/assets'
import { Card, Avatar, AnimatedLogo } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { convertFileSrc } from '@tauri-apps/api/core'
import { kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener } from '@/helpers/events'
import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js'
import { trackEvent } from '@/helpers/analytics'
const props = defineProps({
instance: {
@ -45,7 +45,7 @@ const play = async (e, context) => {
)
modLoading.value = false
mixpanel_track('InstancePlay', {
trackEvent('InstancePlay', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
@ -58,7 +58,7 @@ const stop = async (e, context) => {
await kill(props.instance.path).catch(handleError)
mixpanel_track('InstanceStop', {
trackEvent('InstanceStop', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,

View File

@ -1,5 +1,5 @@
<template>
<Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
<ModalWrapper ref="modal" header="Create instance">
<div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
</div>
@ -193,10 +193,11 @@
/>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import {
PlusIcon,
UploadIcon,
@ -207,18 +208,16 @@ import {
FolderSearchIcon,
UpdatedIcon,
} from '@modrinth/assets'
import { Avatar, Button, Chips, Modal, Checkbox } from '@modrinth/ui'
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui'
import { computed, onUnmounted, ref, shallowRef } from 'vue'
import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile'
import { open } from '@tauri-apps/api/dialog'
import { tauri } from '@tauri-apps/api'
import { open } from '@tauri-apps/plugin-dialog'
import { convertFileSrc } from '@tauri-apps/api/core'
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect'
import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event'
import { trackEvent } from '@/helpers/analytics'
import { install_from_file } from '@/helpers/pack.js'
import {
get_default_launcher_path,
@ -226,8 +225,7 @@ import {
import_instance,
} from '@/helpers/import.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
const themeStore = useTheming()
import { getCurrentWebview } from '@tauri-apps/api/webview'
const profile_name = ref('')
const game_version = ref('')
@ -257,20 +255,22 @@ defineExpose({
isShowing.value = true
modal.value.show()
unlistener.value = await listen('tauri://file-drop', async (event) => {
unlistener.value = await getCurrentWebview().onDragDropEvent(async (event) => {
// Only if modal is showing
if (!isShowing.value) return
if (event.payload.type !== 'drop') return
if (creationType.value !== 'from file') return
hide()
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
await install_from_file(event.payload[0]).catch(handleError)
mixpanel_track('InstanceCreate', {
const { paths } = event.payload
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) {
await install_from_file(paths[0]).catch(handleError)
trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
})
mixpanel_track('InstanceCreateStart', { source: 'CreationModal' })
trackEvent('InstanceCreateStart', { source: 'CreationModal' })
},
})
@ -360,7 +360,7 @@ const create_instance = async () => {
icon.value,
).catch(handleError)
mixpanel_track('InstanceCreate', {
trackEvent('InstanceCreate', {
profile_name: profile_name.value,
game_version: game_version.value,
loader: loader.value,
@ -371,7 +371,7 @@ const create_instance = async () => {
}
const upload_icon = async () => {
icon.value = await open({
const res = await open({
multiple: false,
filters: [
{
@ -381,8 +381,10 @@ const upload_icon = async () => {
],
})
icon.value = res.path ?? res
if (!icon.value) return
display_icon.value = tauri.convertFileSrc(icon.value)
display_icon.value = convertFileSrc(icon.value)
}
const reset_icon = () => {
@ -417,9 +419,9 @@ const openFile = async () => {
const newProject = await open({ multiple: false })
if (!newProject) return
hide()
await install_from_file(newProject).catch(handleError)
await install_from_file(newProject.path ?? newProject).catch(handleError)
mixpanel_track('InstanceCreate', {
trackEvent('InstanceCreate', {
source: 'CreationModalFileOpen',
})
}
@ -462,7 +464,7 @@ const promises = profileOptions.value.map(async (option) => {
option.name,
instances.map((name) => ({ name, selected: false })),
)
} catch (error) {
} catch {
// Allow failure silently
}
})

View File

@ -1,5 +1,5 @@
<template>
<Modal ref="detectJavaModal" header="Select java version" :noblur="!themeStore.advancedRendering">
<ModalWrapper ref="detectJavaModal" header="Select java version">
<div class="auto-detect-modal">
<div class="table">
<div class="table-row table-head">
@ -32,18 +32,16 @@
</Button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<script setup>
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
import { Modal, Button } from '@modrinth/ui'
import { Button } from '@modrinth/ui'
import { ref } from 'vue'
import { find_filtered_jres } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const chosenInstallOptions = ref([])
const detectJavaModal = ref(null)
@ -67,7 +65,7 @@ const emit = defineEmits(['submit'])
function setJavaInstall(javaInstall) {
emit('submit', javaInstall)
detectJavaModal.value.hide()
mixpanel_track('JavaAutoDetect', {
trackEvent('JavaAutoDetect', {
path: javaInstall.path,
version: javaInstall.version,
})

View File

@ -63,10 +63,10 @@ import {
import { Button } from '@modrinth/ui'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
import { ref } from 'vue'
import { open } from '@tauri-apps/api/dialog'
import { open } from '@tauri-apps/plugin-dialog'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
const props = defineProps({
version: {
@ -113,7 +113,7 @@ async function testJava() {
)
testingJava.value = false
mixpanel_track('JavaTest', {
trackEvent('JavaTest', {
path: props.modelValue ? props.modelValue.path : '',
success: testingJavaSuccess.value,
})
@ -124,20 +124,19 @@ async function testJava() {
}
async function handleJavaFileInput() {
let filePath = await open()
const filePath = await open()
if (filePath) {
let result = await get_jre(filePath)
let result = await get_jre(filePath.path ?? filePath)
if (!result) {
result = {
path: filePath,
path: filePath.path ?? filePath,
version: props.version.toString(),
architecture: 'x86',
}
}
mixpanel_track('JavaManualSelect', {
path: filePath,
trackEvent('JavaManualSelect', {
version: props.version,
})
@ -150,7 +149,7 @@ async function autoDetect() {
if (!props.compact) {
detectJavaModal.value.show(props.version, props.modelValue)
} else {
let versions = await find_filtered_jres(props.version).catch(handleError)
const versions = await find_filtered_jres(props.version).catch(handleError)
if (versions.length > 0) {
emit('update:modelValue', versions[0])
}
@ -170,7 +169,7 @@ async function reinstallJava() {
}
}
mixpanel_track('JavaReInstall', {
trackEvent('JavaReInstall', {
path: path,
version: props.version,
})

View File

@ -1,11 +1,11 @@
<script setup>
import { CheckIcon } from '@modrinth/assets'
import { Button, Modal, Badge } from '@modrinth/ui'
import { Button, Badge } from '@modrinth/ui'
import { computed, ref } from 'vue'
import { useTheming } from '@/store/theme'
import { update_managed_modrinth_version } from '@/helpers/profile'
import { releaseColor } from '@/helpers/utils'
import { SwapIcon } from '@/assets/icons/index.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({
versions: {
@ -33,8 +33,6 @@ const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false)
const themeStore = useTheming()
const switchVersion = async (versionId) => {
inProgress.value = true
await update_managed_modrinth_version(props.instance.path, versionId)
@ -43,11 +41,10 @@ const switchVersion = async (versionId) => {
</script>
<template>
<Modal
<ModalWrapper
ref="modpackVersionModal"
class="modpack-version-modal"
header="Change modpack version"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-body">
<Card v-if="instance.linked_data" class="mod-card">
@ -111,7 +108,7 @@ const switchVersion = async (versionId) => {
</div>
</Card>
</div>
</Modal>
</ModalWrapper>
</template>
<style scoped lang="scss">

View File

@ -1,12 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Promotion } from '@modrinth/ui'
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true)
defineExpose({
scroll() {
updateAdPosition()
},
})
const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) {
const user = await get_user(creds.user_id).catch(handleError)
@ -16,8 +24,103 @@ if (creds && creds.user_id) {
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>
<template>
<Promotion v-if="showAd" :external="false" query-param="?r=launcher" />
<div
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>

View File

@ -117,9 +117,9 @@ import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { ChatIcon } from '@/assets/icons'
import { get_many } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
const router = useRouter()
const card = ref(null)
@ -164,7 +164,7 @@ const stop = async (process) => {
try {
await killProcess(process.uuid).catch(handleError)
mixpanel_track('InstanceStop', {
trackEvent('InstanceStop', {
loader: process.profile.loader,
game_version: process.profile.game_version,
source: 'AppBar',

View File

@ -1,10 +1,10 @@
<template>
<div v-if="!hidden" class="splash-screen dark" :class="{ 'fade-out': doneLoading }">
<div v-if="os !== 'MacOS'" class="app-buttons">
<button class="btn icon-only transparent" icon-only @click="() => appWindow.minimize()">
<button class="btn icon-only transparent" icon-only @click="() => getCurrent().minimize()">
<MinimizeIcon />
</button>
<button class="btn icon-only transparent" @click="() => appWindow.toggleMaximize()">
<button class="btn icon-only transparent" @click="() => getCurrent().toggleMaximize()">
<MaximizeIcon />
</button>
<button class="btn icon-only transparent" @click="handleClose">
@ -85,12 +85,9 @@
import { ref, watch } from 'vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { loading_listener } from '@/helpers/events.js'
import { appWindow } from '@tauri-apps/api/window'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { XIcon } from '@modrinth/assets'
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 { useLoading } from '@/store/loading.js'
@ -131,20 +128,22 @@ const os = ref('')
getOS().then((x) => (os.value = x))
loading_listener(async (e) => {
console.log(e)
if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1)
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 () => {
await saveWindowState(StateFlags.ALL)
await TauriWindow.getCurrent().close()
await getCurrentWindow().close()
}
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
</script>
<style scoped lang="scss">

View File

@ -1,11 +1,12 @@
<script setup>
import { Modal, Button } from '@modrinth/ui'
import { Button } from '@modrinth/ui'
import { ref } from 'vue'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get_categories } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { get_version, get_project } from '@/helpers/cache.js'
import { install as installVersion } from '@/store/install.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const confirmModal = ref(null)
const project = ref(null)
@ -41,7 +42,7 @@ async function install() {
</script>
<template>
<Modal ref="confirmModal" :header="`Install ${project?.title}`">
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
<div class="modal-body">
<SearchCard
:project="project"
@ -60,7 +61,7 @@ async function install() {
</div>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style scoped lang="scss">

View File

@ -1,10 +1,5 @@
<template>
<Modal
ref="incompatibleModal"
header="Incompatibility warning"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
<div class="modal-body">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
@ -51,19 +46,18 @@
</Button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button, Modal, DropdownSelect } from '@modrinth/ui'
import { Button, DropdownSelect } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue'
import { handleError, useTheming } from '@/store/state.js'
import { mixpanel_track } from '@/helpers/mixpanel'
const themeStore = useTheming()
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
const instance = ref(null)
const project = ref(null)
@ -72,7 +66,7 @@ const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
let onInstall = ref(() => {})
const onInstall = ref(() => {})
defineExpose({
show: (instanceVal, projectVal, projectVersions, callback) => {
@ -87,7 +81,7 @@ defineExpose({
incompatibleModal.value.show()
mixpanel_track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
@ -98,7 +92,7 @@ const install = async () => {
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
mixpanel_track('ProjectInstall', {
trackEvent('ProjectInstall', {
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,

View File

@ -1,20 +1,18 @@
<script setup>
import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button, Modal } from '@modrinth/ui'
import { Button } from '@modrinth/ui'
import { install as pack_install } from '@/helpers/pack'
import { ref } from 'vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js'
import { trackEvent } from '@/helpers/analytics'
import { handleError } from '@/store/state.js'
const themeStore = useTheming()
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const versionId = ref()
const project = ref()
const confirmModal = ref(null)
const installing = ref(false)
let onInstall = ref(() => {})
const onInstall = ref(() => {})
defineExpose({
show: (projectVal, versionIdVal, callback) => {
@ -25,7 +23,7 @@ defineExpose({
onInstall.value = callback
mixpanel_track('PackInstallStart')
trackEvent('PackInstallStart')
},
})
@ -39,7 +37,7 @@ async function install() {
project.value.title,
project.value.icon_url,
).catch(handleError)
mixpanel_track('PackInstall', {
trackEvent('PackInstall', {
id: project.value.id,
version_id: versionId.value,
title: project.value.title,
@ -52,12 +50,7 @@ async function install() {
</script>
<template>
<Modal
ref="confirmModal"
header="Are you sure?"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
<div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">
@ -67,7 +60,7 @@ async function install() {
>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style lang="scss" scoped>

View File

@ -7,7 +7,7 @@ import {
RightArrowIcon,
CheckIcon,
} from '@modrinth/assets'
import { Avatar, Modal, Button, Card } from '@modrinth/ui'
import { Avatar, Button, Card } from '@modrinth/ui'
import { computed, ref } from 'vue'
import {
add_project_from_version as installMod,
@ -16,15 +16,14 @@ import {
list,
create,
} from '@/helpers/profile'
import { open } from '@tauri-apps/api/dialog'
import { open } from '@tauri-apps/plugin-dialog'
import { installVersionDependencies } from '@/store/install.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 { tauri } from '@tauri-apps/api'
import { convertFileSrc } from '@tauri-apps/api/core'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const router = useRouter()
const versions = ref()
@ -49,7 +48,7 @@ const shownProfiles = computed(() =>
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
})
.filter((profile) => {
let loaders = versions.value.flatMap((v) => v.loaders)
const loaders = versions.value.flatMap((v) => v.loaders)
return (
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
@ -60,7 +59,7 @@ const shownProfiles = computed(() =>
}),
)
let onInstall = ref(() => {})
const onInstall = ref(() => {})
defineExpose({
show: async (projectVal, versionsVal, callback) => {
@ -78,7 +77,7 @@ defineExpose({
onInstall.value = callback
const profilesVal = await list().catch(handleError)
for (let profile of profilesVal) {
for (const profile of profilesVal) {
profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
handleError,
@ -88,7 +87,7 @@ defineExpose({
installModal.value.show()
mixpanel_track('ProjectInstallStart', { source: 'ProjectInstallModal' })
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
},
})
@ -115,7 +114,7 @@ async function install(instance) {
instance.installedMod = true
instance.installing = false
mixpanel_track('ProjectInstall', {
trackEvent('ProjectInstall', {
loader: instance.loader,
game_version: instance.game_version,
id: project.value.id,
@ -137,12 +136,12 @@ const toggleCreation = () => {
loader.value = null
if (showCreation.value) {
mixpanel_track('InstanceCreateStart', { source: 'ProjectInstallModal' })
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
}
}
const upload_icon = async () => {
icon.value = await open({
const res = await open({
multiple: false,
filters: [
{
@ -151,9 +150,10 @@ const upload_icon = async () => {
},
],
})
icon.value = res.path ?? res
if (!icon.value) return
display_icon.value = tauri.convertFileSrc(icon.value)
display_icon.value = convertFileSrc(icon.value)
}
const reset_icon = () => {
@ -186,7 +186,7 @@ const createInstance = async () => {
const instance = await get(id, true)
await installVersionDependencies(instance, versions.value[0])
mixpanel_track('InstanceCreate', {
trackEvent('InstanceCreate', {
profile_name: name.value,
game_version: versions.value[0].game_versions[0],
loader: loader,
@ -195,7 +195,7 @@ const createInstance = async () => {
source: 'ProjectInstallModal',
})
mixpanel_track('ProjectInstall', {
trackEvent('ProjectInstall', {
loader: loader,
game_version: versions.value[0].game_versions[0],
id: project.value,
@ -213,12 +213,7 @@ const createInstance = async () => {
</script>
<template>
<Modal
ref="installModal"
header="Install project to instance"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
<div class="modal-body">
<input
v-model="searchFilter"
@ -235,7 +230,7 @@ const createInstance = async () => {
@click="installModal.hide()"
>
<Avatar
:src="profile.icon_path ? tauri.convertFileSrc(profile.icon_path) : null"
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
class="profile-image"
/>
{{ profile.name }}
@ -304,7 +299,7 @@ const createInstance = async () => {
<Button @click="installModal.hide()">Cancel</Button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style scoped lang="scss">

View File

@ -0,0 +1,69 @@
<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>

View File

@ -0,0 +1,49 @@
<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>

View File

@ -0,0 +1,61 @@
<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>

View File

@ -1,6 +1,6 @@
<script setup>
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
import { Button, Card, Checkbox, Modal } from '@modrinth/ui'
import { Button, Card, Checkbox } from '@modrinth/ui'
import {
DiscordIcon,
GithubIcon,
@ -13,6 +13,7 @@ import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.
import { handleError, useNotifications } from '@/store/state.js'
import { ref } from 'vue'
import { handleSevereError } from '@/store/error.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({
callback: {
@ -132,7 +133,7 @@ async function createAccount() {
</script>
<template>
<Modal ref="modal" :on-hide="removeWidget">
<ModalWrapper ref="modal" :on-hide="removeWidget">
<Card>
<template v-if="twoFactorFlow">
<h1>Enter two-factor code</h1>
@ -217,17 +218,17 @@ async function createAccount() {
v-else-if="loggingIn"
color="primary"
large
@click="signIn"
:disabled="!turnstileToken"
@click="signIn"
>
Login
</Button>
<Button v-else color="primary" large @click="createAccount" :disabled="!turnstileToken">
<Button v-else color="primary" large :disabled="!turnstileToken" @click="createAccount">
Create account
</Button>
</div>
</Card>
</Modal>
</ModalWrapper>
</template>
<style scoped lang="scss">

View File

@ -0,0 +1,20 @@
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)
}
}

View File

@ -0,0 +1,21 @@
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 })
}

View File

@ -0,0 +1,23 @@
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)
}

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
// Example function:
// User goes to auth_url to complete flow, and when completed, authenticate_await_completion() returns the credentials
@ -13,35 +13,46 @@ import { invoke } from '@tauri-apps/api/tauri'
// await authenticate_await_completion()
// }
/// Authenticate a user with Hydra - part 1
/// 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
/// - user_code: the code to enter on the verification_uri page
/**
* Authenticate a user with Hydra - part 1.
* This begins the authentication flow quasi-synchronously.
*
* @returns {Promise<DeviceLoginSuccess>} A DeviceLoginSuccess object with two relevant fields:
* @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() {
return await invoke('auth_login')
return await invoke('plugin:auth|login')
}
/// Retrieves the default user
/// user is UUID
/**
* Retrieves the default user
* @return {Promise<UUID | undefined>}
*/
export async function get_default_user() {
return await invoke('plugin:auth|auth_get_default_user')
return await invoke('plugin:auth|get_default_user')
}
/// Updates the default user
/// user is UUID
/**
* Updates the default user
* @param {UUID} user
*/
export async function set_default_user(user) {
return await invoke('plugin:auth|auth_set_default_user', { user })
return await invoke('plugin:auth|set_default_user', { user })
}
/// Remove a user account from the database
/// user is UUID
/**
* Remove a user account from the database
* @param {UUID} user
*/
export async function remove_user(user) {
return await invoke('plugin:auth|auth_remove_user', { user })
return await invoke('plugin:auth|remove_user', { user })
}
/// Returns a list of users
/// Returns an Array of Credentials
/**
* Returns a list of users
* @returns {Promise<Credential[]>}
*/
export async function users() {
return await invoke('plugin:auth|auth_users')
return await invoke('plugin:auth|get_users')
}

View File

@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
export async function get_project(id, cacheBehaviour) {
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
import { create } from './profile'
/*
@ -27,7 +27,7 @@ import { create } from './profile'
/// eg: get_importable_instances("MultiMC", "C:/MultiMC")
/// returns ["Instance 1", "Instance 2"]
export async function get_importable_instances(launcherType, basePath) {
return await invoke('plugin:import|import_get_importable_instances', { launcherType, basePath })
return await invoke('plugin:import|get_importable_instances', { launcherType, basePath })
}
/// Import an instance from a launcher type and base path
@ -38,7 +38,7 @@ export async function import_instance(launcherType, basePath, instanceFolder) {
// fs watching will be enabled once the instance is imported
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
return await invoke('plugin:import|import_import_instance', {
return await invoke('plugin:import|import_instance', {
profilePath,
launcherType,
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
/// eg: is_valid_importable_instance("C:/MultiMC/Instance 1", "MultiMC")
export async function is_valid_importable_instance(instanceFolder, launcherType) {
return await invoke('plugin:import|import_is_valid_importable_instance', {
return await invoke('plugin:import|is_valid_importable_instance', {
instanceFolder,
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
/// eg: get_default_launcher_path("MultiMC")
export async function get_default_launcher_path(launcherType) {
return await invoke('plugin:import|import_get_default_launcher_path', { launcherType })
return await invoke('plugin:import|get_default_launcher_path', { launcherType })
}

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
/*

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
/*
A log is a struct containing the filename string, stdout, and stderr, as follows:

View File

@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
/// Gets the game versions from daedalus
// Returns a VersionManifest

View File

@ -1,57 +0,0 @@
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
}

View File

@ -3,22 +3,22 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
export async function login(provider) {
return await invoke('modrinth_auth_login', { provider })
}
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) {
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) {
return await invoke('plugin:mr_auth|create_account', {
return await invoke('plugin:mr-auth|create_account', {
username,
email,
password,
@ -28,9 +28,9 @@ export async function create_account(username, email, password, challenge, signU
}
export async function logout() {
return await invoke('plugin:mr_auth|logout')
return await invoke('plugin:mr-auth|logout')
}
export async function get() {
return await invoke('plugin:mr_auth|get')
return await invoke('plugin:mr-auth|get')
}

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
import { create } from './profile'
// Installs pack from a version ID

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
/// Gets all running process IDs with a given profile path
/// Returns [u32]

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
/// Add instance
/*
@ -19,7 +19,7 @@ import { invoke } from '@tauri-apps/api/tauri'
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
//Trim string name to avoid "Unable to find directory"
name = name.trim()
return await invoke('plugin:profile_create|profile_create', {
return await invoke('plugin:profile-create|profile_create', {
name,
gameVersion,
modloader,
@ -31,7 +31,7 @@ export async function create(name, gameVersion, modloader, loaderVersion, iconPa
// duplicate a profile
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

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
// Settings object
/*

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
// Initialize the theseus API state
// This should be called during the initializion/opening of the launcher

View File

@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
// Gets cached category tags
export async function get_categories() {

View File

@ -1,5 +1,5 @@
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
import { invoke } from '@tauri-apps/api/tauri'
import { invoke } from '@tauri-apps/api/core'
export async function isDev() {
return await invoke('is_dev')
@ -33,6 +33,10 @@ export async function highlightModInProfile(profilePath, projectPath) {
return await highlightInFolder(fullPath)
}
export async function restartApp() {
return await invoke('restart_app')
}
export const releaseColor = (releaseType) => {
switch (releaseType) {
case 'release':
@ -52,7 +56,7 @@ export function debounce(fn, wait) {
if (timer) {
clearTimeout(timer) // clear any pre-existing timer
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this // get the current context
timer = setTimeout(() => {
fn.apply(context, args) // call the function if time expires

View File

@ -4,8 +4,8 @@ import App from '@/App.vue'
import { createPinia } from 'pinia'
import FloatingVue from 'floating-vue'
import 'floating-vue/dist/style.css'
import loadCssMixin from './mixins/macCssFix.js'
import { createPlugin } from '@vintl/vintl/plugin'
import * as Sentry from '@sentry/vue'
const VIntlPlugin = createPlugin({
controllerOpts: {
@ -27,10 +27,17 @@ const VIntlPlugin = createPlugin({
const pinia = createPinia()
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(pinia)
app.use(FloatingVue)
app.mixin(loadCssMixin)
app.use(VIntlPlugin)
app.mount('#app')

View File

@ -1,27 +0,0 @@
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)
}
},
},
}

View File

@ -19,7 +19,7 @@ import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute, useRouter } from 'vue-router'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { convertFileSrc } from '@tauri-apps/api/core'
import { get_search_results } from '@/helpers/cache.js'
import { debounce } from '@/helpers/utils.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
@ -381,20 +381,20 @@ const sortedCategories = computed(() => {
// identifier[0], then if it ties, identifier[1], etc
async function sortByNameOrNumber(sortable, identifiers) {
sortable.sort((a, b) => {
for (let identifier of identifiers) {
let aNum = parseFloat(a[identifier])
let bNum = parseFloat(b[identifier])
for (const identifier of identifiers) {
const aNum = parseFloat(a[identifier])
const bNum = parseFloat(b[identifier])
if (isNaN(aNum) && isNaN(bNum)) {
// Both are strings, sort alphabetically
let stringComp = a[identifier].localeCompare(b[identifier])
const stringComp = a[identifier].localeCompare(b[identifier])
if (stringComp != 0) return stringComp
} else if (!isNaN(aNum) && !isNaN(bNum)) {
// Both are numbers, sort numerically
let numComp = aNum - bNum
const numComp = aNum - bNum
if (numComp != 0) return numComp
} else {
// One is a number and one is a string, numbers go first
let numStringComp = isNaN(aNum) ? 1 : -1
const numStringComp = isNaN(aNum) ? 1 : -1
if (numStringComp != 0) return numStringComp
}
}
@ -528,7 +528,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
<template>
<div ref="searchWrapper" class="search-container">
<aside class="filter-panel">
<aside class="filter-panel" @scroll="$refs.promo.scroll()">
<PromotionWrapper ref="promo" />
<Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar
@ -675,8 +676,7 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
</Card>
</aside>
<div class="search">
<PromotionWrapper class="mt-4" />
<Card class="project-type-container">
<Card class="project-type-container mt-4">
<NavRow :links="selectableProjectTypes" />
</Card>
<Card class="search-panel-container">
@ -878,13 +878,13 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
.filter-panel {
position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem;
display: flex;
flex-direction: column;
height: fit-content;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
width: 20rem;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
@ -903,8 +903,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
}
.search {
margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - 20.5rem);
margin: 0 1rem 0.5rem calc(20rem + 1rem);
width: calc(100% - calc(20rem + 1rem));
.offline {
margin: 1rem;

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onUnmounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js'
@ -8,6 +8,11 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs'
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 featuredMods = ref({})
@ -42,7 +47,7 @@ const getInstances = async () => {
return dateB - dateA
})
let filters = []
const filters = []
for (const instance of recentInstances.value) {
if (instance.linked_data && instance.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)

View File

@ -1,5 +1,5 @@
<script setup>
import { onUnmounted, ref, shallowRef } from 'vue'
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
import GridDisplay from '@/components/GridDisplay.vue'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router'
@ -10,6 +10,11 @@ import { Button } from '@modrinth/ui'
import { PlusIcon } from '@modrinth/assets'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons'
import { hide_ads_window } from '@/helpers/ads.js'
onMounted(() => {
hide_ads_window(true)
})
const route = useRoute()
const breadcrumbs = useBreadcrumbs()

View File

@ -1,18 +1,24 @@
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui'
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings'
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
import { open } from '@tauri-apps/api/dialog'
import { optOutAnalytics, optInAnalytics } from '@/helpers/analytics'
import { open } from '@tauri-apps/plugin-dialog'
import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app'
import { get_user, purge_cache_types } from '@/helpers/cache.js'
import { hide_ads_window } from '@/helpers/ads.js'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
onMounted(() => {
hide_ads_window(true)
})
const pageOptions = ['Home', 'Library']
@ -45,9 +51,9 @@ watch(
const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.telemetry) {
mixpanel_opt_out_tracking()
optInAnalytics()
} else {
mixpanel_opt_in_tracking()
optOutAnalytics()
}
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
@ -169,13 +175,12 @@ async function purgeCache() {
Sign in
</button>
</div>
<ConfirmModal
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false"
proceed-label="Purge cache"
:noblur="!themeStore.advancedRendering"
@proceed="purgeCache"
/>
<div class="adjacent-input">
@ -358,6 +363,25 @@ async function purgeCache() {
<span class="label__title size-card-header">Privacy</span>
</h3>
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Personalized ads</span>
<span class="label__description">
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
option, you opt out and ads will no longer be shown based on your interests.
</span>
</label>
<Toggle
id="opt-out-analytics"
:model-value="settings.personalized_ads"
:checked="settings.personalized_ads"
@update:model-value="
(e) => {
settings.personalized_ads = e
}
"
/>
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Telemetry</span>
@ -401,14 +425,14 @@ async function purgeCache() {
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<template v-for="version in [21, 17, 8]">
<label :for="'java-' + version">
<span class="label__title">Java {{ version }} location</span>
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
<label :for="'java-' + javaVersion">
<span class="label__title">Java {{ javaVersion }} location</span>
</label>
<JavaSelector
:id="'java-selector-' + version"
v-model="javaVersions[version]"
:version="version"
:id="'java-selector-' + javaVersion"
v-model="javaVersions[javaVersion]"
:version="javaVersion"
@update:model-value="updateJavaVersion"
/>
</template>

View File

@ -1,8 +1,8 @@
<template>
<div class="instance-container">
<div class="side-cards">
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()">
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="lg" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
<div class="instance-info">
<h2 class="name">{{ instance.name }}</h2>
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
@ -61,9 +61,9 @@
</RouterLink>
</div>
</Card>
<PromotionWrapper ref="promo" class="mt-4" />
</div>
<div class="content">
<PromotionWrapper />
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
@ -131,9 +131,8 @@ import { ref, onUnmounted } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useFetch } from '@/helpers/fetch'
import { trackEvent } from '@/helpers/analytics'
import { convertFileSrc } from '@tauri-apps/api/core'
import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs'
@ -183,7 +182,7 @@ const startInstance = async (context) => {
}
loading.value = false
mixpanel_track('InstanceStart', {
trackEvent('InstanceStart', {
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
@ -220,7 +219,7 @@ const stopInstance = async (context) => {
playing.value = false
await kill(route.params.id).catch(handleError)
mixpanel_track('InstanceStop', {
trackEvent('InstanceStop', {
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
@ -312,7 +311,6 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
gap: 1rem;
width: 17rem;
}
Button {
@ -326,12 +324,13 @@ Button {
}
.side-cards {
position: absolute;
position: fixed;
width: 300px;
display: flex;
flex-direction: column;
padding: 1rem;
min-height: calc(100% - 3.25rem);
max-height: calc(100% - 3.25rem);
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
@ -375,10 +374,7 @@ Button {
overflow: auto;
gap: 1rem;
min-height: 100%;
}
.content {
margin-left: 19rem;
padding: 1rem;
}
.instance-info {
@ -452,10 +448,10 @@ Button {
}
.content {
width: 100%;
margin: 0 1rem 0.5rem 20rem;
width: calc(100% - 20rem);
display: flex;
flex-direction: column;
padding: 1rem 1rem 0 0;
overflow: auto;
}

View File

@ -76,7 +76,7 @@
</div>
</RecycleScroller>
</div>
<ShareModal
<ShareModalWrapper
ref="shareModal"
header="Share Log"
share-title="Instance Log"
@ -89,7 +89,7 @@
<script setup>
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
import { Button, Card, ShareModal, Checkbox, DropdownSelect } from '@modrinth/ui'
import { Button, Card, Checkbox, DropdownSelect } from '@modrinth/ui'
import {
delete_logs_by_filename,
get_logs,
@ -107,6 +107,7 @@ import { handleError } from '@/store/notifications.js'
import { ofetch } from 'ofetch'
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
dayjs.extend(isToday)
dayjs.extend(isYesterday)
@ -295,7 +296,7 @@ if (logs.value.length > 1 && !props.playing) {
const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
let deleteIndex = selectedLogIndex.value
const deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_filename(
props.instance.path,

View File

@ -284,7 +284,7 @@
:link-function="(page) => `?page=${page}`"
@switch-page="switchPage"
/>
<Modal ref="deleteWarning" header="Are you sure?">
<ModalWrapper ref="deleteWarning" header="Are you sure?">
<div class="modal-body">
<div class="markdown-body">
<p>
@ -302,8 +302,8 @@
</Button>
</div>
</div>
</Modal>
<Modal ref="deleteDisabledWarning" header="Are you sure?">
</ModalWrapper>
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?">
<div class="modal-body">
<div class="markdown-body">
<p>
@ -325,8 +325,8 @@
</Button>
</div>
</div>
</Modal>
<ShareModal
</ModalWrapper>
<ShareModalWrapper
ref="shareModal"
share-title="Sharing modpack content"
share-text="Check out the projects I'm using in my modpack!"
@ -360,8 +360,6 @@ import {
import {
Pagination,
DropdownSelect,
ShareModal,
Modal,
Checkbox,
AnimatedLogo,
Avatar,
@ -379,8 +377,7 @@ import {
update_project,
} from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { listen } from '@tauri-apps/api/event'
import { trackEvent } from '@/helpers/analytics'
import { highlightModInProfile } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue'
@ -393,6 +390,9 @@ import {
get_version_many,
} from '@/helpers/cache.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({
instance: {
@ -682,7 +682,7 @@ const updateAll = async () => {
projects.value[project].updating = false
}
mixpanel_track('InstanceUpdateAll', {
trackEvent('InstanceUpdateAll', {
loader: props.instance.loader,
game_version: props.instance.game_version,
count: setProjects.length,
@ -708,7 +708,7 @@ const updateProject = async (mod) => {
mod.version = mod.updateVersion.version_number
mod.updateVersion = null
mixpanel_track('InstanceProjectUpdate', {
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
@ -717,7 +717,7 @@ const updateProject = async (mod) => {
})
}
let locks = {}
const locks = {}
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.
@ -725,7 +725,7 @@ const toggleDisableMod = async (mod) => {
locks[mod.id] = ref(null)
}
let lock = locks[mod.id]
const lock = locks[mod.id]
while (lock.value) {
await lock.value
@ -735,7 +735,7 @@ const toggleDisableMod = async (mod) => {
.then((newPath) => {
mod.path = newPath
mod.disabled = !mod.disabled
mixpanel_track('InstanceProjectDisable', {
trackEvent('InstanceProjectDisable', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
@ -756,7 +756,7 @@ const removeMod = async (mod) => {
await remove_project(props.instance.path, mod.path).catch(handleError)
projects.value = projects.value.filter((x) => mod.path !== x.path)
mixpanel_track('InstanceProjectRemove', {
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
@ -784,6 +784,7 @@ const deleteDisabled = async () => {
}
const shareNames = async () => {
console.log(functionValues.value)
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
}
@ -878,8 +879,10 @@ async function refreshProjects() {
refreshingProjects.value = false
}
const unlisten = await listen('tauri://file-drop', async (event) => {
for (const file of event.payload) {
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop') return
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}

View File

@ -1,18 +1,13 @@
<template>
<ConfirmModal
<ConfirmModalWrapper
ref="modal_confirm"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="removeProfile"
/>
<Modal
ref="modalConfirmUnlock"
header="Are you sure you want to unlock this instance?"
:noblur="!themeStore.advancedRendering"
>
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?">
<div class="modal-delete">
<div
class="markdown-body"
@ -31,13 +26,9 @@
</button>
</div>
</div>
</Modal>
</ModalWrapper>
<Modal
ref="modalConfirmUnpair"
header="Are you sure you want to unpair this instance?"
:noblur="!themeStore.advancedRendering"
>
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?">
<div class="modal-delete">
<div
class="markdown-body"
@ -56,13 +47,9 @@
</button>
</div>
</div>
</Modal>
</ModalWrapper>
<Modal
ref="changeVersionsModal"
header="Change instance versions"
:noblur="!themeStore.advancedRendering"
>
<ModalWrapper ref="changeVersionsModal" header="Change instance versions">
<div class="change-versions-modal universal-body">
<div class="input-row">
<p class="input-label">Loader</p>
@ -106,7 +93,7 @@
</button>
</div>
</div>
</Modal>
</ModalWrapper>
<section class="card">
<div class="label">
<h3>
@ -511,18 +498,7 @@ import {
DownloadIcon,
ClipboardCopyIcon,
} from '@modrinth/assets'
import {
Button,
Toggle,
ConfirmModal,
Card,
Slider,
Checkbox,
Avatar,
Modal,
Chips,
DropdownSelect,
} from '@modrinth/ui'
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui'
import { SwapIcon } from '@/assets/icons'
import { Multiselect } from 'vue-multiselect'
@ -541,15 +517,16 @@ import { computed, readonly, ref, shallowRef, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre.js'
import { get } from '@/helpers/settings.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import { get_loader_versions } from '@/helpers/metadata.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.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 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()
@ -570,8 +547,6 @@ const props = defineProps({
},
})
const themeStore = useTheming()
const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
@ -590,7 +565,7 @@ const availableGroups = ref([
async function resetIcon() {
icon.value = null
await edit_icon(props.instance.path, null).catch(handleError)
mixpanel_track('InstanceRemoveIcon')
trackEvent('InstanceRemoveIcon')
}
async function setIcon() {
@ -606,10 +581,10 @@ async function setIcon() {
if (!value) return
icon.value = value
icon.value = value.path ?? value
await edit_icon(props.instance.path, icon.value).catch(handleError)
mixpanel_track('InstanceSetIcon')
trackEvent('InstanceSetIcon')
}
const globalSettings = await get().catch(handleError)
@ -621,12 +596,12 @@ const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
)
const overrideEnvVars = ref(!!props.instance.custom_env_vars)
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
const envVars = ref(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
@ -710,19 +685,15 @@ const editProfileObject = computed(() => {
}
if (overrideJavaArgs.value) {
if (javaArgs.value !== '') {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
if (overrideEnvVars.value) {
if (envVars.value !== '') {
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
if (overrideMemorySettings.value) {
@ -754,7 +725,7 @@ const repairing = ref(false)
async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError)
mixpanel_track('InstanceDuplicate', {
trackEvent('InstanceDuplicate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@ -765,7 +736,7 @@ async function repairProfile(force) {
await install(props.instance.path, force).catch(handleError)
repairing.value = false
mixpanel_track('InstanceRepair', {
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@ -796,7 +767,7 @@ async function repairModpack() {
await update_repair_modrinth(props.instance.path).catch(handleError)
inProgress.value = false
mixpanel_track('InstanceRepair', {
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@ -808,7 +779,7 @@ async function removeProfile() {
await remove(props.instance.path).catch(handleError)
removing.value = false
mixpanel_track('InstanceRemove', {
trackEvent('InstanceRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@ -905,7 +876,7 @@ const editing = ref(false)
async function saveGvLoaderEdits() {
editing.value = true
let editProfile = editProfileObject.value
const editProfile = editProfileObject.value
editProfile.loader = loader.value
editProfile.game_version = gameVersion.value

View File

@ -20,14 +20,14 @@
</span>
</Card>
</div>
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="expandedGalleryItem = null">
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage">
<div class="content">
<img
class="image"
:class="{ 'zoomed-in': zoomedIn }"
:src="
expandedGalleryItem.url
? expandedGalleryItem.url
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@ -45,15 +45,15 @@
</div>
<div class="controls">
<div class="buttons">
<Button class="close" icon-only @click="expandedGalleryItem = null">
<Button class="close" icon-only @click="hideImage">
<XIcon aria-hidden="true" />
</Button>
<a
class="open btn icon-only"
target="_blank"
:href="
expandedGalleryItem.url
? expandedGalleryItem.url
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
>
@ -93,7 +93,8 @@ import {
} from '@modrinth/assets'
import { Button, Card } from '@modrinth/ui'
import { ref } from 'vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { trackEvent } from '@/helpers/analytics'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const props = defineProps({
project: {
@ -102,9 +103,14 @@ const props = defineProps({
},
})
let expandedGalleryItem = ref(null)
let expandedGalleryIndex = ref(0)
let zoomedIn = ref(false)
const expandedGalleryItem = ref(null)
const expandedGalleryIndex = ref(0)
const zoomedIn = ref(false)
const hideImage = () => {
expandedGalleryItem.value = null
show_ads_window()
}
const nextImage = () => {
expandedGalleryIndex.value++
@ -112,7 +118,7 @@ const nextImage = () => {
expandedGalleryIndex.value = 0
}
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
mixpanel_track('GalleryImageNext', {
trackEvent('GalleryImageNext', {
project_id: props.project.id,
url: expandedGalleryItem.value.url,
})
@ -124,22 +130,37 @@ const previousImage = () => {
expandedGalleryIndex.value = props.project.gallery.length - 1
}
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
mixpanel_track('GalleryImagePrevious', {
trackEvent('GalleryImagePrevious', {
project_id: props.project.id,
url: expandedGalleryItem.value,
})
}
const expandImage = (item, index) => {
hide_ads_window()
expandedGalleryItem.value = item
expandedGalleryIndex.value = index
zoomedIn.value = false
mixpanel_track('GalleryImageExpand', {
trackEvent('GalleryImageExpand', {
project_id: props.project.id,
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>
<style scoped lang="scss">

View File

@ -1,6 +1,6 @@
<template>
<div class="root-container">
<div v-if="data" class="project-sidebar">
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()">
<Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar
@ -20,7 +20,7 @@
</router-link>
</Card>
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="lg" :src="data.icon_url" />
<Avatar size="md" :src="data.icon_url" />
<div class="instance-info">
<h2 class="name">{{ data.title }}</h2>
{{ data.description }}
@ -61,7 +61,9 @@
Site
</a>
</div>
<hr class="card-divider" />
</Card>
<PromotionWrapper ref="promo" />
<Card class="sidebar-card">
<div class="stats">
<div class="stat">
<DownloadIcon aria-hidden="true" />
@ -163,7 +165,6 @@
</Card>
</div>
<div v-if="data" class="content-container">
<PromotionWrapper />
<Card class="tabs">
<NavRow
v-if="data.gallery.length > 0"
@ -231,15 +232,7 @@ import {
GlobeIcon,
ClipboardCopyIcon,
} from '@modrinth/assets'
import {
Categories,
EnvironmentIndicator,
Card,
Avatar,
Button,
Promotion,
NavRow,
} from '@modrinth/ui'
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui'
import { formatNumber } from '@modrinth/utils'
import {
BuyMeACoffeeIcon,
@ -257,10 +250,10 @@ import { useRoute } from 'vue-router'
import { ref, shallowRef, watch } from 'vue'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { convertFileSrc } from '@tauri-apps/api/core'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { install as installVersion } from '@/store/install.js'
import { get_project, get_project_many, get_team, get_version_many } from '@/helpers/cache.js'
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
dayjs.extend(relativeTime)
@ -309,11 +302,13 @@ async function fetchProjectData() {
await fetchProjectData()
const promo = ref(null)
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/project')) {
await fetchProjectData()
promo.value.scroll()
}
},
)
@ -377,7 +372,7 @@ const handleOptionsClick = (args) => {
.project-sidebar {
position: fixed;
width: 20rem;
width: calc(300px + 1.5rem);
min-height: calc(100vh - 3.25rem);
height: fit-content;
max-height: calc(100vh - 3.25rem);
@ -403,7 +398,7 @@ const handleOptionsClick = (args) => {
flex-direction: column;
width: 100%;
padding: 1rem;
margin-left: 19.5rem;
margin-left: calc(300px + 1rem);
}
.button-group {

View File

@ -139,7 +139,7 @@ export default new createRouter({
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior() {
// 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').scrollTop = 0
document.querySelector('.router-view')?.scrollTo(0, 0)
return {
el: '.router-view',
top: 0,

View File

@ -10,7 +10,7 @@ import {
import { handleError } from '@/store/notifications.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import { install as packInstall } from '@/helpers/pack.js'
import { mixpanel_track } from '@/helpers/mixpanel.js'
import { trackEvent } from '@/helpers/analytics.js'
import dayjs from 'dayjs'
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)) {
await packInstall(project.id, version, project.title, project.icon_url).catch(handleError)
mixpanel_track('PackInstall', {
trackEvent('PackInstall', {
id: project.id,
version_id: version,
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 installVersionDependencies(instance, version)
mixpanel_track('ProjectInstall', {
trackEvent('ProjectInstall', {
loader: instance.loader,
game_version: instance.game_version,
id: project.id,

View File

@ -23,7 +23,3 @@ export const handleError = (err) => {
})
console.error(err)
}
export const handleMixpanelError = (err) => {
console.error(err)
}

View File

@ -0,0 +1,24 @@
{
"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"]
}

View File

@ -1,12 +1,4 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ESNext"
},
"exclude": ["node_modules", "dist"]
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,17 @@
{
"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"]
}

View File

@ -42,8 +42,8 @@ export default defineConfig({
port: 1420,
strictPort: true,
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
// to make use of `TAURI_ENV_DEBUG` and other env variables
// https://v2.tauri.app/reference/environment-variables/#tauri-cli-hook-commands
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri supports es2021

View File

@ -10,14 +10,12 @@ theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.7.1", features = ["shell-open"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
url = "2.2"
webbrowser = "0.8.13"
dunce = "1.0.3"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3"
uuid = { version = "1.1", features = ["serde", "v4"] }

View File

@ -2,7 +2,7 @@
"name": "@modrinth/app-playground",
"scripts": {
"build": "cargo build --release",
"lint": "cargo fmt --check && cargo clippy -- -D warnings",
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
"fix": "cargo fmt && cargo clippy --fix",
"dev": "cargo run",
"test": "cargo test"

View File

@ -1,17 +1,14 @@
[package]
name = "theseus_gui"
version = "0.8.3-1"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
version = "0.8.9"
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/"
edition = "2021"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.3", features = [] }
tauri-build = { version = "2.0.0-rc", features = ["codegen"] }
[dependencies]
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
@ -19,14 +16,17 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
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-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-deep-link = "0.1.2"
tauri = { version = "2.0.0-rc", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
tauri-plugin-window-state = "2.0.0-rc"
tauri-plugin-deep-link = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc"
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"] }
thiserror = "1.0"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3"
daedalus = "0.2.3"
chrono = "0.4.26"
@ -56,6 +56,10 @@ window-shadows = "0.2.1"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"
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]
# by default Tauri runs in production mode
@ -64,3 +68,4 @@ default = ["custom-protocol"]
# this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
updater = []

View File

@ -6,63 +6,17 @@
<array>
<dict>
<key>CFBundleURLName</key>
<!-- Obviously needs to be replaced with your app's bundle identifier -->
<string>ModrinthApp</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- register the myapp:// and myscheme:// schemes -->
<string>modrinth</string>
<string>modrinthscheme</string>
</array>
</dict>
</array>
<!-- Declare file types your app can open -->
<key>CFBundleDocumentTypes</key>
<array>
<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>
<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>
</plist>

View File

@ -15,7 +15,7 @@ Before you begin, ensure you have the following installed on your machine:
- [Node.js](https://nodejs.org/en/)
- [pnpm](https://pnpm.io/)
- [Rust](https://www.rust-lang.org/tools/install)
- [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/#installing)
- [Tauri](https://v2.tauri.app/start/prerequisites/)
### Setup

View File

@ -1,4 +1,239 @@
use tauri_build::{DefaultPermissionRule, InlinedPlugin};
fn main() {
// Build the Tauri app
tauri_build::build();
// Sadly, there is no better way to do it right now
// You could try parsing source code here and detecting #[tauri::command]
// 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");
}

View File

@ -0,0 +1,14 @@
{
"identifier": "ads",
"description": "",
"local": false,
"remote": {
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
},
"webviews": [
"ads-window"
],
"permissions": [
"ads:default"
]
}

View File

@ -0,0 +1,30 @@
{
"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"
]
}

View File

@ -0,0 +1,41 @@
{
"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"
]
}

View File

@ -0,0 +1,11 @@
{
"identifier": "updater",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"updater:default"
]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"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

View File

@ -1,346 +0,0 @@
<?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
apps/app/nsis/hooks.nsi Normal file
View File

@ -0,0 +1,41 @@
!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

View File

@ -2,16 +2,17 @@
"name": "@modrinth/app",
"scripts": {
"build": "tauri build",
"tauri": "tauri",
"dev": "tauri dev",
"test": "cargo test",
"lint": "cargo fmt --check && cargo clippy -- -D warnings",
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
"fix": "cargo fmt && cargo clippy --fix"
},
"devDependencies": {
"@tauri-apps/cli": "^1.6.0"
"@tauri-apps/cli": "2.0.0-rc.16"
},
"dependencies": {
"@modrinth/app-lib": "workspace:*",
"@modrinth/app-frontend": "workspace:*"
"@modrinth/app-frontend": "workspace:*",
"@modrinth/app-lib": "workspace:*"
}
}
}

View File

@ -0,0 +1,23 @@
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
apps/app/src/api/ads.rs Normal file
View File

@ -0,0 +1,216 @@
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)
}

View File

@ -1,16 +1,17 @@
use crate::api::Result;
use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin;
use tauri::{Manager, UserAttentionType};
use tauri::{Manager, Runtime, UserAttentionType};
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("auth")
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("auth")
.invoke_handler(tauri::generate_handler![
auth_get_default_user,
auth_set_default_user,
auth_remove_user,
auth_users,
login,
remove_user,
get_default_user,
set_default_user,
get_users,
])
.build()
}
@ -18,19 +19,21 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
/// 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)
#[tauri::command]
pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
pub async fn login<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Option<Credentials>> {
let flow = minecraft_auth::begin_login().await?;
let start = Utc::now();
if let Some(window) = app.get_window("signin") {
if let Some(window) = app.get_webview_window("signin") {
window.close()?;
}
let window = tauri::WindowBuilder::new(
let window = tauri::WebviewWindowBuilder::new(
&app,
"signin",
tauri::WindowUrl::External(flow.redirect_uri.parse().map_err(
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err(
|_| {
theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(),
@ -53,12 +56,12 @@ pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
}
if window
.url()
.url()?
.as_str()
.starts_with("https://login.live.com/oauth20_desktop.srf")
{
if let Some((_, code)) =
window.url().query_pairs().find(|x| x.0 == "code")
window.url()?.query_pairs().find(|x| x.0 == "code")
{
window.close()?;
let val =
@ -75,23 +78,22 @@ pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
Ok(None)
}
#[tauri::command]
pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> {
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::remove_user(user).await?)
}
#[tauri::command]
pub async fn auth_get_default_user() -> Result<Option<uuid::Uuid>> {
pub async fn get_default_user() -> Result<Option<uuid::Uuid>> {
Ok(minecraft_auth::get_default_user().await?)
}
#[tauri::command]
pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
pub async fn set_default_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::set_default_user(user).await?)
}
/// Get a copy of the list of all user credentials
// invoke('plugin:auth|auth_users',user)
#[tauri::command]
pub async fn auth_users() -> Result<Vec<Credentials>> {
pub async fn get_users() -> Result<Vec<Credentials>> {
Ok(minecraft_auth::users().await?)
}

View File

@ -8,10 +8,10 @@ use theseus::pack::import;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("import")
.invoke_handler(tauri::generate_handler![
import_get_importable_instances,
import_import_instance,
import_is_valid_importable_instance,
import_get_default_launcher_path,
get_importable_instances,
import_instance,
is_valid_importable_instance,
get_default_launcher_path,
])
.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"))
/// returns ["Instance 1", "Instance 2"]
#[tauri::command]
pub async fn import_get_importable_instances(
pub async fn get_importable_instances(
launcher_type: ImportLauncherType,
base_path: PathBuf,
) -> Result<Vec<String>> {
@ -31,7 +31,7 @@ pub async fn import_get_importable_instances(
/// 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")
#[tauri::command]
pub async fn import_import_instance(
pub async fn import_instance(
profile_path: &str,
launcher_type: ImportLauncherType,
base_path: PathBuf,
@ -50,7 +50,7 @@ pub async fn import_import_instance(
/// 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)
#[tauri::command]
pub async fn import_is_valid_importable_instance(
pub async fn is_valid_importable_instance(
instance_folder: PathBuf,
launcher_type: ImportLauncherType,
) -> Result<bool> {
@ -63,7 +63,7 @@ pub async fn import_is_valid_importable_instance(
/// Returns the default path for the given launcher type
/// None if it can't be found or doesn't exist
#[tauri::command]
pub async fn import_get_default_launcher_path(
pub async fn get_default_launcher_path(
launcher_type: ImportLauncherType,
) -> Result<Option<PathBuf>> {
Ok(import::get_default_launcher_path(launcher_type))

View File

@ -16,6 +16,7 @@ pub mod settings;
pub mod tags;
pub mod utils;
pub mod ads;
pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
@ -40,9 +41,9 @@ pub enum TheseusSerializableError {
#[error("Tauri error: {0}")]
Tauri(#[from] tauri::Error),
#[cfg(target_os = "macos")]
#[error("Callback error: {0}")]
Callback(String),
#[cfg(feature = "updater")]
#[error("Tauri updater error: {0}")]
TauriUpdater(#[from] tauri_plugin_updater::Error),
}
// Generic implementation of From<T> for ErrorTypeA
@ -90,15 +91,15 @@ macro_rules! impl_serialize {
}
// Use the macro to implement Serialize for TheseusSerializableError
#[cfg(target_os = "macos")]
#[cfg(not(feature = "updater"))]
impl_serialize! {
IO,
Tauri,
Callback
}
#[cfg(not(target_os = "macos"))]
#[cfg(feature = "updater")]
impl_serialize! {
IO,
Tauri,
TauriUpdater,
}

View File

@ -5,7 +5,7 @@ use tauri::{Manager, UserAttentionType};
use theseus::prelude::*;
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![
login_pass,
login_2fa,
@ -25,14 +25,14 @@ pub async fn modrinth_auth_login(
let start = Utc::now();
if let Some(window) = app.get_window("modrinth-signin") {
if let Some(window) = app.get_webview_window("modrinth-signin") {
window.close()?;
}
let window = tauri::WindowBuilder::new(
let window = tauri::WebviewWindowBuilder::new(
&app,
"modrinth-signin",
tauri::WindowUrl::External(redirect_uri.parse().map_err(|_| {
tauri::WebviewUrl::External(redirect_uri.parse().map_err(|_| {
theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(),
)
@ -53,12 +53,12 @@ pub async fn modrinth_auth_login(
}
if window
.url()
.url()?
.as_str()
.starts_with("https://launcher-files.modrinth.com/detect.txt")
{
let query = window
.url()
.url()?
.query_pairs()
.map(|(key, val)| {
(

View File

@ -2,7 +2,7 @@ use crate::api::Result;
use theseus::prelude::*;
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![
profile_create,
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
// invoke('plugin:profile_create|profile_add',profile)
// invoke('plugin:profile-create|profile_add',profile)
#[tauri::command]
pub async fn profile_create(
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
// invoke('plugin:profile_create|profile_duplicate',profile)
// invoke('plugin:profile-create|profile_duplicate',profile)
#[tauri::command]
pub async fn profile_duplicate(path: &str) -> Result<String> {
let res = profile::create::profile_create_from_duplicate(path).await?;

View File

@ -137,5 +137,6 @@ 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)
// We hijack the deep link library (which also contains functionality for instance-checking)
pub async fn handle_command(command: String) -> Result<()> {
tracing::info!("handle command: {command}");
Ok(theseus::handler::parse_and_emit_command(&command).await?)
}

View File

@ -1,6 +1,28 @@
use std::sync::Arc;
use tauri::{Manager, Runtime};
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct InitialPayload {
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
}

Some files were not shown because too many files have changed in this diff Show More