Compare commits

..

1 Commits

Author SHA1 Message Date
Prospector
e7d096e768 Add plus theme, settings appearance redesign 2024-08-27 21:08:28 -07:00
117 changed files with 2523 additions and 16121 deletions

View File

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

View File

@@ -1,6 +1,6 @@
name: 🌐 Website bug (modrinth.com)
description: Report an issue on the Modrinth website.
labels: [bug, web]
labels: [bug, frontend]
body:
- type: checkboxes
attributes:
@@ -49,4 +49,4 @@ body:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false
required: false

View File

@@ -9,8 +9,6 @@ body:
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests
required: true
- label: I checked the [existing discussions](https://github.com/orgs/modrinth/discussions) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true
- type: dropdown
@@ -45,4 +43,4 @@ body:
label: Additional context
description: Add any other context or screenshots about the suggested enhancement here.
validations:
required: false
required: false

View File

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

View File

@@ -11,7 +11,7 @@ on:
jobs:
build:
name: Build, Test, and Lint
runs-on: ubuntu-22.04
runs-on: ubuntu-20.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 libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libselinux1
- name: Setup Node.JS environment
uses: actions/setup-node@v4

2202
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,26 +13,22 @@
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@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-window-state": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/api": "^1.6.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",
"vite-svg-loader": "^5.1.0",
"vue": "^3.4.21",
"vue-multiselect": "3.0.0",
"vue-router": "4.3.0",
"vue-virtual-scroller": "v2.0.0-beta.8",
"posthog-js": "^1.158.2",
"@sentry/vue": "^8.27.0"
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0-rc",
"@tauri-apps/cli": "^1.6.0",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",

View File

@@ -15,21 +15,27 @@ 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/plugin-os'
import { type } from '@tauri-apps/api/os'
import { appWindow } from '@tauri-apps/api/window'
import { isDev, getOS } from '@/helpers/utils.js'
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window'
import {
mixpanel_track,
mixpanel_init,
mixpanel_opt_out_tracking,
mixpanel_is_loaded,
} from '@/helpers/mixpanel'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import { getVersion } from '@tauri-apps/api/app'
import { 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/core'
import { open } from '@tauri-apps/plugin-shell'
import { invoke } from '@tauri-apps/api/tauri'
import { get_opening_command, initialize_state } from '@/helpers/state'
const themeStore = useTheming()
@@ -51,10 +57,6 @@ const os = ref('')
const stateInitialized = ref(false)
onMounted(async () => {
await useCheckDisableMouseover()
})
async function setupApp() {
stateInitialized.value = true
const {
@@ -77,23 +79,21 @@ async function setupApp() {
showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
if (os.value !== 'MacOS') await appWindow.setDecorations(native_decorations)
themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering
initAnalytics()
mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' })
if (!telemetry) {
optOutAnalytics()
mixpanel_opt_out_tracking()
}
if (dev) debugAnalytics()
trackEvent('Launched', { version, dev, onboarded })
mixpanel_track('Launched', { version, dev, onboarded })
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
const osType = await type()
if (osType === 'macos') {
if ((await type()) === 'Darwin') {
document.getElementsByTagName('html')[0].classList.add('mac')
} else {
document.getElementsByTagName('html')[0].classList.add('windows')
@@ -126,12 +126,19 @@ initialize_state()
})
const handleClose = async () => {
await getCurrentWindow().close()
await saveWindowState(StateFlags.ALL)
await TauriWindow.getCurrent().close()
}
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
const router = useRouter()
router.afterEach((to, from, failure) => {
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
if (mixpanel_is_loaded()) {
mixpanel_track('PageView', { path: to.path, fromPath: from.path, failed: failure })
}
})
const route = useRoute()
const isOnBrowse = computed(() => route.path.startsWith('/browse'))
@@ -173,7 +180,13 @@ document.querySelector('body').addEventListener('click', function (e) {
!target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost')
) {
open(target.href)
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: target.href,
},
})
}
e.preventDefault()
break
@@ -206,7 +219,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)
trackEvent('InstanceCreate', {
mixpanel_track('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
@@ -275,14 +288,10 @@ async function handleCommand(e) {
</section>
</div>
<section v-if="!nativeDecorations" class="window-controls">
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
<Button class="titlebar-button" icon-only @click="() => appWindow.minimize()">
<MinimizeIcon />
</Button>
<Button
class="titlebar-button"
icon-only
@click="() => getCurrentWindow().toggleMaximize()"
>
<Button class="titlebar-button" icon-only @click="() => appWindow.toggleMaximize()">
<MaximizeIcon />
</Button>
<Button class="titlebar-button close" icon-only @click="handleClose">

View File

@@ -23,7 +23,7 @@ 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 { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
@@ -125,14 +125,14 @@ const handleOptionsClick = async (args) => {
await run(args.item.path).catch((err) =>
handleSevereError(err, { profilePath: args.item.path }),
)
trackEvent('InstanceStart', {
mixpanel_track('InstanceStart', {
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'stop':
await kill(args.item.path).catch(handleError)
trackEvent('InstanceStop', {
mixpanel_track('InstanceStop', {
loader: args.item.loader,
game_version: args.item.game_version,
})

View File

@@ -70,7 +70,7 @@ import {
get_default_user,
} from '@/helpers/auth'
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
import { process_listener } from '@/helpers/events'
import { handleSevereError } from '@/store/error.js'
@@ -118,7 +118,7 @@ async function login() {
await refreshValues()
}
trackEvent('AccountLogIn')
mixpanel_track('AccountLogIn')
}
const logout = async (id) => {
@@ -130,7 +130,7 @@ const logout = async (id) => {
} else {
emit('change')
}
trackEvent('AccountLogOut')
mixpanel_track('AccountLogOut')
}
let showCard = ref(false)

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/plugin-dialog'
import { open } from '@tauri-apps/api/dialog'
import { add_project_from_path } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router'

View File

@@ -5,10 +5,10 @@ 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'
const errorModal = ref()
const error = ref()
@@ -85,7 +85,7 @@ async function loginMinecraft() {
await set_default_user(loggedIn.id).catch(handleError)
}
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
await mixpanel.track('AccountLogIn')
loadingMinecraft.value = false
errorModal.value.hide()
} catch (err) {

View File

@@ -4,7 +4,7 @@ import { Button, Checkbox, Modal } 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/plugin-dialog'
import { open } from '@tauri-apps/api/dialog'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme'

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/core'
import { convertFileSrc } from '@tauri-apps/api/tauri'
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
trackEvent('InstancePlay', {
mixpanel_track('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)
trackEvent('InstanceStop', {
mixpanel_track('InstanceStop', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,

View File

@@ -211,12 +211,12 @@ import { Avatar, Button, Chips, Modal, 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/plugin-dialog'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/api/dialog'
import { tauri } from '@tauri-apps/api'
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect'
import { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event'
import { install_from_file } from '@/helpers/pack.js'
@@ -264,13 +264,13 @@ defineExpose({
hide()
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
await install_from_file(event.payload[0]).catch(handleError)
trackEvent('InstanceCreate', {
mixpanel_track('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
})
trackEvent('InstanceCreateStart', { source: 'CreationModal' })
mixpanel_track('InstanceCreateStart', { source: 'CreationModal' })
},
})
@@ -360,7 +360,7 @@ const create_instance = async () => {
icon.value,
).catch(handleError)
trackEvent('InstanceCreate', {
mixpanel_track('InstanceCreate', {
profile_name: profile_name.value,
game_version: game_version.value,
loader: loader.value,
@@ -382,7 +382,7 @@ const upload_icon = async () => {
})
if (!icon.value) return
display_icon.value = convertFileSrc(icon.value)
display_icon.value = tauri.convertFileSrc(icon.value)
}
const reset_icon = () => {
@@ -419,7 +419,7 @@ const openFile = async () => {
hide()
await install_from_file(newProject).catch(handleError)
trackEvent('InstanceCreate', {
mixpanel_track('InstanceCreate', {
source: 'CreationModalFileOpen',
})
}

View File

@@ -40,8 +40,8 @@ import { Modal, 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'
import { trackEvent } from '@/helpers/analytics'
const themeStore = useTheming()
@@ -67,7 +67,7 @@ const emit = defineEmits(['submit'])
function setJavaInstall(javaInstall) {
emit('submit', javaInstall)
detectJavaModal.value.hide()
trackEvent('JavaAutoDetect', {
mixpanel_track('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/plugin-dialog'
import { open } from '@tauri-apps/api/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
trackEvent('JavaTest', {
mixpanel_track('JavaTest', {
path: props.modelValue ? props.modelValue.path : '',
success: testingJavaSuccess.value,
})
@@ -136,7 +136,7 @@ async function handleJavaFileInput() {
}
}
trackEvent('JavaManualSelect', {
mixpanel_track('JavaManualSelect', {
path: filePath,
version: props.version,
})
@@ -170,7 +170,7 @@ async function reinstallJava() {
}
}
trackEvent('JavaReInstall', {
mixpanel_track('JavaReInstall', {
path: path,
version: props.version,
})

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)
trackEvent('InstanceStop', {
mixpanel_track('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="() => getCurrent().minimize()">
<button class="btn icon-only transparent" icon-only @click="() => appWindow.minimize()">
<MinimizeIcon />
</button>
<button class="btn icon-only transparent" @click="() => getCurrent().toggleMaximize()">
<button class="btn icon-only transparent" @click="() => appWindow.toggleMaximize()">
<MaximizeIcon />
</button>
<button class="btn icon-only transparent" @click="handleClose">
@@ -85,11 +85,12 @@
import { ref, watch } from 'vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { loading_listener } from '@/helpers/events.js'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { appWindow } from '@tauri-apps/api/window'
import { XIcon } from '@modrinth/assets'
import { 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-apps/plugin-window-state'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import { getOS } from '@/helpers/utils.js'
import { useLoading } from '@/store/loading.js'
@@ -137,8 +138,13 @@ loading_listener(async (e) => {
})
const handleClose = async () => {
await getCurrentWindow().close()
await saveWindowState(StateFlags.ALL)
await TauriWindow.getCurrent().close()
}
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
</script>
<style scoped lang="scss">

View File

@@ -61,7 +61,7 @@ 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 { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
const themeStore = useTheming()
@@ -87,7 +87,7 @@ defineExpose({
incompatibleModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
mixpanel_track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
@@ -98,7 +98,7 @@ const install = async () => {
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
trackEvent('ProjectInstall', {
mixpanel_track('ProjectInstall', {
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,

View File

@@ -3,7 +3,7 @@ import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button, Modal } from '@modrinth/ui'
import { install as pack_install } from '@/helpers/pack'
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js'
import { handleError } from '@/store/state.js'
@@ -25,7 +25,7 @@ defineExpose({
onInstall.value = callback
trackEvent('PackInstallStart')
mixpanel_track('PackInstallStart')
},
})
@@ -39,7 +39,7 @@ async function install() {
project.value.title,
project.value.icon_url,
).catch(handleError)
trackEvent('PackInstall', {
mixpanel_track('PackInstall', {
id: project.value.id,
version_id: versionId.value,
title: project.value.title,

View File

@@ -16,13 +16,13 @@ import {
list,
create,
} from '@/helpers/profile'
import { open } from '@tauri-apps/plugin-dialog'
import { open } from '@tauri-apps/api/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 { convertFileSrc } from '@tauri-apps/api/core'
import { trackEvent } from '@/helpers/analytics'
import { tauri } from '@tauri-apps/api'
const themeStore = useTheming()
const router = useRouter()
@@ -88,7 +88,7 @@ defineExpose({
installModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
mixpanel_track('ProjectInstallStart', { source: 'ProjectInstallModal' })
},
})
@@ -115,7 +115,7 @@ async function install(instance) {
instance.installedMod = true
instance.installing = false
trackEvent('ProjectInstall', {
mixpanel_track('ProjectInstall', {
loader: instance.loader,
game_version: instance.game_version,
id: project.value.id,
@@ -137,7 +137,7 @@ const toggleCreation = () => {
loader.value = null
if (showCreation.value) {
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
mixpanel_track('InstanceCreateStart', { source: 'ProjectInstallModal' })
}
}
@@ -153,7 +153,7 @@ const upload_icon = async () => {
})
if (!icon.value) return
display_icon.value = convertFileSrc(icon.value)
display_icon.value = tauri.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])
trackEvent('InstanceCreate', {
mixpanel_track('InstanceCreate', {
profile_name: name.value,
game_version: versions.value[0].game_versions[0],
loader: loader,
@@ -195,7 +195,7 @@ const createInstance = async () => {
source: 'ProjectInstallModal',
})
trackEvent('ProjectInstall', {
mixpanel_track('ProjectInstall', {
loader: loader,
game_version: versions.value[0].game_versions[0],
id: project.value,

View File

@@ -1,20 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
import cssContent from '@/assets/stylesheets/macFix.css?inline'
export async function useCheckDisableMouseover() {
try {
// Fetch the CSS content from the Rust backend
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
if (should_disable_mouseover) {
// Create a style element and set its content
const styleElement = document.createElement('style')
styleElement.innerHTML = cssContent
// Append the style element to the document's head
document.head.appendChild(styleElement)
}
} catch (error) {
console.error('Error checking OS version from Rust backend', error)
}
}

View File

@@ -1,23 +0,0 @@
import { posthog } from 'posthog-js'
export const initAnalytics = () => {
posthog.init('phc_hm2ihMpTAoE86xIm7XzsCB8RPiTRKivViK5biiHedm', {
persistence: 'localStorage',
})
}
export const debugAnalytics = () => {
posthog.debug()
}
export const optOutAnalytics = () => {
posthog.opt_out_capturing()
}
export const optInAnalytics = () => {
posthog.opt_in_capturing()
}
export const trackEvent = (eventName, properties) => {
posthog.capture(eventName, properties)
}

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/core'
import { invoke } from '@tauri-apps/api/tauri'
// Example function:
// User goes to auth_url to complete flow, and when completed, authenticate_await_completion() returns the credentials
@@ -13,46 +13,35 @@ import { invoke } from '@tauri-apps/api/core'
// await authenticate_await_completion()
// }
/**
* 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.
*/
/// 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
export async function login() {
return await invoke('plugin:auth|login')
return await invoke('auth_login')
}
/**
* Retrieves the default user
* @return {Promise<UUID | undefined>}
*/
/// Retrieves the default user
/// user is UUID
export async function get_default_user() {
return await invoke('plugin:auth|get_default_user')
return await invoke('plugin:auth|auth_get_default_user')
}
/**
* Updates the default user
* @param {UUID} user
*/
/// Updates the default user
/// user is UUID
export async function set_default_user(user) {
return await invoke('plugin:auth|set_default_user', { user })
return await invoke('plugin:auth|auth_set_default_user', { user })
}
/**
* Remove a user account from the database
* @param {UUID} user
*/
/// Remove a user account from the database
/// user is UUID
export async function remove_user(user) {
return await invoke('plugin:auth|remove_user', { user })
return await invoke('plugin:auth|auth_remove_user', { user })
}
/**
* Returns a list of users
* @returns {Promise<Credential[]>}
*/
/// Returns a list of users
/// Returns an Array of Credentials
export async function users() {
return await invoke('plugin:auth|get_users')
return await invoke('plugin:auth|auth_users')
}

View File

@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@tauri-apps/api/tauri'
export async function get_project(id, cacheBehaviour) {
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/core'
import { invoke } from '@tauri-apps/api/tauri'
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|get_importable_instances', { launcherType, basePath })
return await invoke('plugin:import|import_get_importable_instances', { launcherType, basePath })
}
/// Import an instance from a launcher type and base path
@@ -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_instance', {
return await invoke('plugin:import|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|is_valid_importable_instance', {
return await invoke('plugin:import|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|get_default_launcher_path', { launcherType })
return await invoke('plugin:import|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/core'
import { invoke } from '@tauri-apps/api/tauri'
/*

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/core'
import { invoke } from '@tauri-apps/api/tauri'
/*
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/core'
import { invoke } from '@tauri-apps/api/tauri'
/// Gets the game versions from daedalus
// Returns a VersionManifest

View File

@@ -0,0 +1,57 @@
import mixpanel from 'mixpanel-browser'
// mixpanel_track
function trackWrapper(originalTrack) {
return function (event_name, properties = {}) {
try {
originalTrack(event_name, properties)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_track = trackWrapper(mixpanel.track.bind(mixpanel))
// mixpanel_opt_out_tracking()
function optOutTrackingWrapper(originalOptOutTracking) {
return function () {
try {
originalOptOutTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_out_tracking = optOutTrackingWrapper(
mixpanel.opt_out_tracking.bind(mixpanel),
)
// mixpanel_opt_in_tracking()
function optInTrackingWrapper(originalOptInTracking) {
return function () {
try {
originalOptInTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_in_tracking = optInTrackingWrapper(
mixpanel.opt_in_tracking.bind(mixpanel),
)
// mixpanel_init
function initWrapper(originalInit) {
return function (token, config = {}) {
try {
originalInit(token, config)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_init = initWrapper(mixpanel.init.bind(mixpanel))
export const mixpanel_is_loaded = () => {
return mixpanel.__loaded
}

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/core'
import { invoke } from '@tauri-apps/api/tauri'
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/core'
import { invoke } from '@tauri-apps/api/tauri'
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/core'
import { invoke } from '@tauri-apps/api/tauri'
/// 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/core'
import { invoke } from '@tauri-apps/api/tauri'
/// Add instance
/*
@@ -19,7 +19,7 @@ import { invoke } from '@tauri-apps/api/core'
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/core'
import { invoke } from '@tauri-apps/api/tauri'
// 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/core'
import { invoke } from '@tauri-apps/api/tauri'
// 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/core'
import { invoke } from '@tauri-apps/api/tauri'
// 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/core'
import { invoke } from '@tauri-apps/api/tauri'
export async function isDev() {
return await invoke('is_dev')

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,17 +27,10 @@ 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

@@ -0,0 +1,27 @@
import { invoke } from '@tauri-apps/api/tauri'
import cssContent from '@/assets/stylesheets/macFix.css?inline'
export default {
async mounted() {
await this.checkDisableMouseover()
},
methods: {
async checkDisableMouseover() {
try {
// Fetch the CSS content from the Rust backend
const should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
if (should_disable_mouseover) {
// Create a style element and set its content
const styleElement = document.createElement('style')
styleElement.innerHTML = cssContent
// Append the style element to the document's head
document.head.appendChild(styleElement)
}
} catch (error) {
console.error('Error checking OS version from Rust backend', error)
}
},
},
}

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/core'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { get_search_results } from '@/helpers/cache.js'
import { debounce } from '@/helpers/utils.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'

View File

@@ -8,8 +8,8 @@ import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/j
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
import { optOutAnalytics, optInAnalytics } from '@/helpers/analytics'
import { open } from '@tauri-apps/plugin-dialog'
import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
import { open } from '@tauri-apps/api/dialog'
import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app'
import { get_user, purge_cache_types } from '@/helpers/cache.js'
@@ -45,9 +45,9 @@ watch(
const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.telemetry) {
optInAnalytics()
mixpanel_opt_out_tracking()
} else {
optOutAnalytics()
mixpanel_opt_in_tracking()
}
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)

View File

@@ -131,8 +131,9 @@ 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 { trackEvent } from '@/helpers/analytics'
import { convertFileSrc } from '@tauri-apps/api/core'
import { mixpanel_track } from '@/helpers/mixpanel'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useFetch } from '@/helpers/fetch'
import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs'
@@ -182,7 +183,7 @@ const startInstance = async (context) => {
}
loading.value = false
trackEvent('InstanceStart', {
mixpanel_track('InstanceStart', {
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
@@ -219,7 +220,7 @@ const stopInstance = async (context) => {
playing.value = false
await kill(route.params.id).catch(handleError)
trackEvent('InstanceStop', {
mixpanel_track('InstanceStop', {
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,

View File

@@ -379,7 +379,7 @@ import {
update_project,
} from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
import { listen } from '@tauri-apps/api/event'
import { highlightModInProfile } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
@@ -682,7 +682,7 @@ const updateAll = async () => {
projects.value[project].updating = false
}
trackEvent('InstanceUpdateAll', {
mixpanel_track('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
trackEvent('InstanceProjectUpdate', {
mixpanel_track('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
@@ -735,7 +735,7 @@ const toggleDisableMod = async (mod) => {
.then((newPath) => {
mod.path = newPath
mod.disabled = !mod.disabled
trackEvent('InstanceProjectDisable', {
mixpanel_track('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)
trackEvent('InstanceProjectRemove', {
mixpanel_track('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,

View File

@@ -541,15 +541,15 @@ 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/core'
import { open } from '@tauri-apps/plugin-dialog'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/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'
const breadcrumbs = useBreadcrumbs()
@@ -590,7 +590,7 @@ const availableGroups = ref([
async function resetIcon() {
icon.value = null
await edit_icon(props.instance.path, null).catch(handleError)
trackEvent('InstanceRemoveIcon')
mixpanel_track('InstanceRemoveIcon')
}
async function setIcon() {
@@ -609,7 +609,7 @@ async function setIcon() {
icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError)
trackEvent('InstanceSetIcon')
mixpanel_track('InstanceSetIcon')
}
const globalSettings = await get().catch(handleError)
@@ -754,7 +754,7 @@ const repairing = ref(false)
async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError)
trackEvent('InstanceDuplicate', {
mixpanel_track('InstanceDuplicate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@@ -765,7 +765,7 @@ async function repairProfile(force) {
await install(props.instance.path, force).catch(handleError)
repairing.value = false
trackEvent('InstanceRepair', {
mixpanel_track('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@@ -796,7 +796,7 @@ async function repairModpack() {
await update_repair_modrinth(props.instance.path).catch(handleError)
inProgress.value = false
trackEvent('InstanceRepair', {
mixpanel_track('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
@@ -808,7 +808,7 @@ async function removeProfile() {
await remove(props.instance.path).catch(handleError)
removing.value = false
trackEvent('InstanceRemove', {
mixpanel_track('InstanceRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})

View File

@@ -93,7 +93,7 @@ import {
} from '@modrinth/assets'
import { Button, Card } from '@modrinth/ui'
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { mixpanel_track } from '@/helpers/mixpanel'
const props = defineProps({
project: {
@@ -112,7 +112,7 @@ const nextImage = () => {
expandedGalleryIndex.value = 0
}
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
trackEvent('GalleryImageNext', {
mixpanel_track('GalleryImageNext', {
project_id: props.project.id,
url: expandedGalleryItem.value.url,
})
@@ -124,7 +124,7 @@ const previousImage = () => {
expandedGalleryIndex.value = props.project.gallery.length - 1
}
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
trackEvent('GalleryImagePrevious', {
mixpanel_track('GalleryImagePrevious', {
project_id: props.project.id,
url: expandedGalleryItem.value,
})
@@ -135,7 +135,7 @@ const expandImage = (item, index) => {
expandedGalleryIndex.value = index
zoomedIn.value = false
trackEvent('GalleryImageExpand', {
mixpanel_track('GalleryImageExpand', {
project_id: props.project.id,
url: item.url,
})

View File

@@ -257,7 +257,7 @@ 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/core'
import { convertFileSrc } from '@tauri-apps/api/tauri'
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'

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')?.scrollTo(0, 0)
document.querySelector('.router-view').scrollTop = 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 { trackEvent } from '@/helpers/analytics.js'
import { mixpanel_track } from '@/helpers/mixpanel.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)
trackEvent('PackInstall', {
mixpanel_track('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)
trackEvent('ProjectInstall', {
mixpanel_track('ProjectInstall', {
loader: instance.loader,
game_version: instance.game_version,
id: project.id,

View File

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

View File

@@ -10,13 +10,14 @@ theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = "2.0.0-rc.4"
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

@@ -1,14 +1,17 @@
[package]
name = "theseus_gui"
version = "0.8.3-1"
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/"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
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 = "2.0.0-rc", features = ["codegen"] }
tauri-build = { version = "1.5.3", features = [] }
[dependencies]
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
@@ -16,16 +19,14 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-rc.6", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
tauri-plugin-window-state = "2.0.0-rc"
tauri-plugin-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.1", optional = true }
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"
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"
@@ -55,7 +56,6 @@ window-shadows = "0.2.1"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"
objc = "0.2.7"
rand = "0.8.5"
[features]
# by default Tauri runs in production mode
@@ -64,4 +64,3 @@ 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 = ["dep:tauri-plugin-updater"]

View File

@@ -6,17 +6,63 @@
<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>
<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>
<!-- 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>
</dict>
</plist>

View File

@@ -1,223 +1,4 @@
use tauri_build::{DefaultPermissionRule, InlinedPlugin};
fn main() {
// 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_versions",
"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,
),
),
)
.expect("Failed to run tauri-build");
// Build the Tauri app
tauri_build::build();
}

View File

@@ -1,30 +0,0 @@
{
"identifier": "core",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"core:path:default",
"core:event:default",
"core:window:default",
"core:app:default",
"core:resources:default",
"core:menu:default",
"core:tray:default",
"core:window:allow-create",
"core:window:allow-maximize",
"core:window:allow-toggle-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-start-dragging",
"core:webview:allow-set-webview-zoom"
]
}

View File

@@ -1,40 +0,0 @@
{
"identifier": "plugins",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"dialog:allow-open",
"dialog:allow-confirm",
"shell:allow-open",
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"deep-link:default",
"window-state:default",
"window-state:allow-restore-state",
"window-state:allow-save-window-state",
"auth:default",
"import:default",
"jre:default",
"logs:default",
"metadata:default",
"mr-auth:default",
"profile-create:default",
"pack:default",
"process:default",
"profile:default",
"cache:default",
"settings:default",
"tags:default",
"utils:default"
]
}

File diff suppressed because one or more lines are too long

View File

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

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

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

View File

@@ -1,17 +1,16 @@
use crate::api::Result;
use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin;
use tauri::{Manager, Runtime, UserAttentionType};
use tauri::{Manager, UserAttentionType};
use theseus::prelude::*;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("auth")
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("auth")
.invoke_handler(tauri::generate_handler![
login,
remove_user,
get_default_user,
set_default_user,
get_users,
auth_get_default_user,
auth_set_default_user,
auth_remove_user,
auth_users,
])
.build()
}
@@ -19,21 +18,19 @@ pub fn init<R: 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 login<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<Option<Credentials>> {
pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
let flow = minecraft_auth::begin_login().await?;
let start = Utc::now();
if let Some(window) = app.get_webview_window("signin") {
if let Some(window) = app.get_window("signin") {
window.close()?;
}
let window = tauri::WebviewWindowBuilder::new(
let window = tauri::WindowBuilder::new(
&app,
"signin",
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err(
tauri::WindowUrl::External(flow.redirect_uri.parse().map_err(
|_| {
theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(),
@@ -56,12 +53,12 @@ pub async fn login<R: Runtime>(
}
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 =
@@ -78,22 +75,23 @@ pub async fn login<R: Runtime>(
Ok(None)
}
#[tauri::command]
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::remove_user(user).await?)
}
#[tauri::command]
pub async fn get_default_user() -> Result<Option<uuid::Uuid>> {
pub async fn auth_get_default_user() -> Result<Option<uuid::Uuid>> {
Ok(minecraft_auth::get_default_user().await?)
}
#[tauri::command]
pub async fn set_default_user(user: uuid::Uuid) -> Result<()> {
pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
Ok(minecraft_auth::set_default_user(user).await?)
}
/// Get a copy of the list of all user credentials
// invoke('plugin:auth|auth_users',user)
#[tauri::command]
pub async fn get_users() -> Result<Vec<Credentials>> {
pub async fn auth_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![
get_importable_instances,
import_instance,
is_valid_importable_instance,
get_default_launcher_path,
import_get_importable_instances,
import_import_instance,
import_is_valid_importable_instance,
import_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 get_importable_instances(
pub async fn import_get_importable_instances(
launcher_type: ImportLauncherType,
base_path: PathBuf,
) -> Result<Vec<String>> {
@@ -31,7 +31,7 @@ pub async fn get_importable_instances(
/// profile_path should be a blank profile for this purpose- if the function fails, it will be deleted
/// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
#[tauri::command]
pub async fn import_instance(
pub async fn import_import_instance(
profile_path: &str,
launcher_type: ImportLauncherType,
base_path: PathBuf,
@@ -50,7 +50,7 @@ pub async fn import_instance(
/// Checks if this instance is valid for importing, given a certain launcher type
/// eg: is_valid_importable_instance(PathBuf::from("C:/MultiMC/Instance 1"), ImportLauncherType::MultiMC)
#[tauri::command]
pub async fn is_valid_importable_instance(
pub async fn import_is_valid_importable_instance(
instance_folder: PathBuf,
launcher_type: ImportLauncherType,
) -> Result<bool> {
@@ -63,7 +63,7 @@ pub async fn 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 get_default_launcher_path(
pub async fn import_get_default_launcher_path(
launcher_type: ImportLauncherType,
) -> Result<Option<PathBuf>> {
Ok(import::get_default_launcher_path(launcher_type))

View File

@@ -39,6 +39,10 @@ pub enum TheseusSerializableError {
#[error("Tauri error: {0}")]
Tauri(#[from] tauri::Error),
#[cfg(target_os = "macos")]
#[error("Callback error: {0}")]
Callback(String),
}
// Generic implementation of From<T> for ErrorTypeA
@@ -86,6 +90,14 @@ macro_rules! impl_serialize {
}
// Use the macro to implement Serialize for TheseusSerializableError
#[cfg(target_os = "macos")]
impl_serialize! {
IO,
Tauri,
Callback
}
#[cfg(not(target_os = "macos"))]
impl_serialize! {
IO,
Tauri,

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_webview_window("modrinth-signin") {
if let Some(window) = app.get_window("modrinth-signin") {
window.close()?;
}
let window = tauri::WebviewWindowBuilder::new(
let window = tauri::WindowBuilder::new(
&app,
"modrinth-signin",
tauri::WebviewUrl::External(redirect_uri.parse().map_err(|_| {
tauri::WindowUrl::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,6 +137,5 @@ pub async fn get_opening_command() -> Result<Option<CommandPayload>> {
// helper function called when redirected by a weblink (ie: modrith://do-something) or when redirected by a .mrpack file (in which case its a filepath)
// 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,28 +1,6 @@
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
}

View File

@@ -0,0 +1,98 @@
use cocoa::{
base::{id, nil},
foundation::NSAutoreleasePool,
};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use once_cell::sync::OnceCell;
use crate::api::TheseusSerializableError;
type Callback = OnceCell<Box<dyn Fn(String) + Send + Sync + 'static>>;
static CALLBACK: Callback = OnceCell::new();
pub struct AppDelegateClass(pub *const Class);
unsafe impl Send for AppDelegateClass {}
unsafe impl Sync for AppDelegateClass {}
// Obj C class for the app delegate
// This inherits from the TaoAppDelegate (used by tauri) so we do not accidentally override any functionality
// The application_open_file method is the only method we override, as it is currently unimplemented in tauri
lazy_static::lazy_static! {
pub static ref THESEUS_APP_DELEGATE_CLASS: AppDelegateClass = unsafe {
let superclass = class!(TaoAppDelegate);
let mut decl = ClassDecl::new("TheseusAppDelegate", superclass).unwrap();
// Add the method to the class
decl.add_method(
sel!(application:openFile:),
application_open_file as extern "C" fn(&Object, Sel, id, id) -> bool,
);
// Other methods are inherited
AppDelegateClass(decl.register())
};
}
extern "C" fn application_open_file(
_: &Object,
_: Sel,
_: id,
file: id,
) -> bool {
let file = nsstring_to_string(file);
callback(file)
}
pub fn callback(file: String) -> bool {
if let Some(callback) = CALLBACK.get() {
callback(file);
true
} else {
false
}
}
pub fn register_open_file<T>(
callback: T,
) -> Result<(), TheseusSerializableError>
where
T: Fn(String) + Send + Sync + 'static,
{
unsafe {
// Modified from tao: https://github.com/tauri-apps/tao
// sets the current app delegate to be the inherited app delegate rather than the default tauri/tao one
let app: id = msg_send![class!(TaoApp), sharedApplication];
let delegate: id = msg_send![THESEUS_APP_DELEGATE_CLASS.0, new];
let pool = NSAutoreleasePool::new(nil);
let _: () = msg_send![app, setDelegate: delegate];
let _: () = msg_send![pool, drain];
}
CALLBACK.set(Box::new(callback)).map_err(|_| {
TheseusSerializableError::Callback("Callback already set".to_string())
})
}
/// Convert an NSString to a Rust `String`
/// From 'fruitbasket' https://github.com/mrmekon/fruitbasket/
#[allow(clippy::cmp_null)]
pub fn nsstring_to_string(nsstring: *mut Object) -> String {
unsafe {
let cstr: *const i8 = msg_send![nsstring, UTF8String];
if cstr != std::ptr::null() {
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
} else {
"".into()
}
}
}

View File

@@ -1,2 +1,3 @@
pub mod deep_link;
pub mod delegate;
pub mod window_ext;

View File

@@ -1,412 +1,70 @@
// Stolen from https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9
use objc::{msg_send, sel, sel_impl};
use rand::{distributions::Alphanumeric, Rng};
use tauri::{
plugin::{Builder, TauriPlugin},
Emitter, Runtime, Window,
}; // 0.8
/// from: https://github.com/tauri-apps/tauri/issues/4789, full credit to haasal
#[cfg(target_os = "macos")]
use tauri::{Runtime, Window};
const WINDOW_CONTROL_PAD_X: f64 = 9.0;
const WINDOW_CONTROL_PAD_Y: f64 = 16.0;
struct UnsafeWindowHandle(*mut std::ffi::c_void);
unsafe impl Send for UnsafeWindowHandle {}
unsafe impl Sync for UnsafeWindowHandle {}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("traffic_light_positioner")
.on_window_ready(|window| {
#[cfg(target_os = "macos")]
setup_traffic_light_positioner(window);
})
.build()
#[cfg(target_os = "macos")]
pub trait WindowExt {
fn set_transparent_titlebar(&self, transparent: bool);
fn position_traffic_lights(&self, x: f64, y: f64);
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(
ns_window_handle: UnsafeWindowHandle,
x: f64,
y: f64,
) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let ns_window = ns_window_handle.0 as cocoa::base::id;
unsafe {
let close = ns_window
.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = ns_window
.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom =
ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
impl<R: Runtime> WindowExt for Window<R> {
fn set_transparent_titlebar(&self, transparent: bool) {
use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};
let window = self.ns_window().unwrap() as cocoa::base::id;
let title_bar_container_view = close.superview().superview();
unsafe {
window.setTitleVisibility_(
NSWindowTitleVisibility::NSWindowTitleHidden,
);
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
if transparent {
window.setTitlebarAppearsTransparent_(cocoa::base::YES);
} else {
window.setTitlebarAppearsTransparent_(cocoa::base::NO);
}
}
}
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y =
NSView::frame(ns_window).size.height - title_bar_frame_height;
let _: () =
msg_send![title_bar_container_view, setFrame: title_bar_rect];
fn position_traffic_lights(&self, x: f64, y: f64) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
use objc::{msg_send, sel, sel_impl};
let window_buttons = vec![close, miniaturize, zoom];
let space_between =
NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
let window = self.ns_window().unwrap() as cocoa::base::id;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
unsafe {
let close = window
.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = window.standardWindowButton_(
NSWindowButton::NSWindowMiniaturizeButton,
);
let zoom = window
.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y =
NSView::frame(window).size.height - title_bar_frame_height;
let _: () =
msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x
- NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct WindowState<R: Runtime> {
window: Window<R>,
}
#[cfg(target_os = "macos")]
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
use cocoa::appkit::NSWindow;
use cocoa::base::{id, BOOL};
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use std::ffi::c_void;
// Do the initial positioning
position_traffic_lights(
UnsafeWindowHandle(
window.ns_window().expect("Failed to create window handle"),
),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
// Ensure they stay in place while resizing the window.
fn with_window_state<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>(
this: &Object,
func: F,
) {
let ptr = unsafe {
let x: *mut c_void = *this.get_ivar("app_box");
&mut *(x as *mut WindowState<R>)
};
func(ptr);
}
unsafe {
let ns_win = window
.ns_window()
.expect("NS Window should exist to mount traffic light delegate.")
as id;
let current_delegate: id = ns_win.delegate();
extern "C" fn on_window_should_close(
this: &Object,
_cmd: Sel,
sender: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, windowShouldClose: sender]
}
}
extern "C" fn on_window_will_close(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillClose: notification];
}
}
extern "C" fn on_window_did_resize<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
let id = state.window.ns_window().expect(
"NS window should exist on state to handle resize",
) as id;
#[cfg(target_os = "macos")]
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResize: notification];
}
}
extern "C" fn on_window_did_move(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidMove: notification];
}
}
extern "C" fn on_window_did_change_backing_properties(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
}
}
extern "C" fn on_window_did_become_key(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, windowDidBecomeKey: notification];
}
}
extern "C" fn on_window_did_resign_key(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, windowDidResignKey: notification];
}
}
extern "C" fn on_dragging_entered(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, draggingEntered: notification]
}
}
extern "C" fn on_prepare_for_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, prepareForDragOperation: notification]
}
}
extern "C" fn on_perform_drag_operation(
this: &Object,
_cmd: Sel,
sender: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, performDragOperation: sender]
}
}
extern "C" fn on_conclude_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, concludeDragOperation: notification];
}
}
extern "C" fn on_dragging_exited(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, draggingExited: notification];
}
}
extern "C" fn on_window_will_use_full_screen_presentation_options(
this: &Object,
_cmd: Sel,
window: id,
proposed_options: NSUInteger,
) -> NSUInteger {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
}
}
extern "C" fn on_window_did_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
}
}
extern "C" fn on_window_will_enter_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-enter-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
}
}
extern "C" fn on_window_did_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-exit-fullscreen", ())
.expect("Failed to emit event");
let id =
state.window.ns_window().expect("Failed to emit event")
as id;
position_traffic_lights(
UnsafeWindowHandle(id as *mut std::ffi::c_void),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () =
msg_send![super_del, windowDidExitFullScreen: notification];
}
}
extern "C" fn on_window_will_exit_full_screen<R: Runtime>(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-exit-fullscreen", ())
.expect("Failed to emit event");
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
}
}
extern "C" fn on_window_did_fail_to_enter_full_screen(
this: &Object,
_cmd: Sel,
window: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
}
}
extern "C" fn on_effective_appearance_did_change(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
}
}
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![
super_del,
effectiveAppearanceDidChangedOnMainThread: notification
];
}
}
// Are we deallocing this properly ? (I miss safe Rust :( )
let window_label = window.label().to_string();
let app_state = WindowState { window };
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
let random_str: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(20)
.map(char::from)
.collect();
// We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate
// delegate with the same name.
let delegate_name =
format!("windowDelegate_{}_{}", window_label, random_str);
ns_win.setDelegate_(delegate!(&delegate_name, {
window: id = ns_win,
app_box: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
}))
}
}

View File

@@ -4,7 +4,8 @@
)]
use native_dialog::{MessageDialog, MessageType};
use tauri::{Listener, Manager};
use tauri::{Manager, PhysicalSize};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use theseus::prelude::*;
mod api;
@@ -13,14 +14,6 @@ mod error;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
// Should be called in launcher initialization
#[tracing::instrument(skip_all)]
#[tauri::command]
@@ -39,7 +32,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
#[tracing::instrument(skip_all)]
#[tauri::command]
fn show_window(app: tauri::AppHandle) {
let win = app.get_webview_window("main").unwrap();
let win = app.get_window("main").unwrap();
if let Err(e) = win.show() {
MessageDialog::new()
.set_type(MessageType::Error)
@@ -52,7 +45,16 @@ fn show_window(app: tauri::AppHandle) {
.unwrap();
panic!("cannot display application window")
} else {
let _ = win.restore_state(StateFlags::all());
let _ = win.set_focus();
// fix issue where window shows as extremely small
if let Ok(size) = win.inner_size() {
let width = if size.width < 1100 { 1280 } else { size.width };
let height = if size.height < 700 { 800 } else { size.height };
let _ = win.set_size(PhysicalSize::new(width, height));
}
}
}
@@ -73,9 +75,17 @@ async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
Ok(())
}
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
cwd: String,
}
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
tauri_plugin_deep_link::prepare("ModrinthApp");
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE
@@ -95,61 +105,73 @@ fn main() {
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
let mut builder = tauri::Builder::default();
#[cfg(feature = "updater")]
{
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
}
builder = builder
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_filename("app-window-state.json")
.build(),
)
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
app.emit_all("single-instance", Payload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_window_state::Builder::default().build())
.setup(|app| {
#[cfg(target_os = "macos")]
{
let payload = macos::deep_link::get_or_init_payload(app);
let res = {
use macos::deep_link::InitialPayload;
let mtx = std::sync::Arc::new(tokio::sync::Mutex::new(None));
let mtx_copy = payload.payload.clone();
app.listen("deep-link://new-url", move |url| {
let mtx_copy_copy = mtx_copy.clone();
let request = url.payload().to_owned();
app.manage(InitialPayload {
payload: mtx.clone(),
});
let actual_request =
serde_json::from_str::<Vec<String>>(&request)
.ok()
.map(|mut x| x.remove(0))
.unwrap_or(request);
let mtx_copy = mtx.clone();
macos::delegate::register_open_file(move |filename| {
let mtx_copy = mtx_copy.clone();
tauri::async_runtime::spawn(async move {
tracing::info!("Handling deep link {actual_request}");
tracing::info!("Handling file open {filename}");
let mut payload = mtx_copy_copy.lock().await;
let mut payload = mtx_copy.lock().await;
if payload.is_none() {
*payload = Some(actual_request.clone());
*payload = Some(filename.clone());
}
let _ =
api::utils::handle_command(actual_request).await;
let _ = api::utils::handle_command(filename).await;
});
});
})
.unwrap();
let mtx_copy = mtx.clone();
tauri_plugin_deep_link::register(
"modrinth",
move |request: String| {
let mtx_copy = mtx_copy.clone();
tauri::async_runtime::spawn(async move {
tracing::info!("Handling deep link {request}");
let mut payload = mtx_copy.lock().await;
if payload.is_none() {
*payload = Some(request.clone());
}
let _ = api::utils::handle_command(request).await;
});
},
)
};
#[cfg(not(target_os = "macos"))]
app.listen("deep-link://new-url", |url| {
let payload = url.payload().to_owned();
tracing::info!("Handling deep link {payload}");
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
));
dbg!(url);
});
let res = tauri_plugin_deep_link::register(
"modrinth",
|request: String| {
tracing::info!("Handling deep link {request}");
tauri::async_runtime::spawn(api::utils::handle_command(
request,
));
},
);
if let Err(e) = res {
tracing::error!("Error registering deep link handler: {}", e);
}
if let Some(window) = app.get_window("main") {
// Hide window to prevent white flash on startup
@@ -157,14 +179,34 @@ fn main() {
#[cfg(not(target_os = "linux"))]
{
window.set_shadow(true).unwrap();
use window_shadows::set_shadow;
set_shadow(&window, true).unwrap();
}
#[cfg(target_os = "macos")]
{
use macos::window_ext::WindowExt;
window.set_transparent_titlebar(true);
window.position_traffic_lights(9.0, 16.0);
}
}
Ok(())
});
builder = builder
#[cfg(target_os = "macos")]
{
use tauri::WindowEvent;
builder = builder.on_window_event(|e| {
use macos::window_ext::WindowExt;
if let WindowEvent::Resized(..) = e.event() {
let win = e.window();
win.position_traffic_lights(9.0, 16.0);
}
})
}
let builder = builder
.plugin(api::auth::init())
.plugin(api::mr_auth::init())
.plugin(api::import::init())
@@ -183,77 +225,39 @@ fn main() {
initialize_state,
is_dev,
toggle_decorations,
api::auth::auth_login,
api::mr_auth::modrinth_auth_login,
show_window,
]);
#[cfg(target_os = "macos")]
{
builder = builder.plugin(macos::window_ext::init());
}
let app = builder.build(tauri::generate_context!());
match app {
Ok(app) => {
#[allow(unused_variables)]
app.run(|app, event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
tracing::info!("Handling webview open {urls:?}");
let file = urls
.into_iter()
.filter_map(|url| url.to_file_path().ok())
.next();
if let Some(file) = file {
let payload =
macos::deep_link::get_or_init_payload(app);
let mtx_copy = payload.payload.clone();
let request = file.to_string_lossy().to_string();
tauri::async_runtime::spawn(async move {
let mut payload = mtx_copy.lock().await;
if payload.is_none() {
*payload = Some(request.clone());
}
let _ = api::utils::handle_command(request).await;
});
}
}
});
}
Err(e) => {
#[cfg(target_os = "windows")]
if let Err(e) = builder.run(tauri::generate_context!()) {
#[cfg(target_os = "windows")]
{
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
if format!("{:?}", e)
.contains("Runtime(CreateWebview(WebView2Error(WindowsError")
{
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
if format!("{:?}", e).contains(
"Runtime(CreateWebview(WebView2Error(WindowsError",
) {
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://docs.modrinth.com/faq/app/webview2")
.show_alert()
.unwrap();
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://docs.modrinth.com/faq/app/webview2")
.show_alert()
.unwrap();
panic!("webview2 initialization failed")
}
panic!("webview2 initialization failed")
}
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text(&format!(
"Cannot initialize application due to an error:\n{:?}",
e
))
.show_alert()
.unwrap();
panic!("{1}: {:?}", e, "error while running tauri application")
}
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Initialization error")
.set_text(&format!(
"Cannot initialize application due to an error:\n{:?}",
e
))
.show_alert()
.unwrap();
panic!("{1}: {:?}", e, "error while running tauri application")
}
}

View File

@@ -1,14 +1,10 @@
{
"bundle": {
"createUpdaterArtifacts": "v1Compatible"
},
"build": {
"features": ["updater"]
},
"plugins": {
"tauri": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
"endpoints": ["https://launcher-files.modrinth.com/updates.json"]
"active": true,
"endpoints": ["https://launcher-files.modrinth.com/updates.json"],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK"
}
}
}

View File

@@ -2,65 +2,83 @@
"build": {
"beforeDevCommand": "pnpm turbo run dev --filter=@modrinth/app-frontend",
"beforeBuildCommand": "pnpm turbo run build --filter=@modrinth/app-frontend",
"frontendDist": "../app-frontend/dist",
"devUrl": "http://localhost:1420"
"devPath": "http://localhost:1420",
"distDir": "../app-frontend/dist",
"withGlobalTauri": false
},
"bundle": {
"active": true,
"category": "Game",
"copyright": "",
"targets": "all",
"externalBin": [],
"icon": [
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com",
"wix": {
"template": "./msi/main.wxs"
"package": {
"productName": "Modrinth App",
"version": "0.8.3-1"
},
"tauri": {
"allowlist": {
"dialog": {
"confirm": true,
"open": true
},
"protocol": {
"asset": true,
"assetScope": []
},
"shell": {
"open": true
},
"window": {
"create": true,
"close": true,
"hide": true,
"show": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true,
"setDecorations": true
},
"os": {
"all": true
},
"app": {
"all": true
}
},
"longDescription": "",
"macOS": {
"entitlements": "App.entitlements",
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"linux": {
"macOSPrivateApi": true,
"bundle": {
"active": true,
"category": "Game",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"identifier": "ModrinthApp",
"longDescription": "",
"macOS": {
"entitlements": "App.entitlements",
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com",
"wix": {
"template": "./msi/main.wxs"
}
}
},
"fileAssociations": [
{
"ext": ["mrpack"],
"mimeType": "application/zip+mrpack"
}
]
},
"productName": "Modrinth App",
"version": "0.8.3-1",
"identifier": "ModrinthApp",
"plugins": {
"deep-link": {
"desktop": {
"schemes": ["modrinth"]
},
"mobile": []
}
},
"app": {
"withGlobalTauri": false,
"macOSPrivateApi": true,
"security": {
"csp": "default-src 'self'; connect-src https://modrinth.com https://*.modrinth.com https://mixpanel.com https://*.mixpanel.com https://*.cloudflare.com https://api.mclo.gs; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src 'unsafe-inline' 'self'"
},
"updater": {
"active": false
},
"windows": [
{
"titleBarStyle": "Overlay",
@@ -73,20 +91,8 @@
"minHeight": 700,
"minWidth": 1100,
"visible": false,
"zoomHotkeysEnabled": true,
"decorations": false
}
],
"security": {
"assetProtocol": {
"scope": [
"$APPDATA/caches/icons/*",
"$APPCONFIG/caches/icons/*",
"$CONFIG/caches/icons/*"
],
"enable": true
},
"csp": "default-src 'self'; connect-src ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src unsafe-inline 'self'"
}
]
}
}

View File

@@ -1,5 +1,5 @@
{
"app": {
"tauri": {
"windows": [
{
"titleBarStyle": "Overlay",
@@ -12,7 +12,6 @@
"minHeight": 700,
"minWidth": 1100,
"visible": false,
"zoomHotkeysEnabled": true,
"decorations": true
}
]

View File

@@ -1,2 +0,0 @@
BASE_URL=https://api.modrinth.com/v2/
BROWSER_BASE_URL=https://api.modrinth.com/v2/

View File

@@ -625,17 +625,17 @@ tr.button-transparent {
.danger-button {
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
--text-color: var(--color-accent-contrast);
}
.moderation-button {
--background-color: var(--color-orange);
--text-color: var(--color-brand-inverted);
--text-color: var(--color-accent-contrast);
}
.brand-button {
--background-color: var(--color-brand);
--text-color: var(--color-brand-inverted);
--text-color: var(--color-accent-contrast);
}
.alt-brand-button {
@@ -715,7 +715,7 @@ tr.button-transparent {
&:after {
transform: translatex(20px);
background: var(--color-brand-inverted);
background: var(--color-accent-contrast);
}
}

View File

@@ -51,9 +51,6 @@ html {
--color-text-inverted: initial !important;
--color-bg-inverted: initial !important;
--color-brand: var(--color-green) !important;
--color-brand-inverted: initial !important;
--tab-underline-hovered: initial !important;
--color-button-text: initial !important;
@@ -184,6 +181,8 @@ html {
--color-green-bg: rgba(0, 175, 92, 0.1);
--color-blue-bg: rgba(31, 104, 192, 0.1);
--color-purple-bg: rgba(142, 50, 243, 0.1);
--color-purple-highlight: rgba(142, 50, 243, 0.25);
--color-purple-shadow: rgba(142, 50, 243, 0.7);
--color-warning-bg: hsl(355, 70%, 88%);
--color-warning-text: hsl(342, 70%, 35%);
@@ -280,6 +279,8 @@ html {
--color-green-bg: rgba(27, 217, 106, 0.2);
--color-blue-bg: rgba(79, 156, 255, 0.2);
--color-purple-bg: rgba(199, 138, 255, 0.2);
--color-purple-highlight: rgba(177, 101, 255, 0.25);
--color-purple-shadow: rgba(191, 114, 255, 0.7);
--color-brand: var(--color-green);
--color-brand-highlight: rgba(27, 217, 106, 0.25);
@@ -391,6 +392,11 @@ html {
}
.oled-mode {
.experimental-styles-within,
&.experimental-styles-within {
--color-button-bg: #222329;
}
--color-bg: #000000;
--color-raised-bg: #101013;
@@ -405,6 +411,11 @@ html {
}
.retro-mode {
.experimental-styles-within,
&.experimental-styles-within {
--color-button-bg: #3a3b38;
}
--color-bg: #191917;
--color-raised-bg: #1d1e1b;
--color-button-bg: #3a3b38;
@@ -426,12 +437,25 @@ html {
--color-purple: rgb(139, 129, 230);
--color-gray: #718096;
--color-red-highlight: rgba(232, 32, 13, 0.25);
--color-orange-highlight: rgba(232, 141, 13, 0.25);
--color-green-highlight: rgba(60, 219, 54, 0.25);
--color-blue-highlight: rgba(9, 159, 239, 0.25);
--color-red-bg: rgba(232, 32, 13, 0.1);
--color-orange-bg: rgba(232, 141, 13, 0.1);
--color-green-bg: rgba(60, 219, 54, 0.1);
--color-blue-bg: rgba(9, 159, 239, 0.1);
--color-purple-bg: rgba(139, 129, 230, 0.1);
--color-purple-highlight: rgba(139, 129, 230, 0.25);
--color-gray-highlight: rgba(113, 128, 150, 0.25);
--color-purple-shadow: rgba(139, 129, 230, 0.7);
}
.green-mode {
--color-brand: var(--color-green);
--color-brand-highlight: var(--color-green-highlight);
--color-brand-shadow: var(--color-green-shadow);
}
.purple-mode {
--color-brand: var(--color-purple);
--color-brand-highlight: var(--color-purple-highlight);
--color-brand-shadow: var(--color-purple-shadow);
}
body {

View File

@@ -74,20 +74,20 @@
.normal-page {
margin: 0 auto;
max-width: 80rem;
column-gap: 0.75rem;
column-gap: 1rem;
grid-template:
"sidebar content" auto
"info content" auto
"dummy content" 1fr
/ 18.75rem 1fr;
/ 300px 1fr;
&.alt-layout {
grid-template:
"content sidebar" auto
"content info" auto
"content dummy" 1fr
/ 1fr 18.75rem;
/ 1fr 300px;
}
&.no-sidebar {
@@ -106,12 +106,12 @@
}
.normal-page__sidebar {
min-width: 18.75rem;
width: 18.75rem;
min-width: 300px;
width: 300px;
}
.normal-page__content {
max-width: calc(80rem - 18.75rem - 0.75rem);
max-width: calc(80rem - 300px - 1rem);
//overflow-x: hidden;
}
}
@@ -120,8 +120,8 @@
display: grid;
margin: 0 auto;
max-width: 80rem;
column-gap: 0.75rem;
padding: 0 0.75rem;
column-gap: 1rem;
padding: 0 1rem;
grid-template:
"header"
@@ -135,14 +135,14 @@
"header header" auto
"content sidebar" auto
"content dummy" 1fr
/ 1fr 18.75rem;
/ 1fr 300px;
&.alt-layout {
grid-template:
"header header" auto
"sidebar content" auto
"dummy content" 1fr
/ 18.75rem 1fr;
/ 300px 1fr;
}
}
@@ -158,7 +158,7 @@
.normal-page__content {
grid-area: content;
max-width: calc(80rem - 18.75rem - 0.75rem);
max-width: calc(80rem - 300px - 1rem);
//overflow-x: hidden;
}
}

View File

@@ -119,7 +119,7 @@ export default {
}
svg {
color: var(--color-brand-inverted);
color: var(--color-accent-contrast);
stroke-width: 0.2rem;
height: 0.8rem;
width: 0.8rem;

View File

@@ -99,8 +99,8 @@ export default {
border-radius: 0.25rem;
.nav-content {
color: var(--color-button-text-active);
background-color: var(--color-button-bg);
color: var(--color-contrast);
background-color: var(--color-brand-highlight);
box-shadow: none;
}
}

View File

@@ -28,7 +28,7 @@ function stopTimer(notif) {
.vue-notification {
background: var(--color-blue) !important;
border-left: 5px solid var(--color-blue) !important;
color: var(--color-brand-inverted) !important;
color: var(--color-accent-contrast) !important;
box-sizing: border-box;
text-align: left;

View File

@@ -135,7 +135,7 @@ a {
&.page-number.current {
background: var(--color-brand);
color: var(--color-brand-inverted);
color: var(--color-accent-contrast);
cursor: default;
outline: 2px solid transparent;
}

View File

@@ -496,7 +496,7 @@ const submitForReview = async () => {
&.done {
--background-color: var(--color-green);
--content-color: var(--color-brand-inverted);
--content-color: var(--color-accent-contrast);
}
}
}

View File

@@ -1,5 +1,13 @@
<template>
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }">
<div
ref="main_page"
class="layout"
:class="{
'expanded-mobile-nav': isBrowseMenuOpen,
'purple-mode':
cosmetics.accentColor === 'purple' && auth.user && isPermission(auth.user.badges, 1 << 0),
}"
>
<div
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
class="email-nag"

View File

@@ -646,7 +646,7 @@ export default defineNuxtComponent({
.label-button[data-active="true"] {
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
--text-color: var(--color-accent-contrast);
}
.links-modal {

View File

@@ -109,7 +109,7 @@
<p>This data is used to deliver statistics.</p>
<h3>Usage data</h3>
<p>When you interact with the Modrinth App or the Website, we collect through PostHog:</p>
<p>When you interact with the Modrinth App or the Website, we collect through MixPanel:</p>
<ul>
<li>Your IP address</li>
<li>Your anonymized user ID</li>
@@ -150,10 +150,9 @@
<a href="https://www.cloudflare.com/en-gb/gdpr/introduction/"> Cloudflare </a>
</li>
<li><a href="https://sentry.io/trust/privacy/">Sentry</a></li>
<li><a href="https://posthog.com/privacy">PostHog</a></li>
<li><a href="https://mixpanel.com/legal/privacy-policy">MixPanel</a></li>
<li><a href="https://www.beehiiv.com/privacy">BeeHiiv</a></li>
<li><a href="https://www.paypal.com/us/legalhub/privacy-full">PayPal</a></li>
<li><a href="https://stripe.com/privacy">Stripe</a></li>
</ul>
<p>
Data that we specifically collect isn't shared with any other third party. We do not sell any

View File

@@ -654,7 +654,7 @@ const onBulkEditLinks = useClientTry(async () => {
.label-button[data-active="true"] {
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
--text-color: var(--color-accent-contrast);
}
.links-modal {

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="experimental-styles-within flex flex-col gap-6">
<MessageBanner v-if="flags.developerMode" message-type="warning" class="developer-message">
<CodeIcon class="inline-flex" />
<IntlFormatted :message-id="developerModeBanner.description">
@@ -13,14 +13,18 @@
{{ formatMessage(developerModeBanner.deactivate) }}
</Button>
</MessageBanner>
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(colorTheme.title) }}</h2>
<p>{{ formatMessage(colorTheme.description) }}</p>
<div class="theme-options mt-4">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="m-0 text-2xl font-extrabold text-contrast">
{{ formatMessage(colorTheme.title) }}
</h2>
<p class="m-0">{{ formatMessage(colorTheme.description) }}</p>
</div>
<section class="theme-options">
<button
v-for="option in themeOptions"
:key="option"
class="preview-radio button-base"
class="preview-radio transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{ selected: theme.preferred === option }"
@click="() => updateColorTheme(option)"
>
@@ -47,187 +51,153 @@
/>
</div>
</button>
</section>
</div>
<div v-if="auth.user && isPermission(auth.user.badges, 1 << 0)" class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="m-0 flex items-center gap-2 text-2xl font-extrabold text-contrast">
{{ formatMessage(accentColor.title) }}
<span
class="flex items-center justify-center gap-1 rounded-full bg-bg-purple px-2 py-0.5 text-sm font-bold text-purple"
>
<ModrinthIcon class="h-4 w-auto" />
Modrinth+
</span>
</h2>
<p class="m-0">{{ formatMessage(accentColor.description) }}</p>
</div>
</section>
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(projectListLayouts.title) }}</h2>
<p class="mb-4">{{ formatMessage(projectListLayouts.description) }}</p>
<div class="project-lists">
<div v-for="projectType in listTypes" :key="projectType.id + '-project-list-layouts'">
<section class="theme-options">
<button
v-for="option in accentOptions"
:key="option"
class="preview-radio transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{
selected:
cosmetics.accentColor === option || (!cosmetics.accentColor && option === 'green'),
}"
@click="() => (cosmetics.accentColor = option)"
>
<div class="preview" :class="`${option}-mode`">
<TextLogo class="h-8 w-auto text-contrast" />
</div>
<div class="label">
<div class="label__title">
{{
projectListLayouts[projectType.id]
? formatMessage(projectListLayouts[projectType.id])
: projectType.id
}}
</div>
<RadioButtonChecked
v-if="
cosmetics.accentColor === option || (!cosmetics.accentColor && option === 'green')
"
class="radio"
/>
<RadioButtonIcon v-else class="radio" />
{{ accentColor[option] ? formatMessage(accentColor[option]) : option }}
</div>
<div class="project-list-layouts">
<button
class="preview-radio button-base"
:class="{ selected: cosmetics.searchDisplayMode[projectType.id] === 'list' }"
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'list')"
>
<div class="preview">
<div class="layout-list-mode">
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
</div>
</div>
<div class="label">
<RadioButtonChecked
v-if="cosmetics.searchDisplayMode[projectType.id] === 'list'"
class="radio"
/>
<RadioButtonIcon v-else class="radio" />
Rows
</div>
</button>
<button
class="preview-radio button-base"
:class="{ selected: cosmetics.searchDisplayMode[projectType.id] === 'grid' }"
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'grid')"
>
<div class="preview">
<div class="layout-grid-mode">
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
</div>
</div>
<div class="label">
<RadioButtonChecked
v-if="cosmetics.searchDisplayMode[projectType.id] === 'grid'"
class="radio"
/>
<RadioButtonIcon v-else class="radio" />
Grid
</div>
</button>
<button
class="preview-radio button-base"
:class="{ selected: cosmetics.searchDisplayMode[projectType.id] === 'gallery' }"
@click="() => (cosmetics.searchDisplayMode[projectType.id] = 'gallery')"
>
<div class="preview">
<div class="layout-gallery-mode">
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
<div class="example-card card"></div>
</div>
</div>
<div class="label">
<RadioButtonChecked
v-if="cosmetics.searchDisplayMode[projectType.id] === 'gallery'"
class="radio"
/>
<RadioButtonIcon v-else class="radio" />
Gallery
</div>
</button>
</div>
</div>
</button>
</section>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="m-0 text-2xl font-extrabold text-contrast">
{{ formatMessage(toggleFeatures.title) }}
</h2>
<p class="m-0">{{ formatMessage(toggleFeatures.description) }}</p>
</div>
</section>
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(toggleFeatures.title) }}</h2>
<p class="mb-4">{{ formatMessage(toggleFeatures.description) }}</p>
<div class="adjacent-input small">
<label for="advanced-rendering">
<span class="label__title">
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.advancedRenderingDescription) }}
</span>
</label>
<label
class="!m-0 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-1 rounded-2xl border-2 border-solid border-bg-raised px-6 py-4 transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{ 'bg-bg-raised': cosmetics.advancedRendering }"
>
<span class="col-start-1 font-extrabold text-contrast">
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
</span>
<span class="col-start-1">{{
formatMessage(toggleFeatures.advancedRenderingDescription)
}}</span>
<input
id="advanced-rendering"
v-model="cosmetics.advancedRendering"
class="switch stylized-toggle"
class="switch stylized-toggle col-start-2 row-span-2 row-start-1"
type="checkbox"
/>
</div>
<div class="adjacent-input small">
<label for="external-links-new-tab">
<span class="label__title">
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.externalLinksNewTabDescription) }}
</span>
</label>
</label>
<label
class="!m-0 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-1 rounded-2xl border-2 border-solid border-bg-raised px-6 py-4 transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{ 'bg-bg-raised': cosmetics.externalLinksNewTab }"
>
<span class="col-start-1 font-extrabold text-contrast">
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
</span>
<span class="col-start-1">{{
formatMessage(toggleFeatures.externalLinksNewTabDescription)
}}</span>
<input
id="external-links-new-tab"
v-model="cosmetics.externalLinksNewTab"
class="switch stylized-toggle"
class="switch stylized-toggle col-start-2 row-span-2 row-start-1"
type="checkbox"
/>
</div>
<div v-if="false" class="adjacent-input small">
<label for="modrinth-app-promos">
<span class="label__title">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosDescription) }}
</span>
</label>
</label>
<label
v-if="false"
class="!m-0 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-1 rounded-2xl border-2 border-solid border-bg-raised px-6 py-4 transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{ 'bg-bg-raised': cosmetics.hideModrinthAppPromos }"
>
<span class="col-start-1 font-extrabold text-contrast">
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
</span>
<span class="col-start-1">{{
formatMessage(toggleFeatures.hideModrinthAppPromosDescription)
}}</span>
<input
id="modrinth-app-promos"
v-model="cosmetics.hideModrinthAppPromos"
class="switch stylized-toggle"
class="switch stylized-toggle col-start-2 row-span-2 row-start-1"
type="checkbox"
/>
</div>
<div class="adjacent-input small">
<label for="search-layout-toggle">
<span class="label__title">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarDescription) }}
</span>
</label>
</label>
<label
class="!m-0 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-1 rounded-2xl border-2 border-solid border-bg-raised px-6 py-4 transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{ 'bg-bg-raised': cosmetics.rightSearchLayout }"
>
<span class="col-start-1 font-extrabold text-contrast">
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarTitle) }}
</span>
<span class="col-start-1">{{
formatMessage(toggleFeatures.rightAlignedFiltersSidebarDescription)
}}</span>
<input
id="search-layout-toggle"
v-model="cosmetics.rightSearchLayout"
class="switch stylized-toggle"
class="switch stylized-toggle col-start-2 row-span-2 row-start-1"
type="checkbox"
/>
</div>
<div class="adjacent-input small">
<label for="project-layout-toggle">
<span class="label__title">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarDescription) }}
</span>
</label>
</label>
<label
class="!m-0 grid cursor-pointer grid-cols-[1fr_auto] items-center gap-1 rounded-2xl border-2 border-solid border-bg-raised px-6 py-4 transition-transform hover:brightness-110 active:scale-[0.99] active:brightness-125"
:class="{ 'bg-bg-raised': cosmetics.leftContentLayout }"
>
<span class="col-start-1 font-extrabold text-contrast">
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarTitle) }}
</span>
<span class="col-start-1">{{
formatMessage(toggleFeatures.leftAlignedContentSidebarDescription)
}}</span>
<input
id="project-layout-toggle"
v-model="cosmetics.leftContentLayout"
class="switch stylized-toggle"
class="switch stylized-toggle col-start-2 row-span-2 row-start-1"
type="checkbox"
/>
</div>
</section>
</label>
</div>
</div>
</template>
<script setup lang="ts">
import { CodeIcon, MoonIcon, RadioButtonChecked, RadioButtonIcon, SunIcon } from "@modrinth/assets";
import { Button } from "@modrinth/ui";
import {
ModrinthIcon,
CodeIcon,
MoonIcon,
RadioButtonChecked,
RadioButtonIcon,
SunIcon,
} from "@modrinth/assets";
import { TextLogo, Button } from "@modrinth/ui";
import MessageBanner from "~/components/ui/MessageBanner.vue";
import type { DisplayLocation } from "~/plugins/cosmetics";
import type { AccentColor, DisplayLocation } from "~/plugins/cosmetics";
import { formatProjectType } from "~/plugins/shorthands.js";
import { isDarkTheme, type Theme } from "~/plugins/theme/index.ts";
@@ -236,6 +206,7 @@ useHead({
});
const { formatMessage } = useVIntl();
const auth = await useAuth();
const developerModeBanner = defineMessages({
description: {
@@ -288,6 +259,26 @@ const colorTheme = defineMessages({
},
});
const accentColor = defineMessages({
title: {
id: "settings.display.accent.title",
defaultMessage: "Accent color",
},
description: {
id: "settings.display.accent.description",
defaultMessage:
"Thank you for supporting Modrinth! You can choose to make the Modrinth accent color purple if you'd like.",
},
green: {
id: "settings.display.accent.green",
defaultMessage: "Modrinth Green",
},
purple: {
id: "settings.display.accent.purple",
defaultMessage: "Plus Purple",
},
});
const projectListLayouts = defineMessages({
title: {
id: "settings.display.project-list-layouts.title",
@@ -415,6 +406,10 @@ const themeOptions = computed(() => {
return options;
});
const accentOptions = computed(() => {
return ["green", "purple"] as AccentColor[];
});
function updateColorTheme(value: Theme | "system") {
if (value !== "system") {
if (isDarkTheme(value)) {
@@ -462,7 +457,7 @@ const listTypes = computed(() => {
border-radius: var(--radius-md);
padding: 0;
overflow: hidden;
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
background-color: var(--color-button-bg);
color: var(--color-base);
display: flex;
@@ -471,6 +466,8 @@ const listTypes = computed(() => {
&.selected {
color: var(--color-contrast);
background-color: var(--color-brand-highlight);
border-color: var(--color-brand-highlight);
.label {
.radio {

View File

@@ -1,6 +1,7 @@
import type { DarkTheme } from "./theme/index.ts";
export type DisplayMode = "list" | "gallery" | "grid";
export type AccentColor = "green" | "purple";
export type DisplayLocation =
| "mod"
@@ -22,6 +23,7 @@ export interface Cosmetics {
preferredDarkTheme: DarkTheme;
searchDisplayMode: Record<DisplayLocation, DisplayMode>;
hideStagingBanner: boolean;
accentColor: AccentColor;
}
export default defineNuxtPlugin({
@@ -40,6 +42,7 @@ export default defineNuxtPlugin({
externalLinksNewTab: true,
notUsingBlockers: false,
hideModrinthAppPromos: false,
accentColor: "green",
preferredDarkTheme: "dark",
searchDisplayMode: {
mod: "list",

View File

@@ -6,12 +6,12 @@
{
"name": "max_concurrent_writes",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "max_concurrent_downloads",
"ordinal": 1,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "theme",
@@ -26,37 +26,37 @@
{
"name": "collapsed_navigation",
"ordinal": 4,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "advanced_rendering",
"ordinal": 5,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "native_decorations",
"ordinal": 6,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "discord_rpc",
"ordinal": 7,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "developer_mode",
"ordinal": 8,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "telemetry",
"ordinal": 9,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "onboarded",
"ordinal": 10,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "extra_launch_args",
@@ -71,27 +71,27 @@
{
"name": "mc_memory_max",
"ordinal": 13,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "mc_force_fullscreen",
"ordinal": 14,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "mc_game_resolution_x",
"ordinal": 15,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "mc_game_resolution_y",
"ordinal": 16,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "hide_on_process_start",
"ordinal": 17,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "hook_pre_launch",
@@ -121,7 +121,7 @@
{
"name": "migrated",
"ordinal": 23,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM processes WHERE pid = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "1769b7033985bfdd04ee8912d9f28e0d15a8b893db47aca3aec054c7134f1f3f"
}

View File

@@ -11,7 +11,7 @@
{
"name": "active",
"ordinal": 1,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "session_id",
@@ -21,7 +21,7 @@
{
"name": "expires",
"ordinal": 3,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {

View File

@@ -6,7 +6,7 @@
{
"name": "major_version",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "full_version",

View File

@@ -26,7 +26,7 @@
{
"name": "expires",
"ordinal": 4,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n pid, start_time, name, executable, profile_path, post_exit_command\n FROM processes\n WHERE 1=$1",
"describe": {
"columns": [
{
"name": "pid",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "start_time",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "executable",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "profile_path",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "post_exit_command",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "3cac786ad15ef1167bc50ca846d98facb3dee35c9e421209c1161ee7380b7a74"
}

View File

@@ -56,32 +56,32 @@
{
"name": "locked",
"ordinal": 10,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "created",
"ordinal": 11,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "modified",
"ordinal": 12,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "last_played",
"ordinal": 13,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "submitted_time_played",
"ordinal": 14,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "recent_time_played",
"ordinal": 15,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_java_path",
@@ -101,22 +101,22 @@
{
"name": "override_mc_memory_max",
"ordinal": 19,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_mc_force_fullscreen",
"ordinal": 20,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_x",
"ordinal": 21,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_y",
"ordinal": 22,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_hook_pre_launch",

View File

@@ -56,32 +56,32 @@
{
"name": "locked",
"ordinal": 10,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "created",
"ordinal": 11,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "modified",
"ordinal": 12,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "last_played",
"ordinal": 13,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "submitted_time_played",
"ordinal": 14,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "recent_time_played",
"ordinal": 15,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_java_path",
@@ -101,22 +101,22 @@
{
"name": "override_mc_memory_max",
"ordinal": 19,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_mc_force_fullscreen",
"ordinal": 20,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_x",
"ordinal": 21,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_y",
"ordinal": 22,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "override_hook_pre_launch",

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