diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2deaabd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.crx +*.pem \ No newline at end of file diff --git a/userscript-client.js b/client/extension/StadiaAvatar.js similarity index 100% rename from userscript-client.js rename to client/extension/StadiaAvatar.js diff --git a/client/extension/_locales/en/messages.json b/client/extension/_locales/en/messages.json new file mode 100644 index 0000000..4ac2f45 --- /dev/null +++ b/client/extension/_locales/en/messages.json @@ -0,0 +1,18 @@ +{ + "extension_description": { + "message": "Custom Stadia avatars for you and your friends", + "description": "Short summary of this extension's function." + }, + "avatar_set": { + "message": "Set avatar from", + "description": "List of different options to set a users's avatar" + }, + "avatar_set_url_support": { + "message": "All image formats supported by Chrome except SVG can be added", + "description": "Disclaimer about supported image formats" + }, + "page_return": { + "message": "Go back", + "description": "Tooltip when hovering the back button on a page" + } +} \ No newline at end of file diff --git a/client/extension/assets/css/popup.css b/client/extension/assets/css/popup.css new file mode 100644 index 0000000..e8491cb --- /dev/null +++ b/client/extension/assets/css/popup.css @@ -0,0 +1,163 @@ +:root { + /* color components */ + --palette-background: 33,33,33; + --palette-contrast: 255,255,255; + --palette-header: 45,45,45; + --palette-accent-high: 253,74,24; + --palette-accent-low: 170,3,88; + + /* compiled colors */ + --color-background: rgb(var(--palette-background)); + --color-contrast: rgb(var(--palette-contrast)); + --color-header: rgb(var(--palette-header)); + --color-accent: linear-gradient(145deg, var(--palette-accent-high) 0%, var(--palette-accent-low) 100%); +} + +* { + margin: 0; + color: var(--color-contrast); +} + +*::-webkit-scrollbar { + display: none; +} + +html, +body { + background-color: var(--color-background); + display: flex; + flex-direction: column; + align-items: center; +} + +body { + --page-depth: 0; + --animation-speed: 400ms; + + width: 400px; + transition: var(--animation-speed) transform cubic-bezier(0.4, 0, 0.2, 1); + transform: translateX(calc((100% * var(--page-depth)) * -1)); +} + +.page .header, +header { + width: 100%; + background-color: var(--color-header); + border-bottom: solid 1px black; +} + +header { + --header-padding: 30px; + --pfp-size: 80px; + --pfp-stroke-offset: 5px; + --pfp-stroke-width: 4px; + --pfp-stroke-size: calc(var(--pfp-stroke-offset) + var(--pfp-stroke-width)); + + height: calc(var(--pfp-size) + (var(--pfp-stroke-size) * 2)); + padding: var(--header-padding) 0 var(--header-padding) 0; + display: flex; + justify-content: center; + align-items: center; +} + +#myAvatar { + width: var(--pfp-size); + height: var(--pfp-size); + box-shadow: + 0 0 0 var(--pfp-stroke-offset) var(--color-header), + 0 0 0 var(--pfp-stroke-size) rgb(var(--palette-accent-high)); + border-radius: 100%; +} + +ol { + list-style-type: none; + width: 100%; + font-size: 15px; + padding-left: 0; +} + +ol li { + --padding: 25px; + + width: calc(100% - (var(--padding) * 2)); + height: 64px; + padding: 0 var(--padding) 0 var(--padding); + display: flex; + align-items: center; + cursor: pointer; +} + +.page .back:hover, +ol li:hover { + background: rgba(var(--palette-contrast),.1); +} + +ol li img { + height: 20px; + filter: brightness(0) invert(1); + margin-right: 20px; +} + +/* ---- */ + +.page { + transition: var(--animation-speed) opacity; + position: absolute; + top: 0; + left: 100%; + width: 100%; + opacity: 1; +} + +.page .header { + --height: 60px; + --button-size: 40px; + --padding: 10px; + + height: 60px; + box-sizing: border-box; + padding: 0 var(--padding) 0 var(--padding); + display: flex; + align-items: center; +} + +.page .back { + --chevron-size: 10px; + --chevron-stroke-width: 3px; + + width: var(--button-size); + height: var(--button-size); + border-radius: 4px; + border: solid 1px var(--color-background); + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; +} + +.page .back::before { + content: ''; + border-style: solid; + border-width: var(--chevron-stroke-width) var(--chevron-stroke-width) 0 0; + display: inline-block; + width: var(--chevron-size); + height: var(--chevron-size); + top: 0; + left: calc(var(--chevron-size) / 3); + transform: rotate(-135deg); + position: relative; + vertical-align: top; +} + +.page .header p { + width: calc(100% - (var(--button-size) * 2)); + font-size: 17px; + text-align: center; +} + +.page .body { + position: relative; + top: var(--height); + box-sizing: border-box; + padding: 20px; +} \ No newline at end of file diff --git a/client/extension/assets/img/icon_set_gravatar.svg b/client/extension/assets/img/icon_set_gravatar.svg new file mode 100644 index 0000000..b14cd67 --- /dev/null +++ b/client/extension/assets/img/icon_set_gravatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/extension/assets/img/icon_set_url.svg b/client/extension/assets/img/icon_set_url.svg new file mode 100644 index 0000000..633692c --- /dev/null +++ b/client/extension/assets/img/icon_set_url.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/extension/assets/img/logo.png b/client/extension/assets/img/logo.png new file mode 100644 index 0000000..89d3e4b Binary files /dev/null and b/client/extension/assets/img/logo.png differ diff --git a/client/extension/assets/img/logo_icon_128.png b/client/extension/assets/img/logo_icon_128.png new file mode 100644 index 0000000..eabf022 Binary files /dev/null and b/client/extension/assets/img/logo_icon_128.png differ diff --git a/client/extension/assets/img/logo_icon_16.png b/client/extension/assets/img/logo_icon_16.png new file mode 100644 index 0000000..e3983a9 Binary files /dev/null and b/client/extension/assets/img/logo_icon_16.png differ diff --git a/client/extension/assets/img/logo_icon_32.png b/client/extension/assets/img/logo_icon_32.png new file mode 100644 index 0000000..a16f8c5 Binary files /dev/null and b/client/extension/assets/img/logo_icon_32.png differ diff --git a/client/extension/assets/img/logo_icon_48.png b/client/extension/assets/img/logo_icon_48.png new file mode 100644 index 0000000..2a63616 Binary files /dev/null and b/client/extension/assets/img/logo_icon_48.png differ diff --git a/client/extension/assets/js/popup.js b/client/extension/assets/js/popup.js new file mode 100644 index 0000000..77a766c --- /dev/null +++ b/client/extension/assets/js/popup.js @@ -0,0 +1,33 @@ +import { AvatarURL } from "./popup_modules/ChangeAvatar.mjs"; + +// Localize by replacing __MSG_***__ strings in DOM +function localizePage() { + for (var i = 0; i < document.body.children.length; i++) { + const element = document.body.children[i]; + + let valStrH = element.innerHTML.toString(); + const valNewH = valStrH.replace(/__MSG_(\w+)__/g, (match,key) => { + return key ? chrome.i18n.getMessage(key) : ""; + }); + + if(valNewH != valStrH) { + element.innerHTML = valNewH; + } + } +} + +function eventHandler(event) { + const target = event.target.closest("[button]"); + switch(target.getAttribute("button")) { + case "avatar:url": new AvatarURL(); break; + } +} + +document.addEventListener("DOMContentLoaded", function () { + // Bind click listeners to all button attributes + for(const button of document.querySelectorAll("[button]")) { + button.addEventListener("click",event => eventHandler(event)); + } +}); + +localizePage(); \ No newline at end of file diff --git a/client/extension/assets/js/popup_modules/ChangeAvatar.mjs b/client/extension/assets/js/popup_modules/ChangeAvatar.mjs new file mode 100644 index 0000000..86752f4 --- /dev/null +++ b/client/extension/assets/js/popup_modules/ChangeAvatar.mjs @@ -0,0 +1,12 @@ +import { Page } from "./Page.mjs"; + +export class AvatarURL extends Page { + + constructor() { + super(chrome.i18n.getMessage("avatar_set") + " URL"); + + this.appendHTML(`

${chrome.i18n.getMessage("avatar_set_url_support")}

`); + this.open(); + } + +} diff --git a/client/extension/assets/js/popup_modules/Page.mjs b/client/extension/assets/js/popup_modules/Page.mjs new file mode 100644 index 0000000..fad30c9 --- /dev/null +++ b/client/extension/assets/js/popup_modules/Page.mjs @@ -0,0 +1,70 @@ +export class Page { + + constructor(title = "") { + this.body = null; + + this.pageDepth = () => { + return parseInt(getComputedStyle(document.body).getPropertyValue("--page-depth")); + } + + this.create(title); + } + + create(title) { + // Create elements + const wrapper = document.createElement("div"); + this.body = document.createElement("div"); + const header = document.createElement("div"); + const backButton = document.createElement("div"); + + const pageDepth = (this.pageDepth() + 1) * 100; + + // Add element attributes + wrapper.classList.add("page"); + wrapper.style.setProperty("left",pageDepth + "%"); + this.body.classList.add("body"); + header.classList.add("header"); + backButton.classList.add("back"); + backButton.setAttribute("title",chrome.i18n.getMessage("page_return")); + + // Attach interfaces + wrapper.close = () => this.close(); // Attach public interface to close this page + backButton.addEventListener("click",() => this.close()); + + // Append document subtree + header.appendChild(backButton); + header.insertAdjacentHTML("beforeend",`

${title}

`); + wrapper.appendChild(header); + wrapper.appendChild(this.body); + document.body.appendChild(wrapper); + } + + destroy() { + const wrapper = this.body.closest(".page"); + + while(wrapper.firstChild) { + wrapper.removeChild(wrapper.lastChild); + } + wrapper.remove(); + } + + appendHTML(HTML) { + this.body.insertAdjacentHTML("beforeend",HTML); + } + + // ---- + + close() { + const delay = parseInt(getComputedStyle(document.body).getPropertyValue("--animation-speed")); + document.body.style.setProperty("--page-depth",this.pageDepth() - 1); + + setTimeout(() => { + this.destroy(); + },delay); + } + + open() { + document.body.style.setProperty("--page-depth",this.pageDepth() + 1); + } + +} diff --git a/client/extension/manifest.json b/client/extension/manifest.json new file mode 100644 index 0000000..8c55508 --- /dev/null +++ b/client/extension/manifest.json @@ -0,0 +1,26 @@ +{ + "name": "Stadia Avatar", + "version": "1.0.1", + "author": "Victor Westerlund", + "default_locale": "en", + "homepage_url": "https://github.com/VictorWesterlund/stadia-avatar", + "description": "__MSG_extension_description__", + "icons": { + "16": "assets/img/logo_icon_16.png", + "32": "assets/img/logo_icon_32.png", + "48": "assets/img/logo_icon_48.png", + "128": "assets/img/logo_icon_128.png" + }, + "host_permissions": [ + "*://stadia.google.com/*" + ], + "action": { + "default_popup": "popup.html", + "default_title": "__MSG_extension_description__", + "default_icon": { + "16": "assets/img/logo_icon_16.png", + "32": "assets/img/logo_icon_32.png" + } + }, + "manifest_version": 3 +} \ No newline at end of file diff --git a/client/extension/popup.html b/client/extension/popup.html new file mode 100644 index 0000000..c74a023 --- /dev/null +++ b/client/extension/popup.html @@ -0,0 +1,22 @@ + + + + + + +
+ +
+
    +
  1. + +

    __MSG_avatar_set__ URL

    +
  2. +
  3. + +

    __MSG_avatar_set__ Gravatar

    +
  4. +
+ + + \ No newline at end of file diff --git a/client/userscript-client.js b/client/userscript-client.js new file mode 100644 index 0000000..18d80fa --- /dev/null +++ b/client/userscript-client.js @@ -0,0 +1,132 @@ +// ==UserScript== +// @name Stadia Avatars +// @namespace https://victorwesterlund.com/ +// @version 1.0 +// @description victorWesterlund/stadia-avatar +// @author VictorWesterlund +// @match https://stadia.google.com/* +// @grant none +// @noframes +// ==/UserScript== + +(function() { + 'use strict'; + + const stadiaAvatar = new URL("https://api.victorwesterlund.com/stadia-avatar/get"); + const gravatar = new URL("https://www.gravatar.com/"); + + /* + G: Suitable for display on all websites with any audience type. + PG: May contain rude gestures, provocatively dressed individuals, the lesser swear words, or mild violence. + R: May contain such things as harsh profanity, intense violence, nudity, or hard drug use. + X: May contain hardcore sexual imagery or extremely disturbing violence. + */ + gravatar.searchParams.set("rating","G"); + + // Stylesheet for Stadia Avatars + class StadiaAvatarCSS { + + constructor() { + this.sheet = null; + this.createStylesheet(); + } + + createStylesheet() { + const style = document.createElement("style"); + style.setAttribute("data-stadia-avatars",""); + style.setAttribute("data-late-css",""); + + document.head.appendChild(style); + this.sheet = style.sheet; + } + + // Serialized group of selectors based on context + selectors(group,id = false) { + switch(group) { + case "me": return ` + .ksZYgc, + .rybUIf + `; + case "friends": return ` + c-wiz[data-p='%.@.null,"${id}"]'] .drvCDc, + .Y1rZWd[data-player-id="${id}"] .Fnd1Pd, + .Y1rZWd[data-playerid="${id}"] .Fnd1Pd, + .w2Sl7c[data-playerid="${id}"] .drvCDc + `; + } + } + + add(selectors,avatar) { + this.sheet.insertRule(`${selectors} { background-image: url(${avatar}) !important; }`); + } + + } + + const avatars = new StadiaAvatarCSS(); + + // ---- + + // Return the player ID attribute of an element + const getID = (target) => { + const id = target.getAttribute("data-player-id") ?? target.getAttribute("data-playerid"); + return id; + } + + async function getStadiaAvatar(playerID) { + stadiaAvatar.searchParams.set("userID",playerID); + + const response = await fetch(stadiaAvatar); + return response.json(); + } + + // Fetch avatar and append to stylesheet + function replaceWithGravatar(group,playerID) { + getStadiaAvatar(playerID).then(response => { + if(response.status !== "OK") { + return false; + } + + gravatar.pathname = "/avatar/" + response.avatar; // Append Gravatar hash + avatars.add(avatars.selectors(group,playerID),gravatar); // Add style override by group + }).catch( + // Ignore missing avatars + ); + } + + // ---- + + replaceWithGravatar("me",getID(document.querySelector("[jsname='HiaYvf']"))); + + function updateFamily(group,wrapper) { + for(const element of wrapper) { + const id = getID(element); + if(!id) { + continue; + } + + replaceWithGravatar(group,id); + } + } + + let timeout = null; + + const friendsList = (mutation,observer) => { + clearTimeout(timeout); + + timeout = setTimeout(() => { + let elements = []; + elements = Array.prototype.concat.apply(elements,document.querySelector("[jsaction='JIbuQc:mbLu7b']").children); + elements = Array.prototype.concat.apply(elements,document.querySelector("[jsname='FhFdCc']").children); + + updateFamily("friends",elements); + },700); + } + + const friendsMenu = document.querySelector("[jsname='TpfyL']"); + const friends = new MutationObserver(friendsList); + friends.observe(friendsMenu,{ + childList: true, + subtree: true + }); + +})(); \ No newline at end of file diff --git a/classes/Database.php b/server/classes/Database.php similarity index 100% rename from classes/Database.php rename to server/classes/Database.php diff --git a/classes/Gravatar.php b/server/classes/Gravatar.php similarity index 100% rename from classes/Gravatar.php rename to server/classes/Gravatar.php diff --git a/classes/Message.php b/server/classes/Message.php similarity index 100% rename from classes/Message.php rename to server/classes/Message.php diff --git a/endpoint/get.php b/server/endpoint/get.php similarity index 100% rename from endpoint/get.php rename to server/endpoint/get.php diff --git a/endpoint/update.php b/server/endpoint/update.php similarity index 100% rename from endpoint/update.php rename to server/endpoint/update.php diff --git a/userscript-bot.js b/server/userscript-bot.js similarity index 100% rename from userscript-bot.js rename to server/userscript-bot.js