You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -31,6 +31,7 @@ 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'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -126,6 +127,7 @@ initialize_state()
|
||||
})
|
||||
|
||||
const handleClose = async () => {
|
||||
await saveWindowState(StateFlags.ALL)
|
||||
await getCurrentWindow().close()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
9
apps/app-frontend/src/helpers/ads.js
Normal file
9
apps/app-frontend/src/helpers/ads.js
Normal 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')
|
||||
}
|
||||
@@ -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,7 +878,6 @@ 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;
|
||||
@@ -903,8 +902,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(300px + 2.5rem);
|
||||
width: calc(100% - calc(300px + 2.5rem));
|
||||
|
||||
.offline {
|
||||
margin: 1rem;
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
const featuredModpacks = ref({})
|
||||
const featuredMods = ref({})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 { handleError, useTheming } from '@/store/state'
|
||||
@@ -13,6 +13,11 @@ 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'
|
||||
|
||||
onMounted(() => {
|
||||
hide_ads_window()
|
||||
})
|
||||
|
||||
const pageOptions = ['Home', 'Library']
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -309,11 +310,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 +380,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 +406,7 @@ const handleOptionsClick = (args) => {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-left: 19.5rem;
|
||||
margin-left: calc(300px + 1rem);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
|
||||
Reference in New Issue
Block a user