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
This commit is contained in:
Geometrically 2024-08-28 21:44:08 -07:00 committed by GitHub
parent 4bafae881f
commit acf26940d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 663 additions and 56 deletions

166
Cargo.lock generated
View File

@ -663,7 +663,7 @@ dependencies = [
"cocoa-foundation 0.1.2", "cocoa-foundation 0.1.2",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"core-graphics 0.23.2", "core-graphics 0.23.2",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
"objc", "objc",
] ]
@ -679,7 +679,7 @@ dependencies = [
"cocoa-foundation 0.2.0", "cocoa-foundation 0.2.0",
"core-foundation 0.10.0", "core-foundation 0.10.0",
"core-graphics 0.24.0", "core-graphics 0.24.0",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
"objc", "objc",
] ]
@ -817,7 +817,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"core-graphics-types 0.1.3", "core-graphics-types 0.1.3",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -830,7 +830,7 @@ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"core-foundation 0.10.0", "core-foundation 0.10.0",
"core-graphics-types 0.2.0", "core-graphics-types 0.2.0",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
] ]
@ -1531,6 +1531,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.5.0" version = "0.5.0"
@ -1538,7 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [ dependencies = [
"foreign-types-macros", "foreign-types-macros",
"foreign-types-shared", "foreign-types-shared 0.3.1",
] ]
[[package]] [[package]]
@ -1552,6 +1561,12 @@ dependencies = [
"syn 2.0.74", "syn 2.0.74",
] ]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "foreign-types-shared" name = "foreign-types-shared"
version = "0.3.1" version = "0.3.1"
@ -2227,6 +2242,22 @@ dependencies = [
"webpki-roots", "webpki-roots",
] ]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.7" version = "0.1.7"
@ -2841,6 +2872,23 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "native-tls"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@ -3221,6 +3269,60 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "openssl"
version = "0.10.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.3.1+3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@ -4008,11 +4110,13 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"hyper-tls",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
@ -4026,6 +4130,7 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tokio-util", "tokio-util",
"tower-service", "tower-service",
@ -4220,6 +4325,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.21" version = "0.8.21"
@ -4272,6 +4386,29 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.22.0" version = "0.22.0"
@ -4581,7 +4718,7 @@ dependencies = [
"bytemuck", "bytemuck",
"cfg_aliases", "cfg_aliases",
"core-graphics 0.23.2", "core-graphics 0.23.2",
"foreign-types", "foreign-types 0.5.0",
"js-sys", "js-sys",
"log", "log",
"objc2", "objc2",
@ -5474,7 +5611,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus" name = "theseus"
version = "0.8.3-1" version = "0.8.3"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"async-tungstenite", "async-tungstenite",
@ -5525,7 +5662,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus_gui" name = "theseus_gui"
version = "0.8.3-1" version = "0.8.3"
dependencies = [ dependencies = [
"chrono", "chrono",
"cocoa 0.25.0", "cocoa 0.25.0",
@ -5702,6 +5839,16 @@ dependencies = [
"syn 2.0.74", "syn 2.0.74",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.0" version = "0.26.0"
@ -6882,8 +7029,7 @@ dependencies = [
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/modrinth/wry?rev=23b0ee4#23b0ee4ea1c1956db7edefac8e4fd710f548c85d"
checksum = "49b8049c8f239cdbfaaea4bacb9646f6b208938ceec0acd5b3e99cd05f70903f"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block", "block",

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "@modrinth/app-frontend", "name": "@modrinth/app-frontend",
"private": true, "private": true,
"version": "0.8.3-1", "version": "0.8.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -31,6 +31,7 @@ import { useInstall } from '@/store/install.js'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-shell' import { open } from '@tauri-apps/plugin-shell'
import { get_opening_command, initialize_state } from '@/helpers/state' import { get_opening_command, initialize_state } from '@/helpers/state'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
const themeStore = useTheming() const themeStore = useTheming()
@ -126,6 +127,7 @@ initialize_state()
}) })
const handleClose = async () => { const handleClose = async () => {
await saveWindowState(StateFlags.ALL)
await getCurrentWindow().close() await getCurrentWindow().close()
} }

View File

@ -1,12 +1,20 @@
<script setup lang="ts"> <script setup>
import { ref } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { Promotion } from '@modrinth/ui'
import { get as getCreds } from '@/helpers/mr_auth.js' import { get as getCreds } from '@/helpers/mr_auth.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_user } from '@/helpers/cache.js' import { get_user } from '@/helpers/cache.js'
import { ChevronRightIcon } from '@modrinth/assets'
import { init_ads_window } from '@/helpers/ads.js'
import { listen } from '@tauri-apps/api/event'
const showAd = ref(true) const showAd = ref(true)
defineExpose({
scroll() {
updateAdPosition()
},
})
const creds = await getCreds().catch(handleError) const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) { if (creds && creds.user_id) {
const user = await get_user(creds.user_id).catch(handleError) const user = await get_user(creds.user_id).catch(handleError)
@ -16,8 +24,98 @@ if (creds && creds.user_id) {
showAd.value = false showAd.value = false
} }
} }
const adsWrapper = ref(null)
let resizeObserver
let scrollHandler
let intersectionObserver
let mutationObserver
onMounted(() => {
if (showAd.value) {
updateAdPosition()
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() {
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)
}
}
const unlisten = await listen('ads-scroll', (event) => {
if (adsWrapper.value) {
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
updateAdPosition()
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
if (intersectionObserver) {
intersectionObserver.disconnect()
}
if (mutationObserver) {
mutationObserver.disconnect()
}
if (scrollHandler) {
window.removeEventListener('scroll', scrollHandler)
}
unlisten()
})
</script> </script>
<template> <template>
<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> </template>

View File

@ -0,0 +1,9 @@
import { invoke } from '@tauri-apps/api/core'
export async function init_ads_window(x, y, width, height) {
return await invoke('plugin:ads|init_ads_window', { x, y, width, height })
}
export async function hide_ads_window() {
return await invoke('plugin:ads|hide_ads_window')
}

View File

@ -528,7 +528,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
<template> <template>
<div ref="searchWrapper" class="search-container"> <div ref="searchWrapper" class="search-container">
<aside class="filter-panel"> <aside class="filter-panel" @scroll="$refs.promo.scroll()">
<PromotionWrapper ref="promo" />
<Card v-if="instanceContext" class="small-instance"> <Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance"> <router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar <Avatar
@ -675,8 +676,7 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
</Card> </Card>
</aside> </aside>
<div class="search"> <div class="search">
<PromotionWrapper class="mt-4" /> <Card class="project-type-container mt-4">
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" /> <NavRow :links="selectableProjectTypes" />
</Card> </Card>
<Card class="search-panel-container"> <Card class="search-panel-container">
@ -878,7 +878,6 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
.filter-panel { .filter-panel {
position: fixed; position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem; padding: 1rem 0.5rem 1rem 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -903,8 +902,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
} }
.search { .search {
margin: 0 1rem 0.5rem 20.5rem; margin: 0 1rem 0.5rem calc(300px + 2.5rem);
width: calc(100% - 20.5rem); width: calc(100% - calc(300px + 2.5rem));
.offline { .offline {
margin: 1rem; margin: 1rem;

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onUnmounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue' import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js' import { list } from '@/helpers/profile.js'
@ -8,6 +8,11 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { get_search_results } from '@/helpers/cache.js' import { get_search_results } from '@/helpers/cache.js'
import { hide_ads_window } from '@/helpers/ads.js'
onMounted(() => {
hide_ads_window()
})
const featuredModpacks = ref({}) const featuredModpacks = ref({})
const featuredMods = ref({}) const featuredMods = ref({})

View File

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

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, watch } from 'vue' import { ref, watch, onMounted } from 'vue'
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets' import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui' import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state' import { handleError, useTheming } from '@/store/state'
@ -13,6 +13,11 @@ import { open } from '@tauri-apps/plugin-dialog'
import { getOS } from '@/helpers/utils.js' import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { get_user, purge_cache_types } from '@/helpers/cache.js' import { get_user, purge_cache_types } from '@/helpers/cache.js'
import { hide_ads_window } from '@/helpers/ads.js'
onMounted(() => {
hide_ads_window()
})
const pageOptions = ['Home', 'Library'] const pageOptions = ['Home', 'Library']

View File

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

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="root-container"> <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"> <Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`"> <router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar <Avatar
@ -20,7 +20,7 @@
</router-link> </router-link>
</Card> </Card>
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick"> <Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="lg" :src="data.icon_url" /> <Avatar size="md" :src="data.icon_url" />
<div class="instance-info"> <div class="instance-info">
<h2 class="name">{{ data.title }}</h2> <h2 class="name">{{ data.title }}</h2>
{{ data.description }} {{ data.description }}
@ -61,7 +61,9 @@
Site Site
</a> </a>
</div> </div>
<hr class="card-divider" /> </Card>
<PromotionWrapper ref="promo" />
<Card class="sidebar-card">
<div class="stats"> <div class="stats">
<div class="stat"> <div class="stat">
<DownloadIcon aria-hidden="true" /> <DownloadIcon aria-hidden="true" />
@ -163,7 +165,6 @@
</Card> </Card>
</div> </div>
<div v-if="data" class="content-container"> <div v-if="data" class="content-container">
<PromotionWrapper />
<Card class="tabs"> <Card class="tabs">
<NavRow <NavRow
v-if="data.gallery.length > 0" v-if="data.gallery.length > 0"
@ -309,11 +310,13 @@ async function fetchProjectData() {
await fetchProjectData() await fetchProjectData()
const promo = ref(null)
watch( watch(
() => route.params.id, () => route.params.id,
async () => { async () => {
if (route.params.id && route.path.startsWith('/project')) { if (route.params.id && route.path.startsWith('/project')) {
await fetchProjectData() await fetchProjectData()
promo.value.scroll()
} }
}, },
) )
@ -377,7 +380,7 @@ const handleOptionsClick = (args) => {
.project-sidebar { .project-sidebar {
position: fixed; position: fixed;
width: 20rem; width: calc(300px + 1.5rem);
min-height: calc(100vh - 3.25rem); min-height: calc(100vh - 3.25rem);
height: fit-content; height: fit-content;
max-height: calc(100vh - 3.25rem); max-height: calc(100vh - 3.25rem);
@ -403,7 +406,7 @@ const handleOptionsClick = (args) => {
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
margin-left: 19.5rem; margin-left: calc(300px + 1rem);
} }
.button-group { .button-group {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "theseus_gui" name = "theseus_gui"
version = "0.8.3-1" version = "0.8.3"
description = "The Modrinth App is a desktop application for managing your Minecraft mods" description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/" repository = "https://github.com/modrinth/code/apps/app/"
@ -57,6 +57,9 @@ cocoa = "0.25.0"
objc = "0.2.7" objc = "0.2.7"
rand = "0.8.5" rand = "0.8.5"
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { version = "2.0.0-rc.1", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

View File

@ -217,6 +217,18 @@ fn main() {
.default_permission( .default_permission(
DefaultPermissionRule::AllowAllCommands, DefaultPermissionRule::AllowAllCommands,
), ),
)
.plugin(
"ads",
InlinedPlugin::new()
.commands(&[
"init_ads_window",
"hide_ads_window",
"scroll_ads_window",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
), ),
) )
.expect("Failed to run tauri-build"); .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", "cache:default",
"settings:default", "settings:default",
"tags:default", "tags:default",
"utils:default" "utils:default",
"ads: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"]}}

View File

@ -299,6 +299,55 @@
}, },
"Identifier": { "Identifier": {
"oneOf": [ "oneOf": [
{
"description": "ads:default -> Default plugin permissions.",
"type": "string",
"enum": [
"ads:default"
]
},
{
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-hide-ads-window"
]
},
{
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-scroll-ads-window"
]
},
{
"description": "ads: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": "auth:default -> Default plugin permissions.", "description": "auth:default -> Default plugin permissions.",
"type": "string", "type": "string",

View File

@ -299,6 +299,55 @@
}, },
"Identifier": { "Identifier": {
"oneOf": [ "oneOf": [
{
"description": "ads:default -> Default plugin permissions.",
"type": "string",
"enum": [
"ads:default"
]
},
{
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-hide-ads-window"
]
},
{
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-init-ads-window"
]
},
{
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
"type": "string",
"enum": [
"ads:allow-scroll-ads-window"
]
},
{
"description": "ads: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": "auth:default -> Default plugin permissions.", "description": "auth:default -> Default plugin permissions.",
"type": "string", "type": "string",

View File

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

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

@ -0,0 +1,82 @@
use serde::Serialize;
use tauri::plugin::TauriPlugin;
use tauri::{
Emitter, LogicalPosition, LogicalSize, Manager, Runtime, WebviewUrl,
};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::<R>::new("ads")
.invoke_handler(tauri::generate_handler![
init_ads_window,
hide_ads_window,
scroll_ads_window,
])
.build()
}
const LINK_SCRIPT: &str = include_str!("ads-init.js");
// TODO: make ads work on linux
#[tauri::command]
pub async fn init_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
x: f32,
y: f32,
width: f32,
height: f32,
) -> crate::api::Result<()> {
#[cfg(not(target_os = "linux"))]
{
if let Some(webview) = app.webviews().get("ads-window") {
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".parse().unwrap(),
),
)
.initialization_script(LINK_SCRIPT)
.user_agent("ModrinthApp Ads Webview")
.zoom_hotkeys_enabled(false)
.transparent(true),
LogicalPosition::new(x, y),
LogicalSize::new(width, height),
);
}
}
Ok(())
}
#[tauri::command]
pub async fn hide_ads_window<R: Runtime>(
app: tauri::AppHandle<R>,
) -> crate::api::Result<()> {
#[cfg(not(target_os = "linux"))]
{
if let Some(webview) = app.webviews().get("ads-window") {
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 tags;
pub mod utils; pub mod utils;
pub mod ads;
pub mod cache; pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>; pub type Result<T> = std::result::Result<T, TheseusSerializableError>;

View File

@ -31,6 +31,8 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
let state = State::get().await?; let state = State::get().await?;
app.asset_protocol_scope() app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir(), true)?; .allow_directory(state.directories.caches_dir(), true)?;
app.asset_protocol_scope()
.allow_directory(state.directories.caches_dir().join("icons"), true)?;
Ok(()) Ok(())
} }
@ -39,7 +41,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[tauri::command] #[tauri::command]
fn show_window(app: tauri::AppHandle) { fn show_window(app: tauri::AppHandle) {
let win = app.get_webview_window("main").unwrap(); let win = app.get_window("main").unwrap();
if let Err(e) = win.show() { if let Err(e) = win.show() {
MessageDialog::new() MessageDialog::new()
.set_type(MessageType::Error) .set_type(MessageType::Error)
@ -179,6 +181,7 @@ fn main() {
.plugin(api::tags::init()) .plugin(api::tags::init())
.plugin(api::utils::init()) .plugin(api::utils::init())
.plugin(api::cache::init()) .plugin(api::cache::init())
.plugin(api::ads::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
initialize_state, initialize_state,
is_dev, is_dev,

View File

@ -48,7 +48,7 @@
] ]
}, },
"productName": "Modrinth App", "productName": "Modrinth App",
"version": "0.8.3-1", "version": "0.8.3",
"identifier": "ModrinthApp", "identifier": "ModrinthApp",
"plugins": { "plugins": {
"deep-link": { "deep-link": {
@ -70,10 +70,10 @@
"resizable": true, "resizable": true,
"title": "Modrinth App", "title": "Modrinth App",
"width": 1280, "width": 1280,
"minHeight": 700, "minHeight": 750,
"minWidth": 1100, "minWidth": 1100,
"visible": false, "visible": false,
"zoomHotkeysEnabled": true, "zoomHotkeysEnabled": false,
"decorations": false "decorations": false
} }
], ],
@ -86,7 +86,17 @@
], ],
"enable": true "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'" "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 '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, "minHeight": 700,
"minWidth": 1100, "minWidth": 1100,
"visible": false, "visible": false,
"zoomHotkeysEnabled": true, "zoomHotkeysEnabled": false,
"decorations": true "decorations": true
} }
] ]

View File

@ -0,0 +1,94 @@
<!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",
},
]);
});
window.addEventListener(
"message",
(event) => {
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:shell|open", {
path: event.data.modrinthOpenUrl,
});
}
},
false,
);
window.addEventListener("mousewheel", (event) => {
if (window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|scroll_ads_window", {
scroll: event.deltaY,
});
}
});
document.getElementById("plus-link").addEventListener("click", (event) => {
event.preventDefault();
if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:shell|open", {
path: this.href,
});
}
});
document.addEventListener("contextmenu", (event) => event.preventDefault());
</script>
</body>
</html>

View File

@ -1,6 +1,6 @@
[package] [package]
name = "theseus" name = "theseus"
version = "0.8.3-1" version = "0.8.3"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2021" edition = "2021"

View File

@ -833,7 +833,6 @@ a,
// MARKDOWN // MARKDOWN
.markdown-body { .markdown-body {
overflow-y: auto;
h1:first-child { h1:first-child {
margin-top: 0; margin-top: 0;
} }