Compare 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
-3
View File
@@ -13,6 +13,3 @@ max_line_length = 100
[*.md]
max_line_length = off
trim_trailing_whitespace = false
[*.rs]
indent_size = 4
+2 -2
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
+1 -3
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
+27 -30
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') }}"
+2 -2
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
Generated
+899 -1303
View File
File diff suppressed because it is too large Load Diff
+5 -9
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",
+36 -27
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">
@@ -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,
})
@@ -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)
@@ -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'
@@ -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) {
@@ -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'
@@ -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,
@@ -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',
})
}
@@ -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,
})
@@ -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,
})
@@ -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',
@@ -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">
@@ -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,
@@ -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,
@@ -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,
@@ -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)
}
}
@@ -1,23 +0,0 @@
import { posthog } from 'posthog-js'
export const initAnalytics = () => {
posthog.init('phc_hm2ihMpTAoE86xIm7XzsCB8RPiTRKivViK5biiHedm', {
persistence: 'localStorage',
})
}
export const debugAnalytics = () => {
posthog.debug()
}
export const optOutAnalytics = () => {
posthog.opt_out_capturing()
}
export const optInAnalytics = () => {
posthog.opt_in_capturing()
}
export const trackEvent = (eventName, properties) => {
posthog.capture(eventName, properties)
}
+19 -30
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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')
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@tauri-apps/api/tauri'
export async function get_project(id, cacheBehaviour) {
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
+5 -5
View File
@@ -3,7 +3,7 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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 })
}
+1 -1
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'
/*
+1 -1
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:
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { invoke } from '@tauri-apps/api/tauri'
/// Gets the game versions from daedalus
// Returns a VersionManifest
+57
View File
@@ -0,0 +1,57 @@
import mixpanel from 'mixpanel-browser'
// mixpanel_track
function trackWrapper(originalTrack) {
return function (event_name, properties = {}) {
try {
originalTrack(event_name, properties)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_track = trackWrapper(mixpanel.track.bind(mixpanel))
// mixpanel_opt_out_tracking()
function optOutTrackingWrapper(originalOptOutTracking) {
return function () {
try {
originalOptOutTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_out_tracking = optOutTrackingWrapper(
mixpanel.opt_out_tracking.bind(mixpanel),
)
// mixpanel_opt_in_tracking()
function optInTrackingWrapper(originalOptInTracking) {
return function () {
try {
originalOptInTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_in_tracking = optInTrackingWrapper(
mixpanel.opt_in_tracking.bind(mixpanel),
)
// mixpanel_init
function initWrapper(originalInit) {
return function (token, config = {}) {
try {
originalInit(token, config)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_init = initWrapper(mixpanel.init.bind(mixpanel))
export const mixpanel_is_loaded = () => {
return mixpanel.__loaded
}
+6 -6
View File
@@ -3,22 +3,22 @@
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* 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')
}
+1 -1
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
+1 -1
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]
+3 -3
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
+1 -1
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
/*
+1 -1
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
+1 -1
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() {
+1 -1
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')
+2 -9
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')
+27
View File
@@ -0,0 +1,27 @@
import { invoke } from '@tauri-apps/api/tauri'
import cssContent from '@/assets/stylesheets/macFix.css?inline'
export default {
async mounted() {
await this.checkDisableMouseover()
},
methods: {
async checkDisableMouseover() {
try {
// Fetch the CSS content from the Rust backend
const should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
if (should_disable_mouseover) {
// Create a style element and set its content
const styleElement = document.createElement('style')
styleElement.innerHTML = cssContent
// Append the style element to the document's head
document.head.appendChild(styleElement)
}
} catch (error) {
console.error('Error checking OS version from Rust backend', error)
}
},
},
}
+1 -1
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'
+4 -4
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)
@@ -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,
@@ -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,
@@ -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,
})
@@ -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,
})
@@ -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'
+1 -1
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,
+3 -3
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,
@@ -23,3 +23,7 @@ export const handleError = (err) => {
})
console.error(err)
}
export const handleMixpanelError = (err) => {
console.error(err)
}
+2 -1
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"] }
+12 -13
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"]
+51 -5
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>
+2 -221
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();
}
-30
View File
@@ -1,30 +0,0 @@
{
"identifier": "core",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"core:path:default",
"core:event:default",
"core:window:default",
"core:app:default",
"core:resources:default",
"core:menu:default",
"core:tray:default",
"core:window:allow-create",
"core:window:allow-maximize",
"core:window:allow-toggle-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-start-dragging",
"core:webview:allow-set-webview-zoom"
]
}
-40
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
-1
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
+3 -4
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:*"
}
}
+18 -20
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?)
}
+8 -8
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))
+12
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,
+6 -6
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)| {
(
+3 -3
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?;
-1
View File
@@ -137,6 +137,5 @@ pub async fn get_opening_command() -> Result<Option<CommandPayload>> {
// helper function called when redirected by a weblink (ie: modrith://do-something) or when redirected by a .mrpack file (in which case its a filepath)
// 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?)
}
-22
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
}
+98
View File
@@ -0,0 +1,98 @@
use cocoa::{
base::{id, nil},
foundation::NSAutoreleasePool,
};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use once_cell::sync::OnceCell;
use crate::api::TheseusSerializableError;
type Callback = OnceCell<Box<dyn Fn(String) + Send + Sync + 'static>>;
static CALLBACK: Callback = OnceCell::new();
pub struct AppDelegateClass(pub *const Class);
unsafe impl Send for AppDelegateClass {}
unsafe impl Sync for AppDelegateClass {}
// Obj C class for the app delegate
// This inherits from the TaoAppDelegate (used by tauri) so we do not accidentally override any functionality
// The application_open_file method is the only method we override, as it is currently unimplemented in tauri
lazy_static::lazy_static! {
pub static ref THESEUS_APP_DELEGATE_CLASS: AppDelegateClass = unsafe {
let superclass = class!(TaoAppDelegate);
let mut decl = ClassDecl::new("TheseusAppDelegate", superclass).unwrap();
// Add the method to the class
decl.add_method(
sel!(application:openFile:),
application_open_file as extern "C" fn(&Object, Sel, id, id) -> bool,
);
// Other methods are inherited
AppDelegateClass(decl.register())
};
}
extern "C" fn application_open_file(
_: &Object,
_: Sel,
_: id,
file: id,
) -> bool {
let file = nsstring_to_string(file);
callback(file)
}
pub fn callback(file: String) -> bool {
if let Some(callback) = CALLBACK.get() {
callback(file);
true
} else {
false
}
}
pub fn register_open_file<T>(
callback: T,
) -> Result<(), TheseusSerializableError>
where
T: Fn(String) + Send + Sync + 'static,
{
unsafe {
// Modified from tao: https://github.com/tauri-apps/tao
// sets the current app delegate to be the inherited app delegate rather than the default tauri/tao one
let app: id = msg_send![class!(TaoApp), sharedApplication];
let delegate: id = msg_send![THESEUS_APP_DELEGATE_CLASS.0, new];
let pool = NSAutoreleasePool::new(nil);
let _: () = msg_send![app, setDelegate: delegate];
let _: () = msg_send![pool, drain];
}
CALLBACK.set(Box::new(callback)).map_err(|_| {
TheseusSerializableError::Callback("Callback already set".to_string())
})
}
/// Convert an NSString to a Rust `String`
/// From 'fruitbasket' https://github.com/mrmekon/fruitbasket/
#[allow(clippy::cmp_null)]
pub fn nsstring_to_string(nsstring: *mut Object) -> String {
unsafe {
let cstr: *const i8 = msg_send![nsstring, UTF8String];
if cstr != std::ptr::null() {
std::ffi::CStr::from_ptr(cstr)
.to_string_lossy()
.into_owned()
} else {
"".into()
}
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod deep_link;
pub mod delegate;
pub mod window_ext;
+58 -400
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)
}))
}
}
+120 -116
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")
}
}
+5 -9
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"
}
}
}
+71 -65
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'"
}
]
}
}
+1 -2
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
}
]
-2
View File
@@ -1,2 +0,0 @@
BASE_URL=https://api.modrinth.com/v2/
BROWSER_BASE_URL=https://api.modrinth.com/v2/
@@ -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);
}
}
+32 -8
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 {
+11 -11
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;
}
}
+1 -1
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;
@@ -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;
}
}
@@ -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;
@@ -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;
}
@@ -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);
}
}
}
+9 -1
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"
@@ -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 {
+2 -3
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
@@ -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 {
+155 -158
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 {
+3
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",
@@ -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": {
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM processes WHERE pid = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "1769b7033985bfdd04ee8912d9f28e0d15a8b893db47aca3aec054c7134f1f3f"
}
@@ -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": {
@@ -6,7 +6,7 @@
{
"name": "major_version",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "full_version",
@@ -26,7 +26,7 @@
{
"name": "expires",
"ordinal": 4,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -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"
}
@@ -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",
@@ -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