Skip to content

Commit

Permalink
Merge branch 'alternative-wishlist-export' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
tfedor committed Aug 28, 2024
2 parents 5aea79f + 0e2f345 commit ea1e958
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 123 deletions.
32 changes: 0 additions & 32 deletions src/css/augmentedsteam.css
Original file line number Diff line number Diff line change
Expand Up @@ -923,38 +923,6 @@ video.highlight_movie:hover + .html5_video_overlay {
color: black;
}

.es-wexport {
margin-bottom: 30px;
}
.es-wexport__label {
margin-right: 30px;
vertical-align: baseline;
}
.es-wexport__label input[type=radio] {
position: relative;
top: 2px;
}
.es-wexport__input {
width: 400px;
color: #b9bfc6;
background-color: #313c48;
box-shadow: 1px 1px 0 rgba(0,0,0,0.2) inset;
border-radius: 3px;
font-size: 12px;
padding: 3px 4px;
border: none;
}
.es-wexport__symbols {
margin-top: 2px;
font-size: 11px;
}
.es-wexport__format.es-grayout {
opacity: 0.4;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}

/***************************************
* App pages
* Common/UserNotes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<svelte:options accessors />

<script lang="ts">
import {L} from "@Core/Localization/Localization";
import {__export_format, __export_text, __export_type} from "@Strings/_strings";
import {slide} from "svelte/transition";
import {createEventDispatcher, onMount} from "svelte";
const dispatch = createEventDispatcher<{setup: void}>();
export let type: "text"|"json";
export let format: string;
let input: HTMLInputElement;
function add(value: string): void {
if (!input) { return; }
format = input.value;
if (input.selectionStart !== null) {
const selection = input.selectionStart;
if (input.selectionEnd && selection !== input.selectionEnd) {
format = format.slice(0, selection) + format.slice(input.selectionEnd);
}
format = format.slice(0, selection) + value + format.slice(selection);
input.selectionStart = selection + value.length;
} else {
format = format + value;
input.selectionStart = format.length;
}
input.selectionEnd = input.selectionStart;
input.focus();
}
$: type, format, dispatch("setup");
onMount(() => {
window.dispatchEvent(new Event("resize"));
});
</script>


<div class="es-wexport">
<h2>{L(__export_type)}</h2>
<div class="es-wexport__buttons">
<label><input type="radio" value="text" bind:group={type} on:change> {L(__export_text)}</label>
<label><input type="radio" value="json" bind:group={type} on:change> JSON</label>
</div>
</div>

{#if type === "text"}
<div class="es-wexport" transition:slide={{axis: "y", duration: 200}}>
<h2>{L(__export_format)}</h2>
<div>
<input type="text" id="es-wexport-format" bind:value={format} bind:this={input} on:change>
<div class="es-wexport__symbols">
{#each ["%title%", "%id%", "%appid%", "%url%", "%release_date%", "%price%", "%discount%", "%base_price%", "%type%", "%note%"] as str, index}
{#if index > 0}, {/if}
<button type="button" on:click={() => add(str)}>{str}</button>
{/each}
</div>
</div>
</div>
{/if}


<style>
.es-wexport {
margin-bottom: 30px;
}
.es-wexport__buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px
}
.es-wexport__symbols {
margin-top: 2px;
font-size: 11px;
}
label {
display: inline-flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-radius: 8px;
border: 1px solid #333643;
text-align: left;
cursor: pointer;
}
label:hover {
color: white;
border-color: white;
}
label:has(input[type=radio]:checked) {
color: white;
border-color: #1a97ff
}
label input[type=radio]{
display: none;
}
input[type=text] {
width: 100%;
color: #b9bfc6;
background-color: #313c48;
box-shadow: 1px 1px 0 rgba(0,0,0,0.2) inset;
border-radius: 3px;
font-size: 12px;
padding: 10px;
border: none;
box-sizing: border-box;
}
button {
background: inherit;
border: 0;
outline: 0;
padding: 0;
color: #acb2b8;
cursor: pointer;
}
button:hover {
text-decoration: underline;
color: white;
}
</style>
157 changes: 66 additions & 91 deletions src/js/Content/Features/Store/Wishlist/FExportWishlist.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import Downloader from "@Core/Downloader";
import {L} from "@Core/Localization/Localization";
import {
__export_copyClipboard,
__export_download,
__export_format,
__export_text,
__export_type,
__export_wishlist,
} from "@Strings/_strings";
import {__export_copyClipboard, __export_download, __export_wishlist,} from "@Strings/_strings";
import Feature from "@Content/Modules/Context/Feature";
import type CWishlist from "@Content/Features/Store/Wishlist/CWishlist";
import HTML from "@Core/Html/Html";
import SteamFacade from "@Content/Modules/Facades/SteamFacade";
import UserNotes from "@Content/Features/Store/Common/UserNotes";
import Clipboard from "@Content/Modules/Clipboard";
import TimeUtils from "@Core/Utils/TimeUtils";
import ExportWishlistForm from "@Content/Features/Store/Wishlist/Components/ExportWishlistForm.svelte";

type Wishlist = Array<[string, {
type WishlistData = Array<[string, {
name: string,
type: string,
release_string: string,
Expand All @@ -29,15 +22,15 @@ type Wishlist = Array<[string, {

enum ExportMethod {
download,
copyToClipboard
copy
}

class WishlistExporter {

private readonly wishlist: Wishlist;
private readonly wishlist: WishlistData;
private readonly notes: Promise<Map<number, string|null>>;

constructor(wishlist: Wishlist) {
constructor(wishlist: WishlistData) {
this.wishlist = wishlist;

const userNotes = new UserNotes()
Expand Down Expand Up @@ -114,102 +107,84 @@ class WishlistExporter {

export default class FExportWishlist extends Feature<CWishlist> {

private type: "text"|"json" = "text";
private format: string = "%title%";

override apply(): void {
HTML.afterBegin("#cart_status_data", `<div class="es-wbtn" id="es_export_wishlist"><div>${L(__export_wishlist)}</div></div>`);

document.querySelector("#es_export_wishlist")!.addEventListener("click", async() => {
const appInfo = await SteamFacade.global("g_rgAppInfo");
const wl: Wishlist = (await SteamFacade.global<{rgVisibleApps: string[]}>("g_Wishlist")).rgVisibleApps.map(
appid => [appid, appInfo[appid]]
);
this._showDialog(wl);
document.querySelector("#es_export_wishlist")!.addEventListener("click", () => {
this.showDialog();
});
}

/*
* Using Valve's CModal API here is very hard, since, when trying to copy data to the clipboard, it has to originate from
* a short-lived event handler for a user action.
* Since we'd use our Messenger class to pass information in between these two contexts, we would "outrange" this specific event
* handler, resulting in a denial of access to the clipboard function.
* This could be circumvented by adding the appropriate permissions, but doing so would prompt users to explicitly accept the
* changed permissions on an update.
*
* If we don't use the Messenger, we'd have to move the whole handler part (including WishlistExporter) to
* the page context side.
*
* Final solution is to query the action buttons of the dialog and adding some extra click handlers on the content script side.
*/
async _showDialog(wl: Array<[string, any]>): Promise<void> {

async function exportWishlist(method: ExportMethod): Promise<void> {
const type = document.querySelector<HTMLInputElement>("input[name='es_wexport_type']:checked")!.value;
const format = document.querySelector<HTMLInputElement>("#es-wexport-format")!.value;

const wishlist = new WishlistExporter(wl);

let result = "";
let filename = "";
let filetype = "";
if (type === "json") {
result = await wishlist.toJson();
filename = "wishlist.json";
filetype = "application/json";
} else if (type === "text" && format) {
result = await wishlist.toText(format);
filename = "wishlist.txt";
filetype = "text/plain";
}
private async showDialog(): Promise<void> {

let form: ExportWishlistForm|undefined;

if (method === ExportMethod.copyToClipboard) {
Clipboard.set(result);
} else if (method === ExportMethod.download) {
Downloader.download(new Blob([result], {"type": `${filetype};charset=UTF-8`}), filename);
const observer = new MutationObserver(() => {
const modal = document.querySelector("#as_export_form");
if (modal) {
form = new ExportWishlistForm({
target: modal,
props: {
type: this.type,
format: this.format
}
});
form.$on("setup", () => {
this.format = form!.format;
this.type = form!.type;
});
observer.disconnect();
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});

SteamFacade.showConfirmDialog(
const response = await SteamFacade.showConfirmDialog(
L(__export_wishlist),
`<div id="es_export_form">
<div class="es-wexport">
<h2>${L(__export_type)}</h2>
<div>
<label class="es-wexport__label"><input type="radio" name="es_wexport_type" value="text" checked> ${L(__export_text)}</label>
<label class="es-wexport__label"><input type="radio" name="es_wexport_type" value="json"> JSON</label>
</div>
</div>
<div class="es-wexport es-wexport__format">
<h2>${L(__export_format)}</h2>
<div>
<input type="text" id="es-wexport-format" class="es-wexport__input" value="%title%"><br>
<div class="es-wexport__symbols">%title%, %id%, %appid%, %url%, %release_date%, %price%, %discount%, %base_price%, %type%, %note%</div>
</div>
</div>
</div>`,
`<div id="as_export_form" style="width:580px"></div>`,
L(__export_download),
null, // use default "Cancel"
L(__export_copyClipboard)
);

for (let i=0; i<10; i++) {
const [dlBtn, copyBtn] = document.querySelectorAll(".newmodal_buttons > .btn_medium");
form?.$destroy();

if (!dlBtn || !copyBtn) {
// wait for popup to show up to apply events
await TimeUtils.timer(10);
continue;
}
if (response === "CANCEL") {
return;
}

// Capture this s.t. the CModal doesn't get destroyed before we can grab this information
dlBtn!.addEventListener("click", () => exportWishlist(ExportMethod.download), true);
copyBtn!.addEventListener("click", () => exportWishlist(ExportMethod.copyToClipboard), true);
const method = response === "OK"
? ExportMethod.download
: ExportMethod.copy;

const format = document.querySelector<HTMLElement>(".es-wexport__format");
for (const el of document.getElementsByName("es_wexport_type")) {
el.addEventListener("click", e => {
const target = e.target as HTMLInputElement;
format!.classList.toggle("es-grayout", target.value === "json");
});
}
const appInfo = await SteamFacade.global("g_rgAppInfo");
const wl: WishlistData = (await SteamFacade.global<{rgVisibleApps: string[]}>("g_Wishlist")).rgVisibleApps.map(
appid => [appid, appInfo[appid]]
);
const wishlist = new WishlistExporter(wl);

let data = "";
let filename = "";
let filetype = "";
if (this.type === "json") {
data = await wishlist.toJson();
filename = "wishlist.json";
filetype = "application/json";
} else if (this.type === "text" && this.format) {
data = await wishlist.toText(this.format);
filename = "wishlist.txt";
filetype = "text/plain";
}

if (method === ExportMethod.copy) {
Clipboard.set(data);
} else if (method === ExportMethod.download) {
Downloader.download(new Blob([data], {"type": `${filetype};charset=UTF-8`}), filename);
}
}
}

0 comments on commit ea1e958

Please sign in to comment.