Compare commits

..

2 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
127 changed files with 6837 additions and 6778 deletions

View File

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

View File

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

View File

@@ -44,28 +44,7 @@ jobs:
- name: Setup rust cache
uses: actions/cache@v4
with:
path: |
target/**
!target/*/release/bundle/*/*.dmg
!target/*/release/bundle/*/*.app.tar.gz
!target/*/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.dmg
!target/release/bundle/*/*.app.tar.gz
!target/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.AppImage
!target/release/bundle/*/*.AppImage.tar.gz
!target/release/bundle/*/*.AppImage.tar.gz.sig
!target/release/bundle/*/*.deb
!target/release/bundle/*/*.rpm
!target/release/bundle/msi/*.msi
!target/release/bundle/msi/*.msi.zip
!target/release/bundle/msi/*.msi.zip.sig
!target/release/bundle/nsis/*.exe
!target/release/bundle/nsis/*.nsis.zip
!target/release/bundle/nsis/*.nsis.zip.sig
path: target/**
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-rust-target-
@@ -99,7 +78,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu')
run: |
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
run: pnpm install
@@ -145,11 +124,8 @@ jobs:
target/release/bundle/*/*.AppImage.tar.gz.sig
target/release/bundle/*/*.deb
target/release/bundle/*/*.rpm
target/release/bundle/*/*.msi
target/release/bundle/*/*.msi.zip
target/release/bundle/*/*.msi.zip.sig
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

921
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,3 @@ strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros]
opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev = "27fb16b" }

View File

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

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',
},
},
])

View File

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

View File

@@ -1,12 +1,11 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.8.8",
"version": "0.8.3-1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"tsc:check": "vue-tsc --noEmit",
"build": "vite build",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write ."
},
@@ -14,41 +13,36 @@
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@tauri-apps/api": "^2.0.0-rc.3",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/plugin-updater": "^2.0.0-rc.0",
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@vintl/vintl": "^4.4.1",
"dayjs": "^1.11.10",
"floating-vue": "^5.2.2",
"ofetch": "^1.3.4",
"pinia": "^2.1.7",
"posthog-js": "^1.158.2",
"vite-svg-loader": "^5.1.0",
"vue": "^3.4.21",
"vue-multiselect": "3.0.0",
"vue-router": "4.3.0",
"vue-virtual-scroller": "v2.0.0-beta.8"
"vue-virtual-scroller": "v2.0.0-beta.8",
"posthog-js": "^1.158.2",
"@sentry/vue": "^8.27.0"
},
"devDependencies": {
"@eslint/compat": "^1.1.1",
"@nuxt/eslint-config": "^0.5.6",
"@tauri-apps/cli": "^2.0.0-rc",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"eslint": "^9.9.1",
"eslint": "^8.57.0",
"eslint-config-custom": "workspace:*",
"eslint-plugin-turbo": "^2.1.1",
"postcss": "^8.4.39",
"prettier": "^3.2.5",
"sass": "^1.74.1",
"tailwindcss": "^3.4.4",
"tsconfig": "workspace:*",
"typescript": "^5.5.4",
"vite": "^5.2.8",
"vue-tsc": "^2.1.6"
"vite": "^5.2.8"
},
"packageManager": "pnpm@9.4.0"
}

View File

@@ -1,15 +1,7 @@
<script setup>
import { computed, ref, onMounted } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import {
HomeIcon,
SearchIcon,
LibraryIcon,
PlusIcon,
SettingsIcon,
XIcon,
DownloadIcon,
} from '@modrinth/assets'
import { HomeIcon, SearchIcon, LibraryIcon, PlusIcon, SettingsIcon, XIcon } from '@modrinth/assets'
import { Button, Notifications } from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state'
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 { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
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 { getCurrentWindow } from '@tauri-apps/api/window'
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 { open } from '@tauri-apps/plugin-shell'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater'
const themeStore = useTheming()
@@ -63,8 +51,6 @@ const os = ref('')
const stateInitialized = ref(false)
const criticalErrorMessage = ref()
onMounted(async () => {
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)
checkUpdates()
}
const stateFailed = ref(false)
@@ -151,7 +126,6 @@ initialize_state()
})
const handleClose = async () => {
await saveWindowState(StateFlags.ALL)
await getCurrentWindow().close()
}
@@ -197,8 +171,7 @@ document.querySelector('body').addEventListener('click', function (e) {
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
!target.classList.contains('router-link-active') &&
!target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
!target.href.startsWith('https://tauri.localhost')
) {
open(target.href)
}
@@ -242,20 +215,6 @@ async function handleCommand(e) {
urlModal.value.show(e)
}
}
const updateAvailable = ref(false)
async function checkUpdates() {
const update = await check()
console.log(update)
updateAvailable.value = !!update
setTimeout(
() => {
checkUpdates()
},
5 * 1000 * 60,
)
}
</script>
<template>
@@ -289,14 +248,6 @@ async function checkUpdates() {
</div>
</div>
<div class="settings pages-list">
<button
v-if="updateAvailable"
v-tooltip="'Install update'"
class="btn btn-outline btn-primary icon-only collapsed-button"
@click="restartApp()"
>
<DownloadIcon />
</button>
<Button
v-tooltip="'Create profile'"
class="sleek-primary collapsed-button"
@@ -312,10 +263,6 @@ async function checkUpdates() {
</div>
</div>
<div class="view">
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
<h1>{{ criticalErrorMessage.header }}</h1>
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
</div>
<div class="appbar-row">
<div data-tauri-drag-region class="appbar">
<section class="navigation-controls">
@@ -428,16 +375,6 @@ async function checkUpdates() {
width: calc(100% - var(--sidebar-width));
background-color: var(--color-raised-bg);
.critical-error-banner {
margin-top: -1.25rem;
padding: 1rem;
background-color: rgba(203, 34, 69, 0.1);
border-left: 2px solid var(--color-red);
border-bottom: 2px solid var(--color-red);
border-right: 2px solid var(--color-red);
border-radius: 1rem;
}
.appbar {
display: flex;
align-items: center;

View File

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

View File

@@ -12,7 +12,7 @@ import {
EyeIcon,
ChevronRightIcon,
} from '@modrinth/assets'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { ConfirmModal } from '@modrinth/ui'
import Instance from '@/components/ui/Instance.vue'
import { computed, onMounted, onUnmounted, ref } from '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 { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
@@ -52,6 +53,7 @@ const instanceComponents = ref(null)
const rows = ref(null)
const deleteConfirmModal = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null)
async function deleteProfile() {
@@ -205,12 +207,13 @@ onUnmounted(() => {
</script>
<template>
<ConfirmModalWrapper
<ConfirmModal
ref="deleteConfirmModal"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="deleteProfile"
/>
<div class="content">

View File

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

View File

@@ -20,7 +20,7 @@ const handleAddContentFromFile = async () => {
if (!newProject) return
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)
}
}

View File

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

View File

@@ -1,5 +1,6 @@
<script setup>
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
import { Modal } from '@modrinth/ui'
import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
@@ -8,7 +9,6 @@ import { handleSevereError } from '@/store/error.js'
import { cancel_directory_change } from '@/helpers/settings.js'
import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref()
const error = ref()
@@ -121,7 +121,7 @@ async function repairInstance() {
</script>
<template>
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
<Modal ref="errorModal" :header="title" :closable="closable">
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
@@ -272,7 +272,7 @@ async function repairInstance() {
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</ModalWrapper>
</Modal>
</template>
<style>

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<ModalWrapper ref="modal" header="Create instance">
<Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
<div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
</div>
@@ -193,11 +193,10 @@
/>
</div>
</div>
</ModalWrapper>
</Modal>
</template>
<script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import {
PlusIcon,
UploadIcon,
@@ -208,7 +207,7 @@ import {
FolderSearchIcon,
UpdatedIcon,
} 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 { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile'
@@ -218,6 +217,7 @@ import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect'
import { trackEvent } from '@/helpers/analytics'
import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event'
import { install_from_file } from '@/helpers/pack.js'
import {
@@ -227,6 +227,8 @@ import {
} from '@/helpers/import.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
const themeStore = useTheming()
const profile_name = ref('')
const game_version = ref('')
const loader = ref('vanilla')
@@ -369,7 +371,7 @@ const create_instance = async () => {
}
const upload_icon = async () => {
const res = await open({
icon.value = await open({
multiple: false,
filters: [
{
@@ -379,8 +381,6 @@ const upload_icon = async () => {
],
})
icon.value = res.path ?? res
if (!icon.value) return
display_icon.value = convertFileSrc(icon.value)
}
@@ -417,7 +417,7 @@ const openFile = async () => {
const newProject = await open({ multiple: false })
if (!newProject) return
hide()
await install_from_file(newProject.path ?? newProject).catch(handleError)
await install_from_file(newProject).catch(handleError)
trackEvent('InstanceCreate', {
source: 'CreationModalFileOpen',
@@ -462,7 +462,7 @@ const promises = profileOptions.value.map(async (option) => {
option.name,
instances.map((name) => ({ name, selected: false })),
)
} catch {
} catch (error) {
// Allow failure silently
}
})

View File

@@ -1,5 +1,5 @@
<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="table">
<div class="table-row table-head">
@@ -32,16 +32,18 @@
</Button>
</div>
</div>
</ModalWrapper>
</Modal>
</template>
<script setup>
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { Modal, Button } from '@modrinth/ui'
import { ref } from 'vue'
import { find_filtered_jres } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const chosenInstallOptions = ref([])
const detectJavaModal = ref(null)

View File

@@ -124,19 +124,20 @@ async function testJava() {
}
async function handleJavaFileInput() {
const filePath = await open()
let filePath = await open()
if (filePath) {
let result = await get_jre(filePath.path ?? filePath)
let result = await get_jre(filePath)
if (!result) {
result = {
path: filePath.path ?? filePath,
path: filePath,
version: props.version.toString(),
architecture: 'x86',
}
}
trackEvent('JavaManualSelect', {
path: filePath,
version: props.version,
})
@@ -149,7 +150,7 @@ async function autoDetect() {
if (!props.compact) {
detectJavaModal.value.show(props.version, props.modelValue)
} 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) {
emit('update:modelValue', versions[0])
}

View File

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

View File

@@ -1,20 +1,12 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
<script setup lang="ts">
import { ref } from 'vue'
import { Promotion } from '@modrinth/ui'
import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true)
defineExpose({
scroll() {
updateAdPosition()
},
})
const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) {
const user = await get_user(creds.user_id).catch(handleError)
@@ -24,103 +16,8 @@ if (creds && creds.user_id) {
showAd.value = false
}
}
const adsWrapper = ref(null)
let resizeObserver
let scrollHandler
let intersectionObserver
let mutationObserver
onMounted(() => {
if (showAd.value) {
updateAdPosition(true)
resizeObserver = new ResizeObserver(() => updateAdPosition())
resizeObserver.observe(adsWrapper.value)
intersectionObserver = new IntersectionObserver(() => updateAdPosition())
intersectionObserver.observe(adsWrapper.value)
mutationObserver = new MutationObserver(() => updateAdPosition())
mutationObserver.observe(adsWrapper.value, { attributes: true, childList: true, subtree: true })
// Add scroll event listener
scrollHandler = () => {
requestAnimationFrame(() => updateAdPosition())
}
window.addEventListener('scroll', scrollHandler, { passive: true })
}
})
function updateAdPosition(overrideShown = false) {
if (adsWrapper.value) {
const rect = adsWrapper.value.getBoundingClientRect()
let y = rect.top + window.scrollY
let height = rect.bottom - rect.top
// Prevent ad from overlaying the app bar
if (y <= 52) {
y = 52
height = rect.bottom - 52
if (height < 0) {
height = 0
y = -1000
}
}
init_ads_window(rect.left + window.scrollX, y, rect.right - rect.left, height, overrideShown)
}
}
async function openPlusLink() {
await record_ads_click()
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
}
const unlisten = await listen('ads-scroll', (event) => {
if (adsWrapper.value) {
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
updateAdPosition()
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
if (intersectionObserver) {
intersectionObserver.disconnect()
}
if (mutationObserver) {
mutationObserver.disconnect()
}
if (scrollHandler) {
window.removeEventListener('scroll', scrollHandler)
}
unlisten()
})
</script>
<template>
<div
v-if="showAd"
ref="adsWrapper"
class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised cursor-pointer"
>
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
<button
class="mt-auto items-center gap-1 text-purple hover:underline bg-transparent border-none text-left cursor-pointer outline-none"
@click="openPlusLink"
>
<span>
Support creators and Modrinth ad-free with
<span class="font-bold">Modrinth+</span>
</span>
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
</button>
</div>
</div>
<Promotion v-if="showAd" :external="false" query-param="?r=launcher" />
</template>

View File

@@ -88,6 +88,8 @@ import { loading_listener } from '@/helpers/events.js'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { XIcon } from '@modrinth/assets'
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 { useLoading } from '@/store/loading.js'
@@ -128,16 +130,9 @@ const os = ref('')
getOS().then((x) => (os.value = x))
loading_listener(async (e) => {
console.log(e)
if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating app directory...'
} else if (e.event.type === 'launcher_update') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating Modrinth App...'
} else if (e.event.type === 'checking_for_updates') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Checking for updates...'
}
})

View File

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

View File

@@ -1,5 +1,10 @@
<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">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
@@ -46,19 +51,20 @@
</Button>
</div>
</div>
</ModalWrapper>
</Modal>
</template>
<script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
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 { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue'
import { handleError } from '@/store/state.js'
import { handleError, useTheming } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
const themeStore = useTheming()
const instance = ref(null)
const project = ref(null)
const versions = ref(null)
@@ -66,7 +72,7 @@ const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
let onInstall = ref(() => {})
defineExpose({
show: (instanceVal, projectVal, projectVersions, callback) => {

View File

@@ -1,18 +1,20 @@
<script setup>
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 { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { useTheming } from '@/store/theme.js'
import { handleError } from '@/store/state.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const versionId = ref()
const project = ref()
const confirmModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
let onInstall = ref(() => {})
defineExpose({
show: (projectVal, versionIdVal, callback) => {
@@ -50,7 +52,12 @@ async function install() {
</script>
<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">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">
@@ -60,7 +67,7 @@ async function install() {
>
</div>
</div>
</ModalWrapper>
</Modal>
</template>
<style lang="scss" scoped>

View File

@@ -7,7 +7,7 @@ import {
RightArrowIcon,
CheckIcon,
} from '@modrinth/assets'
import { Avatar, Button, Card } from '@modrinth/ui'
import { Avatar, Modal, Button, Card } from '@modrinth/ui'
import { computed, ref } from 'vue'
import {
add_project_from_version as installMod,
@@ -19,11 +19,12 @@ import {
import { open } from '@tauri-apps/plugin-dialog'
import { installVersionDependencies } from '@/store/install.js'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { useRouter } from 'vue-router'
import { convertFileSrc } from '@tauri-apps/api/core'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const themeStore = useTheming()
const router = useRouter()
const versions = ref()
@@ -48,7 +49,7 @@ const shownProfiles = computed(() =>
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
})
.filter((profile) => {
const loaders = versions.value.flatMap((v) => v.loaders)
let loaders = versions.value.flatMap((v) => v.loaders)
return (
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({
show: async (projectVal, versionsVal, callback) => {
@@ -77,7 +78,7 @@ defineExpose({
onInstall.value = callback
const profilesVal = await list().catch(handleError)
for (const profile of profilesVal) {
for (let profile of profilesVal) {
profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
handleError,
@@ -141,7 +142,7 @@ const toggleCreation = () => {
}
const upload_icon = async () => {
const res = await open({
icon.value = await open({
multiple: false,
filters: [
{
@@ -150,7 +151,6 @@ const upload_icon = async () => {
},
],
})
icon.value = res.path ?? res
if (!icon.value) return
display_icon.value = convertFileSrc(icon.value)
@@ -213,7 +213,12 @@ const createInstance = async () => {
</script>
<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">
<input
v-model="searchFilter"
@@ -230,7 +235,7 @@ const createInstance = async () => {
@click="installModal.hide()"
>
<Avatar
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
:src="profile.icon_path ? tauri.convertFileSrc(profile.icon_path) : null"
class="profile-image"
/>
{{ profile.name }}
@@ -299,7 +304,7 @@ const createInstance = async () => {
<Button @click="installModal.hide()">Cancel</Button>
</div>
</div>
</ModalWrapper>
</Modal>
</template>
<style scoped lang="scss">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

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 })
}

View File

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

View File

@@ -381,20 +381,20 @@ const sortedCategories = computed(() => {
// identifier[0], then if it ties, identifier[1], etc
async function sortByNameOrNumber(sortable, identifiers) {
sortable.sort((a, b) => {
for (const identifier of identifiers) {
const aNum = parseFloat(a[identifier])
const bNum = parseFloat(b[identifier])
for (let identifier of identifiers) {
let aNum = parseFloat(a[identifier])
let bNum = parseFloat(b[identifier])
if (isNaN(aNum) && isNaN(bNum)) {
// Both are strings, sort alphabetically
const stringComp = a[identifier].localeCompare(b[identifier])
let stringComp = a[identifier].localeCompare(b[identifier])
if (stringComp != 0) return stringComp
} else if (!isNaN(aNum) && !isNaN(bNum)) {
// Both are numbers, sort numerically
const numComp = aNum - bNum
let numComp = aNum - bNum
if (numComp != 0) return numComp
} else {
// 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
}
}
@@ -528,8 +528,7 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
<template>
<div ref="searchWrapper" class="search-container">
<aside class="filter-panel" @scroll="$refs.promo.scroll()">
<PromotionWrapper ref="promo" />
<aside class="filter-panel">
<Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar
@@ -676,7 +675,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
</Card>
</aside>
<div class="search">
<Card class="project-type-container mt-4">
<PromotionWrapper class="mt-4" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
</Card>
<Card class="search-panel-container">
@@ -878,13 +878,13 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
.filter-panel {
position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem;
display: flex;
flex-direction: column;
height: fit-content;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
width: 20rem;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
@@ -903,8 +903,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
}
.search {
margin: 0 1rem 0.5rem calc(20rem + 1rem);
width: calc(100% - calc(20rem + 1rem));
margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - 20.5rem);
.offline {
margin: 1rem;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, watch, onMounted } from 'vue'
import { ref, watch } from 'vue'
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 { get, set } from '@/helpers/settings'
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 { getVersion } from '@tauri-apps/api/app'
import { get_user, purge_cache_types } from '@/helpers/cache.js'
import { hide_ads_window } from '@/helpers/ads.js'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
onMounted(() => {
hide_ads_window(true)
})
const pageOptions = ['Home', 'Library']
@@ -175,12 +169,13 @@ async function purgeCache() {
Sign in
</button>
</div>
<ConfirmModalWrapper
<ConfirmModal
ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false"
proceed-label="Purge cache"
:noblur="!themeStore.advancedRendering"
@proceed="purgeCache"
/>
<div class="adjacent-input">
@@ -363,25 +358,6 @@ async function purgeCache() {
<span class="label__title size-card-header">Privacy</span>
</h3>
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Personalized ads</span>
<span class="label__description">
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
option, you opt out and ads will no longer be shown based on your interests.
</span>
</label>
<Toggle
id="opt-out-analytics"
:model-value="settings.personalized_ads"
:checked="settings.personalized_ads"
@update:model-value="
(e) => {
settings.personalized_ads = e
}
"
/>
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Telemetry</span>
@@ -425,14 +401,14 @@ async function purgeCache() {
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
<label :for="'java-' + javaVersion">
<span class="label__title">Java {{ javaVersion }} location</span>
<template v-for="version in [21, 17, 8]">
<label :for="'java-' + version">
<span class="label__title">Java {{ version }} location</span>
</label>
<JavaSelector
:id="'java-selector-' + javaVersion"
v-model="javaVersions[javaVersion]"
:version="javaVersion"
:id="'java-selector-' + version"
v-model="javaVersions[version]"
:version="version"
@update:model-value="updateJavaVersion"
/>
</template>

View File

@@ -1,8 +1,8 @@
<template>
<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">
<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">
<h2 class="name">{{ instance.name }}</h2>
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
@@ -61,9 +61,9 @@
</RouterLink>
</div>
</Card>
<PromotionWrapper ref="promo" class="mt-4" />
</div>
<div class="content">
<PromotionWrapper />
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
@@ -311,6 +311,7 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
gap: 1rem;
width: 17rem;
}
Button {
@@ -324,13 +325,12 @@ Button {
}
.side-cards {
position: fixed;
width: 300px;
position: absolute;
display: flex;
flex-direction: column;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
padding: 1rem;
min-height: calc(100% - 3.25rem);
max-height: calc(100% - 3.25rem);
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
@@ -374,7 +374,10 @@ Button {
overflow: auto;
gap: 1rem;
min-height: 100%;
padding: 1rem;
}
.content {
margin-left: 19rem;
}
.instance-info {
@@ -448,10 +451,10 @@ Button {
}
.content {
margin: 0 1rem 0.5rem 20rem;
width: calc(100% - 20rem);
width: 100%;
display: flex;
flex-direction: column;
padding: 1rem 1rem 0 0;
overflow: auto;
}

View File

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

View File

@@ -284,7 +284,7 @@
:link-function="(page) => `?page=${page}`"
@switch-page="switchPage"
/>
<ModalWrapper ref="deleteWarning" header="Are you sure?">
<Modal ref="deleteWarning" header="Are you sure?">
<div class="modal-body">
<div class="markdown-body">
<p>
@@ -302,8 +302,8 @@
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?">
</Modal>
<Modal ref="deleteDisabledWarning" header="Are you sure?">
<div class="modal-body">
<div class="markdown-body">
<p>
@@ -325,8 +325,8 @@
</Button>
</div>
</div>
</ModalWrapper>
<ShareModalWrapper
</Modal>
<ShareModal
ref="shareModal"
share-title="Sharing modpack content"
share-text="Check out the projects I'm using in my modpack!"
@@ -360,6 +360,8 @@ import {
import {
Pagination,
DropdownSelect,
ShareModal,
Modal,
Checkbox,
AnimatedLogo,
Avatar,
@@ -391,8 +393,6 @@ import {
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
const props = defineProps({
instance: {
@@ -717,7 +717,7 @@ const updateProject = async (mod) => {
})
}
const locks = {}
let locks = {}
const toggleDisableMod = async (mod) => {
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
@@ -725,7 +725,7 @@ const toggleDisableMod = async (mod) => {
locks[mod.id] = ref(null)
}
const lock = locks[mod.id]
let lock = locks[mod.id]
while (lock.value) {
await lock.value
@@ -784,7 +784,6 @@ const deleteDisabled = async () => {
}
const shareNames = async () => {
console.log(functionValues.value)
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
}

View File

@@ -1,13 +1,18 @@
<template>
<ConfirmModalWrapper
<ConfirmModal
ref="modal_confirm"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="removeProfile"
/>
<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="markdown-body"
@@ -26,9 +31,13 @@
</button>
</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="markdown-body"
@@ -47,9 +56,13 @@
</button>
</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="input-row">
<p class="input-label">Loader</p>
@@ -93,7 +106,7 @@
</button>
</div>
</div>
</ModalWrapper>
</Modal>
<section class="card">
<div class="label">
<h3>
@@ -498,7 +511,18 @@ import {
DownloadIcon,
ClipboardCopyIcon,
} 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 { 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_game_versions, get_loaders } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
const breadcrumbs = useBreadcrumbs()
@@ -547,6 +570,8 @@ const props = defineProps({
},
})
const themeStore = useTheming()
const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
@@ -581,7 +606,7 @@ async function setIcon() {
if (!value) return
icon.value = value.path ?? value
icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError)
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 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(
(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(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
@@ -685,15 +710,19 @@ const editProfileObject = computed(() => {
}
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) {
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
if (envVars.value !== '') {
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
}
if (overrideMemorySettings.value) {
@@ -876,7 +905,7 @@ const editing = ref(false)
async function saveGvLoaderEdits() {
editing.value = true
const editProfile = editProfileObject.value
let editProfile = editProfileObject.value
editProfile.loader = loader.value
editProfile.game_version = gameVersion.value

View File

@@ -20,14 +20,14 @@
</span>
</Card>
</div>
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage">
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="expandedGalleryItem = null">
<div class="content">
<img
class="image"
:class="{ 'zoomed-in': zoomedIn }"
:src="
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
expandedGalleryItem.url
? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -45,15 +45,15 @@
</div>
<div class="controls">
<div class="buttons">
<Button class="close" icon-only @click="hideImage">
<Button class="close" icon-only @click="expandedGalleryItem = null">
<XIcon aria-hidden="true" />
</Button>
<a
class="open btn icon-only"
target="_blank"
:href="
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
expandedGalleryItem.url
? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
>
@@ -94,7 +94,6 @@ import {
import { Button, Card } from '@modrinth/ui'
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
const props = defineProps({
project: {
@@ -103,14 +102,9 @@ const props = defineProps({
},
})
const expandedGalleryItem = ref(null)
const expandedGalleryIndex = ref(0)
const zoomedIn = ref(false)
const hideImage = () => {
expandedGalleryItem.value = null
show_ads_window()
}
let expandedGalleryItem = ref(null)
let expandedGalleryIndex = ref(0)
let zoomedIn = ref(false)
const nextImage = () => {
expandedGalleryIndex.value++
@@ -137,7 +131,6 @@ const previousImage = () => {
}
const expandImage = (item, index) => {
hide_ads_window()
expandedGalleryItem.value = item
expandedGalleryIndex.value = index
zoomedIn.value = false
@@ -147,20 +140,6 @@ const expandImage = (item, index) => {
url: item.url,
})
}
function keyListener(e) {
if (expandedGalleryItem.value) {
e.preventDefault()
if (e.key === 'Escape') {
hideImage()
} else if (e.key === 'ArrowLeft') {
previousImage()
} else if (e.key === 'ArrowRight') {
nextImage()
}
}
}
document.addEventListener('keypress', keyListener)
</script>
<style scoped lang="scss">

View File

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

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"]
}

View File

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

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"]
}

View File

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

View File

@@ -10,6 +10,7 @@ theseus = { path = "../../packages/app-lib", features = ["cli"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = "2.0.0-rc.4"
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
url = "2.2"

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus_gui"
version = "0.8.8"
version = "0.8.3-1"
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/"
@@ -16,14 +16,13 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
serde_json = "1.0"
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-deep-link = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc"
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-updater = { version = "2.0.0-rc" }
tauri-plugin-single-instance = { version = "2.0.0-rc" }
tauri-plugin-updater = { version = "2.0.0-rc.1", optional = true }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
@@ -58,9 +57,6 @@ cocoa = "0.25.0"
objc = "0.2.7"
rand = "0.8.5"
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { version = "2.0.0-rc", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
[features]
# by default Tauri runs in production mode
# 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
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
updater = []
updater = ["dep:tauri-plugin-updater"]

View File

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

View File

@@ -62,7 +62,7 @@ fn main() {
InlinedPlugin::new()
.commands(&[
"get_java_versions",
"set_java_version",
"set_java_versions",
"jre_find_filtered_jres",
"jre_get_jre",
"jre_test_jre",
@@ -217,22 +217,6 @@ fn main() {
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"ads",
InlinedPlugin::new()
.commands(&[
"init_ads_window",
"hide_ads_window",
"scroll_ads_window",
"show_ads_window",
"record_ads_click",
"open_link",
"get_ads_personalization",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
),
)
.expect("Failed to run tauri-build");

View File

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

View File

@@ -35,7 +35,6 @@
"cache:default",
"settings:default",
"tags:default",
"utils:default",
"ads:default"
"utils:default"
]
}
}

View File

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

File diff suppressed because one or more lines are too long

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

View File

@@ -299,69 +299,6 @@
},
"Identifier": {
"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.",
"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",
"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",
"enum": [
"jre:deny-set-java-version"
"jre:deny-set-java-versions"
]
},
{

File diff suppressed because it is too large Load Diff

346
apps/app/msi/main.wxs Normal file
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>

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

View File

@@ -9,7 +9,7 @@
"fix": "cargo fmt && cargo clippy --fix"
},
"devDependencies": {
"@tauri-apps/cli": "2.0.0-rc.16"
"@tauri-apps/cli": "2.0.0-rc.5"
},
"dependencies": {
"@modrinth/app-frontend": "workspace:*",

View File

@@ -1,106 +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')
}
function muteAudioContext() {
if (window.AudioContext || window.webkitAudioContext) {
const AudioContext = window.AudioContext || window.webkitAudioContext
const originalCreateMediaElementSource = AudioContext.prototype.createMediaElementSource
const originalCreateMediaStreamSource = AudioContext.prototype.createMediaStreamSource
const originalCreateMediaStreamTrackSource = AudioContext.prototype.createMediaStreamTrackSource
const originalCreateBufferSource = AudioContext.prototype.createBufferSource
const originalCreateOscillator = AudioContext.prototype.createOscillator
AudioContext.prototype.createGain = function () {
const gain = originalCreateGain.call(this)
gain.gain.value = 0
return gain
}
AudioContext.prototype.createMediaElementSource = function (mediaElement) {
const source = originalCreateMediaElementSource.call(this, mediaElement)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createMediaStreamSource = function (mediaStream) {
const source = originalCreateMediaStreamSource.call(this, mediaStream)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createMediaStreamTrackSource = function (mediaStreamTrack) {
const source = originalCreateMediaStreamTrackSource.call(this, mediaStreamTrack)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createBufferSource = function () {
const source = originalCreateBufferSource.call(this)
source.connect(this.createGain())
return source
}
AudioContext.prototype.createOscillator = function () {
const oscillator = originalCreateOscillator.call(this)
oscillator.connect(this.createGain())
return oscillator
}
}
}
function muteVideo(mediaElement) {
let count = Number(mediaElement.getAttribute('data-modrinth-muted-count') ?? 0)
if (!mediaElement.muted || mediaElement.volume !== 0) {
mediaElement.muted = true
mediaElement.volume = 0
mediaElement.setAttribute('data-modrinth-muted-count', count + 1)
}
if (count > 5) {
// Video is detected as malicious, so it is removed from the page
mediaElement.remove()
}
}
function muteVideos() {
document.querySelectorAll('video, audio').forEach(function (mediaElement) {
muteVideo(mediaElement)
if (!mediaElement.hasAttribute('data-modrinth-muted')) {
mediaElement.addEventListener('volumechange', () => muteVideo(mediaElement))
mediaElement.setAttribute('data-modrinth-muted', 'true')
}
})
}
document.addEventListener('DOMContentLoaded', () => {
muteVideos()
muteAudioContext()
const observer = new MutationObserver(muteVideos)
observer.observe(document.body, { childList: true, subtree: true })
})

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)
}

View File

@@ -16,7 +16,6 @@ pub mod settings;
pub mod tags;
pub mod utils;
pub mod ads;
pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
@@ -40,10 +39,6 @@ pub enum TheseusSerializableError {
#[error("Tauri error: {0}")]
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
@@ -91,15 +86,7 @@ macro_rules! impl_serialize {
}
// Use the macro to implement Serialize for TheseusSerializableError
#[cfg(not(feature = "updater"))]
impl_serialize! {
IO,
Tauri,
}
#[cfg(feature = "updater")]
impl_serialize! {
IO,
Tauri,
TauriUpdater,
}

View File

@@ -26,73 +26,11 @@ extern crate objc;
#[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app.clone()).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?;
}
State::init().await?;
let state = State::get().await?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir(), true)?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir().join("icons"), true)?;
Ok(())
}
@@ -101,7 +39,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
#[tracing::instrument(skip_all)]
#[tauri::command]
fn show_window(app: tauri::AppHandle) {
let win = app.get_window("main").unwrap();
let win = app.get_webview_window("main").unwrap();
if let Err(e) = win.show() {
MessageDialog::new()
.set_type(MessageType::Error)
@@ -135,11 +73,6 @@ async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
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
// ie: deep links or filepaths for .mrpacks
fn main() {
@@ -169,19 +102,6 @@ fn main() {
}
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_dialog::init())
.plugin(tauri_plugin_deep_link::init())
@@ -259,14 +179,12 @@ fn main() {
.plugin(api::tags::init())
.plugin(api::utils::init())
.plugin(api::cache::init())
.plugin(api::ads::init())
.invoke_handler(tauri::generate_handler![
initialize_state,
is_dev,
toggle_decorations,
api::mr_auth::modrinth_auth_login,
show_window,
restart_app,
]);
#[cfg(target_os = "macos")]

View File

@@ -5,15 +5,10 @@
"build": {
"features": ["updater"]
},
"app": {
"security": {
"capabilities": ["ads", "core", "plugins", "updater"]
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
"endpoints": ["https://launcher-files.modrinth.com/updates.json"]
}
}
}
}

View File

@@ -21,9 +21,8 @@
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com",
"nsis": {
"installMode": "perMachine",
"installerHooks": "./nsis/hooks.nsi"
"wix": {
"template": "./msi/main.wxs"
}
},
"longDescription": "",
@@ -49,8 +48,7 @@
]
},
"productName": "Modrinth App",
"version": "0.8.8",
"mainBinaryName": "Modrinth App",
"version": "0.8.3-1",
"identifier": "ModrinthApp",
"plugins": {
"deep-link": {
@@ -72,10 +70,10 @@
"resizable": true,
"title": "Modrinth App",
"width": 1280,
"minHeight": 750,
"minHeight": 700,
"minWidth": 1100,
"visible": false,
"zoomHotkeysEnabled": false,
"zoomHotkeysEnabled": true,
"decorations": false
}
],
@@ -88,18 +86,7 @@
],
"enable": true
},
"capabilities": ["ads", "core", "plugins"],
"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'"
}
"csp": "default-src 'self'; connect-src ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src unsafe-inline 'self'"
}
}
}

View File

@@ -1,3 +0,0 @@
{
"mainBinaryName": "ModrinthApp"
}

View File

@@ -12,7 +12,7 @@
"minHeight": 700,
"minWidth": 1100,
"visible": false,
"zoomHotkeysEnabled": false,
"zoomHotkeysEnabled": true,
"decorations": true
}
]

View File

@@ -62,10 +62,6 @@
.normal-page__content {
grid-area: content;
}
.normal-page__header {
grid-area: header;
}
}
@media (min-width: 1024px) {
@@ -165,8 +161,4 @@
max-width: calc(80rem - 18.75rem - 0.75rem);
//overflow-x: hidden;
}
.normal-page__header {
grid-area: header;
}
}

View File

@@ -1,7 +1,7 @@
<template>
<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">
<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">
<span>
Support creators and Modrinth ad-free with

View File

@@ -57,7 +57,7 @@ export function createDisplayNames(
try {
return dict.of(lookup);
} catch {
} catch (err) {
console.warn(
`Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale

View File

@@ -1,21 +1,21 @@
import hljs from "highlight.js/lib/core";
// Scripting
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 lua from "highlight.js/lib/languages/lua";
// Coding
import groovy from "highlight.js/lib/languages/groovy";
import java from "highlight.js/lib/languages/java";
import kotlin from "highlight.js/lib/languages/kotlin";
import scala from "highlight.js/lib/languages/scala";
import groovy from "highlight.js/lib/languages/groovy";
// Configs
import { configuredXss, md } from "@modrinth/utils";
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 properties from "highlight.js/lib/languages/properties";
import xml from "highlight.js/lib/languages/xml";
import ini from "highlight.js/lib/languages/ini";
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 */
// Scripting
@@ -54,7 +54,7 @@ export const renderHighlightedString = (string) =>
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch {
} catch (__) {
/* empty */
}
}

View File

@@ -231,7 +231,6 @@
</template>
<template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </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>
</OverflowMenu>
<ButtonStyled v-else color="brand">
@@ -779,7 +778,6 @@ const userMenuOptions = computed(() => {
link: "/settings",
},
];
// TODO: Only show if user has projects
options = [
...options,
@@ -803,24 +801,6 @@ const userMenuOptions = computed(() => {
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,
{

View File

@@ -430,208 +430,220 @@
}"
>
<div class="normal-page__header relative my-4">
<ContentPageHeader>
<template #icon>
<div
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" />
</template>
<template #title>
{{ project.title }}
</template>
<template #title-suffix>
<Badge v-if="auth.user && currentMember" :type="project.status" class="status-badge" />
</template>
<template #summary>
{{ project.description }}
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ $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 class="flex flex-col gap-1">
<div class="flex flex-wrap items-center gap-2">
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
{{ project.title }}
</h1>
<Badge
v-if="auth.user && currentMember"
:type="project.status"
class="status-badge"
/>
</div>
<p class="m-0 line-clamp-2 max-w-[40rem]">
{{ project.description }}
</p>
<div class="mt-auto flex flex-wrap gap-4">
<div
v-for="(category, index) in project.categories"
:key="index"
class="tag-list__item"
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
>
{{ 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>
</template>
<template #actions>
<div class="hidden sm:contents">
<ButtonStyled
size="large"
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
>
<button @click="(event) => downloadModal.show(event)">
<DownloadIcon aria-hidden="true" />
Download
</button>
</ButtonStyled>
</div>
<div class="contents sm:hidden">
</div>
<div class="flex flex-col justify-center gap-4">
<div class="flex flex-wrap gap-2">
<div class="hidden sm:contents">
<ButtonStyled
size="large"
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
>
<button @click="(event) => downloadModal.show(event)">
<DownloadIcon aria-hidden="true" />
Download
</button>
</ButtonStyled>
</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
size="large"
circular
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
:color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
>
<button
aria-label="Download"
class="flex sm:hidden"
@click="(event) => downloadModal.show(event)"
v-if="auth.user"
v-tooltip="following ? `Unfollow` : `Follow`"
:aria-label="following ? `Unfollow` : `Follow`"
@click="userFollowProject(project)"
>
<DownloadIcon aria-hidden="true" />
<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>
</div>
<ButtonStyled
size="large"
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>
</div>
</div>
<ProjectMemberHeader
v-if="currentMember"
:project="project"
@@ -689,13 +701,13 @@
"
>
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="tag-list">
<div class="status-list">
<div
v-if="
(project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="tag-list__item"
class="status-list__item"
>
<ClientIcon aria-hidden="true" />
Client-side
@@ -705,29 +717,34 @@
(project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="tag-list__item"
class="status-list__item"
>
<ServerIcon aria-hidden="true" />
Server-side
</div>
<div v-if="false" class="tag-list__item">
<div v-if="false" class="status-list__item">
<UserIcon aria-hidden="true" />
Singleplayer
</div>
<div
v-if="
project.project_type !== 'datapack' &&
((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"
v-if="project.client_side === 'required' && project.server_side === 'required'"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server
</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>
</section>
</div>
@@ -1027,7 +1044,6 @@ import {
OverflowMenu,
PopoutMenu,
ScrollablePanel,
ContentPageHeader,
} from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs";
@@ -1398,7 +1414,7 @@ try {
versions = shallowRef(toRaw(versions));
featuredVersions = shallowRef(toRaw(featuredVersions));
} catch {
} catch (error) {
throw createError({
fatal: true,
statusCode: 404,
@@ -1718,6 +1734,10 @@ const navLinks = computed(() => {
});
</script>
<style lang="scss" scoped>
.normal-page__header {
grid-area: header;
}
.settings-header {
display: flex;
flex-direction: row;

View File

@@ -139,8 +139,8 @@
class="image"
:class="{ 'zoomed-in': zoomedIn }"
:src="
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
expandedGalleryItem.url
? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -165,8 +165,8 @@
class="open circle-button"
target="_blank"
:href="
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
expandedGalleryItem.url
? expandedGalleryItem.url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
>

View File

@@ -95,7 +95,7 @@ const props = defineProps<{
members: User[];
currentMember: User;
dependencies: Dependency[];
resetProject: (opts?: { dedupe?: "cancel" | "defer" }) => Promise<void>;
resetProject: Function;
}>();
const version = computed(() => {

View File

@@ -567,7 +567,6 @@
:show-labels="false"
:limit="6"
:hide-selected="true"
:custom-label="(version) => version"
placeholder="Choose versions..."
/>
<Checkbox

View File

@@ -27,7 +27,6 @@ const windowsLink = ref(null);
const linuxLinks = {
appImage: null,
deb: null,
rpm: null,
thirdParty: "https://support.modrinth.com/en/articles/9298760",
};
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];
linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1];
linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0];
linuxLinks.rpm = launcherUpdates.value.platforms["linux-x86_64"].install_urls[2];
onMounted(() => {
os.value = navigator?.platform.toString();
@@ -881,11 +879,7 @@ useSeoMeta({
</a>
<a :href="linuxLinks.deb" download="">
<DownloadIcon />
<span> Download the DEB </span>
</a>
<a :href="linuxLinks.rpm" download="">
<DownloadIcon />
<span> Download the RPM </span>
<span> Download the Deb </span>
</a>
<a :href="linuxLinks.thirdParty" download="">
<LinkIcon />

View File

@@ -195,7 +195,7 @@ const onAuthorize = async () => {
}
throw new Error(formatMessage(messages.noRedirectUrlError));
} catch {
} catch (error) {
data.$notify({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),
@@ -222,7 +222,7 @@ const onReject = async () => {
}
throw new Error(formatMessage(messages.noRedirectUrlError));
} catch {
} catch (error) {
data.$notify({
group: "main",
title: formatMessage(commonMessages.errorNotificationTitle),

View File

@@ -145,7 +145,7 @@ if (route.query.flow) {
await useAuth(auth.value.token);
}
}
} catch {
} catch (err) {
success.value = false;
}
}

View File

@@ -88,6 +88,20 @@
></span
>
</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>
</section>
</div>

View File

@@ -2,24 +2,21 @@
<div>
<section class="universal-card">
<h2 class="text-2xl">Revenue</h2>
<div v-if="userBalance.available >= minWithdraw">
<div v-if="auth.user.payout_data.balance >= minWithdraw">
<p>
You have
<strong>{{ $formatMoney(userBalance.available) }}</strong>
available to withdraw. <strong>{{ $formatMoney(userBalance.pending) }}</strong> of your
balance is <nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong>
available to withdraw.
</p>
</div>
<p v-else>
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.
<strong>{{ $formatMoney(userBalance.pending) }}</strong> of your balance is
<nuxt-link class="text-link" to="/legal/cmp-info#pending">pending</nuxt-link>.
</p>
<div class="input-group mt-4">
<nuxt-link
v-if="userBalance.available >= minWithdraw"
v-if="auth.user.payout_data.balance >= minWithdraw"
class="iconified-button brand-button"
to="/dashboard/revenue/withdraw"
>
@@ -84,10 +81,6 @@ import { TransferIcon, HistoryIcon, PayPalIcon, SaveIcon, XIcon } from "@modrint
const auth = await useAuth();
const minWithdraw = ref(0.01);
const { data: userBalance } = await useAsyncData(`payout/balance`, () =>
useBaseFetch(`payout/balance`, { apiVersion: 3 }),
);
async function updateVenmo() {
startLoading();
try {

View File

@@ -88,7 +88,7 @@
<p>
You are initiating a transfer of your revenue from Modrinth's Creator Monetization Program.
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 }}?
</p>
<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")),
);
const [{ data: userBalance }, { data: payoutMethods, refresh: refreshPayoutMethods }] =
await Promise.all([
useAsyncData(`payout/balance`, () => useBaseFetch(`payout/balance`, { apiVersion: 3 })),
useAsyncData(`payout/methods?country=${country.value.id}`, () =>
useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 }),
),
]);
const { data: payoutMethods, refresh: refreshPayoutMethods } = await useAsyncData(
`payout/methods?country=${country.value.id}`,
() => useBaseFetch(`payout/methods?country=${country.value.id}`, { apiVersion: 3 }),
);
const selectedMethodId = ref(payoutMethods.value[0].id);
const selectedMethod = computed(() =>
@@ -298,10 +295,10 @@ const knownErrors = computed(() => {
if (!parsedAmount.value && amount.value.length > 0) {
errors.push(`${amount.value} is not a valid amount`);
} else if (
parsedAmount.value > userBalance.value.available ||
parsedAmount.value > auth.value.user.payout_data.balance ||
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)}`);
} else if (parsedAmount.value <= fees.value || parsedAmount.value < minWithdrawAmount.value) {
const minAmount = Math.max(fees.value + 0.01, minWithdrawAmount.value);

View File

@@ -1,7 +1,7 @@
<template>
<div class="markdown-body">
<h1>Rewards Program Information</h1>
<p><em>Last modified: Sep 12, 2024</em></p>
<p><em>Last modified: May 13, 2024</em></p>
<p>
This page was created for transparency for how the rewards program works on Modrinth. Feel
free to join our Discord or email
@@ -15,11 +15,10 @@
<h2>Rewards Distribution</h2>
<p>
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://aditude.io">Aditude</a>. We then distribute this ad revenue to creators.
<a href="https://adrinth.com">Adrinth</a>. We then distribute this ad revenue to creators.
</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.
</p>
<p>
@@ -43,10 +42,10 @@
</ul>
<p>In this scenario, the earnings for each creator and Modrinth would be as follows:</p>
<ul>
<li>Modrinth: $25 (25% of $100, the site's earnings for the day)</li>
<li>User A: $48.91 ($75 * (10 + 30 + 100 + 10)/230)</li>
<li>User B: $10.43 (0.4 * $75 * (50 + 20 + 10 + 0)/230)</li>
<li>User C: $15.65 (0.6 * $75 * (50 + 20 + 10 + 0)/230)</li>
<li>Modrinth: $10 (10% of $100, the site's earnings for the day)</li>
<li>User A: $58.69 ($90 * (10 + 30 + 100 + 10)/230)</li>
<li>User B: $12.52 (0.4 * $90 * (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>
</ul>
<p>
@@ -73,62 +72,19 @@
</p>
<h3>What methods can I use withdraw money from my account? Are there any fees?</h3>
<p>
Right now, you can use PayPal or Venmo to withdraw money from your Modrinth account. Gift card
withdrawal is also available. We are working on more methods to withdraw money from your
account. There are fees to withdraw money from your Modrinth accountsee the revenue page in
your dashboard for more information.
Right now, you can use PayPal or Venmo to withdraw money from your Modrinth account. We are
working on more methods to withdraw money from your account. There are fees to withdraw money
from your Modrinth account—see the revenue page in your dashboard for more information.
</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>
Modrinth receives ad revenue from our ad providers on a NET 60 day basis. Due to this, not all
revenue is not immediately available to withdraw. We pay creators as soon as we receive the
money from our ad providers, which is 60 days after the last day of each month. This table
outlines some example dates of how NET 60 payments are made:
</p>
<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.
While this is true, our new system (as of 08/05/23) gives more of the site's revenue to
creators, so creators will earn more. In the old system, we would earn revenue through
advertisements in search and user profile pages. This amounted on average each month to about
15-20% of the site's total advertising revenue (so a 80-85% split to creators). The new system
gives creators more revenue and a more favorable split towards creators (90%).
</p>
<h3></h3>
</div>
</template>

View File

@@ -1,260 +1,240 @@
<template>
<div
v-if="organization"
class="experimental-styles-within new-page sidebar"
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }"
>
<ModalCreation ref="modal_creation" :organization-id="organization.id" />
<template v-if="routeHasSettings">
<div class="normal-page__sidebar">
<div v-if="organization" class="normal-page">
<div class="normal-page__sidebar">
<div v-if="routeHasSettings" class="universal-card">
<Breadcrumbs
current-title="Settings"
: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>
<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">
<Breadcrumbs
current-title="Settings"
: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 class="page-header__icon">
<Avatar size="md" :src="organization.icon_url" />
</div>
<h2>Organization settings</h2>
<div class="page-header__text">
<h1 class="title">{{ organization.name }}</h1>
<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>
<span class="organization-label"><OrganizationIcon /> Organization</span>
</div>
<div class="organization-description">
<div class="metadata-item markdown-body collection-description">
<p>{{ organization.description }}</p>
</div>
<hr class="card-divider" />
<div class="primary-stat">
<UserIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ $formatNumber(acceptedMembers?.length || 0) }}
</span>
member<template v-if="acceptedMembers?.length !== 1">s</template>
</div>
</div>
<div class="primary-stat">
<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 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
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<div class="card flex-card">
<h2>Members</h2>
<div class="details-list">
<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 class="creator-list universal-card">
<div class="title-and-link">
<h3>Members</h3>
</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">
<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>
<template v-for="member in acceptedMembers" :key="member.user.id">
<nuxt-link class="creator button-base" :to="`/user/${member.user.username}`">
<Avatar :src="member.user.avatar_url" circle />
<p class="name">
{{ member.user.username }}
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
</p>
<p class="role">{{ member.role }}</p>
</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>
</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>
</template>
<script setup>
import {
BoxIcon,
MoreVerticalIcon,
UserIcon,
UsersIcon,
SettingsIcon,
ChartIcon,
CheckIcon,
XIcon,
ClipboardCopyIcon,
} 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 NavStackItem from "~/components/ui/NavStackItem.vue";
import NavRow from "~/components/ui/NavRow.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
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 CrownIcon from "~/assets/images/utils/crown.svg?component";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
import NavTabs from "~/components/ui/NavTabs.vue";
const vintl = useVIntl();
const { formatMessage } = vintl;
@@ -472,26 +451,6 @@ useSeoMeta({
ogDescription: organization.value.description,
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>
<style scoped lang="scss">

View File

@@ -371,22 +371,6 @@
</div>
</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">
<h2>Delete account</h2>
<p>
@@ -407,18 +391,16 @@
<script setup>
import {
CheckIcon,
EditIcon,
ExternalIcon,
LeftArrowIcon,
PlusIcon,
RightArrowIcon,
SaveIcon,
SettingsIcon,
TrashIcon,
UpdatedIcon,
PlusIcon,
SettingsIcon,
XIcon,
DownloadIcon,
LeftArrowIcon,
RightArrowIcon,
CheckIcon,
ExternalIcon,
} from "@modrinth/assets";
import QrcodeVue from "qrcode.vue";
import GitHubIcon from "assets/icons/auth/sso-github.svg";
@@ -556,7 +538,7 @@ async function verifyTwoFactorCode() {
backupCodes.value = res.backup_codes;
twoFactorStep.value = 2;
await useAuth(auth.value.token);
} catch {
} catch (err) {
twoFactorIncorrect.value = true;
}
stopLoading();
@@ -573,7 +555,7 @@ async function removeTwoFactor() {
});
manageTwoFactorModal.value.hide();
await useAuth(auth.value.token);
} catch {
} catch (err) {
twoFactorIncorrect.value = true;
}
stopLoading();
@@ -632,34 +614,6 @@ async function deleteAccount() {
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>
<style lang="scss" scoped>
canvas {

View File

@@ -301,7 +301,7 @@ const canSubmit = computed(() => {
const url = new URL(uri);
return !!url;
});
} catch {
} catch (err) {
allValid = false;
}
return filledIn && (oneValid || allValid);

View File

@@ -226,7 +226,7 @@ async function changeLocale(value: string) {
try {
await vintl.changeLocale(value);
$failedLocale.value = undefined;
} catch {
} catch (err) {
$failedLocale.value = value;
} finally {
$changingTo.value = undefined;

View File

@@ -3,88 +3,75 @@
<ModalCreation ref="modal_creation" />
<CollectionCreateModal ref="modal_collection_creation" />
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<div class="normal-page__header py-4">
<ContentPageHeader>
<template #icon>
<div class="normal-page__header pt-4">
<div
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 />
</template>
<template #title>
{{ user.username }}
</template>
<template #summary>
{{
user.bio
? user.bio
: projects.length === 0
? "A Modrinth user."
: "A Modrinth creator."
}}
</template>
<template #stats>
<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 class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-2">
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
{{ user.username }}
</h1>
</div>
<p class="m-0 line-clamp-2 max-w-[40rem]">
{{
user.bio
? user.bio
: projects.length === 0
? "A Modrinth user."
: "A Modrinth creator."
}}
</p>
</div>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
downloads
</div>
<div class="flex flex-col justify-center gap-4">
<div class="flex flex-wrap gap-2">
<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>
</div>
<div class="flex items-center gap-2 font-semibold">
<CalendarIcon class="h-6 w-6 text-secondary" />
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>
<div class="normal-page__content">
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
@@ -207,6 +194,72 @@
</div>
</div>
<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">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
<div class="flex flex-wrap gap-2">
@@ -235,9 +288,6 @@
</div>
</div>
</div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
</div>
</div>
</div>
@@ -254,7 +304,7 @@ import {
ClipboardCopyIcon,
MoreVerticalIcon,
} from "@modrinth/assets";
import { OverflowMenu, ButtonStyled, ContentPageHeader } from "@modrinth/ui";
import { OverflowMenu, ButtonStyled } from "@modrinth/ui";
import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue";
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 UpToDate from "~/assets/images/illustrations/up_to_date.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 ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from "~/components/ui/Avatar.vue";
@@ -454,6 +505,15 @@ const sumDownloads = computed(() => {
return sum;
});
const sumFollows = computed(() => {
let sum = 0;
for (const project of projects.value) {
sum += project.followers;
}
return sum;
});
const badges = computed(() => {
const badges = [];
@@ -593,4 +653,8 @@ export default defineNuxtComponent({
}
}
}
.normal-page__header {
grid-area: header;
}
</style>

View File

@@ -472,18 +472,3 @@ visiblemeasures.com, 1041, DIRECT #yieldmo
iqzone.com,IQ190,RESELLER
krushmedia.com, AJxF6R537a9M6CaTvK, RESELLER
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

View File

@@ -1,110 +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://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>
<script>
function initAds(personalized) {
window.tude = window.tude || { cmd: [] };
tude.cmd.push(function () {
tude.refreshAdsViaDivMappings([
{
divId: "modrinth-rail-1",
baseDivId: "pb-slot-square-2",
},
]);
tude.setPrivacySettings({
personalizedAds: personalized ?? true,
});
});
}
window.__TAURI_INTERNALS__
.invoke("plugin:ads|get_ads_personalization", {})
.then(initAds)
.catch(() => initAds(true));
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>

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -54,78 +54,73 @@
"type_info": "Integer"
},
{
"name": "personalized_ads",
"name": "onboarded",
"ordinal": 10,
"type_info": "Integer"
},
{
"name": "onboarded",
"ordinal": 11,
"type_info": "Integer"
},
{
"name": "extra_launch_args",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "custom_env_vars",
"ordinal": 13,
"ordinal": 12,
"type_info": "Text"
},
{
"name": "mc_memory_max",
"ordinal": 14,
"ordinal": 13,
"type_info": "Integer"
},
{
"name": "mc_force_fullscreen",
"ordinal": 15,
"ordinal": 14,
"type_info": "Integer"
},
{
"name": "mc_game_resolution_x",
"ordinal": 16,
"ordinal": 15,
"type_info": "Integer"
},
{
"name": "mc_game_resolution_y",
"ordinal": 17,
"ordinal": 16,
"type_info": "Integer"
},
{
"name": "hide_on_process_start",
"ordinal": 18,
"ordinal": 17,
"type_info": "Integer"
},
{
"name": "hook_pre_launch",
"ordinal": 19,
"ordinal": 18,
"type_info": "Text"
},
{
"name": "hook_wrapper",
"ordinal": 20,
"ordinal": 19,
"type_info": "Text"
},
{
"name": "hook_post_exit",
"ordinal": 21,
"ordinal": 20,
"type_info": "Text"
},
{
"name": "custom_dir",
"ordinal": 22,
"ordinal": 21,
"type_info": "Text"
},
{
"name": "prev_custom_dir",
"ordinal": 23,
"ordinal": 22,
"type_info": "Text"
},
{
"name": "migrated",
"ordinal": 24,
"ordinal": 23,
"type_info": "Integer"
}
],
@@ -144,7 +139,6 @@
false,
false,
false,
false,
null,
null,
false,
@@ -160,5 +154,5 @@
false
]
},
"hash": "8e19c9cdb0aaa48509724e82f6e8f212c9cd2112fdba77cfeee206025af47761"
"hash": "03d1aeddf7788320530c447a82342aecdb4099ce183dd9106c4bcc47604cb080"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus"
version = "0.8.8"
version = "0.8.3-1"
authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2021"
@@ -36,7 +36,7 @@ tracing-error = "0.2.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 }
async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] }

View File

@@ -1 +0,0 @@
ALTER TABLE settings ADD COLUMN personalized_ads INTEGER NOT NULL DEFAULT TRUE;

View File

@@ -68,7 +68,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
pub name: PathBuf,
}
emit_loading(&loading_bar, 0.0, Some("Fetching java version"))?;
emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
let packages = fetch_json::<Vec<Package>>(
Method::GET,
&format!(
@@ -80,7 +80,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
&state.fetch_semaphore,
&state.pool,
).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version"))?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
if let Some(download) = packages.first() {
let file = fetch_advanced(
@@ -115,13 +115,13 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
}
}
emit_loading(&loading_bar, 0.0, Some("Extracting java"))?;
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
archive.extract(&path).map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to extract java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 10.0, Some("Done extracting java"))?;
emit_loading(&loading_bar, 10.0, Some("Done extracting java")).await?;
let mut base_path = path.join(
download
.name

View File

@@ -287,7 +287,7 @@ pub async fn copy_dotminecraft(
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
emit_loading(&loading_bar, 1.0, None)?;
emit_loading(&loading_bar, 1.0, None).await?;
}
Ok(loading_bar)
}

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