Add affiliate prompt branding option

This commit is contained in:
2025-03-21 12:57:53 +01:00
parent d894246a38
commit 7aff2a2ead
7 changed files with 312 additions and 150 deletions

View File

@@ -70,6 +70,7 @@ let toolbar_toggle = () => {
let downloader let downloader
let list_updater let list_updater
let details_window let details_window
let affiliate_prompt
let details_visible = false let details_visible = false
let qr_window let qr_window
let qr_visible = false let qr_visible = false
@@ -256,6 +257,9 @@ const apply_customizations = file => {
if (file.branding.footer_link) { if (file.branding.footer_link) {
custom_footer_link = file.branding.footer_link custom_footer_link = file.branding.footer_link
} }
if (file.branding.affiliate_prompt) {
affiliate_prompt.prompt(file.branding.affiliate_prompt)
}
if (file.branding.disable_download_button && !file.can_edit) { if (file.branding.disable_download_button && !file.can_edit) {
disable_download_button = true disable_download_button = true
} }
@@ -619,7 +623,7 @@ const keyboard_event = evt => {
<!-- At the bottom so it renders over everything else --> <!-- At the bottom so it renders over everything else -->
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>
<AffiliatePrompt/> <AffiliatePrompt bind:this={affiliate_prompt}/>
</div> </div>
<style> <style>

View File

@@ -3,10 +3,18 @@ export let on = false
export let group_first = false export let group_first = false
export let group_middle = false export let group_middle = false
export let group_last = false export let group_last = false
export let action = (e: MouseEvent) => {}
const click = (e: MouseEvent) => {
on = !on
if (typeof action === "function") {
action(e)
}
}
</script> </script>
<button <button
on:click={() => on = !on} on:click={click}
type="button" type="button"
class="button" class="button"
class:button_highlight={on} class:button_highlight={on}

View File

@@ -0,0 +1,107 @@
// Response types
// ==============
export type GenericResponse = {
value: string,
message: string,
}
export type User = {
username: string,
email: string,
subscription: Subscription,
storage_space_used: number,
filesystem_storage_used: number,
is_admin: boolean,
balance_micro_eur: number,
hotlinking_enabled: boolean,
monthly_transfer_cap: number,
monthly_transfer_used: number,
file_viewer_branding: Map<string, string>,
file_embed_domains: string,
skip_file_viewer: boolean,
affiliate_user_name: string,
}
export type Subscription = {
id: string,
name: string,
type: string,
file_size_limit: number,
file_expiry_days: number,
storage_space: number,
price_per_tb_storage: number,
price_per_tb_bandwidth: number,
monthly_transfer_cap: number,
file_viewer_branding: boolean,
filesystem_access: boolean,
filesystem_storage_limit: number,
}
// Utility funcs
// =============
export const get_endpoint = () => {
if ((window as any).api_endpoint !== undefined) {
return (window as any).api_endpoint as string
}
console.warn("api_endpoint property is not defined on window")
return "/api"
}
export const check_response = async (resp: Response) => {
let text = await resp.text()
if (resp.status >= 400) {
let error: any
try {
error = JSON.parse(text) as GenericResponse
} catch (err) {
error = text
}
throw error
}
return JSON.parse(text)
}
export const dict_to_form = (dict: Object) => {
let form = new FormData()
for (let key of Object.keys(dict)) {
if (dict[key] === undefined) {
continue
} else if (dict[key] instanceof Date) {
form.append(key, new Date(dict[key]).toISOString())
} else if (typeof dict[key] === "object") {
form.append(key, JSON.stringify(dict[key]))
} else {
form.append(key, dict[key])
}
}
return form
}
// API methods
// ===========
export const get_user = async () => {
if ((window as any).user !== undefined) {
return (window as any).user as User
}
console.warn("user property is not defined on window")
return await check_response(await fetch(get_endpoint() + "/user")) as User
}
export const put_user = async (data: Object) => {
check_response(await fetch(
get_endpoint() + "/user",
{ method: "PUT", body: dict_to_form(data) },
))
// Update the window.user variable
for (let key of Object.keys(data)) {
((window as any).user as User)[key] = data[key]
}
}

View File

@@ -1,8 +1,18 @@
<script> <script>
import { onMount } from "svelte";
import CopyButton from "../layout/CopyButton.svelte"; import CopyButton from "../layout/CopyButton.svelte";
import Form from "./../util/Form.svelte"; import Form from "./../util/Form.svelte";
import Button from "../layout/Button.svelte";
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + window.user.username let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
let affiliate_deny = false
onMount(() => {
affiliate_deny = localStorage.getItem("affiliate_deny") === "1"
})
const enable_affiliate_prompt = () => {
affiliate_deny = false
localStorage.removeItem("affiliate_deny")
}
let account_settings = { let account_settings = {
name: "account_settings", name: "account_settings",
@@ -144,15 +154,27 @@ let delete_account = {
Your own affiliate link is Your own affiliate link is
<a href="{affiliate_link}">{affiliate_link}</a> <a href="{affiliate_link}">{affiliate_link}</a>
<CopyButton small_icon text={affiliate_link}/>. Share this link <CopyButton small_icon text={affiliate_link}/>. Share this link
with premium pixeldrain users to gain commissions. For a with premium pixeldrain users to gain commissions. You can use
detailed description of the affiliate program please check out the "?ref={encodeURIComponent(window.user.username)}" referral
the <a href="/about#toc_12">Q&A page</a>. code on download pages too. For a detailed description of the
affiliate program please check out the <a
href="/about#toc_12">Q&A page</a>.
</p> </p>
<p> <p>
Note that the link includes the name of your pixeldrain Note that the link includes the name of your pixeldrain
account. If you change your account name the link will stop account. If you change your account name the link will stop
working and you might stop receiving commissions. working and you might stop receiving commissions.
</p> </p>
{#if affiliate_deny}
<div class="highlight_blue">
You currently have affiliate prompts disabled. You will not
see affiliate requests from other users. If you wish to
enable it again, click here:
<br/>
<Button click={enable_affiliate_prompt} label="Enable affiliate prompts"/>
</div>
{/if}
</div> </div>
</fieldset> </fieldset>

View File

@@ -1,22 +1,35 @@
<script> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import Modal from "../util/Modal.svelte"; import Modal from "../util/Modal.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
import { get_user, put_user } from "../lib/PixeldrainAPI.mjs";
// When the always flag is set then the pop-up will also show if the user // When the always flag is set then the pop-up will also show if the user
// already has an affiliate ID set // already has an affiliate ID set
export let always = false export let always = false
let modal let modal: Modal
let loading let loading: boolean
let ref let referral: string
let shown = false
export const prompt = async (ref: string) => {
referral = ref
const user = await get_user()
if (referral === null) {
return
} else if (referral === user.affiliate_user_name) {
return // User is already supporting this affiliate ID
} else if (referral === user.username) {
return // This is your own referral link
}
onMount(() => {
if (!always) { if (!always) {
if (window.user.subscription.id === "") { if (user.subscription.id === "") {
// User does not have an active subscription, setting referral will // User does not have an active subscription, setting referral will
// not have effect // not have effect
return return
} else if (window.user.affiliate_user_name !== "") { } else if (user.affiliate_user_name !== "") {
// User is already sponsoring someone // User is already sponsoring someone
return return
} else if (localStorage.getItem("affiliate_deny") === "1") { } else if (localStorage.getItem("affiliate_deny") === "1") {
@@ -25,28 +38,21 @@ onMount(() => {
} }
} }
ref = new URLSearchParams(document.location.search).get("ref") // The prompt can only be shown once per page. This should prevent it from
if (ref === null) { // showing up every time someone loads a new file.
if (shown === true) {
return return
} else if (ref === window.user.affiliate_user_name) {
return // User is already supporting this affiliate ID
} }
shown = true
modal.show() modal.show()
}) }
onMount(() => prompt(new URLSearchParams(document.location.search).get("ref")))
const allow = async () => { const allow = async () => {
loading = true loading = true
try { try {
const form = new FormData() await put_user({affiliate_user_name: referral})
form.append("affiliate_user_name", ref)
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
if(resp.status >= 400) {
throw (await resp.json()).message
}
// Update the window.user variable
window.user.affiliate_user_name = ref
// Close the popup // Close the popup
modal.hide() modal.hide()
@@ -67,10 +73,10 @@ const deny = () => {
<LoadingIndicator bind:loading={loading} /> <LoadingIndicator bind:loading={loading} />
<section> <section>
<p> <p>
Hi! {ref} wants you to sponsor their pixeldrain account. This will Hi! {referral} wants you to sponsor their pixeldrain account. This
give them €0.50 every month in pixeldrain prepaid credit. They can will give them €0.50 every month in pixeldrain prepaid credit. They
use this credit to get a discount on their file sharing and storage can use this credit to get a discount on their file storage and
efforts. Here is a short summary of what this entails: sharing costs. Here is a short summary of what this entails:
</p> </p>
<ul> <ul>
<li> <li>

View File

@@ -6,6 +6,7 @@ import SuccessMessage from "../util/SuccessMessage.svelte";
import ThemePicker from "../util/ThemePicker.svelte"; import ThemePicker from "../util/ThemePicker.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Persistence from "../icons/Persistence.svelte"; import Persistence from "../icons/Persistence.svelte";
import ToggleButton from "../layout/ToggleButton.svelte";
let loading = false let loading = false
let success_message let success_message
@@ -19,6 +20,7 @@ let header_link = ""
let background_image = "" let background_image = ""
let footer_image = "" let footer_image = ""
let footer_link = "" let footer_link = ""
let affiliate_prompt = false
let disable_download_button = false let disable_download_button = false
let disable_share_button = false let disable_share_button = false
@@ -59,6 +61,9 @@ let save = async () => {
form.append("footer_link", footer_link) form.append("footer_link", footer_link)
form.append("disable_download_button", disable_download_button) form.append("disable_download_button", disable_download_button)
form.append("disable_share_button", disable_share_button) form.append("disable_share_button", disable_share_button)
if (affiliate_prompt) {
form.append("affiliate_prompt", window.user.username)
}
try { try {
const resp = await fetch( const resp = await fetch(
@@ -90,6 +95,7 @@ onMount(() => {
background_image = b.background_image ? b.background_image : "" background_image = b.background_image ? b.background_image : ""
footer_image = b.footer_image ? b.footer_image : "" footer_image = b.footer_image ? b.footer_image : ""
footer_link = b.footer_link ? b.footer_link : "" footer_link = b.footer_link ? b.footer_link : ""
affiliate_prompt = b.affiliate_prompt === window.user.username ? true : false
disable_download_button = b.disable_download_button ? b.disable_download_button : false disable_download_button = b.disable_download_button ? b.disable_download_button : false
disable_share_button = b.disable_share_button ? b.disable_share_button : false disable_share_button = b.disable_share_button ? b.disable_share_button : false
} }
@@ -105,12 +111,6 @@ onMount(() => {
Sharing settings are not available for your account. Subscribe to Sharing settings are not available for your account. Subscribe to
the Persistence plan or higher to enable these features. the Persistence plan or higher to enable these features.
</div> </div>
{:else if !window.user.hotlinking_enabled}
<div class="highlight_yellow">
To use custom file viewer branding hotlinking needs to be
enabled. Enable hotlinking on the
<a href="/user/sharing/bandwidth">sharing settings page</a>.
</div>
{/if} {/if}
<SuccessMessage bind:this={success_message}></SuccessMessage> <SuccessMessage bind:this={success_message}></SuccessMessage>
@@ -125,122 +125,135 @@ onMount(() => {
should use APNG or WebP. Avoid using animated GIFs as they are very slow should use APNG or WebP. Avoid using animated GIFs as they are very slow
to load. to load.
</p> </p>
<h3>Theme</h3> <fieldset>
<p> <legend>Theme</legend>
Choose a theme for your download pages. This theme will override the <p>
theme preference of the person viewing the file. Set to 'None' to let Choose a theme for your download pages. This theme will override the
the viewer choose their own theme. theme preference of the person viewing the file. Set to 'None' to let
</p> the viewer choose their own theme.
<ThemePicker </p>
theme={theme} <ThemePicker
on:theme_change={e => {theme = e.detail; save()}}> theme={theme}
</ThemePicker> on:theme_change={e => {theme = e.detail; save()}}>
</ThemePicker>
</fieldset>
<h3>Header image</h3> <fieldset>
<p> <legend>Header image</legend>
Will be shown above the file. Maximum height is 90px. Will be shrunk if <p>
larger. You can also add a link to open when the visitor clicks the Will be shown above the file. Maximum height is 90px. Will be shrunk if
image. The link needs to start with 'https://'. larger. You can also add a link to open when the visitor clicks the
</p> image. The link needs to start with 'https://'.
<button on:click={() => {select_file("header")}}> </p>
<i class="icon">add_photo_alternate</i> <button on:click={() => {select_file("header")}}>
Select header image <i class="icon">add_photo_alternate</i>
</button> Select header image
<button on:click={() => {header_image = ""; save()}}> </button>
<i class="icon">close</i> <button on:click={() => {header_image = ""; save()}}>
Remove <i class="icon">close</i>
</button> Remove
<br/> </button>
Header image link:<br/> <br/>
<form class="form_row" on:submit|preventDefault={save}> Header image link:<br/>
<input class="grow" bind:value={header_link} type="text" placeholder="https://"/> <form class="form_row" on:submit|preventDefault={save}>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button> <input class="grow" bind:value={header_link} type="text" placeholder="https://"/>
</form> <button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if header_image} {#if header_image}
<div class="highlight_shaded"> <div class="highlight_shaded">
<CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner> <CustomBanner src={"/api/file/"+header_image} link={header_link}></CustomBanner>
</div> </div>
{/if}
<h3>Background image</h3>
<p>
This image will be shown behind the file which is being viewed. I
recommend choosing something dark and not too distracting. Try to keep
the file below 1 MB to not harm page loading times. Using a JPEG image
with a quality value of 60 is usually good enough.
</p>
<button on:click={() => {select_file("background")}}>
<i class="icon">add_photo_alternate</i>
Select background image
</button>
<button on:click={() => {background_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
{#if background_image}
<div class="highlight_shaded">
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if}
<h3>Footer image</h3>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
</p>
<button on:click={() => {select_file("footer")}}>
<i class="icon">add_photo_alternate</i>
Select footer image
</button>
<button on:click={() => {footer_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if footer_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
<h3>Toolbar buttons</h3>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
can use these options.
</p>
<p>
The buttons will be hidden, however your files can still be downloaded
and shared through the API. The changes are purely cosmetic.
</p>
<p>
For convenience these options only apply when other people view your
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
Disable download button:
<button on:click={() => {disable_download_button = !disable_download_button; save()}}>
{#if disable_download_button}
<i class="icon">check</i> ON (click to turn off)
{:else}
<i class="icon">close</i> OFF (click to turn on)
{/if} {/if}
</button> </fieldset>
<br/>
Disable share button: <fieldset>
<button on:click={() => {disable_share_button = !disable_share_button; save()}}> <legend>Background image</legend>
{#if disable_share_button} <p>
<i class="icon">check</i> ON (click to turn off) This image will be shown behind the file which is being viewed. I
{:else} recommend choosing something dark and not too distracting. Try to keep
<i class="icon">close</i> OFF (click to turn on) the file below 1 MB to not harm page loading times. Using a JPEG image
with a quality value of 60 is usually good enough.
</p>
<button on:click={() => {select_file("background")}}>
<i class="icon">add_photo_alternate</i>
Select background image
</button>
<button on:click={() => {background_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
{#if background_image}
<div class="highlight_shaded">
<img class="background_preview" src="/api/file/{background_image}" alt="Custom file viewer background"/>
</div>
{/if} {/if}
</button> </fieldset>
<fieldset>
<legend>Footer image</legend>
<p>
Will be shown below the file. Maximum height is 90px. Will be shrunk if
larger.
</p>
<button on:click={() => {select_file("footer")}}>
<i class="icon">add_photo_alternate</i>
Select footer image
</button>
<button on:click={() => {footer_image = ""; save()}}>
<i class="icon">close</i>
Remove
</button>
<br/>
Footer image link:<br/>
<form class="form_row" on:submit|preventDefault={save}>
<input class="grow" bind:value={footer_link} type="text" placeholder="https://"/>
<button class="shrink" action="submit"><i class="icon">save</i> Save</button>
</form>
{#if footer_image}
<div class="highlight_shaded">
<CustomBanner src={"/api/file/"+footer_image} link={footer_link}></CustomBanner>
</div>
{/if}
</fieldset>
<fieldset>
<legend>Affiliate prompt</legend>
<p>
When this is enabled premium users on your download pages will be
asked to support you through pixeldrain's <a
href="/about#toc_12">affiliate program</a>.
</p>
<ToggleButton bind:on={affiliate_prompt} action={save}>
Enable affiliate prompt
</ToggleButton>
</fieldset>
<fieldset>
<legend>Toolbar buttons</legend>
<p>
If you don't want to make it obvious that your files can be downloaded
or shared while still allowing people to view them through the site you
can use these options.
</p>
<p>
The buttons will be hidden, however your files can still be downloaded
and shared through the API. The changes are purely cosmetic.
</p>
<p>
For convenience these options only apply when other people view your
files. The buttons are still available to you. If you want to see the
effects you can open your file in an incognito window.
</p>
<ToggleButton bind:on={disable_download_button} action={save}>
Disable download button
</ToggleButton>
<br/>
<ToggleButton bind:on={disable_share_button} action={save}>
Disable share button
</ToggleButton>
</fieldset>
</section> </section>
<FilePicker <FilePicker

View File

@@ -141,6 +141,7 @@ onMount(() => {
</ul> </ul>
</div> </div>
</div> </div>
<div> <div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid"}> <div class="feat_label" class:feat_highlight={subscription === "prepaid"}>
Prepaid<br/> Prepaid<br/>
@@ -188,6 +189,7 @@ onMount(() => {
</ul> </ul>
</div> </div>
</div> </div>
<div> <div>
<div class="feat_label" class:feat_highlight={subscription === ""}> <div class="feat_label" class:feat_highlight={subscription === ""}>
Free<br/> Free<br/>