Compare commits

...

20 Commits

Author SHA1 Message Date
Geometrically
0221034b60 Fix file paths opening (#2382) 2024-09-10 01:36:56 -07:00
Jai A
9500384100 Update ads.txt 2024-09-10 00:32:14 -07:00
Geometrically
0b31f2eb41 0.8.5 fixes (#2369)
* 0.8.5 fixes

* Attempt to fix binary name

* bump version + add nsis installmode

* (temp) Use cargo version of tauri CLI

* fix cli build

* Fix build env var

* Bump tauri version

* remove old invalid installs

* Fix old shortcuts + NSIS build
2024-09-10 00:24:17 -07:00
Sasha Sorokin
b3a6393c91 Add TypeScript to app-frontend (#2364)
* Add TypeScript to app-frontend

Co-authored-by: Evan Song <52982404+ferothefox@users.noreply.github.com>

* Switch app-frontend to ESLint 9 & Nuxt config

* Fix ESLint issues after config change in app-frontend

---------

Co-authored-by: Evan Song <52982404+ferothefox@users.noreply.github.com>
2024-09-07 23:40:40 -07:00
Jai A
16c5a5a3a6 revert ads changes 2024-09-05 14:56:17 -07:00
Jai A
2e7db502a9 fix empty tag 2024-09-04 09:37:58 -07:00
Jai A
d29b71ec45 Fix app ads styling 2024-09-04 09:37:07 -07:00
Jai A
9cd0af914a fix slot not rendering 2024-09-04 09:31:39 -07:00
Jai A
4a575393f0 remove inmobi 2024-09-04 09:29:06 -07:00
Jai A
76c93c767d rev.iq support 2024-09-04 09:23:18 -07:00
Jai A
e69337a1fc update ads.txt with new dsps 2024-09-03 21:22:24 -07:00
Sasha Sorokin
50734af6cd Fix Game versions input not showing (#2358)
There is a bug in vue-multiselect library where it will default
`custom-label` property to raw properies object. Presumably this is a
bug that appeared because they ported library to Vue 3 and have not
considered that the signature for `default` prop's function has changed.

To work around this, we can provide custom `custom-label` property that
is just a function that returns the input itself. In other places, where
objects are passed, `label` property can be used. In this case the first
suggestion applies. DO NOT USE VUE-MULTISELECT WITOUT `LABEL` OR
`CUSTOM-LABEL` PROPS!
2024-09-03 16:44:21 +00:00
Norbiros
81b0922c93 chore(theseus): Updated Tauri links to v2 (#2345) 2024-09-01 21:57:13 +00:00
Norbiros
d4f8fff7af fix(theseus): Posthog scripts, focus app when opening deeplink, update linux-schema (#2351)
* fix(theseus): Focus app when opening deeplink (#2347)

* fix(theseus): Allow for posthog scripts

* chores(theseus): Update `linux-schema.json`
2024-09-01 21:56:53 +00:00
Geometrically
bd61f5d591 Fix auto updater, add failure message, fix modals (#2335)
* Fix auto updater, add failure message, fix modals

* Fix ads hiding, updater UI

* dummy version, fix gh actions cache

* fix release conf

* actual version bump

* Fix ads hiding sometimes

* Fix event state init

* fix remaining bugs

* Fix lint on linux

* Fix deep linking on Windows

* Fix ad links opening multiple times
2024-08-30 10:42:58 -07:00
Geometrically
016c3d779b Fix localhost windows opening (#2331) 2024-08-28 23:21:35 -07:00
Geometrically
acf26940d6 Implement ads in desktop app (#2318)
* Implement ads in desktop app

* Finish ads

* use git dep instead

* attempt to fix linux build (temp)

* bump version + lint

* comment more

* fix build

* try to fix linux build

* Fix crashing on windows

* Fix icons not showing

* Remove useless env vars

* Actual linux build fix

* Run fmt

* Fix scrolling

* fix clippy

* bump version + fix localhost

* rev linux build patch

* update version num

* update csp

* update csp

* update csp

* Switch to mousewheel event
2024-08-28 21:44:08 -07:00
Norbiros
4bafae881f fix: Correctly fix App Build workflow (#2322)
Signed-off-by: Norbiros <norbiros@protonmail.com>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
2024-08-28 10:18:54 -07:00
Prospector
8311451420 Update public-facing orgs page, componetize page headers (#2307)
* Update public-facing orgs page, componetize page headers

* Improve supported environments

* Move user page stats to top and remove details card

* Fix padding on orgs page when no navlinks

* fix lint

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
2024-08-28 10:12:25 -07:00
Norbiros
4b75cb8357 fix: App Build workflow (#2320)
Signed-off-by: Norbiros <norbiros@protonmail.com>
2024-08-28 14:25:53 +00:00
100 changed files with 3664 additions and 1653 deletions

View File

@@ -41,10 +41,34 @@ jobs:
with:
components: rustfmt, clippy
- name: Install Tauri CLI
run: cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
- name: Setup rust cache
uses: actions/cache@v4
with:
path: target/**
path: |
target/**
!target/*/release/bundle/*/*.dmg
!target/*/release/bundle/*/*.app.tar.gz
!target/*/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.dmg
!target/release/bundle/*/*.app.tar.gz
!target/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.AppImage
!target/release/bundle/*/*.AppImage.tar.gz
!target/release/bundle/*/*.AppImage.tar.gz.sig
!target/release/bundle/*/*.deb
!target/release/bundle/*/*.rpm
!target/release/bundle/msi/*.msi
!target/release/bundle/msi/*.msi.zip
!target/release/bundle/msi/*.msi.zip.sig
!target/release/bundle/nsis/*.exe
!target/release/bundle/nsis/*.nsis.zip
!target/release/bundle/nsis/*.nsis.zip.sig
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-rust-target-
@@ -78,7 +102,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev sqlite3
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
- name: Install frontend dependencies
run: pnpm install
@@ -97,6 +121,7 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
V1_COMPATIBLE_BIN_NAME: true
- name: build app
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
@@ -106,6 +131,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
V1_COMPATIBLE_BIN_NAME: true
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v4
@@ -124,8 +150,11 @@ 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

View File

@@ -32,6 +32,9 @@ jobs:
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Tauri CLI
run: cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
- name: Setup Node.JS environment
uses: actions/setup-node@v4
with:

754
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
import { fixupPluginRules } from '@eslint/compat'
import turboPlugin from 'eslint-plugin-turbo'
export default createConfigForNuxt().append([
{
name: 'turbo',
plugins: {
turbo: fixupPluginRules(turboPlugin),
},
rules: {
'turbo/no-undeclared-env-vars': 'error',
},
},
{
name: 'modrinth',
rules: {
'vue/html-self-closing': 'off',
'vue/multi-word-component-names': 'off',
},
},
])

View File

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

View File

@@ -1,11 +1,12 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.8.3-1",
"version": "0.8.7",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vue-tsc --noEmit && vite build",
"tsc:check": "vue-tsc --noEmit",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write ."
},
@@ -13,36 +14,41 @@
"@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-window-state": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@tauri-apps/plugin-updater": "^2.0.0-rc.0",
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
"@vintl/vintl": "^4.4.1",
"dayjs": "^1.11.10",
"floating-vue": "^5.2.2",
"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",
"posthog-js": "^1.158.2",
"@sentry/vue": "^8.27.0"
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0-rc",
"@eslint/compat": "^1.1.1",
"@nuxt/eslint-config": "^0.5.6",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint": "^9.9.1",
"eslint-config-custom": "workspace:*",
"eslint-plugin-turbo": "^2.1.1",
"postcss": "^8.4.39",
"prettier": "^3.2.5",
"sass": "^1.74.1",
"tailwindcss": "^3.4.4",
"tsconfig": "workspace:*",
"vite": "^5.2.8"
"typescript": "^5.5.4",
"vite": "^5.2.8",
"vue-tsc": "^2.1.6"
},
"packageManager": "pnpm@9.4.0"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ const handleAddContentFromFile = async () => {
if (!newProject) return
for (const project of newProject) {
await add_project_from_path(props.instance.path, project).catch(handleError)
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
}
}

View File

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

View File

@@ -1,6 +1,5 @@
<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'
@@ -9,6 +8,7 @@ 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>
<Modal ref="errorModal" :header="title" :closable="closable">
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
@@ -272,7 +272,7 @@ async function repairInstance() {
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style>

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
<ModalWrapper ref="modal" header="Create instance">
<div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
</div>
@@ -193,10 +193,11 @@
/>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import {
PlusIcon,
UploadIcon,
@@ -207,7 +208,7 @@ import {
FolderSearchIcon,
UpdatedIcon,
} from '@modrinth/assets'
import { Avatar, Button, Chips, Modal, Checkbox } from '@modrinth/ui'
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui'
import { computed, onUnmounted, ref, shallowRef } from 'vue'
import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile'
@@ -217,7 +218,6 @@ 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,8 +227,6 @@ 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')
@@ -371,7 +369,7 @@ const create_instance = async () => {
}
const upload_icon = async () => {
icon.value = await open({
const res = await open({
multiple: false,
filters: [
{
@@ -381,6 +379,8 @@ 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).catch(handleError)
await install_from_file(newProject.path ?? 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 (error) {
} catch {
// Allow failure silently
}
})

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Promotion } from '@modrinth/ui'
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true)
defineExpose({
scroll() {
updateAdPosition()
},
})
const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) {
const user = await get_user(creds.user_id).catch(handleError)
@@ -16,8 +24,98 @@ 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)
}
}
const unlisten = await listen('ads-scroll', (event) => {
if (adsWrapper.value) {
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
updateAdPosition()
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
if (intersectionObserver) {
intersectionObserver.disconnect()
}
if (mutationObserver) {
mutationObserver.disconnect()
}
if (scrollHandler) {
window.removeEventListener('scroll', scrollHandler)
}
unlisten()
})
</script>
<template>
<Promotion v-if="showAd" :external="false" query-param="?r=launcher" />
<div
v-if="showAd"
ref="adsWrapper"
class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised cursor-pointer"
>
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">90% of ad revenue goes to creators</p>
<a
href="https://modrinth.com/plus"
class="mt-auto items-center gap-1 text-purple hover:underline"
>
<span>
Support creators and Modrinth ad-free with
<span class="font-bold">Modrinth+</span>
</span>
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
</a>
</div>
</div>
</template>

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import {
RightArrowIcon,
CheckIcon,
} from '@modrinth/assets'
import { Avatar, Modal, Button, Card } from '@modrinth/ui'
import { Avatar, Button, Card } from '@modrinth/ui'
import { computed, ref } from 'vue'
import {
add_project_from_version as installMod,
@@ -19,12 +19,11 @@ 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()
@@ -49,7 +48,7 @@ const shownProfiles = computed(() =>
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
})
.filter((profile) => {
let loaders = versions.value.flatMap((v) => v.loaders)
const loaders = versions.value.flatMap((v) => v.loaders)
return (
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
@@ -60,7 +59,7 @@ const shownProfiles = computed(() =>
}),
)
let onInstall = ref(() => {})
const onInstall = ref(() => {})
defineExpose({
show: async (projectVal, versionsVal, callback) => {
@@ -78,7 +77,7 @@ defineExpose({
onInstall.value = callback
const profilesVal = await list().catch(handleError)
for (let profile of profilesVal) {
for (const profile of profilesVal) {
profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
handleError,
@@ -142,7 +141,7 @@ const toggleCreation = () => {
}
const upload_icon = async () => {
icon.value = await open({
const res = await open({
multiple: false,
filters: [
{
@@ -151,6 +150,7 @@ const upload_icon = async () => {
},
],
})
icon.value = res.path ?? res
if (!icon.value) return
display_icon.value = convertFileSrc(icon.value)
@@ -213,12 +213,7 @@ const createInstance = async () => {
</script>
<template>
<Modal
ref="installModal"
header="Install project to instance"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
<div class="modal-body">
<input
v-model="searchFilter"
@@ -304,7 +299,7 @@ const createInstance = async () => {
<Button @click="installModal.hide()">Cancel</Button>
</div>
</div>
</Modal>
</ModalWrapper>
</template>
<style scoped lang="scss">

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ConfirmModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
confirmationText: {
type: String,
default: '',
},
hasToType: {
type: Boolean,
default: false,
},
title: {
type: String,
default: 'No title defined',
required: true,
},
description: {
type: String,
default: 'No description defined',
required: true,
},
proceedLabel: {
type: String,
default: 'Proceed',
},
})
const emit = defineEmits(['proceed'])
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
function proceed() {
emit('proceed')
}
</script>
<template>
<ConfirmModal
ref="modal"
:confirmation-text="confirmationText"
:has-to-type="hasToType"
:title="title"
:description="description"
:proceed-label="proceedLabel"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
@proceed="proceed"
/>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Modal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const props = defineProps({
header: {
type: String,
default: null,
},
closable: {
type: Boolean,
default: true,
},
onHide: {
type: Function,
default() {
return () => {}
},
},
})
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
props.onHide()
}
</script>
<template>
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
<slot />
</Modal>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ShareModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
header: {
type: String,
default: 'Share',
},
shareTitle: {
type: String,
default: 'Modrinth',
},
shareText: {
type: String,
default: null,
},
link: {
type: Boolean,
default: false,
},
openInNewTab: {
type: Boolean,
default: true,
},
})
const modal = ref(null)
defineExpose({
show: (passedContent) => {
hide_ads_window()
modal.value.show(passedContent)
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
</script>
<template>
<ShareModal
ref="modal"
:header="header"
:share-title="shareTitle"
:share-text="shareText"
:link="link"
:open-in-new-tab="openInNewTab"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
/>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui'
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings'
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
@@ -13,6 +13,12 @@ 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']
@@ -169,13 +175,12 @@ async function purgeCache() {
Sign in
</button>
</div>
<ConfirmModal
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false"
proceed-label="Purge cache"
:noblur="!themeStore.advancedRendering"
@proceed="purgeCache"
/>
<div class="adjacent-input">
@@ -401,14 +406,14 @@ async function purgeCache() {
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<template v-for="version in [21, 17, 8]">
<label :for="'java-' + version">
<span class="label__title">Java {{ version }} location</span>
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
<label :for="'java-' + javaVersion">
<span class="label__title">Java {{ javaVersion }} location</span>
</label>
<JavaSelector
:id="'java-selector-' + version"
v-model="javaVersions[version]"
:version="version"
:id="'java-selector-' + javaVersion"
v-model="javaVersions[javaVersion]"
:version="javaVersion"
@update:model-value="updateJavaVersion"
/>
</template>

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,13 @@
<template>
<ConfirmModal
<ConfirmModalWrapper
ref="modal_confirm"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:noblur="!themeStore.advancedRendering"
@proceed="removeProfile"
/>
<Modal
ref="modalConfirmUnlock"
header="Are you sure you want to unlock this instance?"
:noblur="!themeStore.advancedRendering"
>
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?">
<div class="modal-delete">
<div
class="markdown-body"
@@ -31,13 +26,9 @@
</button>
</div>
</div>
</Modal>
</ModalWrapper>
<Modal
ref="modalConfirmUnpair"
header="Are you sure you want to unpair this instance?"
:noblur="!themeStore.advancedRendering"
>
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?">
<div class="modal-delete">
<div
class="markdown-body"
@@ -56,13 +47,9 @@
</button>
</div>
</div>
</Modal>
</ModalWrapper>
<Modal
ref="changeVersionsModal"
header="Change instance versions"
:noblur="!themeStore.advancedRendering"
>
<ModalWrapper ref="changeVersionsModal" header="Change instance versions">
<div class="change-versions-modal universal-body">
<div class="input-row">
<p class="input-label">Loader</p>
@@ -106,7 +93,7 @@
</button>
</div>
</div>
</Modal>
</ModalWrapper>
<section class="card">
<div class="label">
<h3>
@@ -511,18 +498,7 @@ import {
DownloadIcon,
ClipboardCopyIcon,
} from '@modrinth/assets'
import {
Button,
Toggle,
ConfirmModal,
Card,
Slider,
Checkbox,
Avatar,
Modal,
Chips,
DropdownSelect,
} from '@modrinth/ui'
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui'
import { SwapIcon } from '@/assets/icons'
import { Multiselect } from 'vue-multiselect'
@@ -546,10 +522,11 @@ 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()
@@ -570,8 +547,6 @@ const props = defineProps({
},
})
const themeStore = useTheming()
const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
@@ -606,7 +581,7 @@ async function setIcon() {
if (!value) return
icon.value = value
icon.value = value.path ?? value
await edit_icon(props.instance.path, icon.value).catch(handleError)
trackEvent('InstanceSetIcon')
@@ -621,12 +596,12 @@ const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
)
const overrideEnvVars = ref(!!props.instance.custom_env_vars)
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
const envVars = ref(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
@@ -710,19 +685,15 @@ const editProfileObject = computed(() => {
}
if (overrideJavaArgs.value) {
if (javaArgs.value !== '') {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
if (overrideEnvVars.value) {
if (envVars.value !== '') {
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
if (overrideMemorySettings.value) {
@@ -905,7 +876,7 @@ const editing = ref(false)
async function saveGvLoaderEdits() {
editing.value = true
let editProfile = editProfileObject.value
const editProfile = editProfileObject.value
editProfile.loader = loader.value
editProfile.game_version = gameVersion.value

View File

@@ -20,14 +20,14 @@
</span>
</Card>
</div>
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="expandedGalleryItem = null">
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage">
<div class="content">
<img
class="image"
:class="{ 'zoomed-in': zoomedIn }"
:src="
expandedGalleryItem.url
? expandedGalleryItem.url
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@@ -45,15 +45,15 @@
</div>
<div class="controls">
<div class="buttons">
<Button class="close" icon-only @click="expandedGalleryItem = null">
<Button class="close" icon-only @click="hideImage">
<XIcon aria-hidden="true" />
</Button>
<a
class="open btn icon-only"
target="_blank"
:href="
expandedGalleryItem.url
? expandedGalleryItem.url
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
>
@@ -94,6 +94,7 @@ 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: {
@@ -102,9 +103,14 @@ const props = defineProps({
},
})
let expandedGalleryItem = ref(null)
let expandedGalleryIndex = ref(0)
let zoomedIn = ref(false)
const expandedGalleryItem = ref(null)
const expandedGalleryIndex = ref(0)
const zoomedIn = ref(false)
const hideImage = () => {
expandedGalleryItem.value = null
show_ads_window()
}
const nextImage = () => {
expandedGalleryIndex.value++
@@ -131,6 +137,7 @@ const previousImage = () => {
}
const expandImage = (item, index) => {
hide_ads_window()
expandedGalleryItem.value = item
expandedGalleryIndex.value = index
zoomedIn.value = false
@@ -140,6 +147,20 @@ 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">
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()">
<Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar
@@ -20,7 +20,7 @@
</router-link>
</Card>
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="lg" :src="data.icon_url" />
<Avatar size="md" :src="data.icon_url" />
<div class="instance-info">
<h2 class="name">{{ data.title }}</h2>
{{ data.description }}
@@ -61,7 +61,9 @@
Site
</a>
</div>
<hr class="card-divider" />
</Card>
<PromotionWrapper ref="promo" />
<Card class="sidebar-card">
<div class="stats">
<div class="stat">
<DownloadIcon aria-hidden="true" />
@@ -163,7 +165,6 @@
</Card>
</div>
<div v-if="data" class="content-container">
<PromotionWrapper />
<Card class="tabs">
<NavRow
v-if="data.gallery.length > 0"
@@ -231,15 +232,7 @@ import {
GlobeIcon,
ClipboardCopyIcon,
} from '@modrinth/assets'
import {
Categories,
EnvironmentIndicator,
Card,
Avatar,
Button,
Promotion,
NavRow,
} from '@modrinth/ui'
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui'
import { formatNumber } from '@modrinth/utils'
import {
BuyMeACoffeeIcon,
@@ -260,7 +253,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_project_many, get_team, get_version_many } from '@/helpers/cache.js'
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
dayjs.extend(relativeTime)
@@ -309,11 +302,13 @@ async function fetchProjectData() {
await fetchProjectData()
const promo = ref(null)
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/project')) {
await fetchProjectData()
promo.value.scroll()
}
},
)
@@ -377,7 +372,7 @@ const handleOptionsClick = (args) => {
.project-sidebar {
position: fixed;
width: 20rem;
width: calc(300px + 1.5rem);
min-height: calc(100vh - 3.25rem);
height: fit-content;
max-height: calc(100vh - 3.25rem);
@@ -403,7 +398,7 @@ const handleOptionsClick = (args) => {
flex-direction: column;
width: 100%;
padding: 1rem;
margin-left: 19.5rem;
margin-left: calc(300px + 1rem);
}
.button-group {

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowJs": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"strict": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

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

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

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

View File

@@ -10,7 +10,6 @@ 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.3-1"
version = "0.8.7"
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/"
@@ -8,7 +8,7 @@ edition = "2021"
build = "build.rs"
[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = ["codegen"] }
tauri-build = { git = "https://github.com/modrinth/tauri", features = ["codegen"], rev = "5e29428" }
[dependencies]
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
@@ -16,13 +16,14 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-rc.6", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
tauri = { git = "https://github.com/modrinth/tauri", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"], rev = "5e29428" }
tauri-plugin-window-state = "2.0.0-rc"
tauri-plugin-deep-link = "2.0.0-rc"
tauri-plugin-os = "2.0.0-rc"
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-dialog = "2.0.0-rc"
tauri-plugin-updater = { version = "2.0.0-rc.1", optional = true }
tauri-plugin-updater = { version = "2.0.0-rc" }
tauri-plugin-single-instance = { version = "2.0.0-rc" }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
@@ -57,6 +58,9 @@ 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
@@ -64,4 +68,4 @@ default = ["custom-protocol"]
# this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
updater = ["dep:tauri-plugin-updater"]
updater = []

View File

@@ -15,13 +15,14 @@ Before you begin, ensure you have the following installed on your machine:
- [Node.js](https://nodejs.org/en/)
- [pnpm](https://pnpm.io/)
- [Rust](https://www.rust-lang.org/tools/install)
- [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/#installing)
- [Tauri](https://v2.tauri.app/start/prerequisites/)
### Setup
Follow these steps to set up your development environment:
```bash
cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
pnpm install
pnpm app:dev
```

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"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":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["shell:allow-open","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"]}}

View File

@@ -299,6 +299,69 @@
},
"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",
@@ -2785,10 +2848,10 @@
]
},
{
"description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.",
"type": "string",
"enum": [
"jre:allow-set-java-versions"
"jre:allow-set-java-version"
]
},
{
@@ -2834,10 +2897,10 @@
]
},
{
"description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.",
"type": "string",
"enum": [
"jre:deny-set-java-versions"
"jre:deny-set-java-version"
]
},
{
@@ -3855,6 +3918,69 @@
"tags:deny-tags-get-report-types"
]
},
{
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string",
"enum": [
"updater:default"
]
},
{
"description": "updater:allow-check -> Enables the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-check"
]
},
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download-and-install"
]
},
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-check"
]
},
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download-and-install"
]
},
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{
"description": "utils:default -> Default plugin permissions.",
"type": "string",

View File

@@ -299,6 +299,69 @@
},
"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",
@@ -2785,10 +2848,10 @@
]
},
{
"description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.",
"type": "string",
"enum": [
"jre:allow-set-java-versions"
"jre:allow-set-java-version"
]
},
{
@@ -2834,10 +2897,10 @@
]
},
{
"description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.",
"type": "string",
"enum": [
"jre:deny-set-java-versions"
"jre:deny-set-java-version"
]
},
{

View File

@@ -299,6 +299,69 @@
},
"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",
@@ -2785,10 +2848,10 @@
]
},
{
"description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.",
"type": "string",
"enum": [
"jre:allow-set-java-versions"
"jre:allow-set-java-version"
]
},
{
@@ -2834,10 +2897,10 @@
]
},
{
"description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.",
"type": "string",
"enum": [
"jre:deny-set-java-versions"
"jre:deny-set-java-version"
]
},
{
@@ -3855,6 +3918,69 @@
"tags:deny-tags-get-report-types"
]
},
{
"description": "updater:default -> This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n",
"type": "string",
"enum": [
"updater:default"
]
},
{
"description": "updater:allow-check -> Enables the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-check"
]
},
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download-and-install"
]
},
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-check"
]
},
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download-and-install"
]
},
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{
"description": "utils:default -> Default plugin permissions.",
"type": "string",

41
apps/app/nsis/hooks.nsi Normal file
View File

@@ -0,0 +1,41 @@
!macro NSIS_HOOK_POSTINSTALL
SetShellVarContext current
IfFileExists "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe" file_found file_not_found
file_found:
Delete "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Delete "$LOCALAPPDATA${PRODUCTNAME}\uninstall.exe"
RMDir "$LOCALAPPDATA${PRODUCTNAME}"
!insertmacro DeleteAppUserModelId
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
!insertmacro IsShortcutTarget "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
RMDir "$SMPROGRAMS$AppStartMenuFolder"
${EndIf}
!insertmacro IsShortcutTarget "$SMPROGRAMS${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS${PRODUCTNAME}.lnk"
Delete "$SMPROGRAMS${PRODUCTNAME}.lnk"
${EndIf}
!insertmacro IsShortcutTarget "$DESKTOP${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$DESKTOP${PRODUCTNAME}.lnk"
Delete "$DESKTOP${PRODUCTNAME}.lnk"
${EndIf}
DeleteRegKey HKCU "${UNINSTKEY}"
goto end_of_test ;<== important for not continuing on the else branch
file_not_found:
end_of_test:
!macroend

View File

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

View File

@@ -0,0 +1,20 @@
if (!window.modrinthClickListener) {
window.modrinthClickListener = true
document.addEventListener('click', function (e) {
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
}
})
}
window.open = (url, target, features) => {
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
}

161
apps/app/src/api/ads.rs Normal file
View File

@@ -0,0 +1,161 @@
use serde::Serialize;
use tauri::plugin::TauriPlugin;
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub size: Option<LogicalSize<f32>>,
pub position: Option<LogicalPosition<f32>>,
}
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,
}));
// 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(
"https://modrinth.com/wrapper/app-ads-cookie"
.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,
])
.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(
"https://modrinth.com/wrapper/app-ads-cookie".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(())
}

View File

@@ -16,6 +16,7 @@ pub mod settings;
pub mod tags;
pub mod utils;
pub mod ads;
pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
@@ -39,6 +40,10 @@ 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
@@ -86,7 +91,15 @@ 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,11 +26,73 @@ extern crate objc;
#[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app.clone()).await?;
State::init().await?;
#[cfg(feature = "updater")]
{
use tauri_plugin_updater::UpdaterExt;
let updater = app.updater_builder().build()?;
let update_fut = updater.check();
State::init().await?;
let check_bar = theseus::init_loading(
theseus::LoadingBarType::CheckingForUpdates,
1.0,
"Checking for updates...",
)
.await?;
let update = update_fut.await;
drop(check_bar);
if let Some(update) = update.ok().flatten() {
tracing::info!("Update found: {:?}", update.download_url);
let loader_bar_id = theseus::init_loading(
theseus::LoadingBarType::LauncherUpdate {
version: update.version.clone(),
current_version: update.current_version.clone(),
},
1.0,
"Updating Modrinth App...",
)
.await?;
// 100 MiB
const DEFAULT_CONTENT_LENGTH: u64 = 1024 * 1024 * 100;
update
.download_and_install(
|chunk_length, content_length| {
let _ = theseus::emit_loading(
&loader_bar_id,
(chunk_length as f64)
/ (content_length
.unwrap_or(DEFAULT_CONTENT_LENGTH)
as f64),
None,
);
},
|| {},
)
.await?;
app.restart();
}
}
#[cfg(not(feature = "updater"))]
{
State::init().await?;
}
let state = State::get().await?;
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(())
}
@@ -39,7 +101,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
#[tracing::instrument(skip_all)]
#[tauri::command]
fn show_window(app: tauri::AppHandle) {
let win = app.get_webview_window("main").unwrap();
let win = app.get_window("main").unwrap();
if let Err(e) = win.show() {
MessageDialog::new()
.set_type(MessageType::Error)
@@ -73,6 +135,11 @@ 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() {
@@ -102,6 +169,19 @@ 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())
@@ -179,12 +259,14 @@ 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,10 +5,15 @@
"build": {
"features": ["updater"]
},
"app": {
"security": {
"capabilities": ["ads", "core", "plugins", "updater"]
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
"endpoints": ["https://launcher-files.modrinth.com/updates.json"]
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -430,220 +430,208 @@
}"
>
<div class="normal-page__header relative my-4">
<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">
<ContentPageHeader>
<template #icon>
<Avatar :src="project.icon_url" :alt="project.title" size="96px" />
<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">
</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 items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
v-for="(category, index) in project.categories"
:key="index"
class="tag-list__item"
>
<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>
{{ formatCategory(category) }}
</div>
</div>
</div>
</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>
</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">
<ButtonStyled
size="large"
circular
:color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
>
<button
v-if="auth.user"
v-tooltip="following ? `Unfollow` : `Follow`"
:aria-label="following ? `Unfollow` : `Follow`"
@click="userFollowProject(project)"
aria-label="Download"
class="flex sm:hidden"
@click="(event) => downloadModal.show(event)"
>
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
<DownloadIcon 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>
</div>
</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>
<ProjectMemberHeader
v-if="currentMember"
:project="project"
@@ -701,13 +689,13 @@
"
>
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="status-list">
<div class="tag-list">
<div
v-if="
(project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="status-list__item"
class="tag-list__item"
>
<ClientIcon aria-hidden="true" />
Client-side
@@ -717,34 +705,29 @@
(project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="status-list__item"
class="tag-list__item"
>
<ServerIcon aria-hidden="true" />
Server-side
</div>
<div v-if="false" class="status-list__item">
<div v-if="false" class="tag-list__item">
<UserIcon aria-hidden="true" />
Singleplayer
</div>
<div
v-if="project.client_side === 'required' && project.server_side === 'required'"
class="status-list__item"
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"
>
<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>
@@ -1044,6 +1027,7 @@ import {
OverflowMenu,
PopoutMenu,
ScrollablePanel,
ContentPageHeader,
} from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs";
@@ -1734,10 +1718,6 @@ 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.url
? expandedGalleryItem.url
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_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.url
? expandedGalleryItem.url
expandedGalleryItem.raw_url
? expandedGalleryItem.raw_url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
>

View File

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

View File

@@ -1,240 +1,260 @@
<template>
<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
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 class="universal-card">
<div class="page-header__icon">
<Avatar size="md" :src="organization.icon_url" />
</div>
<div class="page-header__text">
<h1 class="title">{{ organization.name }}</h1>
<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>
<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>
</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="creator-list universal-card">
<div class="title-and-link">
<h3>Members</h3>
<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>
<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>
<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 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>
</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 v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
<NavTabs :links="navLinks" />
</div>
</template>
<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 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>
</div>
<NuxtPage />
</template>
</div>
</template>
<script setup>
import {
BoxIcon,
UserIcon,
MoreVerticalIcon,
UsersIcon,
SettingsIcon,
ChartIcon,
CheckIcon,
XIcon,
ClipboardCopyIcon,
} from "@modrinth/assets";
import { Avatar, Breadcrumbs } from "@modrinth/ui";
import { Avatar, ButtonStyled, Breadcrumbs, ContentPageHeader, OverflowMenu } 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";
@@ -244,6 +264,7 @@ 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;
@@ -451,6 +472,26 @@ 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

@@ -3,75 +3,88 @@
<ModalCreation ref="modal_creation" />
<CollectionCreateModal ref="modal_collection_creation" />
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<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">
<div class="normal-page__header py-4">
<ContentPageHeader>
<template #icon>
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
<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>
</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>
</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
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>
</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 class="normal-page__content">
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
@@ -194,72 +207,6 @@
</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">
@@ -288,6 +235,9 @@
</div>
</div>
</div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
</div>
</div>
</div>
@@ -304,7 +254,7 @@ import {
ClipboardCopyIcon,
MoreVerticalIcon,
} from "@modrinth/assets";
import { OverflowMenu, ButtonStyled } from "@modrinth/ui";
import { OverflowMenu, ButtonStyled, ContentPageHeader } from "@modrinth/ui";
import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import { reportUser } from "~/utils/report-helpers.ts";
@@ -318,7 +268,6 @@ 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";
@@ -505,15 +454,6 @@ 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 = [];
@@ -653,8 +593,4 @@ export default defineNuxtComponent({
}
}
}
.normal-page__header {
grid-area: header;
}
</style>

View File

@@ -472,3 +472,5 @@ visiblemeasures.com, 1041, DIRECT #yieldmo
iqzone.com,IQ190,RESELLER
krushmedia.com, AJxF6R537a9M6CaTvK, RESELLER
consumable.com, 2001460, DIRECT, aefcd3d2f45b5070
google.com, pub-6729046591418183, RESELLER, f08c47fec0942fa0

View File

@@ -0,0 +1,90 @@
<!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;
bottom: 0;
z-index: 2;
}
</style>
</head>
<body>
<div class="ads-container">
<a id="plus-link" href="https://modrinth.com/plus" target="_blank"></a>
<div id="modrinth-rail-1" />
</div>
<script>
window.tude = window.tude || { cmd: [] };
tude.cmd.push(function () {
tude.refreshAdsViaDivMappings([
{
divId: "modrinth-rail-1",
baseDivId: "pb-slot-square-2",
},
]);
});
let lastUrl = null
window.addEventListener(
"message",
(event) => {
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__ && lastUrl !== event.data.modrinthOpenUrl) {
lastUrl = event.data.modrinthOpenUrl
window.__TAURI_INTERNALS__.invoke("plugin:shell|open", {
path: event.data.modrinthOpenUrl,
});
setTimeout(() => {
lastUrl = null
}, 500)
}
},
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());
</script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus"
version = "0.8.3-1"
version = "0.8.7"
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.4", optional = true }
tauri = { git = "https://github.com/modrinth/tauri", rev = "5e29428", optional = true }
indicatif = { version = "0.17.3", optional = true }
async-tungstenite = { version = "0.27.0", features = ["tokio-runtime", "tokio-rustls-webpki-roots"] }

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")).await?;
emit_loading(&loading_bar, 0.0, Some("Fetching java version"))?;
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")).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version"))?;
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")).await?;
emit_loading(&loading_bar, 0.0, Some("Extracting java"))?;
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")).await?;
emit_loading(&loading_bar, 10.0, Some("Done extracting java"))?;
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).await?;
emit_loading(&loading_bar, 1.0, None)?;
}
Ok(loading_bar)
}

View File

@@ -191,7 +191,7 @@ pub async fn generate_pack_from_version_id(
let state = State::get().await?;
let loading_bar = if let Some(bar) = initialized_loading_bar {
emit_loading(&bar, 0.0, Some("Downloading pack file")).await?;
emit_loading(&bar, 0.0, Some("Downloading pack file"))?;
bar
} else {
init_loading(
@@ -207,7 +207,7 @@ pub async fn generate_pack_from_version_id(
.await?
};
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
emit_loading(&loading_bar, 0.0, Some("Fetching version"))?;
let version = CachedEntry::get_version(
&version_id,
None,
@@ -220,7 +220,7 @@ pub async fn generate_pack_from_version_id(
"Invalid version ID specified!".to_string(),
)
})?;
emit_loading(&loading_bar, 10.0, None).await?;
emit_loading(&loading_bar, 10.0, None)?;
let (url, hash) =
if let Some(file) = version.files.iter().find(|x| x.primary) {
@@ -248,7 +248,7 @@ pub async fn generate_pack_from_version_id(
&state.pool,
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata"))?;
let project = CachedEntry::get_project(
&version.project_id,
@@ -263,7 +263,7 @@ pub async fn generate_pack_from_version_id(
)
})?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes =
@@ -287,7 +287,7 @@ pub async fn generate_pack_from_version_id(
} else {
None
};
emit_loading(&loading_bar, 10.0, None).await?;
emit_loading(&loading_bar, 10.0, None)?;
Ok(CreatePack {
file,

View File

@@ -203,7 +203,7 @@ pub async fn install_zipped_mrpack_files(
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Extracting overrides")).await?;
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))?;
let mut total_len = 0;
@@ -270,8 +270,7 @@ pub async fn install_zipped_mrpack_files(
"Extracting override {}/{}",
index, total_len
)),
)
.await?;
)?;
}
}

View File

@@ -40,15 +40,10 @@ pub mod update;
#[tracing::instrument]
pub async fn remove(path: &str) -> crate::Result<()> {
let state = State::get().await?;
let mut transaction = state.pool.begin().await?;
Profile::remove(path, &mut transaction).await?;
Profile::remove(path, &state.pool).await?;
emit_profile(path, ProfilePayloadType::Removed).await?;
transaction.commit().await?;
Ok(())
}
@@ -510,7 +505,7 @@ pub async fn export_mrpack(
// Iterate over every file in the folder
// Every file that is NOT in the config file is added to the zip, in overrides
for path in path_list {
emit_loading(&loading_bar, 1.0, None).await?;
emit_loading(&loading_bar, 1.0, None)?;
let relative_path = pack_get_relative_path(&profile_base_path, &path)?;

View File

@@ -9,7 +9,7 @@ use crate::event::{
};
use futures::prelude::*;
#[cfg(feature = "tauri")]
use tauri::Emitter;
use tauri::{Emitter, Manager};
use uuid::Uuid;
#[cfg(feature = "cli")]
@@ -33,7 +33,7 @@ const CLI_PROGRESS_BAR_TOTAL: u64 = 1000;
pub async fn loading_function() -> crate::Result<()> {
let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Loading something long...").await;
for i in 0..100 {
emit_loading(&loading_bar, 1.0, None).await?;
emit_loading(&loading_bar, 1.0, None)?;
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
@@ -62,7 +62,7 @@ pub async fn init_loading_unsafe(
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
let key = LoadingBarId(Uuid::new_v4());
event_state.loading_bars.insert(
@@ -91,7 +91,7 @@ pub async fn init_loading_unsafe(
},
);
// attempt an initial loading_emit event to the frontend
emit_loading(&key, 0.0, None).await?;
emit_loading(&key, 0.0, None)?;
Ok(key)
}
@@ -118,7 +118,7 @@ pub async fn edit_loading(
total: f64,
title: &str,
) -> crate::Result<()> {
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
if let Some(mut bar) = event_state.loading_bars.get_mut(&id.0) {
bar.bar_type = bar_type;
@@ -132,7 +132,7 @@ pub async fn edit_loading(
}
};
emit_loading(id, 0.0, None).await?;
emit_loading(id, 0.0, None)?;
Ok(())
}
@@ -144,12 +144,12 @@ pub async fn edit_loading(
#[allow(unused_variables)]
#[tracing::instrument(level = "debug")]
pub async fn emit_loading(
pub fn emit_loading(
key: &LoadingBarId,
increment_frac: f64,
message: Option<&str>,
) -> crate::Result<()> {
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
let mut loading_bar = match event_state.loading_bars.get_mut(&key.0) {
Some(f) => f,
@@ -211,7 +211,7 @@ pub async fn emit_loading(
pub async fn emit_warning(message: &str) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
@@ -228,18 +228,22 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> {
// emit_command(CommandPayload::Something { something })
// ie: installing a pack, opening an .mrpack, etc
// Generally used for url deep links and file opens that we we want to handle in the frontend
// Generally used for url deep links and file opens that we want to handle in the frontend
#[allow(dead_code)]
#[allow(unused_variables)]
pub async fn emit_command(command: CommandPayload) -> crate::Result<()> {
tracing::debug!("Command: {}", serde_json::to_string(&command)?);
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
event_state
.app
.emit("command", command)
.map_err(EventError::from)?;
if let Some(window) = event_state.app.get_window("main") {
let _ = window.set_focus();
}
}
Ok(())
}
@@ -254,7 +258,7 @@ pub async fn emit_process(
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
@@ -279,7 +283,7 @@ pub async fn emit_profile(
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
@@ -329,7 +333,7 @@ macro_rules! loading_join {
async move {
let res = $task.await;
if let Some(key) = key {
$crate::event::emit::emit_loading(key, increment, message).await?;
$crate::event::emit::emit_loading(key, increment, message)?;
}
res
}
@@ -376,8 +380,7 @@ where
async move {
f.await?;
if let Some(key) = key {
emit_loading(key, total / (num_futs as f64), message)
.await?;
emit_loading(key, total / (num_futs as f64), message)?;
}
Ok(())
}

View File

@@ -45,21 +45,14 @@ impl EventState {
.cloned()
}
#[cfg(feature = "tauri")]
pub async fn get() -> crate::Result<Arc<Self>> {
pub fn get() -> crate::Result<Arc<Self>> {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
}
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
}
// Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date
pub async fn list_progress_bars() -> crate::Result<DashMap<Uuid, LoadingBar>>
{
let value = Self::get().await?;
let value = Self::get()?;
Ok(value.loading_bars.clone())
}
@@ -67,7 +60,7 @@ impl EventState {
pub async fn get_main_window() -> crate::Result<Option<tauri::WebviewWindow>>
{
use tauri::Manager;
let value = Self::get().await?;
let value = Self::get()?;
Ok(value.app.get_webview_window("main"))
}
}
@@ -95,7 +88,7 @@ impl Drop for LoadingBarId {
fn drop(&mut self) {
let loader_uuid = self.0;
tokio::spawn(async move {
if let Ok(event_state) = EventState::get().await {
if let Ok(event_state) = EventState::get() {
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some((_, bar)) =
event_state.loading_bars.remove(&loader_uuid)
@@ -180,6 +173,11 @@ pub enum LoadingBarType {
import_location: PathBuf,
profile_name: String,
},
CheckingForUpdates,
LauncherUpdate {
version: String,
current_version: String,
},
}
#[derive(Serialize, Clone)]

View File

@@ -111,7 +111,7 @@ pub async fn download_version_info(
}?;
if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 5.0, None).await?;
emit_loading(loading_bar, 5.0, None)?;
}
tracing::debug!("Loaded version info for Minecraft {version_id}");
@@ -154,7 +154,7 @@ pub async fn download_client(
tracing::trace!("Fetched client version {version}");
}
if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 9.0, None).await?;
emit_loading(loading_bar, 9.0, None)?;
}
tracing::debug!("Client loaded for version {version}!");
@@ -196,7 +196,7 @@ pub async fn download_assets_index(
}?;
if let Some(loading_bar) = loading_bar {
emit_loading(loading_bar, 5.0, None).await?;
emit_loading(loading_bar, 5.0, None)?;
}
tracing::debug!("Assets index successfully loaded!");
Ok(res)

View File

@@ -336,8 +336,7 @@ pub async fn install_minecraft(
server => "";
}
emit_loading(&loading_bar, 0.0, Some("Running forge processors"))
.await?;
emit_loading(&loading_bar, 0.0, Some("Running forge processors"))?;
let total_length = processors.len();
// Forge processors (90-100)
@@ -402,8 +401,7 @@ pub async fn install_minecraft(
"Running forge processor {}/{}",
index, total_length
)),
)
.await?;
)?;
}
}
}
@@ -414,7 +412,7 @@ pub async fn install_minecraft(
async { Ok(()) }
})
.await?;
emit_loading(&loading_bar, 1.0, Some("Finished installing")).await?;
emit_loading(&loading_bar, 1.0, Some("Finished installing"))?;
Ok(())
}

View File

@@ -20,6 +20,9 @@ mod state;
pub use api::*;
pub use error::*;
pub use event::{EventState, LoadingBar, LoadingBarType};
pub use event::{
emit::emit_loading, emit::init_loading, EventState, LoadingBar,
LoadingBarType,
};
pub use logger::start_logger;
pub use state::State;

View File

@@ -271,6 +271,7 @@ pub struct License {
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GalleryItem {
pub url: String,
pub raw_url: String,
pub featured: bool,
pub title: Option<String>,
pub description: Option<String>,
@@ -703,6 +704,16 @@ impl CachedEntry {
.await?;
for row in query {
let row_exists = row.data.is_some();
let parsed_data = row
.data
.and_then(|x| serde_json::from_value::<CacheValue>(x).ok());
// If data is corrupted/failed to parse ignore it
if row_exists && parsed_data.is_none() {
continue;
}
if row.expires <= Utc::now().timestamp() {
if cache_behaviour == CacheBehaviour::MustRevalidate {
continue;
@@ -727,10 +738,7 @@ impl CachedEntry {
.unwrap_or(false)
});
if let Some(data) = row
.data
.and_then(|x| serde_json::from_value::<CacheValue>(x).ok())
{
if let Some(data) = parsed_data {
return_vals.push(Self {
id: row.id,
alias: row.alias,

View File

@@ -330,8 +330,7 @@ impl DirectoryInfo {
&loader_bar_id,
10.0 / (MOVE_DIRS.len() as f64),
None,
)
.await?;
)?;
}
let paths_len = paths.len();
@@ -380,8 +379,7 @@ impl DirectoryInfo {
&loader_bar_id,
90.0 / paths_len as f64,
None,
)
.await;
);
success_idxs.insert(idx);
@@ -433,8 +431,7 @@ impl DirectoryInfo {
&loader_bar_id,
((x.size as f64) / (total_size as f64)) * 60.0,
None,
)
.await;
);
Ok::<(), crate::Error>(())
}
@@ -453,8 +450,7 @@ impl DirectoryInfo {
&loader_bar_id,
30.0 / paths_len as f64,
None,
)
.await?;
)?;
Ok::<(), crate::Error>(())
};

View File

@@ -119,7 +119,7 @@ pub(crate) async fn watch_profile(
{
let path = profile_path.join(folder);
if !path.exists() {
if !path.exists() && !path.is_symlink() {
crate::util::io::create_dir_all(&path).await?;
}

View File

@@ -501,13 +501,8 @@ impl Profile {
pub async fn remove(
profile_path: &str,
transaction: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
pool: &SqlitePool,
) -> crate::Result<()> {
if let Ok(path) = crate::api::profile::get_full_path(profile_path).await
{
io::remove_dir_all(&path).await?;
}
sqlx::query!(
"
DELETE FROM profiles
@@ -515,9 +510,14 @@ impl Profile {
",
profile_path
)
.execute(&mut **transaction)
.execute(pool)
.await?;
if let Ok(path) = crate::api::profile::get_full_path(profile_path).await
{
io::remove_dir_all(&path).await?;
}
Ok(())
}

View File

@@ -130,8 +130,7 @@ pub async fn fetch_advanced(
(chunk.len() as f64 / total_size as f64)
* total,
None,
)
.await?;
)?;
}
Ok(bytes::Bytes::from(bytes))

View File

@@ -9,9 +9,21 @@ pub trait OsExt {
/// Gets the OS + Arch of the current system
fn native_arch(java_arch: &str) -> Self;
/// Gets the OS from an OS + Arch
fn get_os(&self) -> Self;
}
impl OsExt for Os {
fn native() -> Self {
match std::env::consts::OS {
"windows" => Self::Windows,
"macos" => Self::Osx,
"linux" => Self::Linux,
_ => Self::Unknown,
}
}
fn native_arch(java_arch: &str) -> Self {
if std::env::consts::OS == "windows" {
if java_arch == "aarch64" {
@@ -38,12 +50,13 @@ impl OsExt for Os {
}
}
fn native() -> Self {
match std::env::consts::OS {
"windows" => Self::Windows,
"macos" => Self::Osx,
"linux" => Self::Linux,
_ => Self::Unknown,
fn get_os(&self) -> Self {
match self {
Os::OsxArm64 => Os::Osx,
Os::LinuxArm32 => Os::Linux,
Os::LinuxArm64 => Os::Linux,
Os::WindowsArm64 => Os::Windows,
_ => self.clone(),
}
}
}
@@ -72,8 +85,8 @@ pub fn os_rule(
if minecraft_updated
&& (name != &Os::LinuxArm64 || name != &Os::LinuxArm32)
{
rule_match &=
&Os::native() == name || &Os::native_arch(java_arch) == name;
rule_match &= Os::native() == name.get_os()
|| &Os::native_arch(java_arch) == name;
} else {
rule_match &= &Os::native_arch(java_arch) == name;
}

View File

@@ -833,7 +833,6 @@ a,
// MARKDOWN
.markdown-body {
overflow-y: auto;
h1:first-child {
margin-top: 0;
}
@@ -887,7 +886,7 @@ a,
a {
cursor: pointer;
color: var(--color-link);
color: var(--color-blue);
&:focus-visible,
&:hover {

View File

@@ -0,0 +1,26 @@
<template>
<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">
<slot name="icon" />
<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">
<slot name="title" />
</h1>
<slot name="title-suffix" />
</div>
<p class="m-0 line-clamp-2 max-w-[40rem]">
<slot name="summary" />
</p>
<div class="mt-auto flex flex-wrap gap-4">
<slot name="stats" />
</div>
</div>
</div>
<div class="flex flex-wrap gap-2 items-center">
<slot name="actions" />
</div>
</div>
</template>

View File

@@ -797,7 +797,7 @@ function openVideoModal() {
.markdown-resource-link {
cursor: pointer;
color: var(--color-link);
color: var(--color-blue);
&:focus-visible,
&:hover {

View File

@@ -7,6 +7,7 @@ export { default as Card } from './base/Card.vue'
export { default as Checkbox } from './base/Checkbox.vue'
export { default as Chips } from './base/Chips.vue'
export { default as ConditionalNuxtLink } from './base/ConditionalNuxtLink.vue'
export { default as ContentPageHeader } from './base/ContentPageHeader.vue'
export { default as CopyCode } from './base/CopyCode.vue'
export { default as DoubleIcon } from './base/DoubleIcon.vue'
export { default as DropArea } from './base/DropArea.vue'

View File

@@ -1,5 +1,5 @@
<template>
<NewModal ref="modal" :noblur="noblur" danger>
<NewModal ref="modal" :noblur="noblur" danger :on-hide="onHide">
<template #title>
<slot name="title">
<span class="font-extrabold text-contrast text-lg">{{ title }}</span>
@@ -76,6 +76,12 @@ const props = defineProps({
type: Boolean,
default: false,
},
onHide: {
type: Function,
default() {
return () => {}
},
},
})
const emit = defineEmits(['proceed'])

View File

@@ -58,6 +58,7 @@ const props = withDefaults(
closeOnEsc?: boolean
warnOnClose?: boolean
header?: string
onHide?: () => void
}>(),
{
type: true,
@@ -65,6 +66,7 @@ const props = withDefaults(
danger: false,
closeOnEsc: true,
warnOnClose: false,
onHide: () => {},
},
)
@@ -87,6 +89,7 @@ function show(event?: MouseEvent) {
}
function hide() {
props.onHide()
visible.value = false
window.removeEventListener('mousedown', updateMousePosition)
window.removeEventListener('keydown', handleKeyDown)

View File

@@ -34,6 +34,16 @@ const props = defineProps({
type: Boolean,
default: true,
},
noblur: {
type: Boolean,
default: false,
},
onHide: {
type: Function,
default() {
return () => {}
},
},
})
const shareModal = ref(null)
@@ -115,7 +125,7 @@ defineExpose({
</script>
<template>
<Modal ref="shareModal" :header="header">
<Modal ref="shareModal" :header="header" :noblur="noblur" :on-hide="onHide">
<div class="share-body">
<div v-if="link" class="qr-wrapper">
<div ref="qrCode">

1359
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff