Compare commits

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

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
name: 🌐 Website bug (modrinth.com) name: 🌐 Website bug (modrinth.com)
description: Report an issue on the Modrinth website. description: Report an issue on the Modrinth website.
labels: [bug, frontend] labels: [bug, web]
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
@@ -49,4 +49,4 @@ body:
label: Additional context label: Additional context
description: Add any other context about the problem here. description: Add any other context about the problem here.
validations: validations:
required: false required: false
+3 -1
View File
@@ -9,6 +9,8 @@ body:
options: options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests - label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests
required: true required: true
- label: I checked the [existing discussions](https://github.com/orgs/modrinth/discussions) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com) - label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true required: true
- type: dropdown - type: dropdown
@@ -43,4 +45,4 @@ body:
label: Additional context label: Additional context
description: Add any other context or screenshots about the suggested enhancement here. description: Add any other context or screenshots about the suggested enhancement here.
validations: validations:
required: false required: false
+6 -30
View File
@@ -44,28 +44,7 @@ jobs:
- name: Setup rust cache - name: Setup rust cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: target/**
target/**
!target/*/release/bundle/*/*.dmg
!target/*/release/bundle/*/*.app.tar.gz
!target/*/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.dmg
!target/release/bundle/*/*.app.tar.gz
!target/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.AppImage
!target/release/bundle/*/*.AppImage.tar.gz
!target/release/bundle/*/*.AppImage.tar.gz.sig
!target/release/bundle/*/*.deb
!target/release/bundle/*/*.rpm
!target/release/bundle/msi/*.msi
!target/release/bundle/msi/*.msi.zip
!target/release/bundle/msi/*.msi.zip.sig
!target/release/bundle/nsis/*.exe
!target/release/bundle/nsis/*.nsis.zip
!target/release/bundle/nsis/*.nsis.zip.sig
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-rust-target- ${{ runner.os }}-rust-target-
@@ -99,7 +78,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu') if: startsWith(matrix.platform, 'ubuntu')
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev sqlite3
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install run: pnpm install
@@ -145,11 +124,8 @@ jobs:
target/release/bundle/*/*.AppImage.tar.gz.sig target/release/bundle/*/*.AppImage.tar.gz.sig
target/release/bundle/*/*.deb target/release/bundle/*/*.deb
target/release/bundle/*/*.rpm target/release/bundle/*/*.rpm
target/release/bundle/*/*.msi
target/release/bundle/*/*.msi.zip
target/release/bundle/*/*.msi.zip.sig
target/release/bundle/msi/*.msi
target/release/bundle/msi/*.msi.zip
target/release/bundle/msi/*.msi.zip.sig
target/release/bundle/nsis/*.exe
target/release/bundle/nsis/*.nsis.zip
target/release/bundle/nsis/*.nsis.zip.sig
Generated
+409 -512
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -16,6 +16,3 @@ strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros] [profile.dev.package.sqlx-macros]
opt-level = 3 opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev = "27fb16b" }
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['custom/vue'],
}
-22
View File
@@ -1,22 +0,0 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
import { fixupPluginRules } from '@eslint/compat'
import turboPlugin from 'eslint-plugin-turbo'
export default createConfigForNuxt().append([
{
name: 'turbo',
plugins: {
turbo: fixupPluginRules(turboPlugin),
},
rules: {
'turbo/no-undeclared-env-vars': 'error',
},
},
{
name: 'modrinth',
rules: {
'vue/html-self-closing': 'off',
'vue/multi-word-component-names': 'off',
},
},
])
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modrinth App</title> <title>Modrinth App</title>
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" /> <link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
+9 -15
View File
@@ -1,12 +1,11 @@
{ {
"name": "@modrinth/app-frontend", "name": "@modrinth/app-frontend",
"private": true, "private": true,
"version": "0.8.9", "version": "0.8.3-1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vite build",
"tsc:check": "vue-tsc --noEmit",
"lint": "eslint . && prettier --check .", "lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write ." "fix": "eslint . --fix && prettier --write ."
}, },
@@ -14,41 +13,36 @@
"@modrinth/assets": "workspace:*", "@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*", "@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*", "@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@tauri-apps/api": "^2.0.0-rc.3", "@tauri-apps/api": "^2.0.0-rc.3",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0", "@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0", "@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/plugin-updater": "^2.0.0-rc.0",
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0", "@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@vintl/vintl": "^4.4.1", "@vintl/vintl": "^4.4.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"ofetch": "^1.3.4", "ofetch": "^1.3.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"posthog-js": "^1.158.2",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-multiselect": "3.0.0", "vue-multiselect": "3.0.0",
"vue-router": "4.3.0", "vue-router": "4.3.0",
"vue-virtual-scroller": "v2.0.0-beta.8" "vue-virtual-scroller": "v2.0.0-beta.8",
"posthog-js": "^1.158.2",
"@sentry/vue": "^8.27.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.1.1", "@tauri-apps/cli": "^2.0.0-rc",
"@nuxt/eslint-config": "^0.5.6",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^9.9.1", "eslint": "^8.57.0",
"eslint-config-custom": "workspace:*", "eslint-config-custom": "workspace:*",
"eslint-plugin-turbo": "^2.1.1",
"postcss": "^8.4.39", "postcss": "^8.4.39",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"sass": "^1.74.1", "sass": "^1.74.1",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"tsconfig": "workspace:*", "tsconfig": "workspace:*",
"typescript": "^5.5.4", "vite": "^5.2.8"
"vite": "^5.2.8",
"vue-tsc": "^2.1.6"
}, },
"packageManager": "pnpm@9.4.0" "packageManager": "pnpm@9.4.0"
} }
+3 -66
View File
@@ -1,15 +1,7 @@
<script setup> <script setup>
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router' import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import { import { HomeIcon, SearchIcon, LibraryIcon, PlusIcon, SettingsIcon, XIcon } from '@modrinth/assets'
HomeIcon,
SearchIcon,
LibraryIcon,
PlusIcon,
SettingsIcon,
XIcon,
DownloadIcon,
} from '@modrinth/assets'
import { Button, Notifications } from '@modrinth/ui' import { Button, Notifications } from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state' import { useLoading, useTheming } from '@/store/state'
import AccountsCard from '@/components/ui/AccountsCard.vue' import AccountsCard from '@/components/ui/AccountsCard.vue'
@@ -24,7 +16,7 @@ import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js' import { command_listener, warning_listener } from '@/helpers/events.js'
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons' import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
import { type } from '@tauri-apps/plugin-os' import { type } from '@tauri-apps/plugin-os'
import { isDev, getOS, restartApp } from '@/helpers/utils.js' import { isDev, getOS } from '@/helpers/utils.js'
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics' import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
@@ -39,10 +31,6 @@ import { useInstall } from '@/store/install.js'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-shell' import { open } from '@tauri-apps/plugin-shell'
import { get_opening_command, initialize_state } from '@/helpers/state' import { get_opening_command, initialize_state } from '@/helpers/state'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater'
const themeStore = useTheming() const themeStore = useTheming()
@@ -63,8 +51,6 @@ const os = ref('')
const stateInitialized = ref(false) const stateInitialized = ref(false)
const criticalErrorMessage = ref()
onMounted(async () => { onMounted(async () => {
await useCheckDisableMouseover() await useCheckDisableMouseover()
}) })
@@ -121,18 +107,7 @@ async function setupApp() {
}), }),
) )
useFetch(
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
'criticalAnnouncements',
true,
).then((res) => {
if (res && res.header && res.body) {
criticalErrorMessage.value = res
}
})
get_opening_command().then(handleCommand) get_opening_command().then(handleCommand)
checkUpdates()
} }
const stateFailed = ref(false) const stateFailed = ref(false)
@@ -151,7 +126,6 @@ initialize_state()
}) })
const handleClose = async () => { const handleClose = async () => {
await saveWindowState(StateFlags.ALL)
await getCurrentWindow().close() await getCurrentWindow().close()
} }
@@ -197,8 +171,7 @@ document.querySelector('body').addEventListener('click', function (e) {
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) && ['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
!target.classList.contains('router-link-active') && !target.classList.contains('router-link-active') &&
!target.href.startsWith('http://localhost') && !target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost') && !target.href.startsWith('https://tauri.localhost')
!target.href.startsWith('http://tauri.localhost')
) { ) {
open(target.href) open(target.href)
} }
@@ -242,20 +215,6 @@ async function handleCommand(e) {
urlModal.value.show(e) urlModal.value.show(e)
} }
} }
const updateAvailable = ref(false)
async function checkUpdates() {
const update = await check()
console.log(update)
updateAvailable.value = !!update
setTimeout(
() => {
checkUpdates()
},
5 * 1000 * 60,
)
}
</script> </script>
<template> <template>
@@ -289,14 +248,6 @@ async function checkUpdates() {
</div> </div>
</div> </div>
<div class="settings pages-list"> <div class="settings pages-list">
<button
v-if="updateAvailable"
v-tooltip="'Install update'"
class="btn btn-outline btn-primary icon-only collapsed-button"
@click="restartApp()"
>
<DownloadIcon />
</button>
<Button <Button
v-tooltip="'Create profile'" v-tooltip="'Create profile'"
class="sleek-primary collapsed-button" class="sleek-primary collapsed-button"
@@ -312,10 +263,6 @@ async function checkUpdates() {
</div> </div>
</div> </div>
<div class="view"> <div class="view">
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
<h1>{{ criticalErrorMessage.header }}</h1>
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
</div>
<div class="appbar-row"> <div class="appbar-row">
<div data-tauri-drag-region class="appbar"> <div data-tauri-drag-region class="appbar">
<section class="navigation-controls"> <section class="navigation-controls">
@@ -428,16 +375,6 @@ async function checkUpdates() {
width: calc(100% - var(--sidebar-width)); width: calc(100% - var(--sidebar-width));
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
.critical-error-banner {
margin-top: -1.25rem;
padding: 1rem;
background-color: rgba(203, 34, 69, 0.1);
border-left: 2px solid var(--color-red);
border-bottom: 2px solid var(--color-red);
border-right: 2px solid var(--color-red);
border-radius: 1rem;
}
.appbar { .appbar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -12,13 +12,13 @@ import {
SearchIcon, SearchIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, Card, DropdownSelect } from '@modrinth/ui' import { ConfirmModal, Button, Card, DropdownSelect } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils' import { formatCategoryHeader } from '@modrinth/utils'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useTheming } from '@/store/theme.js'
import { duplicate, remove } from '@/helpers/profile.js' import { duplicate, remove } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const props = defineProps({ const props = defineProps({
instances: { instances: {
@@ -35,6 +35,7 @@ const props = defineProps({
const instanceOptions = ref(null) const instanceOptions = ref(null)
const instanceComponents = ref(null) const instanceComponents = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null) const currentDeleteInstance = ref(null)
const confirmModal = ref(null) const confirmModal = ref(null)
@@ -229,12 +230,13 @@ const filteredResults = computed(() => {
}) })
</script> </script>
<template> <template>
<ConfirmModalWrapper <ConfirmModal
ref="confirmModal" ref="confirmModal"
title="Are you sure you want to delete this instance?" title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it." description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false" :has-to-type="false"
proceed-label="Delete" proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile" @proceed="deleteProfile"
/> />
<Card class="header"> <Card class="header">
@@ -12,7 +12,7 @@ import {
EyeIcon, EyeIcon,
ChevronRightIcon, ChevronRightIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue' import { ConfirmModal } from '@modrinth/ui'
import Instance from '@/components/ui/Instance.vue' import Instance from '@/components/ui/Instance.vue'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
@@ -22,6 +22,7 @@ import { handleError } from '@/store/notifications.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js' import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js' import { install as installVersion } from '@/store/install.js'
@@ -52,6 +53,7 @@ const instanceComponents = ref(null)
const rows = ref(null) const rows = ref(null)
const deleteConfirmModal = ref(null) const deleteConfirmModal = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null) const currentDeleteInstance = ref(null)
async function deleteProfile() { async function deleteProfile() {
@@ -205,12 +207,13 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<ConfirmModalWrapper <ConfirmModal
ref="deleteConfirmModal" ref="deleteConfirmModal"
title="Are you sure you want to delete this instance?" title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it." description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false" :has-to-type="false"
proceed-label="Delete" proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile" @proceed="deleteProfile"
/> />
<div class="content"> <div class="content">
@@ -5,7 +5,7 @@
v-tooltip.right="'Minecraft accounts'" v-tooltip.right="'Minecraft accounts'"
class="button-base avatar-button" class="button-base avatar-button"
:class="{ expanded: mode === 'expanded' }" :class="{ expanded: mode === 'expanded' }"
@click="toggleMenu" @click="showCard = !showCard"
> >
<Avatar <Avatar
:size="mode === 'expanded' ? 'xs' : 'sm'" :size="mode === 'expanded' ? 'xs' : 'sm'"
@@ -73,7 +73,6 @@ import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
defineProps({ defineProps({
mode: { mode: {
@@ -134,9 +133,9 @@ const logout = async (id) => {
trackEvent('AccountLogOut') trackEvent('AccountLogOut')
} }
const showCard = ref(false) let showCard = ref(false)
const card = ref(null) let card = ref(null)
const button = ref(null) let button = ref(null)
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY) const elements = document.elementsFromPoint(event.clientX, event.clientY)
if ( if (
@@ -145,20 +144,7 @@ const handleClickOutside = (event) => {
!elements.includes(card.value.$el) && !elements.includes(card.value.$el) &&
!button.value.contains(event.target) !button.value.contains(event.target)
) { ) {
toggleMenu(false)
}
}
function toggleMenu(override = true) {
if (showCard.value || !override) {
if (showCard.value) {
show_ads_window()
}
showCard.value = false showCard.value = false
} else {
hide_ads_window()
showCard.value = true
} }
} }
@@ -20,7 +20,7 @@ const handleAddContentFromFile = async () => {
if (!newProject) return if (!newProject) return
for (const project of newProject) { for (const project of newProject) {
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError) await add_project_from_path(props.instance.path, project).catch(handleError)
} }
} }
@@ -25,7 +25,6 @@
<script setup> <script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const emit = defineEmits(['menu-closed', 'option-clicked']) const emit = defineEmits(['menu-closed', 'option-clicked'])
@@ -38,7 +37,6 @@ const shown = ref(false)
defineExpose({ defineExpose({
showMenu: (event, passedItem, passedOptions) => { showMenu: (event, passedItem, passedOptions) => {
hide_ads_window()
item.value = passedItem item.value = passedItem
options.value = passedOptions options.value = passedOptions
@@ -71,9 +69,6 @@ const isLinkedData = (item) => {
} }
const hideContextMenu = () => { const hideContextMenu = () => {
if (shown.value) {
show_ads_window()
}
shown.value = false shown.value = false
emit('menu-closed') emit('menu-closed')
} }
@@ -1,5 +1,6 @@
<script setup> <script setup>
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets' import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
import { Modal } from '@modrinth/ui'
import { ChatIcon } from '@/assets/icons' import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue' import { ref } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js' import { login as login_flow, set_default_user } from '@/helpers/auth.js'
@@ -8,7 +9,6 @@ import { handleSevereError } from '@/store/error.js'
import { cancel_directory_change } from '@/helpers/settings.js' import { cancel_directory_change } from '@/helpers/settings.js'
import { install } from '@/helpers/profile.js' import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref() const errorModal = ref()
const error = ref() const error = ref()
@@ -121,7 +121,7 @@ async function repairInstance() {
</script> </script>
<template> <template>
<ModalWrapper ref="errorModal" :header="title" :closable="closable"> <Modal ref="errorModal" :header="title" :closable="closable">
<div class="modal-body"> <div class="modal-body">
<div class="markdown-body"> <div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'"> <template v-if="errorType === 'minecraft_auth'">
@@ -230,7 +230,7 @@ async function repairInstance() {
</p> </p>
<p>You may be able to fix it through one of the following ways:</p> <p>You may be able to fix it through one of the following ways:</p>
<ul> <ul>
<li>Ensuring you are connected to the internet, then try restarting the app.</li> <li>Ennsuring you are connected to the internet, then try restarting the app.</li>
<li>Redownloading the app.</li> <li>Redownloading the app.</li>
</ul> </ul>
</template> </template>
@@ -272,7 +272,7 @@ async function repairInstance() {
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button> <button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style> <style>
@@ -1,12 +1,12 @@
<script setup> <script setup>
import { XIcon, PlusIcon } from '@modrinth/assets' import { XIcon, PlusIcon } from '@modrinth/assets'
import { Button, Checkbox } from '@modrinth/ui' import { Button, Checkbox, Modal } from '@modrinth/ui'
import { PackageIcon, VersionIcon } from '@/assets/icons' import { PackageIcon, VersionIcon } from '@/assets/icons'
import { ref } from 'vue' import { ref } from 'vue'
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js' import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { useTheming } from '@/store/theme'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -30,6 +30,8 @@ const files = ref([])
const folders = ref([]) const folders = ref([])
const showingFiles = ref(false) const showingFiles = ref(false)
const themeStore = useTheming()
const initFiles = async () => { const initFiles = async () => {
const newFolders = new Map() const newFolders = new Map()
const sep = '/' const sep = '/'
@@ -104,7 +106,7 @@ const exportPack = async () => {
</script> </script>
<template> <template>
<ModalWrapper ref="exportModal" header="Export modpack"> <Modal ref="exportModal" header="Export modpack" :noblur="!themeStore.advancedRendering">
<div class="modal-body"> <div class="modal-body">
<div class="labeled_input"> <div class="labeled_input">
<p>Modpack Name</p> <p>Modpack Name</p>
@@ -206,7 +208,7 @@ const exportPack = async () => {
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,5 +1,5 @@
<template> <template>
<ModalWrapper ref="modal" header="Create instance"> <Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
<div class="modal-header"> <div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" /> <Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
</div> </div>
@@ -193,11 +193,10 @@
/> />
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { import {
PlusIcon, PlusIcon,
UploadIcon, UploadIcon,
@@ -208,7 +207,7 @@ import {
FolderSearchIcon, FolderSearchIcon,
UpdatedIcon, UpdatedIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui' import { Avatar, Button, Chips, Modal, Checkbox } from '@modrinth/ui'
import { computed, onUnmounted, ref, shallowRef } from 'vue' import { computed, onUnmounted, ref, shallowRef } from 'vue'
import { get_loaders } from '@/helpers/tags' import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile' import { create } from '@/helpers/profile'
@@ -218,6 +217,8 @@ import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event'
import { install_from_file } from '@/helpers/pack.js' import { install_from_file } from '@/helpers/pack.js'
import { import {
get_default_launcher_path, get_default_launcher_path,
@@ -225,7 +226,8 @@ import {
import_instance, import_instance,
} from '@/helpers/import.js' } from '@/helpers/import.js'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview'
const themeStore = useTheming()
const profile_name = ref('') const profile_name = ref('')
const game_version = ref('') const game_version = ref('')
@@ -255,15 +257,13 @@ defineExpose({
isShowing.value = true isShowing.value = true
modal.value.show() modal.value.show()
unlistener.value = await getCurrentWebview().onDragDropEvent(async (event) => { unlistener.value = await listen('tauri://file-drop', async (event) => {
// Only if modal is showing // Only if modal is showing
if (!isShowing.value) return if (!isShowing.value) return
if (event.payload.type !== 'drop') return
if (creationType.value !== 'from file') return if (creationType.value !== 'from file') return
hide() hide()
const { paths } = event.payload if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) { await install_from_file(event.payload[0]).catch(handleError)
await install_from_file(paths[0]).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop', source: 'CreationModalFileDrop',
}) })
@@ -371,7 +371,7 @@ const create_instance = async () => {
} }
const upload_icon = async () => { const upload_icon = async () => {
const res = await open({ icon.value = await open({
multiple: false, multiple: false,
filters: [ filters: [
{ {
@@ -381,8 +381,6 @@ const upload_icon = async () => {
], ],
}) })
icon.value = res.path ?? res
if (!icon.value) return if (!icon.value) return
display_icon.value = convertFileSrc(icon.value) display_icon.value = convertFileSrc(icon.value)
} }
@@ -419,7 +417,7 @@ const openFile = async () => {
const newProject = await open({ multiple: false }) const newProject = await open({ multiple: false })
if (!newProject) return if (!newProject) return
hide() hide()
await install_from_file(newProject.path ?? newProject).catch(handleError) await install_from_file(newProject).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
source: 'CreationModalFileOpen', source: 'CreationModalFileOpen',
@@ -464,7 +462,7 @@ const promises = profileOptions.value.map(async (option) => {
option.name, option.name,
instances.map((name) => ({ name, selected: false })), instances.map((name) => ({ name, selected: false })),
) )
} catch { } catch (error) {
// Allow failure silently // Allow failure silently
} }
}) })
@@ -1,5 +1,5 @@
<template> <template>
<ModalWrapper ref="detectJavaModal" header="Select java version"> <Modal ref="detectJavaModal" header="Select java version" :noblur="!themeStore.advancedRendering">
<div class="auto-detect-modal"> <div class="auto-detect-modal">
<div class="table"> <div class="table">
<div class="table-row table-head"> <div class="table-row table-head">
@@ -32,16 +32,18 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<script setup> <script setup>
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets' import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui' import { Modal, Button } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { find_filtered_jres } from '@/helpers/jre.js' import { find_filtered_jres } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const chosenInstallOptions = ref([]) const chosenInstallOptions = ref([])
const detectJavaModal = ref(null) const detectJavaModal = ref(null)
@@ -124,19 +124,20 @@ async function testJava() {
} }
async function handleJavaFileInput() { async function handleJavaFileInput() {
const filePath = await open() let filePath = await open()
if (filePath) { if (filePath) {
let result = await get_jre(filePath.path ?? filePath) let result = await get_jre(filePath)
if (!result) { if (!result) {
result = { result = {
path: filePath.path ?? filePath, path: filePath,
version: props.version.toString(), version: props.version.toString(),
architecture: 'x86', architecture: 'x86',
} }
} }
trackEvent('JavaManualSelect', { trackEvent('JavaManualSelect', {
path: filePath,
version: props.version, version: props.version,
}) })
@@ -149,7 +150,7 @@ async function autoDetect() {
if (!props.compact) { if (!props.compact) {
detectJavaModal.value.show(props.version, props.modelValue) detectJavaModal.value.show(props.version, props.modelValue)
} else { } else {
const versions = await find_filtered_jres(props.version).catch(handleError) let versions = await find_filtered_jres(props.version).catch(handleError)
if (versions.length > 0) { if (versions.length > 0) {
emit('update:modelValue', versions[0]) emit('update:modelValue', versions[0])
} }
@@ -1,11 +1,11 @@
<script setup> <script setup>
import { CheckIcon } from '@modrinth/assets' import { CheckIcon } from '@modrinth/assets'
import { Button, Badge } from '@modrinth/ui' import { Button, Modal, Badge } from '@modrinth/ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useTheming } from '@/store/theme'
import { update_managed_modrinth_version } from '@/helpers/profile' import { update_managed_modrinth_version } from '@/helpers/profile'
import { releaseColor } from '@/helpers/utils' import { releaseColor } from '@/helpers/utils'
import { SwapIcon } from '@/assets/icons/index.js' import { SwapIcon } from '@/assets/icons/index.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({ const props = defineProps({
versions: { versions: {
@@ -33,6 +33,8 @@ const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installing = computed(() => props.instance.install_stage !== 'installed') const installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false) const inProgress = ref(false)
const themeStore = useTheming()
const switchVersion = async (versionId) => { const switchVersion = async (versionId) => {
inProgress.value = true inProgress.value = true
await update_managed_modrinth_version(props.instance.path, versionId) await update_managed_modrinth_version(props.instance.path, versionId)
@@ -41,10 +43,11 @@ const switchVersion = async (versionId) => {
</script> </script>
<template> <template>
<ModalWrapper <Modal
ref="modpackVersionModal" ref="modpackVersionModal"
class="modpack-version-modal" class="modpack-version-modal"
header="Change modpack version" header="Change modpack version"
:noblur="!themeStore.advancedRendering"
> >
<div class="modal-body"> <div class="modal-body">
<Card v-if="instance.linked_data" class="mod-card"> <Card v-if="instance.linked_data" class="mod-card">
@@ -108,7 +111,7 @@ const switchVersion = async (versionId) => {
</div> </div>
</Card> </Card>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,20 +1,12 @@
<script setup> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref } from 'vue'
import { Promotion } from '@modrinth/ui'
import { get as getCreds } from '@/helpers/mr_auth.js' import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js' import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true) const showAd = ref(true)
defineExpose({
scroll() {
updateAdPosition()
},
})
const creds = await getCreds().catch(handleError) const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) { if (creds && creds.user_id) {
const user = await get_user(creds.user_id).catch(handleError) const user = await get_user(creds.user_id).catch(handleError)
@@ -24,103 +16,8 @@ if (creds && creds.user_id) {
showAd.value = false showAd.value = false
} }
} }
const adsWrapper = ref(null)
let resizeObserver
let scrollHandler
let intersectionObserver
let mutationObserver
onMounted(() => {
if (showAd.value) {
updateAdPosition(true)
resizeObserver = new ResizeObserver(() => updateAdPosition())
resizeObserver.observe(adsWrapper.value)
intersectionObserver = new IntersectionObserver(() => updateAdPosition())
intersectionObserver.observe(adsWrapper.value)
mutationObserver = new MutationObserver(() => updateAdPosition())
mutationObserver.observe(adsWrapper.value, { attributes: true, childList: true, subtree: true })
// Add scroll event listener
scrollHandler = () => {
requestAnimationFrame(() => updateAdPosition())
}
window.addEventListener('scroll', scrollHandler, { passive: true })
}
})
function updateAdPosition(overrideShown = false) {
if (adsWrapper.value) {
const rect = adsWrapper.value.getBoundingClientRect()
let y = rect.top + window.scrollY
let height = rect.bottom - rect.top
// Prevent ad from overlaying the app bar
if (y <= 52) {
y = 52
height = rect.bottom - 52
if (height < 0) {
height = 0
y = -1000
}
}
init_ads_window(rect.left + window.scrollX, y, rect.right - rect.left, height, overrideShown)
}
}
async function openPlusLink() {
await record_ads_click()
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
}
const unlisten = await listen('ads-scroll', (event) => {
if (adsWrapper.value) {
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
updateAdPosition()
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
if (intersectionObserver) {
intersectionObserver.disconnect()
}
if (mutationObserver) {
mutationObserver.disconnect()
}
if (scrollHandler) {
window.removeEventListener('scroll', scrollHandler)
}
unlisten()
})
</script> </script>
<template> <template>
<div <Promotion v-if="showAd" :external="false" query-param="?r=launcher" />
v-if="showAd"
ref="adsWrapper"
class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised cursor-pointer"
>
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
<button
class="mt-auto items-center gap-1 text-purple hover:underline bg-transparent border-none text-left cursor-pointer outline-none"
@click="openPlusLink"
>
<span>
Support creators and Modrinth ad-free with
<span class="font-bold">Modrinth+</span>
</span>
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
</button>
</div>
</div>
</template> </template>
@@ -88,6 +88,8 @@ import { loading_listener } from '@/helpers/events.js'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { XIcon } from '@modrinth/assets' import { XIcon } from '@modrinth/assets'
import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js' import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js'
import { TauriEvent } from '@tauri-apps/api/event'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { getOS } from '@/helpers/utils.js' import { getOS } from '@/helpers/utils.js'
import { useLoading } from '@/store/loading.js' import { useLoading } from '@/store/loading.js'
@@ -128,16 +130,9 @@ const os = ref('')
getOS().then((x) => (os.value = x)) getOS().then((x) => (os.value = x))
loading_listener(async (e) => { loading_listener(async (e) => {
console.log(e)
if (e.event.type === 'directory_move') { if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1) loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating app directory...' message.value = 'Updating app directory...'
} else if (e.event.type === 'launcher_update') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating Modrinth App...'
} else if (e.event.type === 'checking_for_updates') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Checking for updates...'
} }
}) })
@@ -1,12 +1,11 @@
<script setup> <script setup>
import { Button } from '@modrinth/ui' import { Modal, Button } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import SearchCard from '@/components/ui/SearchCard.vue' import SearchCard from '@/components/ui/SearchCard.vue'
import { get_categories } from '@/helpers/tags.js' import { get_categories } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_version, get_project } from '@/helpers/cache.js' import { get_version, get_project } from '@/helpers/cache.js'
import { install as installVersion } from '@/store/install.js' import { install as installVersion } from '@/store/install.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const confirmModal = ref(null) const confirmModal = ref(null)
const project = ref(null) const project = ref(null)
@@ -42,7 +41,7 @@ async function install() {
</script> </script>
<template> <template>
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`"> <Modal ref="confirmModal" :header="`Install ${project?.title}`">
<div class="modal-body"> <div class="modal-body">
<SearchCard <SearchCard
:project="project" :project="project"
@@ -61,7 +60,7 @@ async function install() {
</div> </div>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,5 +1,10 @@
<template> <template>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall"> <Modal
ref="incompatibleModal"
header="Incompatibility warning"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body"> <div class="modal-body">
<p> <p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
@@ -46,19 +51,20 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { XIcon, DownloadIcon } from '@modrinth/assets' import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button, DropdownSelect } from '@modrinth/ui' import { Button, Modal, DropdownSelect } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils' import { formatCategory } from '@modrinth/utils'
import { add_project_from_version as installMod } from '@/helpers/profile' import { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue' import { ref } from 'vue'
import { handleError } from '@/store/state.js' import { handleError, useTheming } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
const themeStore = useTheming()
const instance = ref(null) const instance = ref(null)
const project = ref(null) const project = ref(null)
const versions = ref(null) const versions = ref(null)
@@ -66,7 +72,7 @@ const selectedVersion = ref(null)
const incompatibleModal = ref(null) const incompatibleModal = ref(null)
const installing = ref(false) const installing = ref(false)
const onInstall = ref(() => {}) let onInstall = ref(() => {})
defineExpose({ defineExpose({
show: (instanceVal, projectVal, projectVersions, callback) => { show: (instanceVal, projectVal, projectVersions, callback) => {
@@ -1,18 +1,20 @@
<script setup> <script setup>
import { XIcon, DownloadIcon } from '@modrinth/assets' import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui' import { Button, Modal } from '@modrinth/ui'
import { install as pack_install } from '@/helpers/pack' import { install as pack_install } from '@/helpers/pack'
import { ref } from 'vue' import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { useTheming } from '@/store/theme.js'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const versionId = ref() const versionId = ref()
const project = ref() const project = ref()
const confirmModal = ref(null) const confirmModal = ref(null)
const installing = ref(false) const installing = ref(false)
const onInstall = ref(() => {}) let onInstall = ref(() => {})
defineExpose({ defineExpose({
show: (projectVal, versionIdVal, callback) => { show: (projectVal, versionIdVal, callback) => {
@@ -50,7 +52,12 @@ async function install() {
</script> </script>
<template> <template>
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall"> <Modal
ref="confirmModal"
header="Are you sure?"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body"> <div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p> <p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right"> <div class="input-group push-right">
@@ -60,7 +67,7 @@ async function install() {
> >
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -7,7 +7,7 @@ import {
RightArrowIcon, RightArrowIcon,
CheckIcon, CheckIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Card } from '@modrinth/ui' import { Avatar, Modal, Button, Card } from '@modrinth/ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { import {
add_project_from_version as installMod, add_project_from_version as installMod,
@@ -19,11 +19,12 @@ import {
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { installVersionDependencies } from '@/store/install.js' import { installVersionDependencies } from '@/store/install.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const router = useRouter() const router = useRouter()
const versions = ref() const versions = ref()
@@ -48,7 +49,7 @@ const shownProfiles = computed(() =>
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase()) return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
}) })
.filter((profile) => { .filter((profile) => {
const loaders = versions.value.flatMap((v) => v.loaders) let loaders = versions.value.flatMap((v) => v.loaders)
return ( return (
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) && versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
@@ -59,7 +60,7 @@ const shownProfiles = computed(() =>
}), }),
) )
const onInstall = ref(() => {}) let onInstall = ref(() => {})
defineExpose({ defineExpose({
show: async (projectVal, versionsVal, callback) => { show: async (projectVal, versionsVal, callback) => {
@@ -77,7 +78,7 @@ defineExpose({
onInstall.value = callback onInstall.value = callback
const profilesVal = await list().catch(handleError) const profilesVal = await list().catch(handleError)
for (const profile of profilesVal) { for (let profile of profilesVal) {
profile.installing = false profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value.id).catch( profile.installedMod = await check_installed(profile.path, project.value.id).catch(
handleError, handleError,
@@ -141,7 +142,7 @@ const toggleCreation = () => {
} }
const upload_icon = async () => { const upload_icon = async () => {
const res = await open({ icon.value = await open({
multiple: false, multiple: false,
filters: [ filters: [
{ {
@@ -150,7 +151,6 @@ const upload_icon = async () => {
}, },
], ],
}) })
icon.value = res.path ?? res
if (!icon.value) return if (!icon.value) return
display_icon.value = convertFileSrc(icon.value) display_icon.value = convertFileSrc(icon.value)
@@ -213,7 +213,12 @@ const createInstance = async () => {
</script> </script>
<template> <template>
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall"> <Modal
ref="installModal"
header="Install project to instance"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body"> <div class="modal-body">
<input <input
v-model="searchFilter" v-model="searchFilter"
@@ -230,7 +235,7 @@ const createInstance = async () => {
@click="installModal.hide()" @click="installModal.hide()"
> >
<Avatar <Avatar
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null" :src="profile.icon_path ? tauri.convertFileSrc(profile.icon_path) : null"
class="profile-image" class="profile-image"
/> />
{{ profile.name }} {{ profile.name }}
@@ -299,7 +304,7 @@ const createInstance = async () => {
<Button @click="installModal.hide()">Cancel</Button> <Button @click="installModal.hide()">Cancel</Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,69 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ConfirmModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
confirmationText: {
type: String,
default: '',
},
hasToType: {
type: Boolean,
default: false,
},
title: {
type: String,
default: 'No title defined',
required: true,
},
description: {
type: String,
default: 'No description defined',
required: true,
},
proceedLabel: {
type: String,
default: 'Proceed',
},
})
const emit = defineEmits(['proceed'])
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
function proceed() {
emit('proceed')
}
</script>
<template>
<ConfirmModal
ref="modal"
:confirmation-text="confirmationText"
:has-to-type="hasToType"
:title="title"
:description="description"
:proceed-label="proceedLabel"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
@proceed="proceed"
/>
</template>
@@ -1,49 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Modal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const props = defineProps({
header: {
type: String,
default: null,
},
closable: {
type: Boolean,
default: true,
},
onHide: {
type: Function,
default() {
return () => {}
},
},
})
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
props.onHide()
}
</script>
<template>
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
<slot />
</Modal>
</template>
@@ -1,61 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ShareModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
header: {
type: String,
default: 'Share',
},
shareTitle: {
type: String,
default: 'Modrinth',
},
shareText: {
type: String,
default: null,
},
link: {
type: Boolean,
default: false,
},
openInNewTab: {
type: Boolean,
default: true,
},
})
const modal = ref(null)
defineExpose({
show: (passedContent) => {
hide_ads_window()
modal.value.show(passedContent)
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
</script>
<template>
<ShareModal
ref="modal"
:header="header"
:share-title="shareTitle"
:share-text="shareText"
:link="link"
:open-in-new-tab="openInNewTab"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
/>
</template>
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets' import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
import { Button, Card, Checkbox } from '@modrinth/ui' import { Button, Card, Checkbox, Modal } from '@modrinth/ui'
import { import {
DiscordIcon, DiscordIcon,
GithubIcon, GithubIcon,
@@ -13,7 +13,6 @@ import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.
import { handleError, useNotifications } from '@/store/state.js' import { handleError, useNotifications } from '@/store/state.js'
import { ref } from 'vue' import { ref } from 'vue'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps({ const props = defineProps({
callback: { callback: {
@@ -133,7 +132,7 @@ async function createAccount() {
</script> </script>
<template> <template>
<ModalWrapper ref="modal" :on-hide="removeWidget"> <Modal ref="modal" :on-hide="removeWidget">
<Card> <Card>
<template v-if="twoFactorFlow"> <template v-if="twoFactorFlow">
<h1>Enter two-factor code</h1> <h1>Enter two-factor code</h1>
@@ -218,17 +217,17 @@ async function createAccount() {
v-else-if="loggingIn" v-else-if="loggingIn"
color="primary" color="primary"
large large
:disabled="!turnstileToken"
@click="signIn" @click="signIn"
:disabled="!turnstileToken"
> >
Login Login
</Button> </Button>
<Button v-else color="primary" large :disabled="!turnstileToken" @click="createAccount"> <Button v-else color="primary" large @click="createAccount" :disabled="!turnstileToken">
Create account Create account
</Button> </Button>
</div> </div>
</Card> </Card>
</ModalWrapper> </Modal>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
-21
View File
@@ -1,21 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
export async function init_ads_window(x, y, width, height, overrideShown = false) {
return await invoke('plugin:ads|init_ads_window', { x, y, width, height, overrideShown })
}
export async function show_ads_window() {
return await invoke('plugin:ads|show_ads_window')
}
export async function hide_ads_window(reset) {
return await invoke('plugin:ads|hide_ads_window', { reset })
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
export async function open_ads_link(path, origin) {
return await invoke('plugin:ads|open_link', { path, origin })
}
+1 -5
View File
@@ -33,10 +33,6 @@ export async function highlightModInProfile(profilePath, projectPath) {
return await highlightInFolder(fullPath) return await highlightInFolder(fullPath)
} }
export async function restartApp() {
return await invoke('restart_app')
}
export const releaseColor = (releaseType) => { export const releaseColor = (releaseType) => {
switch (releaseType) { switch (releaseType) {
case 'release': case 'release':
@@ -56,7 +52,7 @@ export function debounce(fn, wait) {
if (timer) { if (timer) {
clearTimeout(timer) // clear any pre-existing timer clearTimeout(timer) // clear any pre-existing timer
} }
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this // get the current context const context = this // get the current context
timer = setTimeout(() => { timer = setTimeout(() => {
fn.apply(context, args) // call the function if time expires fn.apply(context, args) // call the function if time expires
+12 -12
View File
@@ -381,20 +381,20 @@ const sortedCategories = computed(() => {
// identifier[0], then if it ties, identifier[1], etc // identifier[0], then if it ties, identifier[1], etc
async function sortByNameOrNumber(sortable, identifiers) { async function sortByNameOrNumber(sortable, identifiers) {
sortable.sort((a, b) => { sortable.sort((a, b) => {
for (const identifier of identifiers) { for (let identifier of identifiers) {
const aNum = parseFloat(a[identifier]) let aNum = parseFloat(a[identifier])
const bNum = parseFloat(b[identifier]) let bNum = parseFloat(b[identifier])
if (isNaN(aNum) && isNaN(bNum)) { if (isNaN(aNum) && isNaN(bNum)) {
// Both are strings, sort alphabetically // Both are strings, sort alphabetically
const stringComp = a[identifier].localeCompare(b[identifier]) let stringComp = a[identifier].localeCompare(b[identifier])
if (stringComp != 0) return stringComp if (stringComp != 0) return stringComp
} else if (!isNaN(aNum) && !isNaN(bNum)) { } else if (!isNaN(aNum) && !isNaN(bNum)) {
// Both are numbers, sort numerically // Both are numbers, sort numerically
const numComp = aNum - bNum let numComp = aNum - bNum
if (numComp != 0) return numComp if (numComp != 0) return numComp
} else { } else {
// One is a number and one is a string, numbers go first // One is a number and one is a string, numbers go first
const numStringComp = isNaN(aNum) ? 1 : -1 let numStringComp = isNaN(aNum) ? 1 : -1
if (numStringComp != 0) return numStringComp if (numStringComp != 0) return numStringComp
} }
} }
@@ -528,8 +528,7 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
<template> <template>
<div ref="searchWrapper" class="search-container"> <div ref="searchWrapper" class="search-container">
<aside class="filter-panel" @scroll="$refs.promo.scroll()"> <aside class="filter-panel">
<PromotionWrapper ref="promo" />
<Card v-if="instanceContext" class="small-instance"> <Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance"> <router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar <Avatar
@@ -676,7 +675,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
</Card> </Card>
</aside> </aside>
<div class="search"> <div class="search">
<Card class="project-type-container mt-4"> <PromotionWrapper class="mt-4" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" /> <NavRow :links="selectableProjectTypes" />
</Card> </Card>
<Card class="search-panel-container"> <Card class="search-panel-container">
@@ -878,13 +878,13 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
.filter-panel { .filter-panel {
position: fixed; position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem; padding: 1rem 0.5rem 1rem 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: fit-content; height: fit-content;
min-height: calc(100vh - 3.25rem); min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem); max-height: calc(100vh - 3.25rem);
width: 20rem;
overflow-y: auto; overflow-y: auto;
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
@@ -903,8 +903,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
} }
.search { .search {
margin: 0 1rem 0.5rem calc(20rem + 1rem); margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - calc(20rem + 1rem)); width: calc(100% - 20.5rem);
.offline { .offline {
margin: 1rem; margin: 1rem;
+2 -7
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue' import { ref, onUnmounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue' import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js' import { list } from '@/helpers/profile.js'
@@ -8,11 +8,6 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { get_search_results } from '@/helpers/cache.js' import { get_search_results } from '@/helpers/cache.js'
import { hide_ads_window } from '@/helpers/ads.js'
onMounted(() => {
hide_ads_window(true)
})
const featuredModpacks = ref({}) const featuredModpacks = ref({})
const featuredMods = ref({}) const featuredMods = ref({})
@@ -47,7 +42,7 @@ const getInstances = async () => {
return dateB - dateA return dateB - dateA
}) })
const filters = [] let filters = []
for (const instance of recentInstances.value) { for (const instance of recentInstances.value) {
if (instance.linked_data && instance.linked_data.project_id) { if (instance.linked_data && instance.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`) filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
+1 -6
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, onUnmounted, ref, shallowRef } from 'vue' import { onUnmounted, ref, shallowRef } from 'vue'
import GridDisplay from '@/components/GridDisplay.vue' import GridDisplay from '@/components/GridDisplay.vue'
import { list } from '@/helpers/profile.js' import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@@ -10,11 +10,6 @@ import { Button } from '@modrinth/ui'
import { PlusIcon } from '@modrinth/assets' import { PlusIcon } from '@modrinth/assets'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue' import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons' import { NewInstanceImage } from '@/assets/icons'
import { hide_ads_window } from '@/helpers/ads.js'
onMounted(() => {
hide_ads_window(true)
})
const route = useRoute() const route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
+10 -34
View File
@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, watch, onMounted } from 'vue' import { ref, watch } from 'vue'
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets' import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui' import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state' import { handleError, useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings'
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre' import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
@@ -13,12 +13,6 @@ import { open } from '@tauri-apps/plugin-dialog'
import { getOS } from '@/helpers/utils.js' import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { get_user, purge_cache_types } from '@/helpers/cache.js' import { get_user, purge_cache_types } from '@/helpers/cache.js'
import { hide_ads_window } from '@/helpers/ads.js'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
onMounted(() => {
hide_ads_window(true)
})
const pageOptions = ['Home', 'Library'] const pageOptions = ['Home', 'Library']
@@ -175,12 +169,13 @@ async function purgeCache() {
Sign in Sign in
</button> </button>
</div> </div>
<ConfirmModalWrapper <ConfirmModal
ref="purgeCacheConfirmModal" ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?" title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily." description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false" :has-to-type="false"
proceed-label="Purge cache" proceed-label="Purge cache"
:noblur="!themeStore.advancedRendering"
@proceed="purgeCache" @proceed="purgeCache"
/> />
<div class="adjacent-input"> <div class="adjacent-input">
@@ -363,25 +358,6 @@ async function purgeCache() {
<span class="label__title size-card-header">Privacy</span> <span class="label__title size-card-header">Privacy</span>
</h3> </h3>
</div> </div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Personalized ads</span>
<span class="label__description">
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
option, you opt out and ads will no longer be shown based on your interests.
</span>
</label>
<Toggle
id="opt-out-analytics"
:model-value="settings.personalized_ads"
:checked="settings.personalized_ads"
@update:model-value="
(e) => {
settings.personalized_ads = e
}
"
/>
</div>
<div class="adjacent-input"> <div class="adjacent-input">
<label for="opt-out-analytics"> <label for="opt-out-analytics">
<span class="label__title">Telemetry</span> <span class="label__title">Telemetry</span>
@@ -425,14 +401,14 @@ async function purgeCache() {
<span class="label__title size-card-header">Java settings</span> <span class="label__title size-card-header">Java settings</span>
</h3> </h3>
</div> </div>
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`"> <template v-for="version in [21, 17, 8]">
<label :for="'java-' + javaVersion"> <label :for="'java-' + version">
<span class="label__title">Java {{ javaVersion }} location</span> <span class="label__title">Java {{ version }} location</span>
</label> </label>
<JavaSelector <JavaSelector
:id="'java-selector-' + javaVersion" :id="'java-selector-' + version"
v-model="javaVersions[javaVersion]" v-model="javaVersions[version]"
:version="javaVersion" :version="version"
@update:model-value="updateJavaVersion" @update:model-value="updateJavaVersion"
/> />
</template> </template>
+14 -11
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="instance-container"> <div class="instance-container">
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()"> <div class="side-cards">
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick"> <Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" /> <Avatar size="lg" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
<div class="instance-info"> <div class="instance-info">
<h2 class="name">{{ instance.name }}</h2> <h2 class="name">{{ instance.name }}</h2>
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span> <span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
@@ -61,9 +61,9 @@
</RouterLink> </RouterLink>
</div> </div>
</Card> </Card>
<PromotionWrapper ref="promo" class="mt-4" />
</div> </div>
<div class="content"> <div class="content">
<PromotionWrapper />
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()"> <Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
@@ -311,6 +311,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
width: 17rem;
} }
Button { Button {
@@ -324,13 +325,12 @@ Button {
} }
.side-cards { .side-cards {
position: fixed; position: absolute;
width: 300px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem;
min-height: calc(100vh - 3.25rem); min-height: calc(100% - 3.25rem);
max-height: calc(100vh - 3.25rem); max-height: calc(100% - 3.25rem);
overflow-y: auto; overflow-y: auto;
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
@@ -374,7 +374,10 @@ Button {
overflow: auto; overflow: auto;
gap: 1rem; gap: 1rem;
min-height: 100%; min-height: 100%;
padding: 1rem; }
.content {
margin-left: 19rem;
} }
.instance-info { .instance-info {
@@ -448,10 +451,10 @@ Button {
} }
.content { .content {
margin: 0 1rem 0.5rem 20rem; width: 100%;
width: calc(100% - 20rem);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1rem 1rem 0 0;
overflow: auto; overflow: auto;
} }
@@ -76,7 +76,7 @@
</div> </div>
</RecycleScroller> </RecycleScroller>
</div> </div>
<ShareModalWrapper <ShareModal
ref="shareModal" ref="shareModal"
header="Share Log" header="Share Log"
share-title="Instance Log" share-title="Instance Log"
@@ -89,7 +89,7 @@
<script setup> <script setup>
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets' import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
import { Button, Card, Checkbox, DropdownSelect } from '@modrinth/ui' import { Button, Card, ShareModal, Checkbox, DropdownSelect } from '@modrinth/ui'
import { import {
delete_logs_by_filename, delete_logs_by_filename,
get_logs, get_logs,
@@ -107,7 +107,6 @@ import { handleError } from '@/store/notifications.js'
import { ofetch } from 'ofetch' import { ofetch } from 'ofetch'
import { RecycleScroller } from 'vue-virtual-scroller' import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
dayjs.extend(isToday) dayjs.extend(isToday)
dayjs.extend(isYesterday) dayjs.extend(isYesterday)
@@ -296,7 +295,7 @@ if (logs.value.length > 1 && !props.playing) {
const deleteLog = async () => { const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) { if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
const deleteIndex = selectedLogIndex.value let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1 selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_filename( await delete_logs_by_filename(
props.instance.path, props.instance.path,
+12 -15
View File
@@ -284,7 +284,7 @@
:link-function="(page) => `?page=${page}`" :link-function="(page) => `?page=${page}`"
@switch-page="switchPage" @switch-page="switchPage"
/> />
<ModalWrapper ref="deleteWarning" header="Are you sure?"> <Modal ref="deleteWarning" header="Are you sure?">
<div class="modal-body"> <div class="modal-body">
<div class="markdown-body"> <div class="markdown-body">
<p> <p>
@@ -302,8 +302,8 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?"> <Modal ref="deleteDisabledWarning" header="Are you sure?">
<div class="modal-body"> <div class="modal-body">
<div class="markdown-body"> <div class="markdown-body">
<p> <p>
@@ -325,8 +325,8 @@
</Button> </Button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ShareModalWrapper <ShareModal
ref="shareModal" ref="shareModal"
share-title="Sharing modpack content" share-title="Sharing modpack content"
share-text="Check out the projects I'm using in my modpack!" share-text="Check out the projects I'm using in my modpack!"
@@ -360,6 +360,8 @@ import {
import { import {
Pagination, Pagination,
DropdownSelect, DropdownSelect,
ShareModal,
Modal,
Checkbox, Checkbox,
AnimatedLogo, AnimatedLogo,
Avatar, Avatar,
@@ -378,6 +380,7 @@ import {
} from '@/helpers/profile.js' } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { listen } from '@tauri-apps/api/event'
import { highlightModInProfile } from '@/helpers/utils.js' import { highlightModInProfile } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons' import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue' import ExportModal from '@/components/ui/ExportModal.vue'
@@ -390,9 +393,6 @@ import {
get_version_many, get_version_many,
} from '@/helpers/cache.js' } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js' import { profile_listener } from '@/helpers/events.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -717,7 +717,7 @@ const updateProject = async (mod) => {
}) })
} }
const locks = {} let locks = {}
const toggleDisableMod = async (mod) => { const toggleDisableMod = async (mod) => {
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property. // Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
@@ -725,7 +725,7 @@ const toggleDisableMod = async (mod) => {
locks[mod.id] = ref(null) locks[mod.id] = ref(null)
} }
const lock = locks[mod.id] let lock = locks[mod.id]
while (lock.value) { while (lock.value) {
await lock.value await lock.value
@@ -784,7 +784,6 @@ const deleteDisabled = async () => {
} }
const shareNames = async () => { const shareNames = async () => {
console.log(functionValues.value)
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n')) await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
} }
@@ -879,10 +878,8 @@ async function refreshProjects() {
refreshingProjects.value = false refreshingProjects.value = false
} }
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => { const unlisten = await listen('tauri://file-drop', async (event) => {
if (event.payload.type !== 'drop') return for (const file of event.payload) {
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError) await add_project_from_path(props.instance.path, file).catch(handleError)
} }
@@ -1,13 +1,18 @@
<template> <template>
<ConfirmModalWrapper <ConfirmModal
ref="modal_confirm" ref="modal_confirm"
title="Are you sure you want to delete this instance?" title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it." description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false" :has-to-type="false"
proceed-label="Delete" proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="removeProfile" @proceed="removeProfile"
/> />
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?"> <Modal
ref="modalConfirmUnlock"
header="Are you sure you want to unlock this instance?"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-delete"> <div class="modal-delete">
<div <div
class="markdown-body" class="markdown-body"
@@ -26,9 +31,13 @@
</button> </button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?"> <Modal
ref="modalConfirmUnpair"
header="Are you sure you want to unpair this instance?"
:noblur="!themeStore.advancedRendering"
>
<div class="modal-delete"> <div class="modal-delete">
<div <div
class="markdown-body" class="markdown-body"
@@ -47,9 +56,13 @@
</button> </button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<ModalWrapper ref="changeVersionsModal" header="Change instance versions"> <Modal
ref="changeVersionsModal"
header="Change instance versions"
:noblur="!themeStore.advancedRendering"
>
<div class="change-versions-modal universal-body"> <div class="change-versions-modal universal-body">
<div class="input-row"> <div class="input-row">
<p class="input-label">Loader</p> <p class="input-label">Loader</p>
@@ -93,7 +106,7 @@
</button> </button>
</div> </div>
</div> </div>
</ModalWrapper> </Modal>
<section class="card"> <section class="card">
<div class="label"> <div class="label">
<h3> <h3>
@@ -498,7 +511,18 @@ import {
DownloadIcon, DownloadIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui' import {
Button,
Toggle,
ConfirmModal,
Card,
Slider,
Checkbox,
Avatar,
Modal,
Chips,
DropdownSelect,
} from '@modrinth/ui'
import { SwapIcon } from '@/assets/icons' import { SwapIcon } from '@/assets/icons'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
@@ -522,11 +546,10 @@ import { open } from '@tauri-apps/plugin-dialog'
import { get_loader_versions } from '@/helpers/metadata.js' import { get_loader_versions } from '@/helpers/metadata.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js' import { get_game_versions, get_loaders } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue' import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
@@ -547,6 +570,8 @@ const props = defineProps({
}, },
}) })
const themeStore = useTheming()
const title = ref(props.instance.name) const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path) const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups) const groups = ref(props.instance.groups)
@@ -581,7 +606,7 @@ async function setIcon() {
if (!value) return if (!value) return
icon.value = value.path ?? value icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError) await edit_icon(props.instance.path, icon.value).catch(handleError)
trackEvent('InstanceSetIcon') trackEvent('InstanceSetIcon')
@@ -596,12 +621,12 @@ const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError)) const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path }) const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined) const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
const javaArgs = ref( const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '), (props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
) )
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined) const overrideEnvVars = ref(!!props.instance.custom_env_vars)
const envVars = ref( const envVars = ref(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars) (props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('=')) .map((x) => x.join('='))
@@ -685,15 +710,19 @@ const editProfileObject = computed(() => {
} }
if (overrideJavaArgs.value) { if (overrideJavaArgs.value) {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean) if (javaArgs.value !== '') {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
} }
if (overrideEnvVars.value) { if (overrideEnvVars.value) {
editProfile.custom_env_vars = envVars.value if (envVars.value !== '') {
.trim() editProfile.custom_env_vars = envVars.value
.split(/\s+/) .trim()
.filter(Boolean) .split(/\s+/)
.map((x) => x.split('=').filter(Boolean)) .filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
} }
if (overrideMemorySettings.value) { if (overrideMemorySettings.value) {
@@ -876,7 +905,7 @@ const editing = ref(false)
async function saveGvLoaderEdits() { async function saveGvLoaderEdits() {
editing.value = true editing.value = true
const editProfile = editProfileObject.value let editProfile = editProfileObject.value
editProfile.loader = loader.value editProfile.loader = loader.value
editProfile.game_version = gameVersion.value editProfile.game_version = gameVersion.value
@@ -20,14 +20,14 @@
</span> </span>
</Card> </Card>
</div> </div>
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage"> <div v-if="expandedGalleryItem" class="expanded-image-modal" @click="expandedGalleryItem = null">
<div class="content"> <div class="content">
<img <img
class="image" class="image"
:class="{ 'zoomed-in': zoomedIn }" :class="{ 'zoomed-in': zoomedIn }"
:src=" :src="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'" :alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -45,15 +45,15 @@
</div> </div>
<div class="controls"> <div class="controls">
<div class="buttons"> <div class="buttons">
<Button class="close" icon-only @click="hideImage"> <Button class="close" icon-only @click="expandedGalleryItem = null">
<XIcon aria-hidden="true" /> <XIcon aria-hidden="true" />
</Button> </Button>
<a <a
class="open btn icon-only" class="open btn icon-only"
target="_blank" target="_blank"
:href=" :href="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
> >
@@ -94,7 +94,6 @@ import {
import { Button, Card } from '@modrinth/ui' import { Button, Card } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const props = defineProps({ const props = defineProps({
project: { project: {
@@ -103,14 +102,9 @@ const props = defineProps({
}, },
}) })
const expandedGalleryItem = ref(null) let expandedGalleryItem = ref(null)
const expandedGalleryIndex = ref(0) let expandedGalleryIndex = ref(0)
const zoomedIn = ref(false) let zoomedIn = ref(false)
const hideImage = () => {
expandedGalleryItem.value = null
show_ads_window()
}
const nextImage = () => { const nextImage = () => {
expandedGalleryIndex.value++ expandedGalleryIndex.value++
@@ -137,7 +131,6 @@ const previousImage = () => {
} }
const expandImage = (item, index) => { const expandImage = (item, index) => {
hide_ads_window()
expandedGalleryItem.value = item expandedGalleryItem.value = item
expandedGalleryIndex.value = index expandedGalleryIndex.value = index
zoomedIn.value = false zoomedIn.value = false
@@ -147,20 +140,6 @@ const expandImage = (item, index) => {
url: item.url, url: item.url,
}) })
} }
function keyListener(e) {
if (expandedGalleryItem.value) {
e.preventDefault()
if (e.key === 'Escape') {
hideImage()
} else if (e.key === 'ArrowLeft') {
previousImage()
} else if (e.key === 'ArrowRight') {
nextImage()
}
}
}
document.addEventListener('keypress', keyListener)
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+16 -11
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="root-container"> <div class="root-container">
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()"> <div v-if="data" class="project-sidebar">
<Card v-if="instance" class="small-instance"> <Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`"> <router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar <Avatar
@@ -20,7 +20,7 @@
</router-link> </router-link>
</Card> </Card>
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick"> <Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="md" :src="data.icon_url" /> <Avatar size="lg" :src="data.icon_url" />
<div class="instance-info"> <div class="instance-info">
<h2 class="name">{{ data.title }}</h2> <h2 class="name">{{ data.title }}</h2>
{{ data.description }} {{ data.description }}
@@ -61,9 +61,7 @@
Site Site
</a> </a>
</div> </div>
</Card> <hr class="card-divider" />
<PromotionWrapper ref="promo" />
<Card class="sidebar-card">
<div class="stats"> <div class="stats">
<div class="stat"> <div class="stat">
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />
@@ -165,6 +163,7 @@
</Card> </Card>
</div> </div>
<div v-if="data" class="content-container"> <div v-if="data" class="content-container">
<PromotionWrapper />
<Card class="tabs"> <Card class="tabs">
<NavRow <NavRow
v-if="data.gallery.length > 0" v-if="data.gallery.length > 0"
@@ -232,7 +231,15 @@ import {
GlobeIcon, GlobeIcon,
ClipboardCopyIcon, ClipboardCopyIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui' import {
Categories,
EnvironmentIndicator,
Card,
Avatar,
Button,
Promotion,
NavRow,
} from '@modrinth/ui'
import { formatNumber } from '@modrinth/utils' import { formatNumber } from '@modrinth/utils'
import { import {
BuyMeACoffeeIcon, BuyMeACoffeeIcon,
@@ -253,7 +260,7 @@ import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import { install as installVersion } from '@/store/install.js' import { install as installVersion } from '@/store/install.js'
import { get_project, get_team, get_version_many } from '@/helpers/cache.js' import { get_project, get_project_many, get_team, get_version_many } from '@/helpers/cache.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
@@ -302,13 +309,11 @@ async function fetchProjectData() {
await fetchProjectData() await fetchProjectData()
const promo = ref(null)
watch( watch(
() => route.params.id, () => route.params.id,
async () => { async () => {
if (route.params.id && route.path.startsWith('/project')) { if (route.params.id && route.path.startsWith('/project')) {
await fetchProjectData() await fetchProjectData()
promo.value.scroll()
} }
}, },
) )
@@ -372,7 +377,7 @@ const handleOptionsClick = (args) => {
.project-sidebar { .project-sidebar {
position: fixed; position: fixed;
width: calc(300px + 1.5rem); width: 20rem;
min-height: calc(100vh - 3.25rem); min-height: calc(100vh - 3.25rem);
height: fit-content; height: fit-content;
max-height: calc(100vh - 3.25rem); max-height: calc(100vh - 3.25rem);
@@ -398,7 +403,7 @@ const handleOptionsClick = (args) => {
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
margin-left: calc(300px + 1rem); margin-left: 19.5rem;
} }
.button-group { .button-group {
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowJs": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"strict": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+10 -2
View File
@@ -1,4 +1,12 @@
{ {
"files": [], "compilerOptions": {
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] "module": "NodeNext",
"moduleResolution": "NodeNext",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ESNext"
},
"exclude": ["node_modules", "dist"]
} }
-17
View File
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+2 -2
View File
@@ -42,8 +42,8 @@ export default defineConfig({
port: 1420, port: 1420,
strictPort: true, strictPort: true,
}, },
// to make use of `TAURI_ENV_DEBUG` and other env variables // to make use of `TAURI_DEBUG` and other env variables
// https://v2.tauri.app/reference/environment-variables/#tauri-cli-hook-commands // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'], envPrefix: ['VITE_', 'TAURI_'],
build: { build: {
// Tauri supports es2021 // Tauri supports es2021
+1
View File
@@ -10,6 +10,7 @@ theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = "2.0.0-rc.4"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
thiserror = "1.0" thiserror = "1.0"
url = "2.2" url = "2.2"
+4 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "theseus_gui" name = "theseus_gui"
version = "0.8.9" version = "0.8.3-1"
description = "The Modrinth App is a desktop application for managing your Minecraft mods" description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/" repository = "https://github.com/modrinth/code/apps/app/"
@@ -16,14 +16,13 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-rc", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] } tauri = { version = "2.0.0-rc.6", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
tauri-plugin-window-state = "2.0.0-rc" tauri-plugin-window-state = "2.0.0-rc"
tauri-plugin-deep-link = "2.0.0-rc" tauri-plugin-deep-link = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc" tauri-plugin-os = "2.0.0-rc"
tauri-plugin-shell = "2.0.0-rc" tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc" tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-updater = { version = "2.0.0-rc" } tauri-plugin-updater = { version = "2.0.0-rc.1", optional = true }
tauri-plugin-single-instance = { version = "2.0.0-rc" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
thiserror = "1.0" thiserror = "1.0"
@@ -58,9 +57,6 @@ cocoa = "0.25.0"
objc = "0.2.7" objc = "0.2.7"
rand = "0.8.5" rand = "0.8.5"
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { version = "2.0.0-rc", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
@@ -68,4 +64,4 @@ default = ["custom-protocol"]
# this feature is used for production builds where `devPath` points to the filesystem # this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this # DO NOT remove this
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
updater = [] updater = ["dep:tauri-plugin-updater"]
+1 -1
View File
@@ -15,7 +15,7 @@ Before you begin, ensure you have the following installed on your machine:
- [Node.js](https://nodejs.org/en/) - [Node.js](https://nodejs.org/en/)
- [pnpm](https://pnpm.io/) - [pnpm](https://pnpm.io/)
- [Rust](https://www.rust-lang.org/tools/install) - [Rust](https://www.rust-lang.org/tools/install)
- [Tauri](https://v2.tauri.app/start/prerequisites/) - [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/#installing)
### Setup ### Setup
+1 -17
View File
@@ -62,7 +62,7 @@ fn main() {
InlinedPlugin::new() InlinedPlugin::new()
.commands(&[ .commands(&[
"get_java_versions", "get_java_versions",
"set_java_version", "set_java_versions",
"jre_find_filtered_jres", "jre_find_filtered_jres",
"jre_get_jre", "jre_get_jre",
"jre_test_jre", "jre_test_jre",
@@ -217,22 +217,6 @@ fn main() {
.default_permission( .default_permission(
DefaultPermissionRule::AllowAllCommands, DefaultPermissionRule::AllowAllCommands,
), ),
)
.plugin(
"ads",
InlinedPlugin::new()
.commands(&[
"init_ads_window",
"hide_ads_window",
"scroll_ads_window",
"show_ads_window",
"record_ads_click",
"open_link",
"get_ads_personalization",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
), ),
) )
.expect("Failed to run tauri-build"); .expect("Failed to run tauri-build");
-14
View File
@@ -1,14 +0,0 @@
{
"identifier": "ads",
"description": "",
"local": false,
"remote": {
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
},
"webviews": [
"ads-window"
],
"permissions": [
"ads:default"
]
}
+2 -3
View File
@@ -35,7 +35,6 @@
"cache:default", "cache:default",
"settings:default", "settings:default",
"tags:default", "tags:default",
"utils:default", "utils:default"
"ads:default"
] ]
} }
-11
View File
@@ -1,11 +0,0 @@
{
"identifier": "updater",
"description": "",
"local": true,
"windows": [
"main"
],
"permissions": [
"updater:default"
]
}
+1 -1
View File
@@ -1 +1 @@
{"ads":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-init-ads-window","allow-hide-ads-window","allow-scroll-ads-window","allow-show-ads-window","allow-record-ads-click","allow-open-link","allow-get-ads-personalization"]},"permissions":{"allow-get-ads-personalization":{"identifier":"allow-get-ads-personalization","description":"Enables the get_ads_personalization command without any pre-configured scope.","commands":{"allow":["get_ads_personalization"],"deny":[]}},"allow-hide-ads-window":{"identifier":"allow-hide-ads-window","description":"Enables the hide_ads_window command without any pre-configured scope.","commands":{"allow":["hide_ads_window"],"deny":[]}},"allow-init-ads-window":{"identifier":"allow-init-ads-window","description":"Enables the init_ads_window command without any pre-configured scope.","commands":{"allow":["init_ads_window"],"deny":[]}},"allow-open-link":{"identifier":"allow-open-link","description":"Enables the open_link command without any pre-configured scope.","commands":{"allow":["open_link"],"deny":[]}},"allow-record-ads-click":{"identifier":"allow-record-ads-click","description":"Enables the record_ads_click command without any pre-configured scope.","commands":{"allow":["record_ads_click"],"deny":[]}},"allow-scroll-ads-window":{"identifier":"allow-scroll-ads-window","description":"Enables the scroll_ads_window command without any pre-configured scope.","commands":{"allow":["scroll_ads_window"],"deny":[]}},"allow-show-ads-window":{"identifier":"allow-show-ads-window","description":"Enables the show_ads_window command without any pre-configured scope.","commands":{"allow":["show_ads_window"],"deny":[]}},"deny-get-ads-personalization":{"identifier":"deny-get-ads-personalization","description":"Denies the get_ads_personalization command without any pre-configured scope.","commands":{"allow":[],"deny":["get_ads_personalization"]}},"deny-hide-ads-window":{"identifier":"deny-hide-ads-window","description":"Denies the hide_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["hide_ads_window"]}},"deny-init-ads-window":{"identifier":"deny-init-ads-window","description":"Denies the init_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["init_ads_window"]}},"deny-open-link":{"identifier":"deny-open-link","description":"Denies the open_link command without any pre-configured scope.","commands":{"allow":[],"deny":["open_link"]}},"deny-record-ads-click":{"identifier":"deny-record-ads-click","description":"Denies the record_ads_click command without any pre-configured scope.","commands":{"allow":[],"deny":["record_ads_click"]}},"deny-scroll-ads-window":{"identifier":"deny-scroll-ads-window","description":"Denies the scroll_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["scroll_ads_window"]}},"deny-show-ads-window":{"identifier":"deny-show-ads-window","description":"Denies the show_ads_window command without any pre-configured scope.","commands":{"allow":[],"deny":["show_ads_window"]}}},"permission_sets":{},"global_scope_schema":null},"auth":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-login","allow-remove-user","allow-get-default-user","allow-set-default-user","allow-get-users"]},"permissions":{"allow-get-default-user":{"identifier":"allow-get-default-user","description":"Enables the get_default_user command without any pre-configured scope.","commands":{"allow":["get_default_user"],"deny":[]}},"allow-get-users":{"identifier":"allow-get-users","description":"Enables the get_users command without any pre-configured scope.","commands":{"allow":["get_users"],"deny":[]}},"allow-login":{"identifier":"allow-login","description":"Enables the login command without any pre-configured scope.","commands":{"allow":["login"],"deny":[]}},"allow-remove-user":{"identifier":"allow-remove-user","description":"Enables the remove_user command without any pre-configured scope.","commands":{"allow":["remove_user"],"deny":[]}},"allow-set-default-user":{"identifier":"allow-set-default-user","description":"Enables the set_default_user command without any pre-configured scope.","commands":{"allow":["set_default_user"],"deny":[]}},"deny-get-default-user":{"identifier":"deny-get-default-user","description":"Denies the get_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["get_default_user"]}},"deny-get-users":{"identifier":"deny-get-users","description":"Denies the get_users command without any pre-configured scope.","commands":{"allow":[],"deny":["get_users"]}},"deny-login":{"identifier":"deny-login","description":"Denies the login command without any pre-configured scope.","commands":{"allow":[],"deny":["login"]}},"deny-remove-user":{"identifier":"deny-remove-user","description":"Denies the remove_user command without any pre-configured scope.","commLine truncated {"auth":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-login","allow-remove-user","allow-get-default-user","allow-set-default-user","allow-get-users"]},"permissions":{"allow-get-default-user":{"identifier":"allow-get-default-user","description":"Enables the get_default_user command without any pre-configured scope.","commands":{"allow":["get_default_user"],"deny":[]}},"allow-get-users":{"identifier":"allow-get-users","description":"Enables the get_users command without any pre-configured scope.","commands":{"allow":["get_users"],"deny":[]}},"allow-login":{"identifier":"allow-login","description":"Enables the login command without any pre-configured scope.","commands":{"allow":["login"],"deny":[]}},"allow-remove-user":{"identifier":"allow-remove-user","description":"Enables the remove_user command without any pre-configured scope.","commands":{"allow":["remove_user"],"deny":[]}},"allow-set-default-user":{"identifier":"allow-set-default-user","description":"Enables the set_default_user command without any pre-configured scope.","commands":{"allow":["set_default_user"],"deny":[]}},"deny-get-default-user":{"identifier":"deny-get-default-user","description":"Denies the get_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["get_default_user"]}},"deny-get-users":{"identifier":"deny-get-users","description":"Denies the get_users command without any pre-configured scope.","commands":{"allow":[],"deny":["get_users"]}},"deny-login":{"identifier":"deny-login","description":"Denies the login command without any pre-configured scope.","commands":{"allow":[],"deny":["login"]}},"deny-remove-user":{"identifier":"deny-remove-user","description":"Denies the remove_user command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_user"]}},"deny-set-default-user":{"identifier":"deny-set-default-user","description":"Denies the set_default_user command without any pre-configured scope.","commands":{"allow":[],"deny":["set_default_user"]}}},"permission_sets":{},"global_scope_schema":null},"cache":{"default_permission":{"identifier":"default","description":"Default plugin permissions.","permissions":["allow-get-project","allow-get-project-many","allow-get-version","allow-get-version-many","allow-get-user","allow-get-user-many","allow-get-team","allow-get-team-many","allow-get-organization","allow-get-organization-many","allow-get-search-results","allow-get-search-results-many","allow-purge-cache-types"]},"permissions":{"allow-get-organization":{"identifier":"allow-get-organization","description":"Enables the get_organization command without any pre-configured scope.","commands":{"allow":["get_organization"],"deny":[]}},"allow-get-organization-many":{"identifier":"allow-get-organization-many","description":"Enables the get_organization_many command without any pre-configured scope.","commands":{"allow":["get_organization_many"],"deny":[]}},"allow-get-project":{"identifier":"allow-get-project","description":"Enables the get_project command without any pre-configured scope.","commands":{"allow":["get_project"],"deny":[]}},"allow-get-project-many":{"identifier":"allow-get-project-many","description":"Enables the get_project_many command without any pre-configured scope.","commands":{"allow":["get_project_many"],"deny":[]}},"allow-get-search-results":{"identifier":"allow-get-search-results","description":"Enables the get_search_results command without any pre-configured scope.","commands":{"allow":["get_search_results"],"deny":[]}},"allow-get-search-results-many":{"identifier":"allow-get-search-results-many","description":"Enables the get_search_results_many command without any pre-configured scope.","commands":{"allow":["get_search_results_many"],"deny":[]}},"allow-get-team":{"identifier":"allow-get-team","description":"Enables the get_team command without any pre-configured scope.","commands":{"allow":["get_team"],"deny":[]}},"allow-get-team-many":{"identifier":"allow-get-team-many","description":"Enables the get_team_many command without any pre-configured scope.","commands":{"allow":["get_team_many"],"deny":[]}},"allow-get-user":{"identifier":"allow-get-user","description":"Enables the get_user command without any pre-configured scope.","commands":{"allow":["get_user"],"deny":[]}},"allow-get-user-many":{"identifier":"allow-get-user-many","description":"Enables the get_user_many command without any pre-configured scope.","commands":{"allow":["get_user_many"],"deny":[]}},"allow-get-version":{"identifier":"allow-get-version","description":"Enables the get_version command without any pre-configured scope.","commands":{"allow":["get_version"],"deny":[]}},"allow-get-version-many":{"identifier":"allow-get-version-many","description":"Enables the get_version_many command without any pre-configured scope.","commands":{"allow":["get_version_many"],"deny":[]}},"allow-purge-cache-types":{"identifier":"allow-purge-cache-typLine truncated
+1 -1
View File
@@ -1 +1 @@
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["ads:default"]},"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default","ads:default"]},"updater":{"identifier":"updater","description":"","local":true,"windows":["main"],"permissions":["updater:default"]}} {"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default"]}}
File diff suppressed because it is too large. Load diff
+4 -67
View File
@@ -299,69 +299,6 @@
}, },
"Identifier": { "Identifier": {
"oneOf": [ "oneOf": [
{
"description": "ads:default -> Default plugin permissions.",
"type": "string",
"enum": [
"ads:default"
]
},
{
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-hide-ads-window"
]
},
{
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-scroll-ads-window"
]
},
{
"description": "ads:allow-show-ads-window -> Enables the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-show-ads-window"
]
},
{
"description": "ads:deny-hide-ads-window -> Denies the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-hide-ads-window"
]
},
{
"description": "ads:deny-init-ads-window -> Denies the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-init-ads-window"
]
},
{
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-scroll-ads-window"
]
},
{
"description": "ads:deny-show-ads-window -> Denies the show_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:deny-show-ads-window"
]
},
{ {
"description": "auth:default -> Default plugin permissions.", "description": "auth:default -> Default plugin permissions.",
"type": "string", "type": "string",
@@ -2848,10 +2785,10 @@
] ]
}, },
{ {
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.", "description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:allow-set-java-version" "jre:allow-set-java-versions"
] ]
}, },
{ {
@@ -2897,10 +2834,10 @@
] ]
}, },
{ {
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.", "description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"type": "string", "type": "string",
"enum": [ "enum": [
"jre:deny-set-java-version" "jre:deny-set-java-versions"
] ]
}, },
{ {
File diff suppressed because it is too large. Load diff
+346
View File
@@ -0,0 +1,346 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<Property Id="WixShellExecTarget" Value="[!Path]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
<!-- THESEUS -->
<ProgId Id="theseus.mrpack.Document" Description="Modrinth File">
<Extension Id="mrpack" ContentType="application/mrpack">
<!-- no flags on argument, so we can hijack deep link library-->
<Verb Id="open" Command="Open" TargetFile="Path" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
<!-- /THESEUS -->
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
<!-- THESEUS -->
<Component Id="FileTypeAssociationsReg" Guid="*">
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationDescription" Value="theseus" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationIcon" Value="[INSTALLDIR]theseus,0" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationName" Value="theseus" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\DefaultIcon" Value="[INSTALLDIR]theseus,1" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\FileAssociations" Name=".mrpack" Value="theseus.mrpack.Document" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\MIMEAssociations" Name="application/mrpack" Value="theseus.mrpack.Document" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\shell\Open\command" Value="&quot;[INSTALLDIR]theseus&quot; -e &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\RegisteredApplications" Name="theseus" Value="SOFTWARE\modrinth\theseus\Capabilities" Type="string" KeyPath="yes" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\theseus.mrpack.Document" Name="MRPACK File" Value="Modrinth Modpack Installer" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack" Name="Content Type" Value="application/mrpack" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithList\theseus" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithProgids" Name="theseus.mrpack.Document" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\SupportedTypes" Name=".mrpack" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\shell\open" Name="FriendlyAppName" Value="theseus" Type="string" />
</Component>
<!-- /THESEUS -->
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
<!-- THESEUS -->
<ComponentRef Id="FileTypeAssociationsReg" />
<!-- /THESEUS -->
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>
-41
View File
@@ -1,41 +0,0 @@
!macro NSIS_HOOK_POSTINSTALL
SetShellVarContext current
IfFileExists "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe" file_found file_not_found
file_found:
Delete "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Delete "$LOCALAPPDATA${PRODUCTNAME}\uninstall.exe"
RMDir "$LOCALAPPDATA${PRODUCTNAME}"
!insertmacro DeleteAppUserModelId
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
!insertmacro IsShortcutTarget "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
RMDir "$SMPROGRAMS$AppStartMenuFolder"
${EndIf}
!insertmacro IsShortcutTarget "$SMPROGRAMS${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS${PRODUCTNAME}.lnk"
${EndIf}
!insertmacro IsShortcutTarget "$DESKTOP${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$DESKTOP${PRODUCTNAME}.lnk"
Delete "$DESKTOP${PRODUCTNAME}.lnk"
${EndIf}
DeleteRegKey HKCU "${UNINSTKEY}"
goto end_of_test ;<== important for not continuing on the else branch
file_not_found:
end_of_test:
!macroend
+1 -1
View File
@@ -9,7 +9,7 @@
"fix": "cargo fmt && cargo clippy --fix" "fix": "cargo fmt && cargo clippy --fix"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "2.0.0-rc.16" "@tauri-apps/cli": "2.0.0-rc.5"
}, },
"dependencies": { "dependencies": {
"@modrinth/app-frontend": "workspace:*", "@modrinth/app-frontend": "workspace:*",
-23
View File
@@ -1,23 +0,0 @@
document.addEventListener(
'click',
function (e) {
window.top.postMessage({ modrinthAdClick: true }, 'https://modrinth.com')
let target = e.target
while (target != null) {
if (target.matches('a')) {
e.preventDefault()
if (target.href) {
window.top.postMessage({ modrinthOpenUrl: target.href }, 'https://modrinth.com')
}
break
}
target = target.parentElement
}
},
true,
)
window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
}
-216
View File
@@ -1,216 +0,0 @@
use serde::Serialize;
use std::collections::HashSet;
use std::time::{Duration, Instant};
use tauri::plugin::TauriPlugin;
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
use tauri_plugin_shell::ShellExt;
use theseus::settings;
use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub size: Option<LogicalSize<f32>>,
pub position: Option<LogicalPosition<f32>>,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("ads")
.setup(|app, _api| {
app.manage(RwLock::new(AdsState {
shown: true,
size: None,
position: None,
last_click: None,
malicious_origins: HashSet::new(),
}));
// We refresh the ads window every 5 minutes for performance
let app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) = app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
tokio::time::sleep(std::time::Duration::from_secs(60 * 5))
.await;
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
init_ads_window,
hide_ads_window,
scroll_ads_window,
show_ads_window,
record_ads_click,
open_link,
get_ads_personalization,
])
.build()
}
#[tauri::command]
#[cfg(not(target_os = "linux"))]
pub async fn init_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
x: f32,
y: f32,
width: f32,
height: f32,
override_shown: bool,
) -> crate::api::Result<()> {
use tauri::WebviewUrl;
const LINK_SCRIPT: &str = include_str!("ads-init.js");
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.size = Some(LogicalSize::new(width, height));
state.position = Some(LogicalPosition::new(x, y));
if override_shown {
state.shown = true;
}
if let Some(webview) = app.webviews().get("ads-window") {
if state.shown {
let _ = webview.set_position(LogicalPosition::new(x, y));
let _ = webview.set_size(LogicalSize::new(width, height));
}
} else if let Some(window) = app.get_window("main") {
let _ = window.add_child(
tauri::webview::WebviewBuilder::new(
"ads-window",
WebviewUrl::External(
AD_LINK.parse().unwrap(),
),
)
.initialization_script(LINK_SCRIPT)
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
.zoom_hotkeys_enabled(false)
.transparent(true),
if state.shown {
LogicalPosition::new(x, y)
} else {
LogicalPosition::new(-1000.0, -1000.0)
},
LogicalSize::new(width, height),
);
}
Ok(())
}
// TODO: make ads work on linux
#[tauri::command]
#[cfg(target_os = "linux")]
pub async fn init_ads_window() {}
#[tauri::command]
pub async fn show_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.shown = true;
if let Some(size) = state.size {
let _ = webview.set_size(size);
}
if let Some(position) = state.position {
let _ = webview.set_position(position);
}
}
Ok(())
}
#[tauri::command]
pub async fn hide_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
reset: Option<bool>,
) -> crate::api::Result<()> {
if let Some(webview) = app.webviews().get("ads-window") {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.shown = false;
if reset.unwrap_or(false) {
state.size = None;
state.position = None;
}
let _ = webview.set_position(LogicalPosition::new(-1000, -1000));
}
Ok(())
}
#[derive(Serialize, Clone)]
struct ScrollEvent {
scroll: f32,
}
#[tauri::command]
pub async fn scroll_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
scroll: f32,
) -> crate::api::Result<()> {
let _ = app.emit("ads-scroll", ScrollEvent { scroll });
Ok(())
}
#[tauri::command]
pub async fn record_ads_click<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
state.last_click = Some(Instant::now());
Ok(())
}
#[tauri::command]
pub async fn open_link<R: Runtime>(
app: tauri::AppHandle<R>,
path: String,
origin: String,
) -> crate::api::Result<()> {
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if url::Url::parse(&path).is_ok()
&& !state.malicious_origins.contains(&origin)
{
if let Some(last_click) = state.last_click {
if last_click.elapsed() < Duration::from_millis(100) {
let _ = app.shell().open(&path, None);
state.last_click = None;
return Ok(());
}
}
}
tracing::info!("Malicious click: {path} origin {origin}");
state.malicious_origins.insert(origin);
Ok(())
}
#[tauri::command]
pub async fn get_ads_personalization() -> crate::api::Result<bool> {
let res = settings::get().await?;
Ok(res.personalized_ads)
}
-13
View File
@@ -16,7 +16,6 @@ pub mod settings;
pub mod tags; pub mod tags;
pub mod utils; pub mod utils;
pub mod ads;
pub mod cache; pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>; pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
@@ -40,10 +39,6 @@ pub enum TheseusSerializableError {
#[error("Tauri error: {0}")] #[error("Tauri error: {0}")]
Tauri(#[from] tauri::Error), Tauri(#[from] tauri::Error),
#[cfg(feature = "updater")]
#[error("Tauri updater error: {0}")]
TauriUpdater(#[from] tauri_plugin_updater::Error),
} }
// Generic implementation of From<T> for ErrorTypeA // Generic implementation of From<T> for ErrorTypeA
@@ -91,15 +86,7 @@ macro_rules! impl_serialize {
} }
// Use the macro to implement Serialize for TheseusSerializableError // Use the macro to implement Serialize for TheseusSerializableError
#[cfg(not(feature = "updater"))]
impl_serialize! { impl_serialize! {
IO, IO,
Tauri, Tauri,
} }
#[cfg(feature = "updater")]
impl_serialize! {
IO,
Tauri,
TauriUpdater,
}
+2 -84
View File
@@ -26,73 +26,11 @@ extern crate objc;
#[tauri::command] #[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app.clone()).await?; theseus::EventState::init(app.clone()).await?;
State::init().await?;
#[cfg(feature = "updater")]
{
use tauri_plugin_updater::UpdaterExt;
let updater = app.updater_builder().build()?;
let update_fut = updater.check();
State::init().await?;
let check_bar = theseus::init_loading(
theseus::LoadingBarType::CheckingForUpdates,
1.0,
"Checking for updates...",
)
.await?;
let update = update_fut.await;
drop(check_bar);
if let Some(update) = update.ok().flatten() {
tracing::info!("Update found: {:?}", update.download_url);
let loader_bar_id = theseus::init_loading(
theseus::LoadingBarType::LauncherUpdate {
version: update.version.clone(),
current_version: update.current_version.clone(),
},
1.0,
"Updating Modrinth App...",
)
.await?;
// 100 MiB
const DEFAULT_CONTENT_LENGTH: u64 = 1024 * 1024 * 100;
update
.download_and_install(
|chunk_length, content_length| {
let _ = theseus::emit_loading(
&loader_bar_id,
(chunk_length as f64)
/ (content_length
.unwrap_or(DEFAULT_CONTENT_LENGTH)
as f64),
None,
);
},
|| {},
)
.await?;
app.restart();
}
}
#[cfg(not(feature = "updater"))]
{
State::init().await?;
}
let state = State::get().await?; let state = State::get().await?;
app.asset_protocol_scope() app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir(), true)?; .allow_directory(state.directories.caches_dir(), true)?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir().join("icons"), true)?;
Ok(()) Ok(())
} }
@@ -101,7 +39,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[tauri::command] #[tauri::command]
fn show_window(app: tauri::AppHandle) { fn show_window(app: tauri::AppHandle) {
let win = app.get_window("main").unwrap(); let win = app.get_webview_window("main").unwrap();
if let Err(e) = win.show() { if let Err(e) = win.show() {
MessageDialog::new() MessageDialog::new()
.set_type(MessageType::Error) .set_type(MessageType::Error)
@@ -135,11 +73,6 @@ async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
Ok(()) Ok(())
} }
#[tauri::command]
fn restart_app(app: tauri::AppHandle) {
app.restart();
}
// if Tauri app is called with arguments, then those arguments will be treated as commands // if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks // ie: deep links or filepaths for .mrpacks
fn main() { fn main() {
@@ -169,19 +102,6 @@ fn main() {
} }
builder = builder builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if let Some(payload) = args.get(1) {
tracing::info!("Handling deep link from arg {payload}");
let payload = payload.clone();
tauri::async_runtime::spawn(api::utils::handle_command(
payload,
));
}
if let Some(win) = app.get_window("main") {
let _ = win.set_focus();
}
}))
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())
@@ -259,14 +179,12 @@ fn main() {
.plugin(api::tags::init()) .plugin(api::tags::init())
.plugin(api::utils::init()) .plugin(api::utils::init())
.plugin(api::cache::init()) .plugin(api::cache::init())
.plugin(api::ads::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
initialize_state, initialize_state,
is_dev, is_dev,
toggle_decorations, toggle_decorations,
api::mr_auth::modrinth_auth_login, api::mr_auth::modrinth_auth_login,
show_window, show_window,
restart_app,
]); ]);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+1 -6
View File
@@ -5,15 +5,10 @@
"build": { "build": {
"features": ["updater"] "features": ["updater"]
}, },
"app": {
"security": {
"capabilities": ["ads", "core", "plugins", "updater"]
}
},
"plugins": { "plugins": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
"endpoints": ["https://launcher-files.modrinth.com/updates.json"] "endpoints": ["https://launcher-files.modrinth.com/updates.json"]
} }
} }
} }
+6 -19
View File
@@ -21,9 +21,8 @@
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com", "timestampUrl": "http://timestamp.digicert.com",
"nsis": { "wix": {
"installMode": "perMachine", "template": "./msi/main.wxs"
"installerHooks": "./nsis/hooks.nsi"
} }
}, },
"longDescription": "", "longDescription": "",
@@ -49,8 +48,7 @@
] ]
}, },
"productName": "Modrinth App", "productName": "Modrinth App",
"version": "0.8.9", "version": "0.8.3-1",
"mainBinaryName": "Modrinth App",
"identifier": "ModrinthApp", "identifier": "ModrinthApp",
"plugins": { "plugins": {
"deep-link": { "deep-link": {
@@ -72,10 +70,10 @@
"resizable": true, "resizable": true,
"title": "Modrinth App", "title": "Modrinth App",
"width": 1280, "width": 1280,
"minHeight": 750, "minHeight": 700,
"minWidth": 1100, "minWidth": 1100,
"visible": false, "visible": false,
"zoomHotkeysEnabled": false, "zoomHotkeysEnabled": true,
"decorations": false "decorations": false
} }
], ],
@@ -88,18 +86,7 @@
], ],
"enable": true "enable": true
}, },
"capabilities": ["ads", "core", "plugins"], "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src unsafe-inline 'self'"
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs https://cmp.inmobi.com",
"font-src": [
"https://cdn-raw.modrinth.com/fonts/inter/"
],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self'",
"script-src": "https://cmp.inmobi.com https://*.cloudflare.com https://*.posthog.com 'self'",
"frame-src": "https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'"
}
} }
} }
} }
-3
View File
@@ -1,3 +0,0 @@
{
"mainBinaryName": "ModrinthApp"
}
+1 -1
View File
@@ -12,7 +12,7 @@
"minHeight": 700, "minHeight": 700,
"minWidth": 1100, "minWidth": 1100,
"visible": false, "visible": false,
"zoomHotkeysEnabled": false, "zoomHotkeysEnabled": true,
"decorations": true "decorations": true
} }
] ]
@@ -62,10 +62,6 @@
.normal-page__content { .normal-page__content {
grid-area: content; grid-area: content;
} }
.normal-page__header {
grid-area: header;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -165,8 +161,4 @@
max-width: calc(80rem - 18.75rem - 0.75rem); max-width: calc(80rem - 18.75rem - 0.75rem);
//overflow-x: hidden; //overflow-x: hidden;
} }
.normal-page__header {
grid-area: header;
}
} }
@@ -1,7 +1,7 @@
<template> <template>
<div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised"> <div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised">
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6"> <div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p> <p class="m-0 text-2xl font-bold text-contrast">90% of ad revenue goes to creators</p>
<nuxt-link to="/plus" class="mt-auto items-center gap-1 text-purple hover:underline"> <nuxt-link to="/plus" class="mt-auto items-center gap-1 text-purple hover:underline">
<span> <span>
Support creators and Modrinth ad-free with Support creators and Modrinth ad-free with
@@ -23,19 +23,9 @@ import { ChevronRightIcon } from "@modrinth/assets";
useHead({ useHead({
script: [ script: [
{ {
// Clean.io
src: "https://cadmus.script.ac/d14pdm1b7fi5kh/script.js",
},
{
// Aditude
src: "https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js", src: "https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js",
async: true, async: true,
}, },
{
// Optima
src: "https://bservr.com/o.js?uid=8118d1fdb2e0d6f32180bd27",
async: true,
},
{ {
src: "/inmobi.js", src: "/inmobi.js",
async: true, async: true,
@@ -57,9 +47,6 @@ onMounted(() => {
{ {
divId: "modrinth-rail-1", divId: "modrinth-rail-1",
baseDivId: "pb-slot-square-2", baseDivId: "pb-slot-square-2",
targeting: {
location: "web",
},
}, },
]); ]);
}); });
@@ -57,7 +57,7 @@ export function createDisplayNames(
try { try {
return dict.of(lookup); return dict.of(lookup);
} catch { } catch (err) {
console.warn( console.warn(
`Failed to get display name for ${lookup} using dictionary for ${ `Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale this.resolvedOptions().locale
+7 -7
View File
@@ -1,21 +1,21 @@
import hljs from "highlight.js/lib/core"; import hljs from "highlight.js/lib/core";
// Scripting // Scripting
import javascript from "highlight.js/lib/languages/javascript"; import javascript from "highlight.js/lib/languages/javascript";
import lua from "highlight.js/lib/languages/lua";
import python from "highlight.js/lib/languages/python"; import python from "highlight.js/lib/languages/python";
import lua from "highlight.js/lib/languages/lua";
// Coding // Coding
import groovy from "highlight.js/lib/languages/groovy";
import java from "highlight.js/lib/languages/java"; import java from "highlight.js/lib/languages/java";
import kotlin from "highlight.js/lib/languages/kotlin"; import kotlin from "highlight.js/lib/languages/kotlin";
import scala from "highlight.js/lib/languages/scala"; import scala from "highlight.js/lib/languages/scala";
import groovy from "highlight.js/lib/languages/groovy";
// Configs // Configs
import { configuredXss, md } from "@modrinth/utils";
import gradle from "highlight.js/lib/languages/gradle"; import gradle from "highlight.js/lib/languages/gradle";
import ini from "highlight.js/lib/languages/ini";
import json from "highlight.js/lib/languages/json"; import json from "highlight.js/lib/languages/json";
import properties from "highlight.js/lib/languages/properties"; import ini from "highlight.js/lib/languages/ini";
import xml from "highlight.js/lib/languages/xml";
import yaml from "highlight.js/lib/languages/yaml"; import yaml from "highlight.js/lib/languages/yaml";
import xml from "highlight.js/lib/languages/xml";
import properties from "highlight.js/lib/languages/properties";
import { md, configuredXss } from "@modrinth/utils";
/* REGISTRATION */ /* REGISTRATION */
// Scripting // Scripting
@@ -54,7 +54,7 @@ export const renderHighlightedString = (string) =>
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return hljs.highlight(str, { language: lang }).value; return hljs.highlight(str, { language: lang }).value;
} catch { } catch (__) {
/* empty */ /* empty */
} }
} }
-20
View File
@@ -231,7 +231,6 @@
</template> </template>
<template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </template> <template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </template>
<template #analytics> <ChartIcon aria-hidden="true" /> Analytics </template> <template #analytics> <ChartIcon aria-hidden="true" /> Analytics </template>
<template #moderation> <ModerationIcon aria-hidden="true" /> Moderation </template>
<template #sign-out> <LogOutIcon aria-hidden="true" /> Sign out </template> <template #sign-out> <LogOutIcon aria-hidden="true" /> Sign out </template>
</OverflowMenu> </OverflowMenu>
<ButtonStyled v-else color="brand"> <ButtonStyled v-else color="brand">
@@ -779,7 +778,6 @@ const userMenuOptions = computed(() => {
link: "/settings", link: "/settings",
}, },
]; ];
// TODO: Only show if user has projects // TODO: Only show if user has projects
options = [ options = [
...options, ...options,
@@ -803,24 +801,6 @@ const userMenuOptions = computed(() => {
link: "/dashboard/analytics", link: "/dashboard/analytics",
}, },
]; ];
if (
(auth.value && auth.value.user && auth.value.user.role === "moderator") ||
auth.value.user.role === "admin"
) {
options = [
...options,
{
divider: true,
},
{
id: "moderation",
color: "orange",
link: "/moderation/review",
},
];
}
options = [ options = [
...options, ...options,
{ {
+222 -202
View File
@@ -430,208 +430,220 @@
}" }"
> >
<div class="normal-page__header relative my-4"> <div class="normal-page__header relative my-4">
<ContentPageHeader> <div
<template #icon> class="grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
>
<div class="flex gap-4">
<Avatar :src="project.icon_url" :alt="project.title" size="96px" /> <Avatar :src="project.icon_url" :alt="project.title" size="96px" />
</template> <div class="flex flex-col gap-1">
<template #title> <div class="flex flex-wrap items-center gap-2">
{{ project.title }} <h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
</template> {{ project.title }}
<template #title-suffix> </h1>
<Badge v-if="auth.user && currentMember" :type="project.status" class="status-badge" /> <Badge
</template> v-if="auth.user && currentMember"
<template #summary> :type="project.status"
{{ project.description }} class="status-badge"
</template> />
<template #stats> </div>
<div <p class="m-0 line-clamp-2 max-w-[40rem]">
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold" {{ project.description }}
> </p>
<DownloadIcon class="h-6 w-6 text-secondary" /> <div class="mt-auto flex flex-wrap gap-4">
{{ $formatNumber(project.downloads) }}
</div>
<div
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
>
<HeartIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.followers) }}
</span>
</div>
<div class="hidden items-center gap-2 md:flex">
<TagsIcon class="h-6 w-6 text-secondary" />
<div class="flex flex-wrap gap-2">
<div <div
v-for="(category, index) in project.categories" class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
:key="index"
class="tag-list__item"
> >
{{ formatCategory(category) }} <DownloadIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.downloads) }}
</span>
</div>
<div
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
>
<HeartIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.followers) }}
</span>
</div>
<div class="hidden items-center gap-2 md:flex">
<TagsIcon class="h-6 w-6 text-secondary" />
<div class="flex flex-wrap gap-2">
<div
v-for="(category, index) in project.categories"
:key="index"
class="tag-list__item"
>
{{ formatCategory(category) }}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </div>
<template #actions> <div class="flex flex-col justify-center gap-4">
<div class="hidden sm:contents"> <div class="flex flex-wrap gap-2">
<ButtonStyled <div class="hidden sm:contents">
size="large" <ButtonStyled
:color="route.name === 'type-id-version-version' ? `standard` : `brand`" size="large"
> :color="route.name === 'type-id-version-version' ? `standard` : `brand`"
<button @click="(event) => downloadModal.show(event)"> >
<DownloadIcon aria-hidden="true" /> <button @click="(event) => downloadModal.show(event)">
Download <DownloadIcon aria-hidden="true" />
</button> Download
</ButtonStyled> </button>
</div> </ButtonStyled>
<div class="contents sm:hidden"> </div>
<div class="contents sm:hidden">
<ButtonStyled
size="large"
circular
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
>
<button
aria-label="Download"
class="flex sm:hidden"
@click="(event) => downloadModal.show(event)"
>
<DownloadIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<ButtonStyled <ButtonStyled
size="large" size="large"
circular circular
:color="route.name === 'type-id-version-version' ? `standard` : `brand`" :color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
> >
<button <button
aria-label="Download" v-if="auth.user"
class="flex sm:hidden" v-tooltip="following ? `Unfollow` : `Follow`"
@click="(event) => downloadModal.show(event)" :aria-label="following ? `Unfollow` : `Follow`"
@click="userFollowProject(project)"
> >
<DownloadIcon aria-hidden="true" /> <HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
</button> </button>
<nuxt-link v-else v-tooltip="'Follow'" to="/auth/sign-in" aria-label="Follow">
<HeartIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular>
<PopoutMenu v-if="auth.user" v-tooltip="'Save'" from="top-right" aria-label="Save">
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
placeholder="Search collections..."
class="search-input menu-search"
/>
<div v-if="collections.length > 0" class="collections-list">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">No collections found.</p>
</div>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
>
<PlusIcon aria-hidden="true" />
Create new collection
</button>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
<nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
>
<SettingsIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'analytics',
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
hoverOnly: true,
shown: auth.user && !!currentMember,
},
{
divider: true,
shown: auth.user && !!currentMember,
},
{
id: 'moderation-checklist',
action: () => (showModerationChecklist = true),
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
divider: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
id: 'report',
action: () =>
auth.user ? reportProject(project.id) : navigateTo('/auth/sign-in'),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #analytics>
<ChartIcon aria-hidden="true" />
Analytics
</template>
<template #moderation-checklist>
<ScaleIcon aria-hidden="true" />
Review project
</template>
<template #report>
<ReportIcon aria-hidden="true" />
Report
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
Copy ID
</template>
</OverflowMenu>
</ButtonStyled> </ButtonStyled>
</div> </div>
<ButtonStyled </div>
size="large" </div>
circular
:color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
>
<button
v-if="auth.user"
v-tooltip="following ? `Unfollow` : `Follow`"
:aria-label="following ? `Unfollow` : `Follow`"
@click="userFollowProject(project)"
>
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
</button>
<nuxt-link v-else v-tooltip="'Follow'" to="/auth/sign-in" aria-label="Follow">
<HeartIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular>
<PopoutMenu v-if="auth.user" v-tooltip="'Save'" from="top-right" aria-label="Save">
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
placeholder="Search collections..."
class="search-input menu-search"
/>
<div v-if="collections.length > 0" class="collections-list">
<Checkbox
v-for="option in collections
.slice()
.sort((a, b) => a.name.localeCompare(b.name))"
:key="option.id"
:model-value="option.projects.includes(project.id)"
class="popout-checkbox"
@update:model-value="() => onUserCollectProject(option, project.id)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">No collections found.</p>
</div>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
>
<PlusIcon aria-hidden="true" />
Create new collection
</button>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
<nuxt-link
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
>
<SettingsIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'analytics',
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
hoverOnly: true,
shown: auth.user && !!currentMember,
},
{
divider: true,
shown: auth.user && !!currentMember,
},
{
id: 'moderation-checklist',
action: () => (showModerationChecklist = true),
color: 'orange',
hoverOnly: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
divider: true,
shown:
auth.user &&
tags.staffRoles.includes(auth.user.role) &&
!showModerationChecklist,
},
{
id: 'report',
action: () =>
auth.user ? reportProject(project.id) : navigateTo('/auth/sign-in'),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #analytics>
<ChartIcon aria-hidden="true" />
Analytics
</template>
<template #moderation-checklist>
<ScaleIcon aria-hidden="true" />
Review project
</template>
<template #report>
<ReportIcon aria-hidden="true" />
Report
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
Copy ID
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
<ProjectMemberHeader <ProjectMemberHeader
v-if="currentMember" v-if="currentMember"
:project="project" :project="project"
@@ -689,13 +701,13 @@
" "
> >
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3> <h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="tag-list"> <div class="status-list">
<div <div
v-if=" v-if="
(project.client_side === 'required' && project.server_side !== 'required') || (project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional') (project.client_side === 'optional' && project.server_side === 'optional')
" "
class="tag-list__item" class="status-list__item"
> >
<ClientIcon aria-hidden="true" /> <ClientIcon aria-hidden="true" />
Client-side Client-side
@@ -705,29 +717,34 @@
(project.server_side === 'required' && project.client_side !== 'required') || (project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional') (project.client_side === 'optional' && project.server_side === 'optional')
" "
class="tag-list__item" class="status-list__item"
> >
<ServerIcon aria-hidden="true" /> <ServerIcon aria-hidden="true" />
Server-side Server-side
</div> </div>
<div v-if="false" class="tag-list__item"> <div v-if="false" class="status-list__item">
<UserIcon aria-hidden="true" /> <UserIcon aria-hidden="true" />
Singleplayer Singleplayer
</div> </div>
<div <div
v-if=" v-if="project.client_side === 'required' && project.server_side === 'required'"
project.project_type !== 'datapack' && class="status-list__item"
((project.client_side === 'required' && project.server_side === 'required') ||
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional'))
"
class="tag-list__item"
> >
<MonitorSmartphoneIcon aria-hidden="true" /> <MonitorSmartphoneIcon aria-hidden="true" />
Client and server Client and server
</div> </div>
<div
v-else-if="
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional')
"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server <span class="text-sm">(optional)</span>
</div>
</div> </div>
</section> </section>
</div> </div>
@@ -1027,7 +1044,6 @@ import {
OverflowMenu, OverflowMenu,
PopoutMenu, PopoutMenu,
ScrollablePanel, ScrollablePanel,
ContentPageHeader,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils"; import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -1398,7 +1414,7 @@ try {
versions = shallowRef(toRaw(versions)); versions = shallowRef(toRaw(versions));
featuredVersions = shallowRef(toRaw(featuredVersions)); featuredVersions = shallowRef(toRaw(featuredVersions));
} catch { } catch (error) {
throw createError({ throw createError({
fatal: true, fatal: true,
statusCode: 404, statusCode: 404,
@@ -1718,6 +1734,10 @@ const navLinks = computed(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.normal-page__header {
grid-area: header;
}
.settings-header { .settings-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -139,8 +139,8 @@
class="image" class="image"
:class="{ 'zoomed-in': zoomedIn }" :class="{ 'zoomed-in': zoomedIn }"
:src=" :src="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'" :alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -165,8 +165,8 @@
class="open circle-button" class="open circle-button"
target="_blank" target="_blank"
:href=" :href="
expandedGalleryItem.raw_url expandedGalleryItem.url
? expandedGalleryItem.raw_url ? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg' : 'https://cdn.modrinth.com/placeholder-banner.svg'
" "
> >
@@ -95,7 +95,7 @@ const props = defineProps<{
members: User[]; members: User[];
currentMember: User; currentMember: User;
dependencies: Dependency[]; dependencies: Dependency[];
resetProject: (opts?: { dedupe?: "cancel" | "defer" }) => Promise<void>; resetProject: Function;
}>(); }>();
const version = computed(() => { const version = computed(() => {
@@ -567,7 +567,6 @@
:show-labels="false" :show-labels="false"
:limit="6" :limit="6"
:hide-selected="true" :hide-selected="true"
:custom-label="(version) => version"
placeholder="Choose versions..." placeholder="Choose versions..."
/> />
<Checkbox <Checkbox
+1 -7
View File
@@ -27,7 +27,6 @@ const windowsLink = ref(null);
const linuxLinks = { const linuxLinks = {
appImage: null, appImage: null,
deb: null, deb: null,
rpm: null,
thirdParty: "https://support.modrinth.com/en/articles/9298760", thirdParty: "https://support.modrinth.com/en/articles/9298760",
}; };
const macLinks = { const macLinks = {
@@ -58,7 +57,6 @@ macLinks.intel = launcherUpdates.value.platforms["darwin-x86_64"].install_urls[0
windowsLink.value = launcherUpdates.value.platforms["windows-x86_64"].install_urls[0]; windowsLink.value = launcherUpdates.value.platforms["windows-x86_64"].install_urls[0];
linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1]; linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1];
linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0]; linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0];
linuxLinks.rpm = launcherUpdates.value.platforms["linux-x86_64"].install_urls[2];
onMounted(() => { onMounted(() => {
os.value = navigator?.platform.toString(); os.value = navigator?.platform.toString();
@@ -881,11 +879,7 @@ useSeoMeta({
</a> </a>
<a :href="linuxLinks.deb" download=""> <a :href="linuxLinks.deb" download="">
<DownloadIcon /> <DownloadIcon />
<span> Download the DEB </span> <span> Download the Deb </span>
</a>
<a :href="linuxLinks.rpm" download="">
<DownloadIcon />
<span> Download the RPM </span>
</a> </a>
<a :href="linuxLinks.thirdParty" download=""> <a :href="linuxLinks.thirdParty" download="">
<LinkIcon /> <LinkIcon />
+2 -2
View File
@@ -195,7 +195,7 @@ const onAuthorize = async () => {
} }
throw new Error(formatMessage(messages.noRedirectUrlError)); throw new Error(formatMessage(messages.noRedirectUrlError));
} catch { } catch (error) {
data.$notify({ data.$notify({
group: "main", group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
@@ -222,7 +222,7 @@ const onReject = async () => {
} }
throw new Error(formatMessage(messages.noRedirectUrlError)); throw new Error(formatMessage(messages.noRedirectUrlError));
} catch { } catch (error) {
data.$notify({ data.$notify({
group: "main", group: "main",
title: formatMessage(commonMessages.errorNotificationTitle), title: formatMessage(commonMessages.errorNotificationTitle),
@@ -145,7 +145,7 @@ if (route.query.flow) {
await useAuth(auth.value.token); await useAuth(auth.value.token);
} }
} }
} catch { } catch (err) {
success.value = false; success.value = false;
} }
} }
+1 -1
View File
@@ -612,7 +612,7 @@ const visibility = ref(collection.value.status);
const removeProjects = ref([]); const removeProjects = ref([]);
async function unfollowProject(project) { async function unfollowProject(project) {
await userFollowProject(project); await userUnfollowProject(project);
projects.value = projects.value.filter((x) => x.id !== project.id); projects.value = projects.value.filter((x) => x.id !== project.id);
} }
@@ -88,6 +88,20 @@
></span ></span
> >
</div> </div>
<div class="grid-display__item">
<div class="label">Current balance</div>
<div class="value">
{{ $formatMoney(auth.user.payout_data.balance, true) }}
</div>
<NuxtLink
v-if="auth.user.payout_data.balance > 0"
class="goto-link"
to="/dashboard/revenue"
>
Withdraw earnings
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</NuxtLink>
</div>
</div> </div>
</section> </section>
</div> </div>
@@ -2,24 +2,21 @@
<div> <div>
<section class="universal-card"> <section class="universal-card">
<h2 class="text-2xl">Revenue</h2> <h2 class="text-2xl">Revenue</h2>
<div v-if="userBalance.available >= minWithdraw"> <div v-if="auth.user.payout_data.balance >= minWithdraw">
<p> <p>
You have You have
<strong>{{ $formatMoney(userBalance.available) }}</strong> <strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong>
available to withdraw. <strong>{{ $formatMoney(userBalance.pending) }}</strong> of your available to withdraw.
balance is <nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p> </p>
</div> </div>
<p v-else> <p v-else>
You have made You have made
<strong>{{ $formatMoney(userBalance.available) }}</strong <strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
>, which is under the minimum of ${{ minWithdraw }} to withdraw. >, which is under the minimum of ${{ minWithdraw }} to withdraw.
<strong>{{ $formatMoney(userBalance.pending) }}</strong> of your balance is
<nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p> </p>
<div class="input-group mt-4"> <div class="input-group mt-4">
<nuxt-link <nuxt-link
v-if="userBalance.available >= minWithdraw" v-if="auth.user.payout_data.balance >= minWithdraw"
class="iconified-button brand-button" class="iconified-button brand-button"
to="/dashboard/revenue/withdraw" to="/dashboard/revenue/withdraw"
> >
@@ -84,10 +81,6 @@ import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from "@modrint
const auth = await useAuth(); const auth = await useAuth();
const minWithdraw = ref(0.01); const minWithdraw = ref(0.01);
const { data: userBalance } = await useAsyncData(`payout/balance`, () =>
useBaseFetch(`payout/balance`, { apiVersion: 3 }),
);
async function updateVenmo() { async function updateVenmo() {
startLoading(); startLoading();
try { try {
@@ -88,7 +88,7 @@
<p> <p>
You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program. You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program.
How much of your How much of your
<strong>{{ $formatMoney(userBalance.available) }}</strong> balance would you like to transfer <strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong> balance would you like to
transfer to {{ selectedMethod.name }}? transfer to {{ selectedMethod.name }}?
</p> </p>
<div class="confirmation-input"> <div class="confirmation-input">
@@ -212,13 +212,10 @@ const country = ref(
countries.value.find((x) => x.id === (auth.value.user.payout_data.paypal_region ?? "US")), countries.value.find((x) => x.id === (auth.value.user.payout_data.paypal_region ?? "US")),
); );
const [{ data: userBalance }, { data: payoutMethods, refresh: refreshPayoutMethods }] = const { data: payoutMethods, refresh: refreshPayoutMethods } = await useAsyncData(
await Promise.all([ `payout/methods?country=${country.value.id}`,
useAsyncData(`payout/balance`, () => useBaseFetch(`payout/balance`, { apiVersion: 3 })), () => useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 }),
useAsyncData(`payout/methods?country=${country.value.id}`, () => );
useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 }),
),
]);
const selectedMethodId = ref(payoutMethods.value[0].id); const selectedMethodId = ref(payoutMethods.value[0].id);
const selectedMethod = computed(() => const selectedMethod = computed(() =>
@@ -298,10 +295,10 @@ const knownErrors = computed(() => {
if (!parsedAmount.value && amount.value.length > 0) { if (!parsedAmount.value && amount.value.length > 0) {
errors.push(`${amount.value} is not a valid amount`); errors.push(`${amount.value} is not a valid amount`);
} else if ( } else if (
parsedAmount.value > userBalance.value.available || parsedAmount.value > auth.value.user.payout_data.balance ||
parsedAmount.value > maxWithdrawAmount.value parsedAmount.value > maxWithdrawAmount.value
) { ) {
const maxAmount = Math.min(userBalance.value.available, maxWithdrawAmount.value); const maxAmount = Math.min(auth.value.user.payout_data.balance, maxWithdrawAmount.value);
errors.push(`The amount must be no more than ${data.$formatMoney(maxAmount)}`); errors.push(`The amount must be no more than ${data.$formatMoney(maxAmount)}`);
} else if (parsedAmount.value <= fees.value || parsedAmount.value < minWithdrawAmount.value) { } else if (parsedAmount.value <= fees.value || parsedAmount.value < minWithdrawAmount.value) {
const minAmount = Math.max(fees.value + 0.01, minWithdrawAmount.value); const minAmount = Math.max(fees.value + 0.01, minWithdrawAmount.value);
+17 -61
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="markdown-body"> <div class="markdown-body">
<h1>Rewards Program Information</h1> <h1>Rewards Program Information</h1>
<p><em>Last modified: Sep 12, 2024</em></p> <p><em>Last modified: May 13, 2024</em></p>
<p> <p>
This page was created for transparency for how the rewards program works on Modrinth. Feel This page was created for transparency for how the rewards program works on Modrinth. Feel
free to join our Discord or email free to join our Discord or email
@@ -15,11 +15,10 @@
<h2>Rewards Distribution</h2> <h2>Rewards Distribution</h2>
<p> <p>
We collect ad revenue on our website and app through our ad network We collect ad revenue on our website and app through our ad network
<a href="https://adrinth.com">Adrinth</a>, which is powered by <a href="https://adrinth.com">Adrinth</a>. We then distribute this ad revenue to creators.
<a href="https://aditude.io">Aditude</a>. We then distribute this ad revenue to creators.
</p> </p>
<p> <p>
The advertising revenue of the entire website and app is split 75% to creators and 25% to The advertising revenue of the entire website and app is split 90% to creators and 10% to
Modrinth. Modrinth.
</p> </p>
<p> <p>
@@ -43,10 +42,10 @@
</ul> </ul>
<p>In this scenario, the earnings for each creator and Modrinth would be as follows:</p> <p>In this scenario, the earnings for each creator and Modrinth would be as follows:</p>
<ul> <ul>
<li>Modrinth: $25 (25% of $100, the site's earnings for the day)</li> <li>Modrinth: $10 (10% of $100, the site's earnings for the day)</li>
<li>User A: $48.91 ($75 * (10 + 30 + 100 + 10)/230)</li> <li>User A: $58.69 ($90 * (10 + 30 + 100 + 10)/230)</li>
<li>User B: $10.43 (0.4 * $75 * (50 + 20 + 10 + 0)/230)</li> <li>User B: $12.52 (0.4 * $90 * (50 + 20 + 10 + 0)/230)</li>
<li>User C: $15.65 (0.6 * $75 * (50 + 20 + 10 + 0)/230)</li> <li>User C: $18.78 (0.6 * $90 * (50 + 20 + 10 + 0)/230)</li>
<li>Note: 230 is the sum of all page views and in-app downloads from above</li> <li>Note: 230 is the sum of all page views and in-app downloads from above</li>
</ul> </ul>
<p> <p>
@@ -73,62 +72,19 @@
</p> </p>
<h3>What methods can I use withdraw money from my account? Are there any fees?</h3> <h3>What methods can I use withdraw money from my account? Are there any fees?</h3>
<p> <p>
Right now, you can use PayPal or Venmo to withdraw money from your Modrinth account. Gift card Right now, you can use PayPal or Venmo to withdraw money from your Modrinth account. We are
withdrawal is also available. We are working on more methods to withdraw money from your working on more methods to withdraw money from your account. There are fees to withdraw money
account. There are fees to withdraw money from your Modrinth account—see the revenue page in from your Modrinth account—see the revenue page in your dashboard for more information.
your dashboard for more information.
</p> </p>
<h3 id="pending">What does "pending" revenue mean in my dashboard?</h3> <h3>Modrinth used to give 100% of project page revenue to creators. What changed?</h3>
<p> <p>
Modrinth receives ad revenue from our ad providers on a NET 60 day basis. Due to this, not all While this is true, our new system (as of 08/05/23) gives more of the site's revenue to
revenue is immediately available to withdraw. We pay creators as soon as we receive the money creators, so creators will earn more. In the old system, we would earn revenue through
from our ad providers, which is 60 days after the last day of each month. This table outlines advertisements in search and user profile pages. This amounted on average each month to about
some example dates of how NET 60 payments are made: 15-20% of the site's total advertising revenue (so a 80-85% split to creators). The new system
</p> gives creators more revenue and a more favorable split towards creators (90%).
<table>
<thead>
<tr>
<th>Date</th>
<th>Payment available date</th>
</tr>
</thead>
<tbody>
<tr>
<td>January 1st</td>
<td>March 31st</td>
</tr>
<tr>
<td>January 15th</td>
<td>March 31st</td>
</tr>
<tr>
<td>March 3rd</td>
<td>May 30th</td>
</tr>
<tr>
<td>June 30th</td>
<td>August 29th</td>
</tr>
<tr>
<td>July 14th</td>
<td>September 29th</td>
</tr>
<tr>
<td>October 12th</td>
<td>December 30th</td>
</tr>
</tbody>
</table>
<h3>How do I know Modrinth is being transparent about revenue?</h3>
<p>
We aim to be as transparent as possible with creator revenue. All of our code is open source,
including our
<a href="https://github.com/modrinth/labrinth/blob/master/src/queue/payouts.rs#L561">
revenue distribution system </a
>. We also have an
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a> that allows users
to query exact daily revenue for the site.
</p> </p>
<h3></h3>
</div> </div>
</template> </template>
+209 -250
View File
@@ -1,260 +1,240 @@
<template> <template>
<div <div v-if="organization" class="normal-page">
v-if="organization" <div class="normal-page__sidebar">
class="experimental-styles-within new-page sidebar" <div v-if="routeHasSettings" class="universal-card">
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }" <Breadcrumbs
> current-title="Settings"
<ModalCreation ref="modal_creation" :organization-id="organization.id" /> :link-stack="[
<template v-if="routeHasSettings"> { href: `/dashboard/organizations`, label: 'Organizations' },
<div class="normal-page__sidebar"> {
href: `/organization/${organization.slug}`,
label: organization.name,
allowTrimming: true,
},
]"
/>
<div class="page-header__settings">
<Avatar size="sm" :src="organization.icon_url" />
<div class="title-section">
<h2 class="settings-title">
<nuxt-link :to="`/organization/${organization.slug}/settings`">
{{ organization.name }}
</nuxt-link>
</h2>
<span>
{{ $formatNumber(acceptedMembers?.length || 0) }}
member<template v-if="acceptedMembers?.length !== 1">s</template>
</span>
</div>
</div>
<h2>Organization settings</h2>
<NavStack>
<NavStackItem :link="`/organization/${organization.slug}/settings`" label="Overview">
<SettingsIcon />
</NavStackItem>
<NavStackItem
:link="`/organization/${organization.slug}/settings/members`"
label="Members"
>
<UsersIcon />
</NavStackItem>
<NavStackItem
:link="`/organization/${organization.slug}/settings/projects`"
label="Projects"
>
<BoxIcon />
</NavStackItem>
<NavStackItem
:link="`/organization/${organization.slug}/settings/analytics`"
label="Analytics"
>
<ChartIcon />
</NavStackItem>
</NavStack>
</div>
<template v-else>
<div class="universal-card"> <div class="universal-card">
<Breadcrumbs <div class="page-header__icon">
current-title="Settings" <Avatar size="md" :src="organization.icon_url" />
:link-stack="[
{ href: `/dashboard/organizations`, label: 'Organizations' },
{
href: `/organization/${organization.slug}`,
label: organization.name,
allowTrimming: true,
},
]"
/>
<div class="page-header__settings">
<Avatar size="sm" :src="organization.icon_url" />
<div class="title-section">
<h2 class="settings-title">
<nuxt-link :to="`/organization/${organization.slug}/settings`">
{{ organization.name }}
</nuxt-link>
</h2>
<span>
{{ $formatNumber(acceptedMembers?.length || 0) }}
member<template v-if="acceptedMembers?.length !== 1">s</template>
</span>
</div>
</div> </div>
<h2>Organization settings</h2> <div class="page-header__text">
<h1 class="title">{{ organization.name }}</h1>
<NavStack> <div>
<NavStackItem :link="`/organization/${organization.slug}/settings`" label="Overview"> <span class="organization-label"><OrganizationIcon /> Organization</span>
<SettingsIcon /> </div>
</NavStackItem>
<NavStackItem <div class="organization-description">
:link="`/organization/${organization.slug}/settings/members`" <div class="metadata-item markdown-body collection-description">
label="Members" <p>{{ organization.description }}</p>
> </div>
<UsersIcon />
</NavStackItem> <hr class="card-divider" />
<NavStackItem
:link="`/organization/${organization.slug}/settings/projects`" <div class="primary-stat">
label="Projects" <UserIcon class="primary-stat__icon" aria-hidden="true" />
> <div class="primary-stat__text">
<BoxIcon /> <span class="primary-stat__counter">
</NavStackItem> {{ $formatNumber(acceptedMembers?.length || 0) }}
<NavStackItem </span>
:link="`/organization/${organization.slug}/settings/analytics`" member<template v-if="acceptedMembers?.length !== 1">s</template>
label="Analytics" </div>
> </div>
<ChartIcon />
</NavStackItem> <div class="primary-stat">
</NavStack> <BoxIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ $formatNumber(projects?.length || 0) }}
</span>
project<span v-if="projects?.length !== 1">s</span>
</div>
</div>
<div class="primary-stat no-margin">
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ formatCompactNumber(sumDownloads) }}
</span>
download<span v-if="sumDownloads !== 1">s</span>
</div>
</div>
</div>
</div>
</div> </div>
</div>
<div class="normal-page__content">
<NuxtPage />
</div>
</template>
<template v-else>
<div class="normal-page__header py-4">
<ContentPageHeader>
<template #icon>
<Avatar :src="organization.icon_url" :alt="organization.name" size="96px" />
</template>
<template #title>
{{ organization.name }}
</template>
<template #title-suffix>
<div class="ml-1 flex items-center gap-2 font-semibold">
<OrganizationIcon /> Organization
</div>
</template>
<template #summary>
{{ organization.description }}
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<UsersIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(acceptedMembers?.length || 0) }}
members
</div>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div>
<div class="flex items-center gap-2 font-semibold">
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
downloads
</div>
</template>
<template #actions>
<ButtonStyled v-if="auth.user && currentMember" size="large">
<NuxtLink :to="`/organization/${organization.slug}/settings`">
<SettingsIcon aria-hidden="true" />
Manage
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () =>
navigateTo('/organization/' + organization.slug + '/settings/projects'),
hoverOnly: true,
shown: auth.user && currentMember,
},
{ divider: true, shown: auth.user && currentMember },
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
Manage projects
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</div>
<div class="normal-page__sidebar">
<AdPlaceholder <AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus" v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/> />
<div class="card flex-card"> <div class="creator-list universal-card">
<h2>Members</h2> <div class="title-and-link">
<div class="details-list"> <h3>Members</h3>
<template v-for="member in acceptedMembers" :key="member.user.id">
<nuxt-link
class="details-list__item details-list__item--type-large"
:to="`/user/${member.user.username}`"
>
<Avatar :src="member.user.avatar_url" circle />
<div class="rows">
<span class="flex items-center gap-1">
{{ member.user.username }}
<CrownIcon
v-if="member.is_owner"
v-tooltip="'Organization owner'"
class="text-brand-orange"
/>
</span>
<span class="details-list__item__text--style-secondary">
{{ member.role ? member.role : "Member" }}
</span>
</div>
</nuxt-link>
</template>
</div> </div>
</div>
</div>
<div class="normal-page__content">
<div v-if="isInvited" class="universal-card information invited">
<h2>Invitation to join {{ organization.name }}</h2>
<p>You have been invited to join {{ organization.name }}.</p>
<div class="input-group">
<button class="iconified-button brand-button" @click="onAcceptInvite">
<CheckIcon />Accept
</button>
<button class="iconified-button danger-button" @click="onDeclineInvite">
<XIcon />Decline
</button>
</div>
</div>
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
<NavTabs :links="navLinks" />
</div>
<template v-if="projects?.length > 0">
<div class="project-list display-mode--list">
<ProjectCard
v-for="project in (route.params.projectType !== undefined
? projects.filter((x) =>
x.project_types.includes(
route.params.projectType.substr(0, route.params.projectType.length - 1),
),
)
: projects
)
.slice()
.sort((a, b) => b.downloads - a.downloads)"
:id="project.slug || project.id"
:key="project.id"
:name="project.name"
:display="cosmetics.searchDisplayMode.user"
:featured-image="project.gallery.find((element) => element.featured)?.url"
project-type-url="project"
:description="project.summary"
:created-at="project.published"
:updated-at="project.updated"
:downloads="project.downloads.toString()"
:follows="project.followers.toString()"
:icon-url="project.icon_url"
:categories="project.categories"
:client-side="project.client_side"
:server-side="project.server_side"
:status="
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
? project.status
: null
"
:type="project.project_types[0] ?? 'project'"
:color="project.color"
/>
</div>
</template>
<div v-else-if="true" class="error"> <template v-for="member in acceptedMembers" :key="member.user.id">
<UpToDate class="icon" /><br /> <nuxt-link class="creator button-base" :to="`/user/${member.user.username}`">
<span class="preserve-lines text"> <Avatar :src="member.user.avatar_url" circle />
This organization doesn't have any projects yet. <p class="name">
<template v-if="isPermission(currentMember?.organization_permissions, 1 << 4)"> {{ member.user.username }}
Would you like to <CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
<a class="link" @click="$refs.modal_creation.show()">create one</a>? </p>
</template> <p class="role">{{ member.role }}</p>
</span> </nuxt-link>
</template>
</div>
</template>
</div>
<div v-if="!routeHasSettings" class="normal-page__content">
<ModalCreation ref="modal_creation" :organization-id="organization.id" />
<div v-if="isInvited" class="universal-card information invited">
<h2>Invitation to join {{ organization.name }}</h2>
<p>You have been invited to join {{ organization.name }}.</p>
<div class="input-group">
<button class="iconified-button brand-button" @click="onAcceptInvite">
<CheckIcon />Accept
</button>
<button class="iconified-button danger-button" @click="onDeclineInvite">
<XIcon />Decline
</button>
</div> </div>
</div> </div>
</template> <nav class="navigation-card">
<NavRow
:links="[
{
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.slug}`,
},
...projectTypes.map((x) => {
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.slug}/${x}s`,
};
}),
]"
/>
<div v-if="auth.user && currentMember" class="input-group">
<nuxt-link :to="`/organization/${organization.slug}/settings`" class="iconified-button">
<SettingsIcon /> Manage
</nuxt-link>
</div>
</nav>
<template v-if="projects?.length > 0">
<div class="project-list display-mode--list">
<ProjectCard
v-for="project in (route.params.projectType !== undefined
? projects.filter((x) =>
x.project_types.includes(
route.params.projectType.substr(0, route.params.projectType.length - 1),
),
)
: projects
)
.slice()
.sort((a, b) => b.downloads - a.downloads)"
:id="project.slug || project.id"
:key="project.id"
:name="project.name"
:display="cosmetics.searchDisplayMode.user"
:featured-image="project.gallery.find((element) => element.featured)?.url"
project-type-url="project"
:description="project.summary"
:created-at="project.published"
:updated-at="project.updated"
:downloads="project.downloads.toString()"
:follows="project.followers.toString()"
:icon-url="project.icon_url"
:categories="project.categories"
:client-side="project.client_side"
:server-side="project.server_side"
:status="
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
? project.status
: null
"
:type="project.project_types[0] ?? 'project'"
:color="project.color"
/>
</div>
</template>
<div v-else-if="true" class="error">
<UpToDate class="icon" /><br />
<span class="preserve-lines text">
This organization doesn't have any projects yet.
<template v-if="isPermission(currentMember?.organization_permissions, 1 << 4)">
Would you like to
<a class="link" @click="$refs.modal_creation.show()">create one</a>?
</template>
</span>
</div>
</div>
<NuxtPage />
</div> </div>
</template> </template>
<script setup> <script setup>
import { import {
BoxIcon, BoxIcon,
MoreVerticalIcon, UserIcon,
UsersIcon, UsersIcon,
SettingsIcon, SettingsIcon,
ChartIcon, ChartIcon,
CheckIcon, CheckIcon,
XIcon, XIcon,
ClipboardCopyIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Avatar, ButtonStyled, Breadcrumbs, ContentPageHeader, OverflowMenu } from "@modrinth/ui"; import { Avatar, Breadcrumbs } from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue"; import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue"; import NavStackItem from "~/components/ui/NavStackItem.vue";
import NavRow from "~/components/ui/NavRow.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
@@ -264,7 +244,6 @@ import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component"; import DownloadIcon from "~/assets/images/utils/download.svg?component";
import CrownIcon from "~/assets/images/utils/crown.svg?component"; import CrownIcon from "~/assets/images/utils/crown.svg?component";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
import NavTabs from "~/components/ui/NavTabs.vue";
const vintl = useVIntl(); const vintl = useVIntl();
const { formatMessage } = vintl; const { formatMessage } = vintl;
@@ -472,26 +451,6 @@ useSeoMeta({
ogDescription: organization.value.description, ogDescription: organization.value.description,
ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png", ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png",
}); });
const navLinks = computed(() => [
{
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.value.slug}`,
},
...projectTypes.value
.map((x) => {
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.value.slug}/${x}s`,
};
})
.slice()
.sort((a, b) => a.label.localeCompare(b.label)),
]);
async function copyId() {
await navigator.clipboard.writeText(organization.value.id);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
+8 -54
View File
@@ -371,22 +371,6 @@
</div> </div>
</section> </section>
<section id="data-export" class="universal-card">
<h2>Data export</h2>
<p>
Request a copy of all your personal data you have uploaded to Modrinth. This may take
several minutes to complete.
</p>
<a v-if="generated" class="iconified-button" :href="generated" download="export.json">
<DownloadIcon />
Download export
</a>
<button v-else class="iconified-button" :disabled="generatingExport" @click="exportData">
<template v-if="generatingExport"> <UpdatedIcon /> Generating export... </template>
<template v-else> <UpdatedIcon /> Generate export </template>
</button>
</section>
<section id="delete-account" class="universal-card"> <section id="delete-account" class="universal-card">
<h2>Delete account</h2> <h2>Delete account</h2>
<p> <p>
@@ -407,18 +391,16 @@
<script setup> <script setup>
import { import {
CheckIcon,
EditIcon, EditIcon,
ExternalIcon,
LeftArrowIcon,
PlusIcon,
RightArrowIcon,
SaveIcon, SaveIcon,
SettingsIcon,
TrashIcon, TrashIcon,
UpdatedIcon, PlusIcon,
SettingsIcon,
XIcon, XIcon,
DownloadIcon, LeftArrowIcon,
RightArrowIcon,
CheckIcon,
ExternalIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import QrcodeVue from "qrcode.vue"; import QrcodeVue from "qrcode.vue";
import GitHubIcon from "assets/icons/auth/sso-github.svg"; import GitHubIcon from "assets/icons/auth/sso-github.svg";
@@ -556,7 +538,7 @@ async function verifyTwoFactorCode() {
backupCodes.value = res.backup_codes; backupCodes.value = res.backup_codes;
twoFactorStep.value = 2; twoFactorStep.value = 2;
await useAuth(auth.value.token); await useAuth(auth.value.token);
} catch { } catch (err) {
twoFactorIncorrect.value = true; twoFactorIncorrect.value = true;
} }
stopLoading(); stopLoading();
@@ -573,7 +555,7 @@ async function removeTwoFactor() {
}); });
manageTwoFactorModal.value.hide(); manageTwoFactorModal.value.hide();
await useAuth(auth.value.token); await useAuth(auth.value.token);
} catch { } catch (err) {
twoFactorIncorrect.value = true; twoFactorIncorrect.value = true;
} }
stopLoading(); stopLoading();
@@ -632,34 +614,6 @@ async function deleteAccount() {
stopLoading(); stopLoading();
} }
const generatingExport = ref(false);
const generated = ref();
async function exportData() {
startLoading();
generatingExport.value = true;
try {
const res = await useBaseFetch("gdpr/export", {
method: "POST",
internal: true,
});
const jsonString = JSON.stringify(res, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
generated.value = URL.createObjectURL(blob);
} catch (err) {
data.$notify({
group: "main",
title: "An error occurred",
text: err.data.description,
type: "error",
});
}
generatingExport.value = false;
stopLoading();
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
canvas { canvas {
@@ -301,7 +301,7 @@ const canSubmit = computed(() => {
const url = new URL(uri); const url = new URL(uri);
return !!url; return !!url;
}); });
} catch { } catch (err) {
allValid = false; allValid = false;
} }
return filledIn && (oneValid || allValid); return filledIn && (oneValid || allValid);
@@ -17,24 +17,33 @@
<span class="font-bold text-primary"> <span class="font-bold text-primary">
<template v-if="charge.product.metadata.type === 'midas'"> Modrinth Plus </template> <template v-if="charge.product.metadata.type === 'midas'"> Modrinth Plus </template>
<template v-else> Unknown product </template> <template v-else> Unknown product </template>
<template v-if="charge.subscription_interval"> <template v-if="charge.metadata.modrinth_subscription_interval">
{{ charge.subscription_interval }} {{ charge.metadata.modrinth_subscription_interval }}
</template> </template>
</span> </span>
⋅ ⋅
<span>{{ formatPrice(charge.amount, charge.currency_code) }}</span> <span>{{ formatPrice(charge.amount, charge.currency) }}</span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" /> <Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" />
⋅ ⋅
{{ $dayjs(charge.due).format("YYYY-MM-DD") }} {{ $dayjs.unix(charge.created).format("YYYY-MM-DD") }}
</div> </div>
</div> </div>
<a
v-if="charge.receipt_url"
class="iconified-button raised-button"
:href="charge.receipt_url"
>
<ReceiptTextIcon />
View receipt
</a>
</div> </div>
</section> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ReceiptTextIcon } from "@modrinth/assets";
import { Breadcrumbs, Badge } from "@modrinth/ui"; import { Breadcrumbs, Badge } from "@modrinth/ui";
import { products } from "~/generated/state.json"; import { products } from "~/generated/state.json";
@@ -49,17 +58,15 @@ const { data: charges } = await useAsyncData(
() => useBaseFetch("billing/payments", { internal: true }), () => useBaseFetch("billing/payments", { internal: true }),
{ {
transform: (charges) => { transform: (charges) => {
return charges return charges.map((charge) => {
.filter((charge) => charge.status !== "open" && charge.status !== "cancelled") const product = products.find((product) =>
.map((charge) => { product.prices.some((price) => price.id === charge.metadata.modrinth_price_id),
const product = products.find((product) => );
product.prices.some((price) => price.id === charge.price_id),
);
charge.product = product; charge.product = product;
return charge; return charge;
}); });
}, },
}, },
); );
@@ -8,20 +8,22 @@
:title="formatMessage(cancelModalMessages.title)" :title="formatMessage(cancelModalMessages.title)"
:description="formatMessage(cancelModalMessages.description)" :description="formatMessage(cancelModalMessages.description)"
:proceed-label="formatMessage(cancelModalMessages.action)" :proceed-label="formatMessage(cancelModalMessages.action)"
@proceed="cancelSubscription(cancelSubscriptionId, true)" @proceed="cancelSubscription(cancelSubscriptionId)"
/> />
<div class="flex flex-wrap justify-between gap-4"> <div class="flex flex-wrap justify-between gap-4">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<template v-if="midasCharge"> <template v-if="midasSubscription">
<span v-if="midasCharge.status === 'open'"> You're currently subscribed to: </span> <span v-if="midasSubscription.status === 'active'">
<span v-else-if="midasCharge.status === 'processing'" class="text-orange"> You're currently subscribed to:
</span>
<span v-else-if="midasSubscription.status === 'payment-processing'" class="text-orange">
Your payment is being processed. Perks will activate once payment is complete. Your payment is being processed. Perks will activate once payment is complete.
</span> </span>
<span v-else-if="midasCharge.status === 'cancelled'"> <span v-else-if="midasSubscription.status === 'cancelled'">
You've cancelled your subscription. <br /> You've cancelled your subscription. <br />
You will retain your perks until the end of the current billing cycle. You will retain your perks until the end of the current billing cycle.
</span> </span>
<span v-else-if="midasCharge.status === 'failed'" class="text-red"> <span v-else-if="midasSubscription.status === 'payment-failed'" class="text-red">
Your subscription payment failed. Please update your payment method. Your subscription payment failed. Please update your payment method.
</span> </span>
</template> </template>
@@ -47,31 +49,34 @@
<div class="flex w-full flex-wrap justify-between gap-4 xl:w-auto xl:flex-col"> <div class="flex w-full flex-wrap justify-between gap-4 xl:w-auto xl:flex-col">
<div class="flex flex-col gap-1 xl:ml-auto xl:text-right"> <div class="flex flex-col gap-1 xl:ml-auto xl:text-right">
<span class="text-2xl font-bold text-dark"> <span class="text-2xl font-bold text-dark">
<template v-if="midasCharge"> <template v-if="midasSubscription">
{{ {{
formatPrice( formatPrice(
vintl.locale, vintl.locale,
midasSubscriptionPrice.prices.intervals[midasCharge.subscription_interval], midasSubscriptionPrice.prices.intervals[midasSubscription.interval],
midasSubscriptionPrice.currency_code, midasSubscriptionPrice.currency_code,
) )
}} }}
/ /
{{ midasCharge.subscription_interval }} {{ midasSubscription.interval }}
</template> </template>
<template v-else> <template v-else>
{{ formatPrice(vintl.locale, price.prices.intervals.monthly, price.currency_code) }} {{ formatPrice(vintl.locale, price.prices.intervals.monthly, price.currency_code) }}
/ month / month
</template> </template>
</span> </span>
<template v-if="midasCharge"> <template v-if="midasSubscription">
<span class="text-sm text-secondary"> <span class="text-sm text-secondary">
Since {{ $dayjs(midasSubscription.created).format("MMMM D, YYYY") }} Since {{ $dayjs(midasSubscription.created).format("MMMM D, YYYY") }}
</span> </span>
<span v-if="midasCharge.status === 'open'" class="text-sm text-secondary"> <span v-if="midasSubscription.status === 'active'" class="text-sm text-secondary">
Renews {{ $dayjs(midasCharge.due).format("MMMM D, YYYY") }} Renews {{ $dayjs(midasSubscription.expires).format("MMMM D, YYYY") }}
</span> </span>
<span v-else-if="midasCharge.status === 'cancelled'" class="text-sm text-secondary"> <span
Expires {{ $dayjs(midasCharge.due).format("MMMM D, YYYY") }} v-else-if="midasSubscription.status === 'cancelled'"
class="text-sm text-secondary"
>
Expires {{ $dayjs(midasSubscription.expires).format("MMMM D, YYYY") }}
</span> </span>
</template> </template>
@@ -85,11 +90,11 @@
</span> </span>
</div> </div>
<div <div
v-if="midasCharge && midasCharge.status === 'failed'" v-if="midasSubscription && midasSubscription.status === 'payment-failed'"
class="ml-auto flex flex-row-reverse items-center gap-2" class="ml-auto flex flex-row-reverse items-center gap-2"
> >
<button <button
v-if="midasCharge && midasCharge.status === 'failed'" v-if="midasSubscription && midasSubscription.status === 'payment-failed'"
class="iconified-button raised-button" class="iconified-button raised-button"
@click=" @click="
() => { () => {
@@ -118,7 +123,7 @@
</OverflowMenu> </OverflowMenu>
</div> </div>
<button <button
v-else-if="midasCharge && midasCharge.status !== 'cancelled'" v-else-if="midasSubscription && midasSubscription.status !== 'cancelled'"
class="iconified-button raised-button !ml-auto" class="iconified-button raised-button !ml-auto"
@click=" @click="
() => { () => {
@@ -129,13 +134,6 @@
> >
<XIcon /> Cancel <XIcon /> Cancel
</button> </button>
<button
v-else-if="midasCharge && midasCharge.status === 'cancelled'"
class="btn btn-purple btn-large ml-auto"
@click="cancelSubscription(midasSubscription.id, false)"
>
<RightArrowIcon /> Resubscribe
</button>
<button <button
v-else v-else
class="btn btn-purple btn-large ml-auto" class="btn btn-purple btn-large ml-auto"
@@ -476,14 +474,12 @@ function loadStripe() {
const [ const [
{ data: paymentMethods, refresh: refreshPaymentMethods }, { data: paymentMethods, refresh: refreshPaymentMethods },
{ data: charges, refresh: refreshCharges },
{ data: customer, refresh: refreshCustomer }, { data: customer, refresh: refreshCustomer },
{ data: subscriptions, refresh: refreshSubscriptions }, { data: subscriptions, refresh: refreshSubscriptions },
] = await Promise.all([ ] = await Promise.all([
useAsyncData("billing/payment_methods", () => useAsyncData("billing/payment_methods", () =>
useBaseFetch("billing/payment_methods", { internal: true }), useBaseFetch("billing/payment_methods", { internal: true }),
), ),
useAsyncData("billing/payments", () => useBaseFetch("billing/payments", { internal: true })),
useAsyncData("billing/customer", () => useBaseFetch("billing/customer", { internal: true })), useAsyncData("billing/customer", () => useBaseFetch("billing/customer", { internal: true })),
useAsyncData("billing/subscriptions", () => useAsyncData("billing/subscriptions", () =>
useBaseFetch("billing/subscriptions", { internal: true }), useBaseFetch("billing/subscriptions", { internal: true }),
@@ -491,30 +487,18 @@ const [
]); ]);
async function refresh() { async function refresh() {
await Promise.all([ await Promise.all([refreshPaymentMethods(), refreshCustomer(), refreshSubscriptions()]);
refreshPaymentMethods(),
refreshCharges(),
refreshCustomer(),
refreshSubscriptions(),
]);
} }
const midasProduct = ref(products.find((x) => x.metadata.type === "midas")); const midasProduct = ref(products.find((x) => x.metadata.type === "midas"));
const midasSubscription = computed(() => const midasSubscription = computed(() =>
subscriptions.value.find( subscriptions.value.find((x) => midasProduct.value.prices.find((y) => y.id === x.price_id)),
(x) => x.status === "provisioned" && midasProduct.value.prices.find((y) => y.id === x.price_id),
),
); );
const midasSubscriptionPrice = computed(() => const midasSubscriptionPrice = computed(() =>
midasSubscription.value midasSubscription.value
? midasProduct.value.prices.find((x) => x.id === midasSubscription.value.price_id) ? midasProduct.value.prices.find((x) => x.id === midasSubscription.value.price_id)
: null, : null,
); );
const midasCharge = computed(() =>
midasSubscription.value
? charges.value.find((x) => x.subscription_id === midasSubscription.value.id)
: null,
);
const purchaseModal = ref(); const purchaseModal = ref();
const country = useUserCountry(); const country = useUserCountry();
@@ -540,18 +524,10 @@ if (route.query.priceId && route.query.plan && route.query.redirect_status) {
price_id: route.query.priceId, price_id: route.query.priceId,
interval: route.query.plan, interval: route.query.plan,
created: Date.now(), created: Date.now(),
expires: route.query.plan === "yearly" ? Date.now() + 31536000000 : Date.now() + 2629746000,
status, status,
}); });
charges.value.push({
id: "temp",
price_id: route.query.priceId,
subscription_id: "temp",
status: "open",
due: Date.now() + (route.query.plan === "yearly" ? 31536000000 : 2629746000),
subscription_interval: route.query.plan,
});
await router.replace({ query: {} }); await router.replace({ query: {} });
} }
@@ -679,15 +655,12 @@ async function removePaymentMethod(index) {
} }
const cancelSubscriptionId = ref(); const cancelSubscriptionId = ref();
async function cancelSubscription(id, cancelled) { async function cancelSubscription(id) {
startLoading(); startLoading();
try { try {
await useBaseFetch(`billing/subscription/${id}`, { await useBaseFetch(`billing/subscription/${id}`, {
internal: true, internal: true,
method: "PATCH", method: "DELETE",
body: {
cancelled,
},
}); });
await refresh(); await refresh();
} catch (err) { } catch (err) {
@@ -226,7 +226,7 @@ async function changeLocale(value: string) {
try { try {
await vintl.changeLocale(value); await vintl.changeLocale(value);
$failedLocale.value = undefined; $failedLocale.value = undefined;
} catch { } catch (err) {
$failedLocale.value = value; $failedLocale.value = value;
} finally { } finally {
$changingTo.value = undefined; $changingTo.value = undefined;
+147 -83
View File
@@ -3,88 +3,75 @@
<ModalCreation ref="modal_creation" /> <ModalCreation ref="modal_creation" />
<CollectionCreateModal ref="modal_collection_creation" /> <CollectionCreateModal ref="modal_collection_creation" />
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }"> <div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<div class="normal-page__header py-4"> <div class="normal-page__header pt-4">
<ContentPageHeader> <div
<template #icon> class="mb-4 grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
>
<div class="flex gap-4">
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle /> <Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
</template> <div class="flex flex-col gap-2">
<template #title> <div class="flex flex-wrap items-center gap-2">
{{ user.username }} <h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
</template> {{ user.username }}
<template #summary> </h1>
{{ </div>
user.bio <p class="m-0 line-clamp-2 max-w-[40rem]">
? user.bio {{
: projects.length === 0 user.bio
? "A Modrinth user." ? user.bio
: "A Modrinth creator." : projects.length === 0
}} ? "A Modrinth user."
</template> : "A Modrinth creator."
<template #stats> }}
<div </p>
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div> </div>
<div </div>
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold" <div class="flex flex-col justify-center gap-4">
> <div class="flex flex-wrap gap-2">
<DownloadIcon class="h-6 w-6 text-secondary" /> <ButtonStyled size="large">
{{ formatCompactNumber(sumDownloads) }} <NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
downloads <EditIcon aria-hidden="true" />
{{ formatMessage(commonMessages.editButton) }}
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () => navigateTo('/dashboard/projects'),
hoverOnly: true,
shown: auth.user && auth.user.id === user.id,
},
{ divider: true, shown: auth.user && auth.user.id === user.id },
{
id: 'report',
action: () => reportUser(user.id),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
{{ formatMessage(messages.profileManageProjectsButton) }}
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</div> </div>
<div class="flex items-center gap-2 font-semibold"> </div>
<CalendarIcon class="h-6 w-6 text-secondary" /> </div>
Joined
{{ formatRelativeTime(user.created) }}
</div>
</template>
<template #actions>
<ButtonStyled size="large">
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
<EditIcon aria-hidden="true" />
{{ formatMessage(commonMessages.editButton) }}
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () => navigateTo('/dashboard/projects'),
hoverOnly: true,
shown: auth.user && auth.user.id === user.id,
},
{ divider: true, shown: auth.user && auth.user.id === user.id },
{
id: 'report',
action: () => reportUser(user.id),
color: 'red',
hoverOnly: true,
},
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
{{ formatMessage(messages.profileManageProjectsButton) }}
</template>
<template #report>
<ReportIcon aria-hidden="true" />
{{ formatMessage(commonMessages.reportButton) }}
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</div> </div>
<div class="normal-page__content"> <div class="normal-page__content">
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto"> <div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
@@ -207,6 +194,72 @@
</div> </div>
</div> </div>
<div class="normal-page__sidebar"> <div class="normal-page__sidebar">
<div class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
<div class="flex items-center gap-2">
<BoxIcon aria-hidden="true" class="stroke-[3] text-secondary" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileProjectsStats"
:values="{ count: formatCompactNumber(projects.length) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<DownloadIcon aria-hidden="true" class="stroke-[3] text-secondary" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileDownloadsStats"
:values="{ count: formatCompactNumber(sumDownloads) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<HeartIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileProjectsFollowersStats"
:values="{ count: formatCompactNumber(sumFollows) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<CalendarIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileJoinedAt"
:values="{ ago: formatRelativeTime(user.created) }"
>
<template #date="{ children }">
<span class="font-bold text-primary">
<component :is="() => children" />
</span>
</template>
</IntlFormatted>
</div>
</div>
</div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<div v-if="organizations.length > 0" class="card flex-card"> <div v-if="organizations.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2> <h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@@ -235,9 +288,6 @@
</div> </div>
</div> </div>
</div> </div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -254,7 +304,7 @@ import {
ClipboardCopyIcon, ClipboardCopyIcon,
MoreVerticalIcon, MoreVerticalIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { OverflowMenu, ButtonStyled, ContentPageHeader } from "@modrinth/ui"; import { OverflowMenu, ButtonStyled } from "@modrinth/ui";
import NavTabs from "~/components/ui/NavTabs.vue"; import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
import { reportUser } from "~/utils/report-helpers.ts"; import { reportUser } from "~/utils/report-helpers.ts";
@@ -268,6 +318,7 @@ import EarlyAdopterBadge from "~/assets/images/badges/early-adopter.svg?componen
import ReportIcon from "~/assets/images/utils/report.svg?component"; import ReportIcon from "~/assets/images/utils/report.svg?component";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component"; import EditIcon from "~/assets/images/utils/edit.svg?component";
import HeartIcon from "~/assets/images/utils/heart.svg?component";
import WorldIcon from "~/assets/images/utils/world.svg?component"; import WorldIcon from "~/assets/images/utils/world.svg?component";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from "~/components/ui/Avatar.vue"; import Avatar from "~/components/ui/Avatar.vue";
@@ -454,6 +505,15 @@ const sumDownloads = computed(() => {
return sum; return sum;
}); });
const sumFollows = computed(() => {
let sum = 0;
for (const project of projects.value) {
sum += project.followers;
}
return sum;
});
const badges = computed(() => { const badges = computed(() => {
const badges = []; const badges = [];
@@ -593,4 +653,8 @@ export default defineNuxtComponent({
} }
} }
} }
.normal-page__header {
grid-area: header;
}
</style> </style>
-166
View File
@@ -472,169 +472,3 @@ visiblemeasures.com, 1041, DIRECT #yieldmo
iqzone.com,IQ190,RESELLER iqzone.com,IQ190,RESELLER
krushmedia.com, AJxF6R537a9M6CaTvK, RESELLER krushmedia.com, AJxF6R537a9M6CaTvK, RESELLER
consumable.com, 2001460, DIRECT, aefcd3d2f45b5070 consumable.com, 2001460, DIRECT, aefcd3d2f45b5070
google.com, pub-6729046591418183, RESELLER, f08c47fec0942fa0
pgamssp.com, 642db2eba4eb3f6b34033505, DIRECT
freewheel.tv, 1489202, RESELLER
freewheel.tv, 1488706, RESELLER
pubmatic.com, 162623, RESELLER, 5d62403b186f2ace
rubiconproject.com, 24852, RESELLER, 0bfd66d529a55807
video.unrulymedia.com, 5921144960123684292, RESELLER
minutemedia.com, 01hp4as4p012, RESELLER
onetag.com, 87f80e5d9d55274, RESELLER
sharethrough.com, FhiWXM0L, RESELLER, d53b998a7bd4ecd2
amxrtb.com, 105199776, RESELLER
33across.com, 0015a00003ALsDfAAL, DIRECT, bbea06d9c4d2853c
risecodes.com, 661fc591c3a3ef0001984071, DIRECT
optimanetwork.com, 10581, DIRECT
rubiconproject.com, 23404, DIRECT, 0bfd66d529a55807
pubmatic.com,163370,RESELLER,5d62403b186f2ace
criteo.com, B-060927, DIRECT, 9fac4a4a87c2a44f
themediagrid.com, TR634Q, DIRECT, 35d5010d7789b49d
google.com, pub-7094677798399606, RESELLER, f08c47fec0942fa0
openx.com, 544021216, RESELLER, 6a698e2ec38604c6
google.com, pub-2403018226404213, RESELLER, f08c47fec0942fa0
google.com, pub-2730263451308801, RESELLER, f08c47fec0942fa0
google.com, pub-5200956238394958, RESELLER, f08c47fec0942fa0
google.com, pub-5761017298734489, RESELLER, f08c47fec0942fa0
Pubmatic.com, 161094, DIRECT, 5d62403b186f2ace
Pubmatic.com, 162736, DIRECT, 5d62403b186f2ace
rubiconproject.com, 23978, RESELLER, 0bfd66d529a55807
openx.com, 545708355, DIRECT, 6a698e2ec38604c6
openx.com, 558218698, DIRECT, 6a698e2ec38604c6
google.com, pub-3479162065560922, DIRECT, f08c47fec0942fa0
google.com, pub-6631347089063164, DIRECT, f08c47fec0942fa0
google.com, pub-1309378188566785, DIRECT, f08c47fec0942fa0
google.com, pub-1305372857617183, DIRECT, f08c47fec0942fa0
crads.in, CR-0029, DIRECT
google.com, pub-6013343350303951, RESELLER, f08c47fec0942fa0
rtbhouse.com, Jz6bvciZ65MalvqnKChX, DIRECT
incrementx.com, 60039, DIRECT, 8728b7e97e589da4
google.com, pub-3977122154505186, RESELLER, f08c47fec0942fa0
pubmatic.com, 163277, RESELLER, 5d62403b186f2ace
smartadserver.com, 4417-OB, RESELLER, 060d053dcf45cbf3
lijit.com, 381276-eb, RESELLER, fafdf38b16bf6b2b
rubiconproject.com, 22884, RESELLER, 0bfd66d529a55807
onetag.com, 80faba581f6cfb8, RESELLER
onetag.com, 80faba581f6cfb8-OB, RESELLER
axonix.com, 59089, RESELLER, bc385f2b4a87b721
outbrain.com,002d7f7ba0bd74452f2b155d0dfb5cd6c8,RESELLER
opera.com,pub10952251675200,RESELLER,55a0c5fd61378de3
pubmatic.com, 161652, RESELLER, 5d62403b186f2ace
pubmatic.com, 164418, RESELLER, 5d62403b186f2ace
152media.info,152M748,RESELLER
appnexus.com, 3153, RESELLER, f5ab79cb980f11d1
vidoomy.com, 61162, DIRECT
video.unrulymedia.com, 1816262719, RESELLER,
pubmatic.com, 156498, RESELLER, 5d62403b186f2ace
freewheel.tv, 872257, RESELLER
freewheel.tv, 894193, RESELLER
openx.com, 540804929, RESELLER, 6a698e2ec38604c6
google.com, pub-2831120411392012, RESELLER, f08c47fec0942fa0
tremorhub.com, 4cywq-a04wk, RESELLER, 1a4e959a1b50034a
rubiconproject.com, 24386, RESELLER, 0bfd66d529a55807
pubmatic.com, 165144, RESELLER, 5d62403b186f2ace
smartadserver.com, 4984, RESELLER, 060d053dcf45cbf3
truvid.com, 2158, DIRECT
smartadserver.com, 3356, RESELLER, 060d053dcf45cbf3
google.com, pub-6322541192886560, RESELLER, f08c47fec0942fa0
google.com, pub-8787923930478618, RESELLER, f08c47fec0942fa0
google.com, pub-9507736279911916, RESELLER, f08c47fec0942fa0
appnexus.com, 12700, RESELLER, f5ab79cb980f11d1
rubiconproject.com, 17412, RESELLER, 0bfd66d529a55807
conversantmedia.com, 100712, RESELLER, 03113cd04947736d
vidcrunch.com, 63c6b945dee370f45c083ed4, DIRECT
vdo.ai, 2213_3552, DIRECT
atlas5.co, 2213_3552, DIRECT
walletcircle.co, 2213, DIRECT
google.com, pub-9417114411593463, RESELLER, f08c47fec0942fa0
google.com, pub-5717092533913515, RESELLER, f08c47fec0942fa0
aps.amazon.com, 24b39613-fd0f-4009-9189-976a7d9bfd3d, DIRECT
gannett.com, 22655109307, RESELLER
rubiconproject.com, 10968, RESELLER, 0bfd66d529a55807
google.com, pub-4836542095728076, RESELLER, f08c47fec0942fa0
google.com, pub-2930805104418204, RESELLER, f08c47fec0942fa0
google.com, pub-9135355251665930, RESELLER, f08c47fec0942fa0
freewheel.tv, 1139281, RESELLER
aps.amazon.com, 00ed17ab-4189-4639-9d5e-15acd40affde, DIRECT
smartadserver.com, 3686, RESELLER
unibots.in, UBC-0165-1, DIRECT
google.com, pub-1290995901905588, RESELLER, f08c47fec0942fa0
google.com, pub-2603664881560000, RESELLER, f08c47fec0942fa0
google.com, pub-3132893725603935, RESELLER, f08c47fec0942fa0
google.com, pub-3191289882045155, RESELLER, f08c47fec0942fa0
google.com, pub-3328898302928686, RESELLER, f08c47fec0942fa0
google.com, pub-3769010358500643, RESELLER, f08c47fec0942fa0
google.com, pub-5995202563537249, RESELLER, f08c47fec0942fa0
google.com, pub-6151720204273327, RESELLER, f08c47fec0942fa0
google.com, pub-6512936480753445, RESELLER, f08c47fec0942fa0
google.com, pub-8241049497608997, RESELLER, f08c47fec0942fa0
google.com, pub-8610050614645263, RESELLER, f08c47fec0942fa0
google.com, pub-8699255262206653, RESELLER, f08c47fec0942fa0
google.com, pub-9033099948928268, RESELLER, f08c47fec0942fa0
google.com, pub-9467340974789471, RESELLER, f08c47fec0942fa0
google.com, pub-9557089510405422, RESELLER, f08c47fec0942fa0
google.com, pub-9685734445476814, RESELLER, f08c47fec0942fa0
biddo.net, bdc-3969, DIRECT
prodooh.com, pdh-3969, DIRECT
invamia.com, ivm-3969, DIRECT
vidverto.com, vdvt-3969, DIRECT
yieldbird.com, 3969, DIRECT
bidfuse.com, bf-3969, DIRECT
arabella.ag, abl-3969, DIRECT
adtech-digital.com, adt3969, DIRECT
pixfuture.com, 9048, DIRECT
openx.com, 540406323, DIRECT, 6a698e2ec38604c6
pubmatic.com, 158127, RESELLER, 5d62403b186f2ace
sonobi.com, 0b24fdfc82, DIRECT, d1a215d9eb5aee9e
sharethrough.com, 62beb9db, DIRECT, d53b998a7bd4ecd2
d-sail.com, ds0453, DIRECT
google.com, pub-2205121062140812, RESELLER, f08c47fec0942fa0
33across.com, 0010b00002VYZoUAAX, DIRECT, bbea06d9c4d2853c
smartadserver.com, 3663, RESELLER
smartadserver.com, 4071, RESELLER
adtelligent.com, 500592, DIRECT
sovrn.com, 331050, DIRECT, fafdf38b16bf6b2b
rubiconproject.com, 23564, DIRECT, 0bfd66d529a55807
rubiconproject.com, 23566, RESELLER, 0bfd66d529a55807
Media.net, 8CUIUMTP7, DIRECT
e-planning.net, 29459e615aaa9065, DIRECT, c1ba615865ed87b2
themediagrid.com, QDC97I, DIRECT, 35d5010d7789b49d
onetag.com, 79c9089f60042c0, DIRECT
smilewanted.com, 3520, RESELLER
smartadserver.com, 2491, RESELLER
minutemedia.com,01hj1rkkswwe, RESELLER
appnexus.com, 8381, RESELLER, f5ab79cb980f11d1
aniview.com, 60c5dbc56627cd1ce66ff020, RESELLER, 78b21b97965ec3f8
google.com, pub-6346866704322274, RESELLER, f08c47fec0942fa0
google.com, pub-4586415728471297, RESELLER, f08c47fec0942fa0
playstream.media, 911, RESELLER
sharethrough.com, UXUWG46h, RESELLER, d53b998a7bd4ecd2
sharethrough.com, zLsEa05k, RESELLER, d53b998a7bd4ecd2
pubmatic.com, 156557, RESELLER, 5d62403b186f2ace
vidgyor.com,20020, DIRECT
google.com, pub-8863191426564274, RESELLER, f08c47fec0942fa0
pubmatic.com, 162319, RESELLER, 5d62403b186f2ace
lijit.com, 230102, DIRECT, fafdf38b16bf6b2b #SOVRN
lijit.com, 230102-eb, DIRECT, fafdf38b16bf6b2b #SOVRN
video.unrulymedia.com, 2444764291, RESELLER
contextweb.com, 558511, RESELLER, 89ff185a4c4e857c
krushmedia.com, AJxF6R572a9M6CaTvK, RESELLER
inmobi.com, b01aa06531c543d8a5fb9982f60afb00, RESELLER, 83e75a7ae333ca9d
motorik.io, 100463, RESELLER
smaato.com, 1100056344, RESELLER, 07bcf65f187117b4
smartadserver.com, 4926, RESELLER, 060d053dcf45cbf3
onetag.com, 5847a7d7e75dee8, DIRECT
video.unrulymedia.com, 351983115, RESELLER
rubiconproject.com, 15268, RESELLER, 0bfd66d529a55807
pubmatic.com, 159277, RESELLER
appnexus.com, 6849, RESELLER
xad.com, 963, RESELLER, 81cbf0a75a5e0e9a
openx.com, 543878511, RESELLER, 6a698e2ec38604c6
smaato.com, 1100047589, RESELLER, 07bcf65f187117b4
inmobi.com, ba5fd3fb82c5412989b23c3eec71baf7, RESELLER, 83e75a7ae333ca9d
#Criteo
themediagrid.com, 4R7XKQ, DIRECT, 35d5010d7789b49d
-132
View File
@@ -1,132 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modrinth App Ad</title>
<script src="https://cadmus.script.ac/d14pdm1b7fi5kh/script.js"></script>
<script
src="https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js"
async
></script>
<link rel="preload" href="https://www.googletagservices.com/tag/js/gpt.js" as="script" />
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
cursor: pointer;
}
.ads-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
#plus-link {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
#modrinth-rail-1 {
border-radius: 1rem;
position: absolute;
left: 0;
top: 0;
z-index: 2;
}
</style>
</head>
<body>
<div class="ads-container">
<div id="plus-link"></div>
<div id="modrinth-rail-1"></div>
</div>
<script>
function getCookie(name) {
function escape(s) {
return s.replace(/([.*+?\^$(){}|\[\]\/\\])/g, "\\$1");
}
const match = document.cookie.match(RegExp("(?:^|;\\s*)" + escape(name) + "=([^;]*)"));
return match ? match[1] : null;
}
function initAds(personalized) {
window.tude = window.tude || { cmd: [] };
tude.cmd.push(function () {
tude.refreshAdsViaDivMappings([
{
divId: "modrinth-rail-1",
baseDivId: "pb-slot-square-2",
targeting: {
location: "app",
},
},
]);
tude.setPrivacySettings({
personalizedAds: personalized ?? true,
});
});
}
try {
if (window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__
.invoke("plugin:ads|get_ads_personalization", {})
.then(initAds)
.catch(() => initAds(true));
} else {
initAds(true);
}
} catch (err) {
initAds(true);
console.error(err);
}
window.addEventListener(
"message",
(event) => {
if (event.data.modrinthAdClick && window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|record_ads_click", {});
}
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|open_link", {
path: event.data.modrinthOpenUrl,
origin: event.origin,
});
}
},
false,
);
window.addEventListener("mousewheel", (event) => {
if (window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|scroll_ads_window", {
scroll: event.deltaY,
});
}
});
document.addEventListener("contextmenu", (event) => event.preventDefault());
const plusLink = document.getElementById("plus-link");
plusLink.addEventListener("click", function () {
window.__TAURI_INTERNALS__.invoke("plugin:ads|record_ads_click", {});
window.__TAURI_INTERNALS__.invoke("plugin:ads|open_link", {
path: "https://modrinth.com/plus",
origin: "https://modrinth.com",
});
});
</script>
</body>
</html>
@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, default_page, collapsed_navigation, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated\n FROM settings\n ", "query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, default_page, collapsed_navigation, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated\n FROM settings\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -54,78 +54,73 @@
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "personalized_ads", "name": "onboarded",
"ordinal": 10, "ordinal": 10,
"type_info": "Integer" "type_info": "Integer"
}, },
{
"name": "onboarded",
"ordinal": 11,
"type_info": "Integer"
},
{ {
"name": "extra_launch_args", "name": "extra_launch_args",
"ordinal": 12, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "custom_env_vars", "name": "custom_env_vars",
"ordinal": 13, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "mc_memory_max", "name": "mc_memory_max",
"ordinal": 14, "ordinal": 13,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "mc_force_fullscreen", "name": "mc_force_fullscreen",
"ordinal": 15, "ordinal": 14,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "mc_game_resolution_x", "name": "mc_game_resolution_x",
"ordinal": 16, "ordinal": 15,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "mc_game_resolution_y", "name": "mc_game_resolution_y",
"ordinal": 17, "ordinal": 16,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "hide_on_process_start", "name": "hide_on_process_start",
"ordinal": 18, "ordinal": 17,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "hook_pre_launch", "name": "hook_pre_launch",
"ordinal": 19, "ordinal": 18,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "hook_wrapper", "name": "hook_wrapper",
"ordinal": 20, "ordinal": 19,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "hook_post_exit", "name": "hook_post_exit",
"ordinal": 21, "ordinal": 20,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "custom_dir", "name": "custom_dir",
"ordinal": 22, "ordinal": 21,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "prev_custom_dir", "name": "prev_custom_dir",
"ordinal": 23, "ordinal": 22,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "migrated", "name": "migrated",
"ordinal": 24, "ordinal": 23,
"type_info": "Integer" "type_info": "Integer"
} }
], ],
@@ -144,7 +139,6 @@
false, false,
false, false,
false, false,
false,
null, null,
null, null,
false, false,
@@ -160,5 +154,5 @@
false false
] ]
}, },
"hash": "8e19c9cdb0aaa48509724e82f6e8f212c9cd2112fdba77cfeee206025af47761" "hash": "03d1aeddf7788320530c447a82342aecdb4099ce183dd9106c4bcc47604cb080"
} }
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n default_page = $4,\n collapsed_navigation = $5,\n advanced_rendering = $6,\n native_decorations = $7,\n\n discord_rpc = $8,\n developer_mode = $9,\n telemetry = $10,\n personalized_ads = $11,\n\n onboarded = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 25
},
"nullable": []
},
"hash": "26e3ed8680f6c492b03b458aabfb3f94fddc753b343ef705263188945d0e578d"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n default_page = $4,\n collapsed_navigation = $5,\n advanced_rendering = $6,\n native_decorations = $7,\n\n discord_rpc = $8,\n developer_mode = $9,\n telemetry = $10,\n\n onboarded = $11,\n\n extra_launch_args = jsonb($12),\n custom_env_vars = jsonb($13),\n mc_memory_max = $14,\n mc_force_fullscreen = $15,\n mc_game_resolution_x = $16,\n mc_game_resolution_y = $17,\n hide_on_process_start = $18,\n\n hook_pre_launch = $19,\n hook_wrapper = $20,\n hook_post_exit = $21,\n\n custom_dir = $22,\n prev_custom_dir = $23,\n migrated = $24\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 24
},
"nullable": []
},
"hash": "d645daf951ff6fead3c86df685d99bacc81cb0a999c0f8d2ff7755b0089a79d8"
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "theseus" name = "theseus"
version = "0.8.9" version = "0.8.3-1"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2021" edition = "2021"
@@ -36,7 +36,7 @@ tracing-error = "0.2.0"
paste = { version = "1.0" } paste = { version = "1.0" }
tauri = { version = "2.0.0-rc", optional = true } tauri = { version = "2.0.0-rc.4", optional = true }
indicatif = { version = "0.17.3", optional = true } indicatif = { version = "0.17.3", optional = true }
async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] } async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] }
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more