├── .eslintignore
├── src
├── components
│ ├── styles
│ │ ├── PopupFix.scss
│ │ ├── ForceCodec.scss
│ │ ├── ForceResolution.scss
│ │ ├── Clock.scss
│ │ ├── LookingForGroup.scss
│ │ ├── Ratings.scss
│ │ ├── NetworkMonitor.scss
│ │ ├── UITab.scss
│ │ ├── StadiaPlusDBHook.scss
│ │ └── StoreFilter.scss
│ ├── Clock.ts
│ ├── PasteFromClipboard.ts
│ ├── StadiaPlusDBHook.ts
│ ├── AllowWindowedMode.ts
│ └── Ratings.ts
├── models
│ ├── NavPosition.ts
│ ├── CheckboxInstance.ts
│ ├── SwitchStyle.ts
│ ├── CheckboxStyle.ts
│ ├── CheckboxShape.ts
│ ├── AppdataManifest.ts
│ ├── UIRowOptions.ts
│ ├── OrderDirection.ts
│ ├── SelectStyle.ts
│ ├── CheckboxColor.ts
│ ├── CheckboxAnimation.ts
│ ├── SelectOptions.ts
│ ├── AFLibraryData.ts
│ ├── Codec.ts
│ ├── Resolution.ts
│ ├── CaptureItem.ts
│ └── FilterOrder.ts
├── styles
│ ├── Grid.scss
│ ├── Typography.scss
│ └── Global.scss
├── popup
│ └── src
│ │ ├── assets
│ │ ├── logo.png
│ │ ├── global.css
│ │ └── Google G.svg
│ │ ├── components
│ │ ├── Icon.vue
│ │ ├── Spinner.vue
│ │ ├── SelectBox.vue
│ │ ├── Button.vue
│ │ ├── PageHeader.vue
│ │ ├── Dropdown.vue
│ │ ├── PageButton.vue
│ │ ├── HelloWorld.vue
│ │ └── Profile.vue
│ │ ├── main.js
│ │ ├── DeveloperPage.vue
│ │ ├── WipeDataPage.vue
│ │ ├── UserPage.vue
│ │ ├── MainPage.vue
│ │ ├── SettingsPage.vue
│ │ ├── App.vue
│ │ └── ComponentPage.vue
├── appdata.json
├── Shortcut.ts
├── Browser.ts
├── ui
│ ├── styles
│ │ ├── Button.scss
│ │ ├── Modal.scss
│ │ ├── Snackbar.scss
│ │ └── Select.scss
│ ├── Snackbar.ts
│ ├── UIRow.ts
│ ├── UIButtonContainer.ts
│ ├── Modal.ts
│ ├── Switch.ts
│ ├── NavButton.ts
│ ├── UIComponent.ts
│ ├── Select.ts
│ ├── UIButton.ts
│ └── Checkbox.ts
├── util
│ ├── EventTracker.ts
│ ├── Util.ts
│ ├── ElGen.ts
│ └── WebScraperRunnable.js
├── logger.ts
├── WebDatabase.ts
├── Component.ts
├── ComponentLoader.ts
├── index.js
├── Storage.ts
├── Language.ts
├── StadiaPlusDB.ts
└── lang
│ ├── gl-ES.json
│ ├── nl-BE.json
│ ├── eu-ES.json
│ ├── ru-RU.json
│ ├── de-DE.json
│ ├── es-ES.json
│ └── en-US.json
├── images
├── Stadia+.ai
├── PlayButton.png
├── Stadia+128.png
├── Stadia+16.png
├── Stadia+32.png
├── Stadia+48.png
├── legacy
│ ├── Stadia+16.png
│ ├── Stadia+32.png
│ ├── Stadia+48.png
│ ├── Stadia+128.png
│ └── Stadia_logo.svg
├── PlayButtonBackground.png
├── icons
│ ├── filter.svg
│ ├── windowed.svg
│ ├── windowed_exit.svg
│ ├── network-monitor.svg
│ ├── visibility.svg
│ ├── visibility_off.svg
│ ├── search.svg
│ └── stadiaplus.svg
└── Stadia+.svg
├── typedoc.json
├── docs
└── assets
│ └── images
│ ├── icons.png
│ ├── icons@2x.png
│ ├── widgets.png
│ └── widgets@2x.png
├── .prettierrc
├── CONTRIBUTING.md
├── tsconfig.json
├── background.js
├── custom.d.ts
├── README.md
├── .gitignore
├── package.json
├── manifest.json
├── webpack.config.js
└── .eslintrc.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.js
2 | **/*.js
3 | dist/**
--------------------------------------------------------------------------------
/src/components/styles/PopupFix.scss:
--------------------------------------------------------------------------------
1 | .zLoQpb.offset {
2 | margin-top: 5rem;
3 | }
--------------------------------------------------------------------------------
/images/Stadia+.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/Stadia+.ai
--------------------------------------------------------------------------------
/src/models/NavPosition.ts:
--------------------------------------------------------------------------------
1 | export enum NavPosition {
2 | LEFT,
3 | RIGHT
4 | }
5 |
--------------------------------------------------------------------------------
/images/PlayButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/PlayButton.png
--------------------------------------------------------------------------------
/images/Stadia+128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/Stadia+128.png
--------------------------------------------------------------------------------
/images/Stadia+16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/Stadia+16.png
--------------------------------------------------------------------------------
/images/Stadia+32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/Stadia+32.png
--------------------------------------------------------------------------------
/images/Stadia+48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/Stadia+48.png
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "inputFiles": ["./src"],
3 | "mode": "modules",
4 | "out": "docs"
5 | }
--------------------------------------------------------------------------------
/images/legacy/Stadia+16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/legacy/Stadia+16.png
--------------------------------------------------------------------------------
/images/legacy/Stadia+32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/legacy/Stadia+32.png
--------------------------------------------------------------------------------
/images/legacy/Stadia+48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/legacy/Stadia+48.png
--------------------------------------------------------------------------------
/src/styles/Grid.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_row {
2 | display: inline-flex;
3 | align-items: flex-end;
4 | }
--------------------------------------------------------------------------------
/docs/assets/images/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/docs/assets/images/icons.png
--------------------------------------------------------------------------------
/images/legacy/Stadia+128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/legacy/Stadia+128.png
--------------------------------------------------------------------------------
/src/components/styles/ForceCodec.scss:
--------------------------------------------------------------------------------
1 | @use "../../ui/styles/Button.scss";
2 | @use "../../styles/Grid.scss";
3 |
--------------------------------------------------------------------------------
/src/popup/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/src/popup/src/assets/logo.png
--------------------------------------------------------------------------------
/docs/assets/images/icons@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/docs/assets/images/icons@2x.png
--------------------------------------------------------------------------------
/docs/assets/images/widgets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/docs/assets/images/widgets.png
--------------------------------------------------------------------------------
/images/PlayButtonBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/images/PlayButtonBackground.png
--------------------------------------------------------------------------------
/src/components/styles/ForceResolution.scss:
--------------------------------------------------------------------------------
1 | @use "../../ui/styles/Button.scss";
2 | @use "../../styles/Grid.scss";
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 4,
4 | "semi": true,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/docs/assets/images/widgets@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mafrans/StadiaPlus/HEAD/docs/assets/images/widgets@2x.png
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Information about contributing to Stadia+ can be found [on the wiki](https://github.com/Mafrans/StadiaPlus/wiki/Contributing).
--------------------------------------------------------------------------------
/src/components/styles/Clock.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_clock {
2 | font-size: 2.5rem;
3 | padding: 1rem 1.5rem;
4 | font-weight: 300;
5 | }
--------------------------------------------------------------------------------
/src/models/CheckboxInstance.ts:
--------------------------------------------------------------------------------
1 | export interface CheckboxInstance {
2 | pretty: HTMLDivElement;
3 | checkbox: HTMLInputElement;
4 | }
5 |
--------------------------------------------------------------------------------
/src/appdata.json:
--------------------------------------------------------------------------------
1 | {
2 | "cache-version": 0.2,
3 | "clear-keys": {
4 | "local": [
5 | "games"
6 | ],
7 | "sync": []
8 | }
9 | }
--------------------------------------------------------------------------------
/src/models/SwitchStyle.ts:
--------------------------------------------------------------------------------
1 | export class SwitchStyle {
2 | public static DEFAULT = '';
3 | public static FILL = 'p-fill';
4 | public static SLIM = 'p-slim';
5 | }
6 |
--------------------------------------------------------------------------------
/src/models/CheckboxStyle.ts:
--------------------------------------------------------------------------------
1 | export class CheckboxStyle {
2 | public static DEFAULT = '';
3 | public static FILL = 'p-fill';
4 | public static THICK = 'p-thick';
5 | }
6 |
--------------------------------------------------------------------------------
/src/models/CheckboxShape.ts:
--------------------------------------------------------------------------------
1 | export class CheckboxShape {
2 | public static DEFAULT = '';
3 | public static CURVED = 'p-curve';
4 | public static ROUND = 'p-round';
5 | }
6 |
--------------------------------------------------------------------------------
/src/models/AppdataManifest.ts:
--------------------------------------------------------------------------------
1 | export interface AppdataManifest {
2 | 'cache-version': number;
3 | 'clear-keys': {
4 | local: string[];
5 | sync: string[];
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/src/popup/src/assets/global.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
2 | @import url('https://fonts.googleapis.com/icon?family=Material+Icons');
--------------------------------------------------------------------------------
/images/icons/filter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/models/UIRowOptions.ts:
--------------------------------------------------------------------------------
1 | import { UIRow } from '../ui/UIRow';
2 |
3 | export class UIRowOptions {
4 | onCreate?: (row: UIRow) => void;
5 | onDestroy?: (row: UIRow) => void;
6 | onReload?: (row: UIRow) => void;
7 | }
8 |
--------------------------------------------------------------------------------
/images/icons/windowed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/models/OrderDirection.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Enum containing different order directions
3 | *
4 | * @export the OrderDirection type.
5 | * @enum {number}
6 | */
7 |
8 | export enum OrderDirection {
9 | ASCENDING,
10 | DESCENDING
11 | }
12 |
--------------------------------------------------------------------------------
/src/models/SelectStyle.ts:
--------------------------------------------------------------------------------
1 | export class SelectStyle {
2 | public static SLIMSELECT = '';
3 | public static SLIMSELECT_LARGE = 'style-slimselect-large';
4 | public static LIGHT = 'style-light';
5 | public static DARK = 'style-dark';
6 | }
7 |
--------------------------------------------------------------------------------
/images/icons/windowed_exit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/models/CheckboxColor.ts:
--------------------------------------------------------------------------------
1 | export class CheckboxColor {
2 | public static BLUE = 'p-primary';
3 | public static GREEN = 'p-success';
4 | public static YELLOW = 'p-warning';
5 | public static CYAN = 'p-info';
6 | public static RED = 'p-danger';
7 | }
8 |
--------------------------------------------------------------------------------
/src/models/CheckboxAnimation.ts:
--------------------------------------------------------------------------------
1 | export class CheckboxAnimation {
2 | public static SMOOTH = 'p-smooth';
3 | public static JELLY = 'p-jelly';
4 | public static TADA = 'p-tada';
5 | public static ROTATE = 'p-rotate';
6 | public static PULSE = 'p-pulse';
7 | }
8 |
--------------------------------------------------------------------------------
/src/models/SelectOptions.ts:
--------------------------------------------------------------------------------
1 | import { Option } from 'slim-select/dist/data';
2 |
3 | export interface SelectOptions {
4 | placeholder: string | undefined;
5 | style?: string;
6 | onChange?: (info: Option) => void;
7 | beforeChange?: (info: Option) => void;
8 | }
9 |
--------------------------------------------------------------------------------
/src/models/AFLibraryData.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line camelcase
2 | export interface AFLibraryData {
3 | data: [boolean, [string, [string, string, boolean, number]], unknown[]];
4 | hash: string,
5 | isError: boolean,
6 | key: string,
7 | sideChannel: unknown,
8 | }
9 |
--------------------------------------------------------------------------------
/images/icons/network-monitor.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "noImplicitAny": true,
6 | "module": "es6",
7 | "target": "es2015",
8 | "jsx": "react",
9 | "allowJs": true,
10 | "moduleResolution": "node",
11 | "strict": true
12 | }
13 | }
--------------------------------------------------------------------------------
/src/styles/Typography.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_muted {
2 | opacity: 0.5;
3 | }
4 |
5 | .stadiaplus_icon-inline {
6 | vertical-align: text-bottom;
7 | padding-left: 0.25rem;
8 | padding-right: 0.25rem;
9 |
10 | &:first-child {
11 | padding-left: initial;
12 | }
13 |
14 | &:last-child {
15 | padding-right: initial;
16 | }
17 | }
--------------------------------------------------------------------------------
/images/icons/visibility.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Shortcut.ts:
--------------------------------------------------------------------------------
1 | import downloader from './util/downloader';
2 |
3 | export class Shortcut {
4 | url: string;
5 | name: string;
6 | constructor(url: string, name: string) {
7 | this.url = url;
8 | this.name = name;
9 | }
10 |
11 | save(): void {
12 | downloader.download(`
`, `${this.name}.htm`, 'text/html');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/popup/src/components/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
--------------------------------------------------------------------------------
/src/models/Codec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The different kinds of codecs, represented as numbers.
3 | *
4 | * @export the Codec type
5 | * @class Codec
6 | */
7 |
8 | export class Codec {
9 | /**
10 | * Automatic codec, let Stadia decide on it's own.
11 | */
12 | static AUTOMATIC = 0;
13 |
14 | /**
15 | * VP9 codec, usually works better than H264 but at the cost of lower quality.
16 | */
17 | static VP9 = 1;
18 |
19 | /**
20 | * H264 codec, high quality and Mac-OS compatible codec but with latency issues.
21 | */
22 | static H264 = 2;
23 | }
24 |
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | chrome.runtime.onInstalled.addListener(() => {
4 | chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
5 | chrome.declarativeContent.onPageChanged.addRules([{
6 | conditions: [
7 | new chrome.declarativeContent.PageStateMatcher({
8 | pageUrl: { hostEquals: 'stadia.google.com' },
9 | }),
10 | ],
11 | actions: [
12 | new chrome.declarativeContent.ShowPageAction(),
13 | ],
14 | }]);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: never;
3 | export default content;
4 | }
5 |
6 | declare module '*.png' {
7 | const content: never;
8 | export default content;
9 | }
10 |
11 | declare module '*.jpg' {
12 | const content: never;
13 | export default content;
14 | }
15 |
16 | declare module '*.css' {
17 | const content: never;
18 | export default content;
19 | }
20 |
21 | declare module '*.scss' {
22 | const content: never;
23 | export default content;
24 | }
25 |
26 | declare module '*.json' {
27 | const value: never;
28 | export default value;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/styles/LookingForGroup.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_lookingforgroup-groups {
2 | background-color: rgba(255,255,255,.06);
3 | padding: .5rem;
4 | margin-top: 1rem;
5 | border-radius: .25rem;
6 | display: none;
7 |
8 | &.visible {
9 | display: block;
10 | }
11 |
12 | >h6 {
13 | display: inline-block;
14 | }
15 |
16 | .refresh {
17 | float: right;
18 | font-size: 30px;
19 | color: #ffffff;
20 | margin: 6px;
21 | cursor: pointer;
22 | }
23 |
24 | .group-list {
25 | margin-top: .5rem;
26 | display: block;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/models/Resolution.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The different kinds of resolutions, represented as numbers.
3 | *
4 | * @export the Resolution type
5 | * @class Resolution
6 | */
7 |
8 | export class Resolution {
9 | /**
10 | * Automatic, let Stadia handle resolutions.
11 | */
12 | static AUTOMATIC = 0;
13 |
14 | /**
15 | * 4K, or 3840x2160
16 | */
17 | static UHD_4K = 1;
18 |
19 | /**
20 | * WQHD, or 2560x1440
21 | */
22 | static WQHD = 2;
23 |
24 | /**
25 | * Full HD, or 1920x1080
26 | */
27 | static FHD = 3;
28 |
29 | /**
30 | * HD, or 1280x720
31 | */
32 | static HD = 4;
33 | }
34 |
--------------------------------------------------------------------------------
/src/styles/Global.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/icon?family=Material+Icons");
2 |
3 | .GqLi4d {
4 | filter: brightness(0.9) contrast(1.1); // This is mostly to make sure that the eye icons in the library can be seen.
5 | }
6 |
7 | html body .dSGvzf {
8 | margin: 0 1rem;
9 | }
10 |
11 | html body .CVVXfc {
12 | flex-direction: column;
13 | align-items: initial;
14 | }
15 |
16 | hr {
17 | border: none;
18 | border-bottom: 1px solid rgba(255,255,255,.06);
19 | }
20 |
21 | ::-webkit-scrollbar {
22 | background-color: rgb(70, 72, 77);
23 | }
24 |
25 | ::-webkit-scrollbar-thumb {
26 | background-color: rgb(80, 82, 87) !important;
27 | }
--------------------------------------------------------------------------------
/images/icons/visibility_off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Browser.ts:
--------------------------------------------------------------------------------
1 | export class Browser {
2 | private static version: number;
3 | static init(): void {
4 | const versionString = (navigator.appVersion.split(' ')).find((e: string) => e.indexOf('Chrome') !== -1);
5 |
6 | if (versionString === undefined) return;
7 | const version = versionString.split('/')[1].split('.').map((s) => parseInt(s, 10));
8 |
9 | let accumulator = 0;
10 | for (let i = 0; i < version.length; i += 1) {
11 | accumulator += version[i] * (10 ** ((version.length - i - 1) * 2));
12 | }
13 |
14 | this.version = accumulator;
15 | }
16 |
17 | static getVersion(): number {
18 | return this.version;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/images/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/src/components/styles/Ratings.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_rating {
2 | margin-top: -1rem;
3 | margin-bottom: 1rem;
4 | position: relative;
5 |
6 | &:hover .stadiaplus_rating-tooltip {
7 | opacity: 1;
8 | transform: translateX(-50%) scale(1);
9 | }
10 |
11 | .stadiaplus_rating-tooltip {
12 | font-family: 'Google Sans', sans-serif;
13 | position: absolute;
14 | top: 100%;
15 | left: 50%;
16 | transform: translateX(-50%) scale(.9);
17 | padding: 0.5rem;
18 | background: rgba(0,0,0,0.8);
19 | border-radius: 0.5rem;
20 | color: #ffffff;
21 | font-size: 20px;
22 | opacity: 0;
23 |
24 | transition: opacity 0.3s ease-in-out 0.3s, transform 0.3s ease-in-out 0.3s;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/popup/src/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
14 |
15 |
37 |
--------------------------------------------------------------------------------
/src/components/styles/NetworkMonitor.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_networkmonitor {
2 | position: absolute;
3 | width: 300px;
4 | top: 0;
5 | left: 0;
6 | z-index: 150;
7 | padding: 1rem;
8 | background-color: rgba(0,0,0,0.4);
9 |
10 | * {
11 | user-select: none;
12 | }
13 |
14 | ul {
15 | list-style-type: none;
16 | padding-inline-start: 0;
17 | margin-block-start: 0;
18 | margin-block-end: 0;
19 | }
20 |
21 | &.editable {
22 | z-index: 300;
23 | cursor: move;
24 | }
25 | }
26 |
27 | .stadiaplus_networkmonitor-tab {
28 | ul {
29 | list-style-type: none;
30 | padding-inline-start: 1rem;
31 | margin-block-start: 0;
32 | margin-block-end: 0;
33 | }
34 | }
35 |
36 | .stadiaplus_networkmonitor-checkbox {
37 | margin: 0.4rem 0;
38 | }
--------------------------------------------------------------------------------
/src/ui/styles/Button.scss:
--------------------------------------------------------------------------------
1 |
2 | .stadiaplus_button {
3 | margin-top: 1rem;
4 | box-shadow: none !important;
5 | }
6 |
7 | .stadiaplus_button-small {
8 | padding: 0.5rem 1rem;
9 | background-color: #3C3E43;
10 | color: #ffffff;
11 | margin: 0 0.5rem;
12 | border-radius: 0.25rem;
13 | }
14 | .stadiaplus_ui-btn-wrapper {
15 | margin-bottom: 70%;
16 | }
17 | .stadiaplus_navbutton {
18 | display: inline-flex;
19 |
20 | height: 2.5rem;
21 | width: 2.5rem;
22 | border-radius: 25px;
23 |
24 | align-items: center;
25 | justify-content: center;
26 | margin-right: 5px;
27 |
28 | color: #E8EAED;
29 | cursor: pointer;
30 |
31 | &.active {
32 | color: #ff773d;
33 | background-color: rgba(255, 255, 255, 0.06);
34 | }
35 |
36 | &:hover {
37 | background-color: rgba(255, 255, 255, 0.06);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/styles/UITab.scss:
--------------------------------------------------------------------------------
1 | @use '../../styles/Typography.scss';
2 |
3 | .stadiaplus_ui-component {
4 | /*
5 | * Must remove 2 x padding or it doesnt work
6 | */
7 | width: calc(100% - 2rem);
8 | height: calc(100% - 2rem);
9 | position: absolute;
10 | top: 0;
11 | left: 0;
12 | transform: translateX(100%);
13 | padding: 1rem;
14 | background-color: #2d2e30;
15 | transition: transform 0.15s ease-in-out;
16 | z-index: 999;
17 |
18 | &.open {
19 | transform: translateX(0);
20 | }
21 |
22 | header {
23 | display: flex;
24 | align-items: center;
25 |
26 | .CwCxFd {
27 | font-size: 22px;
28 | }
29 | }
30 | }
31 |
32 | .stadiaplus_ui-btn-container {
33 | margin-top: -16px;
34 |
35 | &.E0Zk9b {
36 | justify-content: space-between;
37 | }
38 | }
39 |
40 | .stadiaplus_ui-button {
41 | width: 130.677px;
42 | }
--------------------------------------------------------------------------------
/images/icons/stadiaplus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## Stadia+ has been archived
3 | As I no longer have the time or motivation to continue work on this project, it has been archived. Please consider [Stadia Enhanced](https://github.com/ChristopherKlay/StadiaEnhanced) as an alternative.
4 |
5 | 
6 |
7 | # Stadia+
8 |
9 | Stadia+ is an addon for Google's [Stadia](https://stadia.google.com) gaming platform. It includes a lot of useful features and additions that help with everything from solving network issues to managing your game library. Stadia+ is developed independently, licensed under GNU GPL v3 and has no connection to Google, Alphabet or any other Google product or subsidiary.
10 |
11 | If you're new to Stadia+, make sure to read through our [Getting Started](https://github.com/Mafrans/StadiaPlus/wiki/Getting-Started) guide.
12 |
13 | ## Installation
14 | [](https://chrome.google.com/webstore/detail/bbhmnnecicphphjamhdefpagipoegijd)
15 |
--------------------------------------------------------------------------------
/src/models/CaptureItem.ts:
--------------------------------------------------------------------------------
1 | import Logger from '../Logger';
2 |
3 | export class CaptureItem {
4 | public id: string | null = null;
5 | public ageString: string | null = null;
6 | public thumbnail: string | null = null;
7 | public isVideo = false;
8 |
9 | constructor(element: HTMLElement) {
10 | if (element.childNodes[3].firstChild === null
11 | || element.childNodes[3].firstChild.firstChild === null) {
12 | Logger.error('A capture couldn\'t be created.');
13 | return;
14 | }
15 |
16 | this.id = element.getAttribute('data-capture-id');
17 | this.ageString = element.childNodes[3].firstChild.firstChild.textContent;
18 | this.thumbnail = (element.childNodes[1] as HTMLElement).getAttribute('data-thumbnail');
19 |
20 | this.isVideo = element.querySelector('.MUpfsb') != null;
21 | }
22 |
23 | open(): void {
24 | if (this.id === null) return;
25 | (document.querySelector(`.MykDQe[data-capture-id="${this.id}"]`) as HTMLElement).click();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/images/legacy/Stadia_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/src/popup/src/components/SelectBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/styles/StadiaPlusDBHook.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_web-scraper-popup {
2 | width: 300px;
3 | height: 90px;
4 | position: absolute;
5 | top: 0;
6 | border-radius: .5rem;
7 | align-items: center;
8 | justify-content: middle;
9 | left: 0;
10 | margin: 1rem;
11 | background-color: #202124;
12 | z-index: 1;
13 | display: flex;
14 | box-shadow: 0 0.125rem 0.75rem rgba(0,0,0,0.32), 0 0.0625rem 0.375rem rgba(0,0,0,0.18);
15 |
16 | .stadiaplus_web-scraper-icon {
17 | font-family: 'Material Icons Extended';
18 | font-size: 48px;
19 | padding: 1rem;
20 |
21 | &.loading {
22 | animation: spinning 1s linear 0s infinite;
23 |
24 | @keyframes spinning {
25 | from {
26 | transform: rotate(0deg);
27 | }
28 | to {
29 | transform: rotate(360deg);
30 | }
31 | }
32 | }
33 | }
34 |
35 | .stadiaplus_web-scraper-title {
36 | font-size: 1rem;
37 | font-weight: 500;
38 | margin-bottom: .25rem;
39 | }
40 |
41 | .stadiaplus_web-scraper-body {
42 | font-size: .75rem;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/popup/src/assets/Google G.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/src/ui/styles/Modal.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_modal {
2 | position: fixed;
3 | background: rgba(0, 0, 0, 0.4);
4 | z-index: 100;
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | padding: 16px;
10 | pointer-events: none;
11 | transition: opacity 0.2s ease;
12 | opacity: 0;
13 |
14 | .stadiaplus_modal-wrapper {
15 | position: fixed;
16 | max-width: 33.333%;
17 | min-width: 20%;
18 | padding: 1rem;
19 | border-radius: 0.5rem;
20 | background: #303236;
21 | left: 50%;
22 | top: 50%;
23 | transform: translate(-50%, -50%) scale(0.8);
24 | transition: transform 0.2s ease;
25 | }
26 |
27 | .stadiaplus_modal-close {
28 | float: right;
29 | padding: 8px;
30 | border-radius: 50%;
31 | color: white;
32 | font-size: 24px;
33 |
34 | &:hover {
35 | background-color: rgba(255, 255, 255, 0.05);
36 | cursor: pointer;
37 | }
38 | }
39 |
40 | &.active {
41 | opacity: 1;
42 | pointer-events: initial;
43 |
44 | .stadiaplus_modal-wrapper {
45 | transform: translate(-50%, -50%) scale(1.0);
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/util/EventTracker.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * JS Event tracker, doesn't work unless we use a bunch of weird
3 | * types and unsafe stuff.
4 | */
5 |
6 | /* eslint-disable no-undef */
7 | /* eslint-disable @typescript-eslint/unbound-method */
8 | export class EventTracker {
9 | target?: EventTarget;
10 |
11 | private original: (
12 | type: string,
13 | listener: EventListener | EventListenerObject | null,
14 | options?: boolean | AddEventListenerOptions | undefined
15 | ) => void;
16 |
17 | constructor(
18 | target: EventTarget,
19 | _listener: (
20 | type: string,
21 | listener?: EventListenerOrEventListenerObject | null,
22 | options?: AddEventListenerOptions | boolean) => () => void,
23 | ) {
24 | this.original = target.addEventListener;
25 | this.target = target;
26 |
27 | const { original } = this;
28 | target.addEventListener = (type, listener, options?) => {
29 | _listener(type, listener, options);
30 | original(type, listener, options);
31 | };
32 | }
33 |
34 | remove(): void {
35 | if (this.target === undefined) return;
36 | this.target.addEventListener = this.original;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/ui/styles/Snackbar.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_snackbar {
2 | width: 400px;
3 | display: flex;
4 | justify-content: space-between;
5 | align-items: center;
6 | background-color: #333;
7 | border-radius: 4px;
8 | z-index: 999; // Always show on top.
9 |
10 | -webkit-box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12);
11 | box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12);
12 |
13 | position: fixed;
14 | bottom: 8px;
15 | left: calc(50% - 200px); // Subtract half-width to center
16 |
17 | transform: scale(0.5) translateY(16px);
18 | opacity: 0;
19 | transition: transform 0.15s cubic-bezier(0,0,.2,1), opacity 0.15s cubic-bezier(0,0,.2,1);
20 |
21 | &.active {
22 | transform: scale(1) translateY(0px);
23 | opacity: 1;
24 | }
25 | }
26 |
27 | .stadiaplus_snackbar-label {
28 | font-size: 16px;
29 | padding: 16px;
30 | }
31 |
32 | .stadiaplus_snackbar-close {
33 | padding: 8px;
34 | margin: 8px;
35 | border-radius: 50%;
36 | color: white;
37 | font-size: 20px;
38 |
39 | &:hover {
40 | background-color: rgba(255, 255, 255, 0.05);
41 | cursor: pointer;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/popup/src/components/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | const prefix = '[Stadia+]';
4 |
5 | class Logger {
6 | info(...obj: any[]) {
7 | console.log(`${prefix} %c📃 ${obj.join(' ')}`, 'color: black');
8 | }
9 |
10 | warning(...obj: any[]) {
11 | console.log(`${prefix} %c😟 ${obj.join(' ')}`, 'color: orange');
12 | }
13 |
14 | error(...obj: any[]) {
15 | console.log(`${prefix} %c❌ ${obj.join(' ')}`, 'color: red; font-weight: 700');
16 | }
17 |
18 | component(...obj: any[]) {
19 | console.log(`${prefix} %c🧩 ${obj.join(' ')}`, 'color: darkgreen');
20 | }
21 |
22 | /**
23 | * Dubiously created by Adrian Cooney
24 | * @author http://adriancooney.github.io
25 | */
26 | image(url: string, width: number, height: number) {
27 | const getBox = (w: number, h: number) => ({
28 | string: '+',
29 | style: `font-size: 1px; padding: ${Math.floor(h / 2)}px ${Math.floor(w / 2)}px; line-height: 0;`,
30 | });
31 |
32 | const dim = getBox(width, height);
33 | console.log(`%c${dim.string}`, `${dim.style}background: url(${url}); background-size: ${width}px ${height}px; color: transparent;`);
34 | }
35 | }
36 |
37 | export default new Logger();
38 |
--------------------------------------------------------------------------------
/src/popup/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import App from './App.vue';
4 | import MainPage from './MainPage.vue';
5 | import UserPage from './UserPage.vue';
6 | import WipeDataPage from './WipeDataPage.vue';
7 | import SettingsPage from './SettingsPage.vue';
8 | import DeveloperPage from './DeveloperPage.vue';
9 | import ComponentPage from './ComponentPage.vue';
10 | import { Language } from '../../Language';
11 |
12 | Vue.config.productionTip = false;
13 | Vue.use(VueRouter);
14 |
15 | const routes = [
16 | { path: '/', component: MainPage },
17 | { path: '/user/', component: UserPage },
18 | { path: '/user/wipedata', component: WipeDataPage },
19 | { path: '/settings/', component: SettingsPage },
20 | { path: '/settings/developer', component: DeveloperPage },
21 | { path: '/settings/components', component: ComponentPage },
22 | ];
23 |
24 | const router = new VueRouter({
25 | base: 'dist/popup.html', // taken from manifest.json
26 | // mode: 'history',
27 | routes, // short for `routes: routes`
28 | });
29 |
30 | // Always load languages first
31 | Language.init();
32 | Language.load().then(() => {
33 | console.log("new Vue")
34 |
35 | const app = document.createElement('div');
36 | document.body.appendChild(app);
37 |
38 | new Vue({
39 | router,
40 | render: (h) => h(App),
41 | }).$mount(app);
42 | });
43 |
--------------------------------------------------------------------------------
/src/ui/Snackbar.ts:
--------------------------------------------------------------------------------
1 | import './styles/Snackbar.scss';
2 |
3 | export class Snackbar {
4 | static instance: Snackbar;
5 |
6 | element: Element;
7 | label: Element;
8 | closeButton: Element;
9 |
10 | constructor() {
11 | this.element = document.createElement('div');
12 | this.element.classList.add('stadiaplus_snackbar');
13 |
14 | this.label = document.createElement('div');
15 | this.label.classList.add('stadiaplus_snackbar-label');
16 |
17 | this.closeButton = document.createElement('i');
18 | this.closeButton.innerHTML = 'close';
19 | this.closeButton.classList.add('material-icons', 'stadiaplus_snackbar-close');
20 |
21 | this.closeButton.addEventListener('click', () => {
22 | this.element.classList.remove('active');
23 | });
24 | }
25 |
26 | create(): void {
27 | document.body.appendChild(this.element);
28 | this.element.appendChild(this.label);
29 | this.element.appendChild(this.closeButton);
30 | }
31 |
32 | static init(): void {
33 | this.instance = new Snackbar();
34 | this.instance.create();
35 | }
36 |
37 | static activate(label: string): void {
38 | const { instance } = this;
39 |
40 | instance.label.innerHTML = label;
41 | instance.element.classList.add('active');
42 |
43 | window.setTimeout(() => {
44 | instance.element.classList.remove('active');
45 | }, 5000);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/popup/src/components/PageHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
arrow_back
4 |
![Header image]()
5 |
6 |
7 |
8 |
{{ icon }}
9 |
10 |
11 |
12 |
34 |
35 |
--------------------------------------------------------------------------------
/src/popup/src/DeveloperPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{
5 | Language.get('popup.developer-page.title')
6 | }}
7 |
8 |
9 |
10 |
{{ Language.get('popup.developer-page.storage') }}
11 |
16 | clear_all
17 | {{ Language.get('popup.developer-page.clear-cache-button') }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
50 |
51 |
56 |
--------------------------------------------------------------------------------
/src/ui/UIRow.ts:
--------------------------------------------------------------------------------
1 | import { UIComponent } from './UIComponent';
2 | import { UIRowOptions } from '../models/UIRowOptions';
3 |
4 | export class UIRow {
5 | title: string;
6 | content: string;
7 | id: string;
8 | options: UIRowOptions = {};
9 | element: Element;
10 |
11 | constructor(title: string, content: string, id: string, options?: UIRowOptions) {
12 | this.title = title;
13 | this.content = content;
14 | if (options !== undefined) {
15 | this.options = options;
16 | }
17 | this.id = id;
18 |
19 | this.element = document.createElement('div');
20 | this.element.id = this.id;
21 | this.element.innerHTML = `
22 | ${this.title}
23 |
24 | ${this.content}
25 |
26 | `;
27 | this.element.classList.add('stadiaplus_ui-row');
28 | }
29 |
30 | exists(): HTMLElement | null {
31 | return document.getElementById(this.id);
32 | }
33 |
34 | destroy(): void {
35 | if (this.options.onDestroy !== undefined) {
36 | this.options.onDestroy(this);
37 | }
38 |
39 | this.element.remove();
40 | }
41 |
42 | reload(): void {
43 | if (this.options.onReload !== undefined) {
44 | this.options.onReload(this);
45 | }
46 | }
47 |
48 | append(component: UIComponent, useHr = false): void {
49 | if (useHr) {
50 | component.element?.appendChild(document.createElement('hr'));
51 | }
52 |
53 | component.element?.appendChild(this.element);
54 |
55 | if (this.options.onCreate !== undefined) {
56 | this.options.onCreate(this);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/popup/src/components/Dropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
46 |
47 |
--------------------------------------------------------------------------------
/src/ui/UIButtonContainer.ts:
--------------------------------------------------------------------------------
1 | import { UIButton } from './UIButton';
2 |
3 | export class UIButtonContainer {
4 | buttons: UIButton[] = [];
5 | container: Element | null;
6 | element: Element;
7 | id: string;
8 | wrapper: Element;
9 |
10 | constructor() {
11 | this.id = `button-container-${Math.floor(Math.random() * 9999)}`;
12 | this.container = document.querySelector('.TZ0BN');
13 |
14 | this.wrapper = document.createElement('div');
15 | this.wrapper.id = this.id;
16 | this.wrapper.classList.add('ZgUMo', 'stadiaplus_ui-btn-wrapper');
17 |
18 | this.element = document.createElement('div');
19 | this.element.classList.add('E0Zk9b', 'stadiaplus_ui-btn-container');
20 | }
21 |
22 | exists(): boolean {
23 | return document.getElementById(this.id) !== null;
24 | }
25 |
26 | create(callback?: () => void): void {
27 | if (!this.exists()) {
28 | this.container = document.querySelector('.TZ0BN'); // Requery in case the container was deleted
29 | this.wrapper.appendChild(this.element);
30 |
31 | if (this.container !== null) {
32 | this.container.appendChild(this.wrapper);
33 | }
34 | }
35 |
36 | this.buttons.forEach((button) => {
37 | if (!button.exists()) {
38 | this.element.appendChild(button.element);
39 | }
40 | });
41 |
42 | if (callback) { callback(); }
43 | }
44 |
45 | addButton(button: UIButton): void {
46 | if (this.buttons.indexOf(button) === -1) {
47 | this.buttons.push(button);
48 | }
49 | }
50 |
51 | removeButton(button: UIButton): void {
52 | this.buttons = this.buttons.filter((b) => b.id !== button.id);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/popup/src/components/PageButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | navigate_previous
4 |
5 |
6 |
7 | navigate_next
8 |
9 |
10 |
11 |
32 |
33 |
79 |
--------------------------------------------------------------------------------
/src/popup/src/WipeDataPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ Language.get('popup.wipe-data-page.title') }}
5 |
6 |
{{ Language.get('popup.wipe-data-page.heading') }}
7 |
8 |
{{ Language.get('popup.wipe-data-page.confirm') }}
9 |
{{ Language.get('popup.wipe-data-page.cancel') }}
10 |
11 |
12 |
13 |
14 |
49 |
50 |
63 |
--------------------------------------------------------------------------------
/src/WebDatabase.ts:
--------------------------------------------------------------------------------
1 | import Logger from './Logger';
2 |
3 | export class WebDatabase {
4 | url: string;
5 | connected = false;
6 | connection: unknown;
7 |
8 | constructor(url: string) {
9 | this.url = url;
10 | }
11 |
12 | connect(): Promise {
13 | return new Promise((resolve, reject) => {
14 | if (this.connected) {
15 | Logger.error('Error: Already connected to the database.');
16 | return;
17 | }
18 |
19 | const xhr = new XMLHttpRequest();
20 | xhr.open('GET', this.url, true);
21 | xhr.onload = () => {
22 | if (xhr.readyState === 4) {
23 | if (xhr.status === 200) {
24 | this.connected = true;
25 | this.connection = JSON.parse(xhr.responseText);
26 | resolve(this.connection);
27 | } else {
28 | this.connected = false;
29 | reject();
30 | Logger.error('Error when connecting to database:', xhr.statusText);
31 | }
32 | }
33 | };
34 |
35 | xhr.onerror = () => {
36 | this.connected = false;
37 | reject();
38 | Logger.error('Error when connecting to database:', xhr.statusText);
39 | };
40 |
41 | xhr.send(null);
42 | });
43 | }
44 |
45 | getConnection(): unknown {
46 | if (!this.connected) {
47 | Logger.error('Error: Not connected to the database');
48 | return null;
49 | }
50 | return this.connection;
51 | }
52 |
53 | disconnect(): void {
54 | this.connection = null;
55 | this.connected = false;
56 | }
57 |
58 | async reconnect(): Promise {
59 | this.disconnect();
60 | return this.connect();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/popup/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
12 |
Essential Links
13 |
20 |
Ecosystem
21 |
28 |
29 |
30 |
31 |
39 |
40 |
41 |
57 |
--------------------------------------------------------------------------------
/src/ui/Modal.ts:
--------------------------------------------------------------------------------
1 | import { ElGen } from '../util/ElGen';
2 | import './styles/Modal.scss';
3 |
4 | export class Modal {
5 | static instance: Modal;
6 |
7 | element: Element;
8 | wrapper: Element;
9 | content: Element;
10 | closeButton: Element;
11 |
12 | constructor() {
13 | this.element = document.createElement('div');
14 | this.element.classList.add('stadiaplus_modal');
15 |
16 | this.wrapper = document.createElement('div');
17 | this.wrapper.classList.add('stadiaplus_modal-wrapper');
18 |
19 | this.content = document.createElement('div');
20 | this.content.classList.add('stadiaplus_modal-content');
21 |
22 | this.closeButton = document.createElement('i');
23 | this.closeButton.innerHTML = 'close';
24 | this.closeButton.classList.add('material-icons', 'stadiaplus_modal-close');
25 |
26 | this.closeButton.addEventListener('click', () => {
27 | this.element.classList.remove('active');
28 | });
29 | }
30 |
31 | create(): void {
32 | document.body.appendChild(this.element);
33 | this.element.appendChild(this.wrapper);
34 | this.wrapper.appendChild(this.closeButton);
35 | this.wrapper.appendChild(this.content);
36 |
37 | this.element.addEventListener('click', () => this.close());
38 | this.wrapper.addEventListener('click', (event) => event.stopPropagation());
39 | }
40 |
41 | close(): void {
42 | this.element.classList.remove('active');
43 | }
44 |
45 | static activate(content: string | ElGen): void {
46 | if (content instanceof ElGen) {
47 | content.appendTo(this.instance.content);
48 | } else {
49 | this.instance.content.innerHTML = content;
50 | }
51 |
52 | this.instance.element.classList.add('active');
53 | }
54 |
55 | static close(): void {
56 | this.instance.close();
57 | }
58 |
59 | static init(): void {
60 | this.instance = new Modal();
61 | this.instance.create();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/util/Util.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | import Logger from '../Logger';
3 |
4 | class Util {
5 | /**
6 | * Stadia's menu bar element, used to know when the player has opened the menu.
7 | */
8 | menuElement!: HTMLElement | null;
9 | renderer?: HTMLElement;
10 |
11 | load() {
12 | this.menuElement = document.querySelector('.X1asv');
13 | }
14 |
15 | isMenuOpen() {
16 | if (this.menuElement === null) {
17 | Logger.error('Could not find the menu element');
18 | return false;
19 | }
20 |
21 | return this.menuElement.style.opacity !== '0';
22 | }
23 |
24 | isInGame() {
25 | return window.location.pathname.indexOf('player') !== -1;
26 | }
27 |
28 | isInHome() {
29 | return window.location.pathname.indexOf('home') !== -1;
30 | }
31 |
32 | isInStore() {
33 | return window.location.pathname.indexOf('store') !== -1 && !this.isInStoreDetail();
34 | }
35 |
36 | isInStoreDetail() {
37 | return window.location.pathname.indexOf('store/details') !== -1;
38 | }
39 |
40 | desandbox(javascript: string) {
41 | const script = document.createElement('script');
42 | script.innerHTML = javascript;
43 | document.body.appendChild(script);
44 | script.remove();
45 | }
46 |
47 | shuffle(array: T[]) {
48 | for (let i = array.length - 1; i > 0; i -= 1) {
49 | const j = Math.floor(Math.random() * i);
50 | const temp = array[i];
51 | array[i] = array[j];
52 | array[j] = temp;
53 | }
54 | return array;
55 | }
56 |
57 | updateRenderer(): void {
58 | const renderers = document.querySelectorAll('.lhsE4e>c-wiz');
59 | let newRenderer = renderers.item(0) as HTMLElement;
60 | if (renderers.length > 1) {
61 | newRenderer = Array.from(renderers).find((renderer: Element) => (renderer as HTMLElement).style.opacity === '1') as HTMLElement;
62 | }
63 |
64 | if (newRenderer != null) this.renderer = newRenderer;
65 | }
66 | }
67 | export default new Util();
68 |
--------------------------------------------------------------------------------
/src/ui/Switch.ts:
--------------------------------------------------------------------------------
1 | import '../../node_modules/pretty-checkbox/src/pretty-checkbox.scss';
2 | import { SwitchStyle } from '../models/SwitchStyle';
3 |
4 | export class Switch {
5 | private label: string;
6 | private style: string = SwitchStyle.DEFAULT;
7 | private color?: string;
8 | private disabled = false;
9 | private bigger = false;
10 |
11 | constructor(label: string) {
12 | this.label = label;
13 | }
14 |
15 | setStyle(style: string): Switch {
16 | this.style = style;
17 | return this;
18 | }
19 |
20 | setColor(color: string): Switch {
21 | this.color = color;
22 | return this;
23 | }
24 |
25 | setDisabled(disabled: boolean): Switch {
26 | this.disabled = disabled;
27 | return this;
28 | }
29 |
30 | setBigger(bigger: boolean): Switch {
31 | this.bigger = bigger;
32 | return this;
33 | }
34 |
35 | build(): { pretty: HTMLElement, checkbox: HTMLInputElement } {
36 | // Create element
37 | const element = document.createElement('div');
38 |
39 | // Add main classes
40 | element.classList.add('pretty', 'p-switch');
41 |
42 | // If style is not default, add style
43 | if (this.style !== undefined) {
44 | element.classList.add(this.style);
45 | }
46 |
47 | // Set bigger
48 | if (this.bigger) {
49 | element.classList.add('p-bigger');
50 | }
51 |
52 | // Add checkbox input
53 | const checkbox = document.createElement('input');
54 | checkbox.type = 'checkbox';
55 | checkbox.disabled = this.disabled;
56 | element.appendChild(checkbox);
57 |
58 | // Add state div
59 | const state = document.createElement('div');
60 | state.classList.add('state');
61 |
62 | // If colored, add color
63 | if (this.color !== undefined) {
64 | state.classList.add(this.color);
65 | }
66 |
67 | // Add label
68 | const label = document.createElement('label');
69 | label.innerHTML = this.label;
70 | state.appendChild(label);
71 |
72 | element.appendChild(state);
73 |
74 | return { pretty: element, checkbox };
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Component.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import { Language } from './Language';
3 | import Util from './util/Util';
4 |
5 | /**
6 | * A generic component of Stadia+
7 | *
8 | * @export the Component type.
9 | * @class Component
10 | */
11 | export class Component {
12 | /**
13 | * The Component's name.
14 | */
15 | name = 'My Component';
16 | tag = 'component';
17 |
18 | /**
19 | * The Component's unique ID, automatically generated on load.
20 | */
21 | id = 'undefined';
22 |
23 | /**
24 | * A boolean keeping track of whether the Component should receive events or not.
25 | */
26 | active = false;
27 |
28 | enabled = false;
29 | renderer?: HTMLElement = Util.renderer;
30 |
31 | /**
32 | * This method is called whenever the component should start loading.
33 | */
34 | load(): void {
35 | this.name = Language.get(`${this.tag}.name`);
36 | this.id = `stadiaplus_${Math.floor(Math.random() * 999999)}`;
37 | this.updateRenderer();
38 | this.onStart();
39 | }
40 |
41 | updateRenderer(): void {
42 | Util.updateRenderer();
43 | this.renderer = Util.renderer;
44 | }
45 |
46 | /**
47 | * Returns whether this Component has an element in the current renderer
48 | *
49 | * @returns {boolean}
50 | */
51 | exists(): boolean {
52 | if (Util.renderer == null || Util.renderer.style.opacity === '0') return false;
53 | return Util.renderer.querySelector(`#${this.id}`) !== null;
54 | }
55 |
56 | /**
57 | * Returns whether this Component has an element anywhere in the DOM
58 | *
59 | * @returns {boolean}
60 | */
61 | existsAnywhere(): boolean {
62 | return document.querySelector(`#${this.id}`) !== null;
63 | }
64 |
65 | /**
66 | * This method is called whenever the component is unloading.
67 | */
68 | unload(): void {
69 | this.onStop();
70 | }
71 |
72 | /**
73 | * This method is called when the Component should start.
74 | */
75 | onStart(): void {}
76 |
77 | /**
78 | * This method is called when the Component should stop.
79 | */
80 | onStop(): void {}
81 |
82 | /**
83 | * This method is called once every second.
84 | */
85 | onUpdate(): void {}
86 | }
87 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | bin/
4 |
5 |
6 | # Created by https://www.gitignore.io/api/node
7 | # Edit at https://www.gitignore.io/?templates=node
8 |
9 | ### Node ###
10 | # Logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | lerna-debug.log*
17 |
18 | # Diagnostic reports (https://nodejs.org/api/report.html)
19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
20 |
21 | # Runtime data
22 | pids
23 | *.pid
24 | *.seed
25 | *.pid.lock
26 |
27 | # Directory for instrumented libs generated by jscoverage/JSCover
28 | lib-cov
29 |
30 | # Coverage directory used by tools like istanbul
31 | coverage
32 | *.lcov
33 |
34 | # nyc test coverage
35 | .nyc_output
36 |
37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
38 | .grunt
39 |
40 | # Bower dependency directory (https://bower.io/)
41 | bower_components
42 |
43 | # node-waf configuration
44 | .lock-wscript
45 |
46 | # Compiled binary addons (https://nodejs.org/api/addons.html)
47 | build/Release
48 |
49 | # Dependency directories
50 | node_modules/
51 | jspm_packages/
52 |
53 | # TypeScript v1 declaration files
54 | typings/
55 |
56 | # TypeScript cache
57 | *.tsbuildinfo
58 |
59 | # Optional npm cache directory
60 | .npm
61 |
62 | # Optional eslint cache
63 | .eslintcache
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 |
81 | # next.js build output
82 | .next
83 |
84 | # nuxt.js build output
85 | .nuxt
86 |
87 | # Uncomment the public line if your project uses Gatsby
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav
90 | # public
91 |
92 | # Storybook build outputs
93 | .out
94 | .storybook-out
95 |
96 | # vuepress build output
97 | .vuepress/dist
98 |
99 | # Serverless directories
100 | .serverless/
101 |
102 | # FuseBox cache
103 | .fusebox/
104 |
105 | # DynamoDB Local files
106 | .dynamodb/
107 |
108 | # Temporary folders
109 | tmp/
110 | temp/
111 |
112 | # End of https://www.gitignore.io/api/node
113 |
114 | dist
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stadiaplus",
3 | "version": "2.0.0",
4 | "description": "A Chrome extension extending the features of Google's Stadia gaming platform.",
5 | "private": true,
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "webpack --env.production",
9 | "build:dev": "webpack --env.development",
10 | "docs": "npx typedoc"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/Mafrans/StadiaPlus.git"
15 | },
16 | "keywords": [
17 | "stadia",
18 | "stadia+",
19 | "stadiaplus",
20 | "chrome",
21 | "extension",
22 | "google",
23 | "gaming",
24 | "addon"
25 | ],
26 | "author": "Malte Klüft",
27 | "license": "GPL-3.0",
28 | "bugs": {
29 | "url": "https://github.com/Mafrans/StadiaPlus/issues"
30 | },
31 | "homepage": "https://github.com/Mafrans/StadiaPlus#readme",
32 | "devDependencies": {
33 | "@types/chrome": "0.0.126",
34 | "@types/es6-promise": "^3.3.0",
35 | "@types/webrtc": "0.0.26",
36 | "@typescript-eslint/parser": "^4.6.1",
37 | "eslint-config-airbnb-base": "^14.2.1",
38 | "eslint-plugin-import": "^2.22.1",
39 | "eslint-plugin-vue": "^7.1.0",
40 | "file-loader": "^5.1.0",
41 | "html-webpack-plugin": "^4.5.1",
42 | "sass": "^1.26.3",
43 | "sass-loader": "^8.0.2",
44 | "ts-loader": "^6.2.1",
45 | "typedoc": "^0.16.11",
46 | "typedoc-webpack-plugin": "^1.1.4",
47 | "typescript": "^3.8.3",
48 | "webpack": "^4.42.0",
49 | "webpack-cli": "^3.3.11"
50 | },
51 | "dependencies": {
52 | "@typescript-eslint/eslint-plugin": "^4.6.1",
53 | "axios": "^0.21.1",
54 | "bootstrap-vue": "^2.11.0",
55 | "css-loader": "^3.4.2",
56 | "deepmerge": "^4.2.2",
57 | "eslint": "^7.13.0",
58 | "fibers": "^4.0.2",
59 | "material-design-icons": "^3.0.1",
60 | "pretty-checkbox": "^3.0.3",
61 | "raw-loader": "^4.0.0",
62 | "slim-select": "^1.25.0",
63 | "style-loader": "^1.1.3",
64 | "vue": "^2.6.11",
65 | "vue-loader": "^15.9.1",
66 | "vue-router": "^3.1.6",
67 | "vue-template-compiler": "^2.6.11"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/Clock.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '../Component';
2 | import Logger from '../Logger';
3 | import Util from '../util/Util';
4 | import './styles/Clock.scss';
5 | import { Language } from '../Language';
6 |
7 | /**
8 | * A simple clock displayed in the Stadia Menu.
9 | *
10 | * @export the Clock type.
11 | * @class Clock
12 | * @extends {Component}
13 | */
14 | export class Clock extends Component {
15 | /**
16 | * The component tag, used in language files.
17 | */
18 | tag = 'clock';
19 |
20 | /**
21 | * The clock element.
22 | */
23 | element!: HTMLElement;
24 |
25 | constructor() {
26 | super();
27 |
28 | this.createElement();
29 | }
30 |
31 | /**
32 | * Creates a simple , adds the right classes, and stores it in [@link #element]
33 | *
34 | * @memberof Clock
35 | */
36 | createElement(): void {
37 | this.element = document.createElement('span');
38 | this.element.classList.add('stadiaplus_clock');
39 | }
40 |
41 | /**
42 | * Called on startup, initializes important variables.
43 | *
44 | * @memberof Clock
45 | */
46 | onStart(): void {
47 | this.active = true;
48 | this.element.id = this.id;
49 |
50 | Logger.component(Language.get('component.enabled', { name: this.name }));
51 | }
52 |
53 | /**
54 | * Called on stop, makes sure to dispose of elements and variables.
55 | *
56 | * @memberof Clock
57 | */
58 | onStop(): void {
59 | this.active = false;
60 | this.element.remove();
61 | Logger.component(Language.get('component.disabled', { name: this.name }));
62 | }
63 |
64 | /**
65 | * Called every second, updates the element to match the clock.
66 | *
67 | * @memberof Clock
68 | */
69 | onUpdate(): void {
70 | // Only update the clock when it's visible
71 | if (Util.isMenuOpen()) {
72 | if (!this.existsAnywhere()) {
73 | const container = document.querySelector('.hxhAyf');
74 | if (container != null) {
75 | container.prepend(this.element);
76 | }
77 | }
78 |
79 | const time = new Date().toLocaleTimeString();
80 | window.requestAnimationFrame(() => {
81 | this.element.innerHTML = time;
82 | });
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/ui/NavButton.ts:
--------------------------------------------------------------------------------
1 | import Logger from '../Logger';
2 | import { $el, ElGen } from '../util/ElGen';
3 | import { NavPosition } from '../models/NavPosition';
4 | import './styles/Button.scss';
5 |
6 | export class NavButton {
7 | private id: string;
8 | public icon: string | undefined;
9 | public text: string | undefined;
10 | public element: ElGen;
11 | private active = false;
12 | private position: NavPosition = NavPosition.LEFT;
13 |
14 | constructor(icon?: string, text?: string, position?: NavPosition) {
15 | this.icon = icon;
16 | this.text = text;
17 | if (position != null) {
18 | this.position = position;
19 | }
20 |
21 | this.id = `stadiaplus_${Math.floor(Math.random() * 999999)}`;
22 |
23 | this.element = $el('div').id(this.id).class({ stadiaplus_navbutton: true });
24 |
25 | if (this.icon != null) {
26 | this.element.child(
27 | $el('i')
28 | .class({ 'material-icons-extended': true })
29 | .text(this.icon),
30 | );
31 | }
32 |
33 | if (this.text != null) {
34 | this.element.child($el('span').text(this.text));
35 | }
36 | }
37 |
38 | setActive(value: boolean): void {
39 | this.active = value;
40 | this.element.class({ active: value });
41 | }
42 |
43 | getActive(): boolean {
44 | return this.active;
45 | }
46 |
47 | onClick(event: (_event: Event) => void): void {
48 | this.element.event({ click: event });
49 | }
50 |
51 | create(): void {
52 | const navbar = document.querySelector('.w5qDee');
53 | if (navbar === null) {
54 | Logger.error('The navbar was not found, please report this to the developer of Stadia+');
55 | return;
56 | }
57 |
58 | if (navbar.querySelector(`#${this.id}`) != null) return;
59 |
60 | switch (this.position) {
61 | case NavPosition.LEFT:
62 | this.element.appendTo(document.querySelector('.tGNEjf>.ZECEje') as Node);
63 | break;
64 | case NavPosition.RIGHT:
65 | this.element.prependTo(document.querySelector('.QBnfOe>.WpnpPe') as Element);
66 | break;
67 | default: break;
68 | }
69 | }
70 |
71 | destroy(): void {
72 | this.element.element.remove();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/ui/UIComponent.ts:
--------------------------------------------------------------------------------
1 | export class UIComponent {
2 | id: string;
3 | html: string;
4 | element: Element | null;
5 | open = false;
6 | openListeners: (() => void)[] = [];
7 | closeListeners: (() => void)[] = [];
8 |
9 | constructor(title: string, content: string, id: string) {
10 | this.id = id;
11 | this.html = `
12 |
13 |
14 | arrow_back
15 |
16 |
17 | ${title}
18 |
19 |
20 |
21 |
22 | ${content}
23 |
24 | `;
25 |
26 | this.element = document.createElement('div');
27 | this.element.id = this.id;
28 | this.element.classList.add('stadiaplus_ui-component');
29 | }
30 |
31 | create(): void {
32 | const container = document.querySelector('.hxhAyf');
33 | if (container === null || this.element === null) return;
34 |
35 | this.element.innerHTML = this.html;
36 | container.appendChild(this.element);
37 |
38 | // ReQuery element since outerHTML breaks it.
39 | this.element = document.getElementById(this.id);
40 |
41 | const backBtn = document.querySelector(
42 | `#${this.id} > header > .rkvT7c`,
43 | );
44 |
45 | if (backBtn !== null) {
46 | backBtn.addEventListener('click', () => {
47 | this.closeTab();
48 | });
49 | }
50 | }
51 |
52 | openTab(): void {
53 | if (this.element === null) return;
54 |
55 | this.element.classList.add('open');
56 | this.open = true;
57 |
58 | this.openListeners.forEach((c) => c());
59 | }
60 |
61 | closeTab(): void {
62 | if (this.element === null) return;
63 |
64 | this.element.classList.remove('open');
65 | this.open = false;
66 |
67 | this.closeListeners.forEach((c) => c());
68 | }
69 |
70 | onOpen(callback?:() => void): void {
71 | if (callback) { this.openListeners.push(callback); }
72 | }
73 |
74 | onClose(callback?:() => void): void {
75 | if (callback) { this.closeListeners.push(callback); }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/images/Stadia+.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/src/ComponentLoader.ts:
--------------------------------------------------------------------------------
1 | import { Component } from './Component';
2 | import { SyncStorage } from './Storage';
3 | import Logger from './Logger';
4 |
5 | /**
6 | * A utility class responsible for loading [[Component|Components]] and delivering events.
7 | *
8 | * @export the ComponentLoader type.
9 | * @class ComponentLoader
10 | */
11 | export class ComponentLoader {
12 | /**
13 | * A list of all registered components.
14 | */
15 | components: Component[];
16 | timer = 0;
17 |
18 | constructor() {
19 | this.components = [];
20 | }
21 |
22 | /**
23 | * Registers a new component.
24 | *
25 | * @param {Component} component the component to register.
26 | */
27 | register(component: Component): void {
28 | this.components.push(component);
29 | }
30 |
31 | /**
32 | * Unregisters a component.
33 | *
34 | * @param {Component} component
35 | */
36 | unregister(component:Component): void {
37 | this.components.filter((e) => e.id !== component.id);
38 | }
39 |
40 | /**
41 | * Starts the component loader.
42 | */
43 | async start(): Promise {
44 | let storage = await SyncStorage.COMPONENTS.get() as {[key: string]: { enabled: boolean }};
45 |
46 | if (storage == null) {
47 | storage = {};
48 | }
49 |
50 | this.components.forEach((component) => {
51 | if (storage[component.tag] == null) {
52 | storage[component.tag] = { enabled: true };
53 | }
54 |
55 | try {
56 | component.enabled = storage[component.tag].enabled;
57 | if (component.enabled && !component.active) component.load();
58 | } catch (e) {
59 | Logger.error(e);
60 | }
61 | });
62 |
63 | void SyncStorage.COMPONENTS.set(storage);
64 | this.startTimer();
65 | }
66 |
67 | /**
68 | * Stops the component loader.
69 | */
70 | stop(): void {
71 | this.components.forEach((component) => {
72 | if (component.active) component.unload();
73 | });
74 | this.stopTimer();
75 | }
76 |
77 | private startTimer() {
78 | this.timer = setInterval(() => {
79 | this.components.forEach((component) => {
80 | if (component.active) component.onUpdate();
81 | });
82 | }, 1000) as unknown as number;
83 | }
84 |
85 | private stopTimer() {
86 | clearInterval(this.timer);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Stadia+ Extension",
3 | "short_name": "Stadia+",
4 | "version": "2.5.8",
5 | "author": "Malte Klüft (Mafrans)",
6 | "description": "Extends Google's Stadia gaming platform with additional features, such as custom filters and in game network monitoring.",
7 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj4rm88698t+T5Pe5jWP8edM6wPBErxwCu0k3Ona/Xc3lt3NJpvW5dabUdcpP3MYBjMQ41V/iq4Q64rObL9csaruxP4Ex3S8SaxBc/sDBxAUjRdYebF76t+APSeTRDs0WiKJgjUoEGBlOtSqffVYO7wpLr/IWUn9z1PfkaV5LY5jrlHJ4o0HUOoVW5v2gTUZV143hdOXQgMH9IFf1MppMzg0m7AVq+8j7L7O5334T0tKzhb2sd9uHp74jv2jTWBK1ykkoDt4ST18ZC6zdXH0/4rI3cJ/r0YlganD6wfNvJTWJ3WMCrvR/7S//qBr9iNrD65BQDFln90JPEeBScjtd8wIDAQAB",
8 | "permissions": [
9 | "declarativeContent",
10 | "storage",
11 | "identity",
12 | "https://stadiagamedb.com/data/gamedb.json",
13 | "https://stadiaplus.dev/*"
14 | ],
15 | "content_security_policy": "script-src 'self' 'unsafe-eval' http://localhost:8098; object-src 'self'",
16 | "oauth2": {
17 | "client_id": "401608975485-29rkti0stvi4odvnn6hlomkjs0lrtqbj.apps.googleusercontent.com",
18 | "scopes": [
19 | "https://stadiaplus.dev/auth/google",
20 | "https://stadiaplus.dev/auth/google/callback"
21 | ]
22 | },
23 | "background": {
24 | "scripts": [
25 | "background.js"
26 | ],
27 | "persistent": false
28 | },
29 | "browser_action": {
30 | "default_popup": "dist/popup.html",
31 | "default_icon": {
32 | "16": "images/Stadia+16.png",
33 | "32": "images/Stadia+32.png",
34 | "48": "images/Stadia+48.png",
35 | "128": "images/Stadia+128.png"
36 | }
37 | },
38 | "icons": {
39 | "16": "images/Stadia+16.png",
40 | "32": "images/Stadia+32.png",
41 | "48": "images/Stadia+48.png",
42 | "128": "images/Stadia+128.png"
43 | },
44 | "content_scripts": [
45 | {
46 | "run_at": "document_start",
47 | "matches": [
48 | "https://stadia.google.com/*"
49 | ],
50 | "js": [
51 | "dist/app.js"
52 | ]
53 | }
54 | ],
55 | "web_accessible_resources": [
56 | "images/icons/stadiaplus.svg",
57 | "images/icons/network-monitor.svg",
58 | "images/icons/windowed.svg",
59 | "images/icons/windowed_exit.svg",
60 | "images/PlayButton.png",
61 | "images/PlayButtonBackground.png"
62 | ],
63 | "manifest_version": 2
64 | }
--------------------------------------------------------------------------------
/src/components/styles/StoreFilter.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_storefilter {
2 | overflow: hidden;
3 | margin: 1.5rem 0;
4 | border-radius: 0.5rem;
5 | box-shadow: 0 0.125rem 0.75rem rgba(0,0,0,0.32), 0 0.0625rem 0.375rem rgba(0,0,0,0.18);
6 |
7 | .bar {
8 | background-color: rgba(255,255,255,.12);
9 | padding: 1rem;
10 | align-items: center;
11 | display: flex;
12 | align-content: center;
13 |
14 | &::before {
15 | content: 'search';
16 | font-size: 32px;
17 | margin-right: 0.5rem;
18 | font-family: 'Material Icons Extended';
19 | }
20 |
21 | input {
22 | width: calc(100% - 1rem);
23 | padding: 0.5rem;
24 | background-color: rgba(255,255,255,.12);
25 | font-family: 'Google Sans',sans-serif;
26 | font-size: 1.25rem;
27 | outline: #ff773d 3px;
28 | color: #ffffff;
29 | font-weight: 500;
30 | border: none;
31 | border-radius: 0.25rem;
32 | }
33 | }
34 |
35 | .games {
36 | display: flex;
37 | flex-flow: column wrap;
38 | background-color: rgba(255,255,255,.06);
39 |
40 | .stadiaplus_storefilter-game {
41 | display: inline-flex;
42 | overflow: hidden;
43 | opacity: 0;
44 | height: 0;
45 | align-content: center;
46 | border-radius: 0.5rem;
47 | background-color: rgba(255,255,255,.06);
48 | margin: 0 1rem;
49 | box-shadow: 0 0 0 0.1875rem transparent;
50 | color: #ffffff;
51 | transition: height 0.2s ease-out, margin 0.2s ease-out, opacity 0.2s ease-out;
52 |
53 | &.shown {
54 | height: 90px;
55 | margin: 1rem;
56 | opacity: 1;
57 |
58 | ~ .shown {
59 | margin-top: -0.5rem;
60 | }
61 |
62 | &:hover {
63 | background-color: rgba(255,255,255,.09);
64 | }
65 | }
66 |
67 | img {
68 | object-fit: cover;
69 | width: 140px;
70 | border-top-left-radius: 0.5rem;
71 | border-bottom-left-radius: 0.5rem;
72 | }
73 |
74 | .detail {
75 | display: flex;
76 | flex-direction: column;
77 | justify-content: center;
78 | margin-left: 1.5rem;
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/popup/src/UserPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ Language.get('popup.user-page.title') }}
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ Language.get( 'popup.user-page.login-button' ) }}
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 | {{ Language.get('loading') }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
73 |
74 |
80 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TypedocWebpackPlugin = require('typedoc-webpack-plugin');
3 | const VueLoaderPlugin = require('vue-loader/lib/plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | module.exports = (env) => ({
7 | entry: {
8 | app: './src/index.js',
9 | popup: './src/popup/src/main.js',
10 | },
11 | devtool: 'inline-source-map',
12 | mode: env.production ? 'production' : 'development',
13 | module: {
14 | rules: [
15 | {
16 | test: /\.tsx?$/,
17 | use: 'ts-loader',
18 | exclude: /node_modules/,
19 | },
20 | {
21 | test: /\.vue$/,
22 | loader: 'vue-loader',
23 | },
24 | {
25 | test: /\.css$/i,
26 | use: [
27 | // Creates `style` nodes from JS strings
28 | {
29 | loader: 'style-loader',
30 | options: {
31 | insert: 'html',
32 | },
33 | },
34 | 'css-loader',
35 | ],
36 | },
37 |
38 | {
39 | test: /\.s[ac]ss$/i,
40 | use: [
41 | // Creates `style` nodes from JS strings
42 | {
43 | loader: 'style-loader',
44 | options: {
45 | insert: 'html',
46 | },
47 | },
48 | // Translates CSS into CommonJS
49 | 'css-loader',
50 | // Compiles Sass to CSS
51 | 'sass-loader',
52 | ],
53 | },
54 | {
55 | test: /\.(png|jpe?g|gif|svg)$/i,
56 | use: [
57 | {
58 | loader: 'file-loader',
59 | },
60 | ],
61 | },
62 | {
63 | test: /\.txt$/i,
64 | use: 'raw-loader',
65 | },
66 | ],
67 | },
68 | plugins: [
69 | new VueLoaderPlugin(),
70 | new TypedocWebpackPlugin({
71 | name: 'Contoso',
72 | mode: 'file',
73 | out: './docs',
74 | }, './src'),
75 | new HtmlWebpackPlugin({
76 | chunks: ['popup'],
77 | filename: 'popup.html',
78 | }),
79 | ],
80 | resolve: {
81 | extensions: ['.tsx', '.ts', '.js'],
82 | },
83 | output: {
84 | filename: '[name].js',
85 | path: path.resolve(__dirname, 'dist'),
86 | },
87 | });
88 |
--------------------------------------------------------------------------------
/src/ui/Select.ts:
--------------------------------------------------------------------------------
1 | import SlimSelect from 'slim-select';
2 | import 'slim-select/dist/slimselect.min.css';
3 | import { SelectStyle } from '../models/SelectStyle';
4 | import { SelectOptions } from '../models/SelectOptions';
5 | import './styles/Select.scss';
6 | import Logger from '../Logger';
7 |
8 | export class Select {
9 | slimselect: SlimSelect | undefined;
10 | element: Element | null;
11 |
12 | constructor(element: Element, options: SelectOptions) {
13 | this.element = element;
14 |
15 | options.style = options.style !== undefined ? options.style : SelectStyle.DARK;
16 |
17 | this.element.classList.add(
18 | 'stadiaplus_select',
19 | options.style,
20 | );
21 |
22 | /**
23 | * Slimselect throws a TypeError if the elements/containers
24 | * have been deleted without properly being destroyed. As Stadia
25 | * runs in a virtual DOM, we have no control of when the DOM changes
26 | * therefore we can't solve it in a proper way.
27 | *
28 | * Let's just hope garbage collection takes care of it.
29 | */
30 | try {
31 | this.slimselect = new SlimSelect({
32 | select: this.element,
33 | showSearch: false,
34 | placeholder: options.placeholder,
35 | onChange: options.onChange,
36 | beforeOnChange: options.beforeChange,
37 | });
38 | } catch (error) {
39 | Logger.error(error);
40 | }
41 | }
42 |
43 | disable(): void {
44 | if (this.element == null) return;
45 | this.element.classList.add('disabled');
46 | }
47 |
48 | enable(): void {
49 | if (this.element == null) return;
50 | this.element.classList.remove('disabled');
51 | }
52 |
53 | get(): string | string[] {
54 | if (this.slimselect == null) return 'undefined';
55 | return this.slimselect.selected();
56 | }
57 |
58 | set(...items: unknown[]): void {
59 | if (this.slimselect == null) return;
60 |
61 | this.slimselect.setData(
62 | items as never[],
63 | );
64 | }
65 |
66 | select(item: unknown): void {
67 | if (this.slimselect == null) return;
68 |
69 | this.slimselect.setSelected(item as never);
70 | }
71 |
72 | search(query: string): void {
73 | if (this.slimselect == null) return;
74 | this.slimselect.search(query);
75 | }
76 |
77 | destroy(): void {
78 | if (this.element == null) return;
79 | if (this.slimselect !== undefined) {
80 | this.slimselect.destroy();
81 | }
82 | this.element.classList.remove('stadiaplus_select');
83 | this.element = null;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/PasteFromClipboard.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '../Component';
2 | import Logger from '../Logger';
3 | import Util from '../util/Util';
4 |
5 | export class Platform {
6 | static WINDOWS = 'Win32';
7 | static MACOS = 'MacIntel';
8 | }
9 |
10 | export class PasteFromClipboard extends Component {
11 | tag = 'paste-from-clipboard';
12 |
13 | protected target: HTMLInputElement | null = null;
14 |
15 | /**
16 | * Called on startup, initializes important variables.
17 | */
18 | onStart(): void {
19 | this.active = true;
20 | }
21 |
22 | /**
23 | * Called on stop, makes sure to dispose of elements and variables.
24 | */
25 | onStop(): void {
26 | this.active = false;
27 | }
28 |
29 | /**
30 | * Called once every second.
31 | */
32 | onUpdate(): void {
33 | super.onUpdate();
34 |
35 | if (Util.isInGame()) {
36 | this.updateRenderer();
37 |
38 | if (this.renderer === undefined) {
39 | Logger.error('Renderer is undefined');
40 | return;
41 | }
42 |
43 | const input: HTMLInputElement = this.renderer.getElementsByTagName('input')[0];
44 |
45 | if (input !== this.target) {
46 | if (undefined !== this.target) {
47 | this.target?.removeEventListener('keydown', (...args) => this.keydownEventListener(...args));
48 | }
49 | this.target = input;
50 | this.target.addEventListener('keydown', (...args) => this.keydownEventListener(...args));
51 | }
52 | }
53 | }
54 |
55 | /**
56 | * @param event
57 | */
58 | keydownEventListener(event: KeyboardEvent): void {
59 | let ctrlKey: boolean;
60 | switch (navigator.platform) {
61 | case Platform.WINDOWS:
62 | ctrlKey = event.ctrlKey;
63 | break;
64 |
65 | case Platform.MACOS:
66 | ctrlKey = event.metaKey;
67 | break;
68 |
69 | default:
70 | ctrlKey = event.ctrlKey;
71 | break;
72 | }
73 |
74 | if (ctrlKey && event.code === 'KeyV') {
75 | void navigator.clipboard.readText().then((data: string) => {
76 | event.target?.dispatchEvent(new InputEvent('input', {
77 | // InputEventInit
78 | data,
79 | inputType: 'insertText',
80 | isComposing: false,
81 |
82 | // UIEventInit
83 | view: null,
84 |
85 | // EventInit
86 | bubbles: true,
87 | cancelable: false,
88 | composed: true,
89 | }));
90 | });
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/popup/src/MainPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ Language.get('popup.main-page.title') }}
5 |
6 | {{ Language.get('popup.main-page.ready-text') }}
7 |
8 |
9 |
10 |
11 |
15 | play_arrow
16 | {{ Language.get('popup.main-page.launch-button') }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | redeem
27 | {{ Language.get('popup.main-page.patreon-button') }}
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
45 |
46 |
91 |
92 |
94 |
--------------------------------------------------------------------------------
/src/ui/UIButton.ts:
--------------------------------------------------------------------------------
1 | import { UIButtonContainer } from './UIButtonContainer';
2 |
3 | export class UIButton {
4 | id: string;
5 | html: string;
6 | element: Element;
7 | container?: UIButtonContainer;
8 | button: HTMLElement;
9 |
10 | static buttonContainers: UIButtonContainer[] = [];
11 |
12 | constructor(icon: string, title: string, id: string) {
13 | this.id = id;
14 | this.html = `
15 |
16 |
17 |
18 |
19 |
20 |
${title}
21 |
22 |
23 | `;
24 |
25 | this.element = document.createElement('div');
26 | this.element.id = id;
27 | this.element.classList.add('Pyf1bb', 'stadiaplus_ui-button');
28 |
29 | this.button = document.createElement('div');
30 | this.button.setAttribute('role', 'button');
31 | this.button.setAttribute('tabindex', '0');
32 | this.button.classList.add('CTvDXd', 'QAAyWd', 'Pjpac', 'zcMYd');
33 | this.button.innerHTML = this.html;
34 | this.element.appendChild(this.button);
35 | }
36 |
37 | create(callback?: () => void): void {
38 | UIButton.buttonContainers.forEach((container) => {
39 | if (container.buttons.length < 3) {
40 | this.container = container;
41 | }
42 | });
43 |
44 | if (this.container === undefined) {
45 | this.container = new UIButtonContainer();
46 | UIButton.buttonContainers.push(this.container);
47 | }
48 | this.container.addButton(this);
49 | this.container.create(callback);
50 | }
51 |
52 | setIcon(icon: string): void {
53 | const iconElement = this.element.querySelector('.uibutton-icon');
54 | if (iconElement !== null) {
55 | iconElement.setAttribute('src', icon);
56 | }
57 | }
58 |
59 | setTitle(title: string): void {
60 | const titleElement = this.element.querySelector('.uibutton-title');
61 | if (titleElement !== null) {
62 | titleElement.textContent = title;
63 | }
64 | }
65 |
66 | update(): void {
67 | if (!this.exists()) {
68 | this.create();
69 | }
70 | }
71 |
72 | exists(): boolean {
73 | return document.getElementById(this.id) !== null;
74 | }
75 |
76 | destroy(): void {
77 | this.element.remove();
78 | if (this.container !== undefined) {
79 | this.container.removeButton(this);
80 | }
81 | }
82 |
83 | onPressed(func: (event: Event) => void): void {
84 | this.button.addEventListener('click', func);
85 | this.button.addEventListener('keyup', (event: KeyboardEvent) => {
86 | if (event.key === 'Enter') {
87 | this.button.click();
88 | }
89 | });
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Logger from './Logger';
2 | import Util from './util/Util';
3 | import './styles/Global.scss';
4 |
5 | import { ComponentLoader } from './ComponentLoader';
6 | import { Clock } from './components/Clock';
7 | import { UITab } from './components/UITab';
8 | import { ForceCodec } from './components/ForceCodec';
9 | import { ForceResolution } from './components/ForceResolution';
10 | import { NetworkMonitor } from './components/NetworkMonitor';
11 | import { Snackbar } from './ui/Snackbar';
12 | import { LibraryFilter } from './components/LibraryFilter';
13 | import { StoreFilter } from './components/StoreFilter';
14 | import { Ratings } from './components/Ratings';
15 | import { Language } from './Language';
16 | import { AllowWindowedMode } from './components/AllowWindowedMode';
17 | import { PasteFromClipboard } from './components/PasteFromClipboard';
18 | import { LocalStorage, StorageManager } from './Storage';
19 | import appdata from './appdata.json';
20 | import { Modal } from './ui/Modal';
21 | import { Browser } from './Browser';
22 | import { StadiaPlusDB } from './StadiaPlusDB';
23 | import { StadiaPlusDBHook } from './components/StadiaPlusDBHook';
24 | import { StadiaGameDB } from './StadiaGameDB';
25 |
26 | // Always load languages first
27 | Language.init();
28 | Language.load();
29 |
30 | Browser.init();
31 |
32 | StadiaGameDB.update();
33 |
34 | const storageManager = new StorageManager(appdata);
35 | storageManager.checkCacheVersion();
36 |
37 | const loader = new ComponentLoader();
38 | const tab = new UITab();
39 | const webScraper = new StadiaPlusDBHook();
40 |
41 | loader.register(new Clock());
42 | // loader.register(new PopupFix());
43 | loader.register(new LibraryFilter(webScraper));
44 | loader.register(new ForceCodec(tab));
45 | loader.register(new ForceResolution(tab));
46 | loader.register(tab);
47 | loader.register(new NetworkMonitor());
48 | loader.register(new StoreFilter());
49 | loader.register(new Ratings());
50 | loader.register(new AllowWindowedMode());
51 | loader.register(new PasteFromClipboard());
52 | // loader.register(new LookingForGroup());
53 | loader.register(webScraper);
54 |
55 | StadiaPlusDB.connect('https://stadiaplus.dev')
56 | .then((connected) => {
57 | if (!connected) {
58 | Logger.error('StadiaPlusDB was unable to connect, is the server down?');
59 | return;
60 | }
61 |
62 | LocalStorage.AUTH_TOKEN.get()
63 | .then((token) => {
64 | StadiaPlusDB.authToken = token;
65 |
66 | StadiaPlusDB.getProfile()
67 | .then((profile) => {
68 | Logger.info(Language.get('stadiaplusdb.signed-in', { user: profile.name + (profile.tag === '0000' ? '✨' : `#${profile.tag}`) }));
69 | })
70 | .catch(() => {
71 | StadiaPlusDB.authToken = null;
72 | Logger.error('Not logged into Stadia+');
73 | });
74 | });
75 | });
76 |
77 | window.addEventListener('load', () => {
78 | Util.load();
79 | Snackbar.init();
80 | Modal.init();
81 | loader.start();
82 | });
83 |
--------------------------------------------------------------------------------
/src/ui/styles/Select.scss:
--------------------------------------------------------------------------------
1 | .stadiaplus_select {
2 | font-family: 'Google Sans', sans-serif;
3 | font-size: 18px;
4 |
5 | &.disabled {
6 | opacity: 0.6;
7 | pointer-events: none;
8 | cursor: default;
9 | }
10 |
11 | &.style-dark {
12 | &.ss-main {
13 | border-color: #3C3E43;
14 | width: auto;
15 |
16 | .ss-content {
17 | border-color: #3C3E43;
18 |
19 | .ss-list .ss-option {
20 | background-color: #3C3E43;
21 | color: rgba(255, 255, 255, 0.8);
22 |
23 | &.ss-disabled {
24 | background-color: #3C3E43;
25 | color: rgba(255, 255, 255, 0.4);
26 | }
27 | }
28 | }
29 |
30 | .ss-multi-selected,
31 | .ss-single-selected {
32 | background: transparent;
33 | border: none;
34 | border-bottom: #93959F 1px solid;
35 | border-radius: 0;
36 | width: 180px;
37 |
38 | .placeholder {
39 | color: rgba(255, 255, 255, 0.8);
40 | font-size: 16px;
41 | }
42 |
43 | .ss-plus span,
44 | .ss-arrow span {
45 | border-color: #93959F;
46 | }
47 |
48 | .ss-option {
49 | font-size: 18px;
50 | padding: 4px 8px;
51 | }
52 |
53 | .ss-value {
54 | margin: 0px 8px 8px 0;
55 | background-color: rgba(255, 255, 255, 0.12);
56 | font-size: 16px;
57 | border-radius: 999px;
58 | padding: 0.25rem 0.5rem;
59 |
60 | .ss-value-delete {
61 | font-family: 'Material Icons Extended';
62 | margin-left: -5px;
63 | font-size: 13px;
64 |
65 | &::after {
66 | content: 'close';
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | &.style-light {
75 | &.ss-main {
76 | width: auto;
77 |
78 | .ss-multi-selected,
79 | .ss-single-selected {
80 | background: transparent;
81 | border: none;
82 | border-bottom: #cccccc 1px solid;
83 | border-radius: 0;
84 | width: 180px;
85 |
86 | .ss-plus span,
87 | .ss-arrow span {
88 | border-color: #cccccc;
89 | }
90 | }
91 | }
92 | }
93 |
94 | &.style-slimselect-large {
95 | &.ss-main {
96 | width: 200px;
97 | height: 40px;
98 |
99 | .ss-multi-selected,
100 | .ss-single-selected {
101 | height: 100%;
102 | }
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/src/util/ElGen.ts:
--------------------------------------------------------------------------------
1 | export class ElGen {
2 | element: HTMLElement;
3 |
4 | constructor(element: string | HTMLElement) {
5 | if (element instanceof HTMLElement) {
6 | this.element = element;
7 | } else {
8 | this.element = document.createElement(element);
9 | }
10 | }
11 |
12 | id(id: string): ElGen {
13 | this.element.id = id;
14 | return this;
15 | }
16 |
17 | css(css: {[key: string]: string}): ElGen {
18 | Object.keys(css).forEach((key: string) => {
19 | this.element.style.setProperty(key, css[key]);
20 | });
21 | return this;
22 | }
23 |
24 | appendTo(element: Node | ElGen): void {
25 | if (element instanceof ElGen) {
26 | (element).element.appendChild(this.element);
27 | } else {
28 | (element).appendChild(this.element);
29 | }
30 | }
31 |
32 | prependTo(element: Element | ElGen): void {
33 | if (element instanceof ElGen) {
34 | (element).element.prepend(this.element);
35 | } else {
36 | (element).prepend(this.element);
37 | }
38 | }
39 |
40 | child(child: HTMLElement | ElGen): ElGen {
41 | if (child instanceof HTMLElement) {
42 | this.element.appendChild((child));
43 | } else {
44 | this.element.appendChild((child).element);
45 | }
46 | return this;
47 | }
48 |
49 | attr(attribute: {[name: string]: unknown}): ElGen {
50 | Object.keys(attribute).forEach((name) => {
51 | this.element.setAttribute(name, attribute[name] as string);
52 | });
53 | return this;
54 | }
55 |
56 | class(classes: {[name: string]: boolean}): ElGen {
57 | Object.keys(classes).forEach((name) => {
58 | this.element.classList.toggle(name, classes[name]);
59 | });
60 | return this;
61 | }
62 |
63 | text(text: string): ElGen {
64 | this.element.textContent = text;
65 | return this;
66 | }
67 |
68 | /**
69 | * @deprecated innerHTML is slow and should only be used if no other solution is sufficient.
70 | * @param html The html to add to this element.
71 | */
72 | html(html: string): ElGen {
73 | this.element.innerHTML = html;
74 | return this;
75 | }
76 |
77 | event(events: {[name: string]: (event: Event) => void}): ElGen {
78 | Object.keys(events).forEach((name) => {
79 | this.element.addEventListener(name, events[name]);
80 | });
81 | return this;
82 | }
83 |
84 | $sel(selector: string): ElGen | null {
85 | const element = this.element.querySelector(selector) as HTMLElement;
86 | return element != null ? new ElGen(element) : null;
87 | }
88 | }
89 |
90 | function $el(tag: string): ElGen {
91 | return new ElGen(tag);
92 | }
93 |
94 | function $sel(element: string | HTMLElement): ElGen {
95 | if (element instanceof HTMLElement) {
96 | return new ElGen(element);
97 | }
98 | return new ElGen(document.querySelector(element) as HTMLElement);
99 | }
100 |
101 | export { $el, $sel };
102 |
--------------------------------------------------------------------------------
/src/popup/src/SettingsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{
5 | Language.get('popup.settings-page.title')
6 | }}
7 |
8 |
9 |
10 | language
13 |
14 | {{ Language.get('popup.settings-page.language') }}
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
extension
30 |
31 | {{ Language.get('popup.settings-page.components') }}
32 |
33 |
{{ Language.get('popup.settings-page.edit-components') }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
89 |
90 |
97 |
--------------------------------------------------------------------------------
/src/popup/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
52 |
53 |
136 |
--------------------------------------------------------------------------------
/src/models/FilterOrder.ts:
--------------------------------------------------------------------------------
1 | import { LibraryGame } from './LibraryGame';
2 | import Util from '../util/Util';
3 |
4 | /**
5 | * Different types of filtering, represented as numbers
6 | *
7 | * @export the FilterOrder type
8 | * @class FilterOrder
9 | */
10 |
11 | export class FilterOrder {
12 | public id!: number;
13 | public name!: string;
14 | public sort!: (games: LibraryGame[]) => LibraryGame[];
15 |
16 | /**
17 | * Default Stadia sorting, recent/new games.
18 | *
19 | * @static
20 | * @memberof FilterOrder
21 | */
22 | static RECENT: FilterOrder = {
23 | id: 0,
24 | name: 'recent',
25 | sort: (games) => FilterOrder.sortRecent(games),
26 | };
27 |
28 | /**
29 | * Alphabetical order.
30 | *
31 | * @static
32 | * @memberof FilterOrder
33 | */
34 | static ALPHABETICAL: FilterOrder = {
35 | id: 1,
36 | name: 'alphabetical',
37 | sort: (games) => FilterOrder.sortAlphabetical(games),
38 | };
39 |
40 | /**
41 | * Random order.
42 | *
43 | * @static
44 | * @memberof FilterOrder
45 | */
46 | static RANDOM: FilterOrder = {
47 | id: 2,
48 | name: 'random',
49 | sort: (games) => FilterOrder.sortRandom(games),
50 | };
51 |
52 | static from(id: number): FilterOrder {
53 | const order = this.values().find((e) => e.id === id);
54 |
55 | if (order === undefined) return FilterOrder.RECENT;
56 | return order;
57 | }
58 |
59 | static values(): FilterOrder[] {
60 | return [FilterOrder.RECENT, FilterOrder.ALPHABETICAL, FilterOrder.RANDOM];
61 | }
62 |
63 | /**
64 | * Get the sorting method of the inputed order.
65 | *
66 | * @static
67 | * @returns a function sorting games by the inputed order.
68 | * @param {FilterOrder} order
69 | * @memberof FilterOrder
70 | */
71 | static getSorter(order: FilterOrder): (games: LibraryGame[]) => LibraryGame[] {
72 | switch (order) {
73 | case this.RECENT:
74 | return (games) => FilterOrder.sortRecent(games);
75 |
76 | case this.ALPHABETICAL:
77 | return (games) => FilterOrder.sortAlphabetical(games);
78 |
79 | case this.RANDOM:
80 | return (games) => FilterOrder.sortRandom(games);
81 |
82 | default:
83 | return (games) => FilterOrder.sortRecent(games);
84 | }
85 | }
86 |
87 | /**
88 | * Sort by recent games.
89 | *
90 | * @private
91 | * @static
92 | * @param {*} a
93 | * @param {*} b
94 | * @returns number representing which parameter is where.
95 | * @memberof FilterOrder
96 | */
97 | private static sortRecent(games: LibraryGame[]): LibraryGame[] {
98 | return games;
99 | }
100 |
101 | /**
102 | * Sort alphabetically.
103 | *
104 | * @private
105 | * @static
106 | * @param {*} a
107 | * @param {*} b
108 | * @returns number representing which parameter is where.
109 | * @memberof FilterOrder
110 | */
111 | private static sortAlphabetical(games: LibraryGame[]): LibraryGame[] {
112 | return games.sort((a, b) => a.name.localeCompare(b.name));
113 | }
114 |
115 | private static sortRandom(games: LibraryGame[]): LibraryGame[] {
116 | return Util.shuffle(games);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/StadiaPlusDBHook.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | /* eslint-disable @typescript-eslint/ban-ts-comment */
3 | /* eslint-disable import/no-webpack-loader-syntax */
4 | import { Component } from '../Component';
5 | import Logger from '../Logger';
6 | import Util from '../util/Util';
7 | import { Language } from '../Language';
8 | import { StadiaPlusDB } from '../StadiaPlusDB';
9 | import './styles/StadiaPlusDBHook.scss';
10 |
11 | // Import the runnable as a raw string
12 | // @ts-ignore
13 | import runnable from '!raw-loader!../util/WebScraperRunnable';
14 |
15 | interface StadiaPlusDBGameData {
16 | game: {
17 | name: string;
18 | }
19 | }
20 |
21 | /**
22 | * A web scraper that tracks http requests and parses them.
23 | *
24 | * @export the WebScraper type.
25 | * @class WebScraper
26 | * @extends {Component}
27 | */
28 | export class StadiaPlusDBHook extends Component {
29 | /**
30 | * The component tag, used in language files.
31 | */
32 | tag = 'stadiaplusdb';
33 |
34 | /**
35 | * The popup element.
36 | */
37 | popup: HTMLElement | null = null;
38 |
39 | /**
40 | * Is in game.
41 | */
42 | inGame = false;
43 |
44 | constructor() {
45 | super();
46 |
47 | window.addEventListener('DOMContentLoaded', () => {
48 | const sandboxer = document.createElement('button');
49 | sandboxer.style.display = 'none';
50 | sandboxer.id = 'web-scraper-sandboxer';
51 | document.body.appendChild(sandboxer);
52 | sandboxer.addEventListener('click', () => {
53 | const dataString = sandboxer.getAttribute('data');
54 |
55 | if (dataString !== null) {
56 | const data = JSON.parse(dataString) as StadiaPlusDBGameData;
57 | Logger.info(Language.get('stadiaplusdb.updating', { game: data.game.name }));
58 | StadiaPlusDB.ProfileConnector.setData(data)
59 | .catch((err) => Logger.error(err));
60 | }
61 | });
62 |
63 | const script = document.createElement('script');
64 | script.innerHTML = runnable as string;
65 | document.body.appendChild(script);
66 | });
67 | }
68 |
69 | /**
70 | * Called on startup, logs to the console.
71 | *
72 | * @memberof WebScraper
73 | */
74 | onStart(): void {
75 | this.active = true;
76 | Logger.component(Language.get('component.enabled', { name: this.name }));
77 | }
78 |
79 | /**
80 | * Called on stop, logs to the console.
81 | *
82 | * @memberof WebScraper
83 | */
84 | onStop(): void {
85 | this.active = false;
86 | Logger.component(Language.get('component.disabled', { name: this.name }));
87 | }
88 |
89 | updateGame(uuid: string): void {
90 | Util.desandbox(`WebScraperRunnable.update('${uuid}')`);
91 | }
92 |
93 | oldURL = '';
94 | onUpdate(): void {
95 | if (StadiaPlusDB.isAuthenticated()) {
96 | if (location.href !== this.oldURL) {
97 | if (location.href.includes('player')) {
98 | Util.desandbox(`WebScraperRunnable.update('${location.href.split('/').pop() as string}')`);
99 | } else if (this.oldURL.includes('player')) {
100 | Util.desandbox(`WebScraperRunnable.update('${this.oldURL.split('/').pop() as string}')`);
101 | }
102 | this.oldURL = location.href;
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/ui/Checkbox.ts:
--------------------------------------------------------------------------------
1 | import '../../node_modules/pretty-checkbox/src/pretty-checkbox.scss';
2 | import { CheckboxShape } from '../models/CheckboxShape';
3 | import { CheckboxStyle } from '../models/CheckboxStyle';
4 | import { CheckboxInstance } from '../models/CheckboxInstance';
5 |
6 | export class Checkbox {
7 | private label: string;
8 | private shape: string = CheckboxShape.DEFAULT;
9 | private style: string = CheckboxStyle.DEFAULT;
10 | private color: string | undefined;
11 | private animation: string | undefined;
12 | private border = true;
13 | private icon: string | undefined;
14 | private disabled = false;
15 | private bigger: boolean | undefined;
16 |
17 | constructor(label: string) {
18 | this.label = label;
19 | }
20 |
21 | setShape(shape: string): Checkbox {
22 | this.shape = shape;
23 | return this;
24 | }
25 |
26 | setStyle(style: string): Checkbox {
27 | this.style = style;
28 | return this;
29 | }
30 |
31 | setColor(color: string): Checkbox {
32 | this.color = color;
33 | return this;
34 | }
35 |
36 | setAnimation(animation: string): Checkbox {
37 | this.animation = animation;
38 | return this;
39 | }
40 |
41 | setBorder(border: boolean): Checkbox {
42 | this.border = border;
43 | return this;
44 | }
45 |
46 | setIcon(icon: string): Checkbox {
47 | this.icon = icon;
48 | return this;
49 | }
50 |
51 | setDisabled(disabled: boolean): Checkbox {
52 | this.disabled = disabled;
53 | return this;
54 | }
55 |
56 | setBigger(bigger: boolean): Checkbox {
57 | this.bigger = bigger;
58 | return this;
59 | }
60 |
61 | build(): CheckboxInstance {
62 | // Create element
63 | const element = document.createElement('div');
64 |
65 | // Add main classes
66 | element.classList.add('pretty', 'p-default');
67 |
68 | // If style is not default, add style
69 | if (this.shape) {
70 | element.classList.add(this.shape);
71 | }
72 |
73 | // If style is not default, add style
74 | if (this.style) {
75 | element.classList.add(this.style);
76 | }
77 |
78 | // If animated, add animation
79 | if (this.animation) {
80 | element.classList.add(this.animation);
81 | }
82 |
83 | // Set bigger
84 | if (this.bigger) {
85 | element.classList.add('p-bigger');
86 | }
87 |
88 | // Set border
89 | if (!this.border) {
90 | element.classList.add('p-plain');
91 | }
92 |
93 | // Add checkbox input
94 | const checkbox = document.createElement('input');
95 | checkbox.type = 'checkbox';
96 | checkbox.disabled = this.disabled;
97 | element.appendChild(checkbox);
98 |
99 | // Add state div
100 | const state = document.createElement('div');
101 | state.classList.add('state');
102 |
103 | // If colored, add color
104 | if (this.color) {
105 | state.classList.add(this.color);
106 | }
107 |
108 | // If has icon, add icon
109 | if (this.icon) {
110 | element.classList.add('p-icon');
111 |
112 | const icon = document.createElement('span');
113 | icon.classList.add('material-icons');
114 | icon.innerHTML = this.icon;
115 |
116 | state.appendChild(icon);
117 | }
118 |
119 | // Add label
120 | const label = document.createElement('label');
121 | label.innerHTML = this.label;
122 | state.appendChild(label);
123 |
124 | element.appendChild(state);
125 |
126 | return { pretty: element, checkbox };
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/popup/src/ComponentPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{
5 | Language.get('popup.component-page.title')
6 | }}
7 |
8 |
9 |
10 |
11 | Hey there! This page isn't available yet as the components haven't been loaded. Go to
stadia.google.com and log in to enable this feature.
12 |
13 |
14 |
15 |
16 |
{{ Language.get(key + '.name') }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Apply
34 |
35 |
36 |
37 |
38 |
39 |
40 |
80 |
81 |
120 |
--------------------------------------------------------------------------------
/src/Storage.ts:
--------------------------------------------------------------------------------
1 | import { AppdataManifest } from './models/AppdataManifest';
2 |
3 | export class LocalStorage {
4 | static CODEC = new LocalStorage('Codec', 'codec');
5 | static RESOLUTION = new LocalStorage('Resolution', 'resolution');
6 | static MONITOR_STATS = new LocalStorage('Monitor Stats', 'monitor-stats');
7 | static CACHE_VERSION = new LocalStorage('Cache Version', 'cache-version');
8 | static AUTH_TOKEN = new LocalStorage('Authentication Token', 'auth-token');
9 |
10 | name: string;
11 | tag: string;
12 |
13 | constructor(name: string, tag: string) {
14 | this.name = name;
15 | this.tag = tag;
16 | }
17 |
18 | get(): Promise {
19 | return new Promise((resolve) => {
20 | chrome.storage.local.get(
21 | [this.tag],
22 | (result: { [tag: string]: unknown }) => resolve(result[this.tag]),
23 | );
24 | });
25 | }
26 |
27 | set(value: unknown): Promise {
28 | return new Promise((resolve) => {
29 | chrome.storage.local.set({ [this.tag]: value }, resolve);
30 | });
31 | }
32 |
33 | static get(storages: LocalStorage[]): Promise {
34 | return new Promise((resolve) => {
35 | chrome.storage.local.get(storages.map((e) => e.tag), resolve);
36 | });
37 | }
38 |
39 | static set(storages: { [key: string]: unknown }): Promise {
40 | return new Promise((resolve) => {
41 | chrome.storage.local.set(storages, resolve);
42 | });
43 | }
44 |
45 | static clear(): void {
46 | chrome.storage.local.clear();
47 | }
48 | }
49 |
50 | export class SyncStorage {
51 | static LIBRARY_GAMES = new SyncStorage('Library Games', 'games');
52 | static LIBRARY_SORT_ORDER = new SyncStorage('Sort Order', 'sort-order');
53 | static LIBRARY_SORT_DIRECTION = new SyncStorage('Sort Direction', 'sort-direction');
54 | static LANGUAGE = new SyncStorage('Language', 'language');
55 | static COMPONENTS = new SyncStorage('Components', 'components');
56 |
57 | name: string;
58 | tag: string;
59 |
60 | constructor(name: string, tag: string) {
61 | this.name = name;
62 | this.tag = tag;
63 | }
64 |
65 | get(): Promise {
66 | return new Promise((resolve) => {
67 | chrome.storage.sync.get(
68 | [this.tag],
69 | (result: { [tag: string]: unknown }) => resolve(result[this.tag]),
70 | );
71 | });
72 | }
73 |
74 | set(value: unknown): Promise {
75 | return new Promise((resolve) => {
76 | chrome.storage.sync.set({ [this.tag]: value }, resolve);
77 | });
78 | }
79 |
80 | static get(storages: LocalStorage[]): Promise {
81 | return new Promise((resolve) => {
82 | chrome.storage.sync.get(storages.map((e) => e.tag), resolve);
83 | });
84 | }
85 |
86 | static set(storages: { [key: string]: unknown }): Promise {
87 | return new Promise((resolve) => {
88 | chrome.storage.sync.set(storages, resolve);
89 | });
90 | }
91 |
92 | static clear(): void {
93 | chrome.storage.sync.clear();
94 | }
95 | }
96 |
97 | export class StorageManager {
98 | appdata: AppdataManifest;
99 | constructor(appdata: AppdataManifest) {
100 | this.appdata = appdata;
101 | }
102 |
103 | async checkCacheVersion(): Promise {
104 | const cacheVersion = await LocalStorage.CACHE_VERSION.get() as number;
105 |
106 | if (cacheVersion === undefined || this.appdata['cache-version'] > cacheVersion) {
107 | this.appdata['clear-keys'].local.forEach((key: string) => {
108 | void LocalStorage.set({ [key]: null });
109 | });
110 |
111 | this.appdata['clear-keys'].sync.forEach((key: string) => {
112 | void SyncStorage.set({ [key]: null });
113 | });
114 | }
115 |
116 | void LocalStorage.CACHE_VERSION.set(this.appdata['cache-version']);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/util/WebScraperRunnable.js:
--------------------------------------------------------------------------------
1 | const WebScraperRunnable = {
2 | games: [],
3 |
4 | fetchData(userid, gameid) {
5 | return new Promise((resolve, reject) => {
6 | fetch(`https://stadia.google.com/profile/${userid}/detail/${gameid}`)
7 | .then((response) => response.text())
8 | .then((text) => {
9 | const playData = text.match(new RegExp(`\\[\\[\\["${gameid}",.+\\n.+\\n,\\[([0-9]+)`));
10 | const achievementData = text.match(new RegExp("AF_initDataCallback\\(\\{ *key: *'ds:3'.*?data: *((.|\\n)*?), *sideChannel: *\\{\\}\\}\\)"));
11 |
12 | if (playData == null) return;
13 |
14 | const data = JSON.parse(achievementData[1])[0];
15 |
16 | const achievements = [];
17 | for (const e of data[5][0]) {
18 | achievements.push({
19 | name: e[0],
20 | description: e[1],
21 | value: e[3],
22 | icon: e[8][0][0][1],
23 | game: e[6],
24 | id: e[7],
25 | });
26 | }
27 |
28 | const user = {
29 | name: data[5][3][0][0],
30 | tag: data[5][3][0][1],
31 | avatar: data[5][3][1][1],
32 | };
33 |
34 | resolve({
35 | game: {
36 | uuid: data[0][0],
37 | name: data[0][1],
38 | },
39 | achievements,
40 | user,
41 | time: playData[1],
42 | });
43 | })
44 | .catch(reject);
45 | });
46 | },
47 |
48 | update(uuid) {
49 | if (uuid == null) return;
50 |
51 | const userId = document.querySelector('.ksZYgc.VGZcUb').getAttribute('data-player-id');
52 | WebScraperRunnable.fetchData(userId, uuid)
53 | .then((data) => {
54 | const sandboxer = document.getElementById('web-scraper-sandboxer');
55 | sandboxer.setAttribute('data', JSON.stringify(data));
56 | sandboxer.click();
57 |
58 | let updated = localStorage.getItem('updatedGames');
59 | if (updated != null) {
60 | updated = JSON.parse(updated);
61 | } else {
62 | updated = {};
63 | }
64 | updated[uuid] = true;
65 | localStorage.setItem('updatedGames', JSON.stringify(updated));
66 | })
67 | .catch((e) => console.error(e));
68 | },
69 |
70 | autoUpdate: false,
71 | autoUpdateInterval: 2 * 60 * 1000, // Two minutes
72 | setAutoUpdate(value) {
73 | this.autoUpdate = value;
74 | if (this.autoUpdate) {
75 | const loop = () => {
76 | let updated = localStorage.getItem('updatedGames');
77 | if (updated != null) {
78 | updated = JSON.parse(updated);
79 | } else {
80 | updated = {};
81 | }
82 |
83 | try {
84 | if (this.games.length > 0) {
85 | let hasUpdated = false;
86 | for (const uuid of this.games) {
87 | if (!updated.hasOwnProperty(uuid) || !updated[uuid]) {
88 | this.update(uuid);
89 | hasUpdated = true;
90 | break;
91 | }
92 | }
93 |
94 | if (!hasUpdated) {
95 | this.setAutoUpdate(false);
96 | }
97 | }
98 | } catch (e) {
99 | console.error(e);
100 | }
101 |
102 | if (this.autoUpdate) {
103 | setTimeout(loop, this.autoUpdateInterval);
104 | }
105 | };
106 | loop();
107 | }
108 | },
109 | };
110 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | module.exports = {
3 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
4 | // This option interrupts the configuration hierarchy at this file
5 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
6 | root: true,
7 |
8 | // https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser
9 | // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
10 | // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
11 | parserOptions: {
12 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration
13 | // https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint
14 | // Needed to make the parser take into account 'vue' files
15 | extraFileExtensions: ['.vue'],
16 | parser: '@typescript-eslint/parser',
17 | project: resolve(__dirname, './tsconfig.json'),
18 | tsconfigRootDir: __dirname,
19 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
20 | sourceType: 'module' // Allows for the use of imports
21 | },
22 |
23 | env: {
24 | browser: true
25 | },
26 |
27 | // Rules order is important, please avoid shuffling them
28 | extends: [
29 | // Base ESLint recommended rules
30 | // 'eslint:recommended',
31 |
32 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
33 | // ESLint typescript rules
34 | 'plugin:@typescript-eslint/recommended',
35 | // consider disabling this class of rules if linting takes too long
36 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
37 |
38 | // Uncomment any of the lines below to choose desired strictness,
39 | // but leave only one uncommented!
40 | // See https://eslint.vuejs.org/rules/#available-rules
41 | 'plugin:vue/essential', // Priority A: Essential (Error Prevention)
42 | 'plugin:vue/strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
43 | // 'plugin:vue/recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
44 |
45 | 'airbnb-base'
46 |
47 | ],
48 |
49 | plugins: [
50 | // required to apply rules which need type information
51 | '@typescript-eslint',
52 |
53 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
54 | // required to lint *.vue files
55 | 'vue',
56 |
57 | ],
58 |
59 | globals: {
60 | ga: true, // Google Analytics
61 | cordova: true,
62 | __statics: true,
63 | process: true,
64 | Capacitor: true,
65 | chrome: true
66 | },
67 |
68 | // add your custom rules here
69 | rules: {
70 | 'no-param-reassign': 'off',
71 | 'indent': ['warn', 4, { "SwitchCase": 1 }],
72 | 'no-void': 'off',
73 | 'no-multiple-empty-lines': 'warn',
74 | 'lines-between-class-members': 'off',
75 | "no-shadow": "off",
76 |
77 | 'import/first': 'off',
78 | 'import/named': 'error',
79 | 'import/namespace': 'error',
80 | 'import/default': 'error',
81 | 'import/export': 'error',
82 | 'import/extensions': 'off',
83 | 'import/no-unresolved': 'off',
84 | 'import/no-extraneous-dependencies': 'off',
85 | 'import/prefer-default-export': 'off',
86 | 'import/no-webpack-loader-syntax': 'off',
87 | 'prefer-promise-reject-errors': 'off',
88 | 'max-classes-per-file': 'off',
89 | 'class-methods-use-this': 'off',
90 | 'no-restricted-globals': 'warn',
91 |
92 | // TypeScript
93 | quotes: ['warn', 'single', { avoidEscape: true }],
94 | "@typescript-eslint/no-shadow": ["error"],
95 | '@typescript-eslint/ban-ts-comment': 'off',
96 | // '@typescript-eslint/explicit-function-return-type': 'off',
97 | // '@typescript-eslint/explicit-module-boundary-types': 'off',
98 | // '@typescript-eslint/no-unsafe-call': 'warn',
99 | // '@typescript-eslint/no-unsafe-member-access': 'off',
100 | // '@typescript-eslint/no-unsafe-assignment': 'off',
101 |
102 | // allow debugger during development only
103 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Language.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import Logger from './Logger';
3 | import { SyncStorage } from './Storage';
4 | import lang_enUS_data from './lang/en-US.json';
5 | import lang_svSE_data from './lang/sv-SE.json';
6 | import lang_frFR_data from './lang/fr-FR.json';
7 | import lang_itIT_data from './lang/it-IT.json';
8 | import lang_esES_data from './lang/es-ES.json';
9 | import lang_deDE_data from './lang/de-DE.json';
10 | import lang_ukUA_data from './lang/uk-UA.json';
11 | // import lang_enSTEEF_data from './lang/en-STEEF.json';
12 | import lang_euES_data from './lang/eu-ES.json';
13 | import lang_glES_data from './lang/gl-ES.json';
14 | import lang_ruRU_data from './lang/ru-RU.json';
15 | import lang_nlBE_data from './lang/nl-BE.json';
16 | import lang_ptBR_data from './lang/pt-BR.json';
17 |
18 | export class Language {
19 | tag: string;
20 | name: string;
21 | data: { [key: string]: unknown } = {};
22 |
23 | constructor(name: string, tag: string, data: { [key: string]: unknown; }) {
24 | this.tag = tag;
25 | this.name = name;
26 | this.data = data;
27 | }
28 |
29 | register(): void {
30 | Language.languages.push(this);
31 | }
32 |
33 | get(name: string, vars?: { [key: string]: unknown }): string {
34 | const keys = name.split(/\./g);
35 | let val: unknown = this.data;
36 | keys.forEach((key) => {
37 | val = (val as { [key: string]: unknown })[key] as ({ [key: string]: unknown } | string);
38 | });
39 |
40 | if (vars !== undefined) {
41 | Object.keys(vars).forEach((variable) => {
42 | val = (val as string).split(`{{${variable}}}`).join(vars[variable] as string);
43 | });
44 | }
45 |
46 | return val as string;
47 | }
48 |
49 | setDefault(): void {
50 | Language.default = this;
51 | }
52 |
53 | static languages: Language[] = [];
54 | static default: Language;
55 | static current: Language;
56 | static async load(): Promise {
57 | // Check for the first language that isn't equal to the default
58 | let language = await SyncStorage.LANGUAGE.get();
59 |
60 | if (language === undefined || language === 'automatic') {
61 | language = this.automatic();
62 | }
63 |
64 | Logger.info(Language.get('lang-config', { language }));
65 | this.languages.forEach((lang) => {
66 | if (lang.tag === language) {
67 | this.current = lang;
68 | }
69 | });
70 | }
71 |
72 | static set(language: Language): void {
73 | this.current = language;
74 | }
75 |
76 | static automatic(): string | undefined {
77 | return window.navigator.languages.find(
78 | (l: string) => l.length >= 5
79 | && (this.default === undefined || l !== this.default.tag),
80 | );
81 | }
82 |
83 | static init(): void {
84 | const lang_deDE = new Language('Deutsche (DE)', 'de-DE', lang_deDE_data);
85 | lang_deDE.register();
86 |
87 | const lang_esES = new Language('Español (ES)', 'es-ES', lang_esES_data);
88 | lang_esES.register();
89 |
90 | const lang_enUS = new Language('English (US)', 'en-US', lang_enUS_data);
91 | lang_enUS.register();
92 | lang_enUS.setDefault();
93 |
94 | // const lang_enSTEEF = new Language('English (Steef)', 'en-STEEF', lang_enSTEEF_data);
95 | // lang_enSTEEF.register();
96 |
97 | const lang_frFR = new Language('Français (FR)', 'fr-FR', lang_frFR_data);
98 | lang_frFR.register();
99 |
100 | const lang_itIT = new Language('Italiano (IT)', 'it-IT', lang_itIT_data);
101 | lang_itIT.register();
102 |
103 | const lang_svSE = new Language('Svenska (SE)', 'sv-SE', lang_svSE_data);
104 | lang_svSE.register();
105 |
106 | const lang_ukUA = new Language('Українська (UA)', 'uk-UA', lang_ukUA_data);
107 | lang_ukUA.register();
108 |
109 | const lang_euES = new Language('Euskara (EU)', 'eu-ES', lang_euES_data);
110 | lang_euES.register();
111 |
112 | const lang_glES = new Language('Galego (GL)', 'gl-ES', lang_glES_data);
113 | lang_glES.register();
114 |
115 | const lang_ruRU = new Language('русский (RU)', 'ru-RU', lang_ruRU_data);
116 | lang_ruRU.register();
117 |
118 | const lang_nlBE = new Language('Nederlands (BE)', 'nl-BE', lang_nlBE_data);
119 | lang_nlBE.register();
120 |
121 | const lang_ptBR = new Language('Português (BR)', 'pt-BR', lang_ptBR_data);
122 | lang_ptBR.register();
123 | }
124 |
125 | static get(name: string, vars?: { [key: string]: unknown }): string {
126 | if (this.current === undefined) {
127 | this.current = this.default;
128 | }
129 | const val = this.current.get(name, vars);
130 |
131 | return val;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/components/AllowWindowedMode.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '../Component';
2 | import Logger from '../Logger';
3 | import { Language } from '../Language';
4 | import { UIButton } from '../ui/UIButton';
5 | import Util from '../util/Util';
6 |
7 | /**
8 | * A button allowing users to play Stadia in windowed mode.
9 | *
10 | * @export the AllowWindowedMode type.
11 | * @class AllowWindowedMode
12 | * @extends {Component}
13 | */
14 | export class AllowWindowedMode extends Component {
15 | /**
16 | * The component tag, used in language files.
17 | */
18 | tag = 'allow-windowed-mode';
19 |
20 | /**
21 | * The [[UIButton]] used to toggle windowed mode.
22 | */
23 | button!: UIButton;
24 |
25 | /**
26 | * Whether windowed mode is enabled or not
27 | */
28 | windowed = false;
29 |
30 | constructor() {
31 | super();
32 |
33 | /**
34 | * Main event, stops built-in fullscreen events from reaching
35 | * Stadia whenever windowed mode is enabled.
36 | * */
37 | window.addEventListener(
38 | 'fullscreenchange',
39 | (event: Event) => {
40 | if (this.windowed) {
41 | event.stopPropagation();
42 | }
43 | },
44 | true,
45 | );
46 | }
47 |
48 | /**
49 | * Enters windowed mode.
50 | *
51 | * @memberof AllowWindowedMode
52 | */
53 | enterWindowed(): void {
54 | this.windowed = true;
55 | void document.exitFullscreen();
56 | }
57 |
58 | /**
59 | * Exits windowed mode
60 | *
61 | * @memberof AllowWindowedMode
62 | */
63 | exitWindowed(): void {
64 | this.windowed = false;
65 | void document.documentElement.requestFullscreen();
66 | }
67 |
68 | /**
69 | * Called on startup, initializes important variables.
70 | *
71 | * @memberof AllowWindowedMode
72 | */
73 | onStart(): void {
74 | Logger.component(
75 | Language.get('component.enabled', { name: this.name }),
76 | );
77 | this.active = true;
78 |
79 | const icon = chrome.runtime.getURL('images/icons/windowed.svg');
80 | this.button = new UIButton(
81 | icon,
82 | Language.get('allow-windowed-mode.button-label.windowed'),
83 | this.id,
84 | );
85 | }
86 |
87 | /**
88 | * Called on stop, makes sure to dispose of elements and variables.
89 | *
90 | * @memberof AllowWindowedMode
91 | */
92 | onStop(): void {
93 | this.exitWindowed();
94 | this.active = false;
95 | }
96 |
97 | /**
98 | * Update button labels and icons to fit current mode.
99 | *
100 | * @memberof AllowWindowedMode
101 | */
102 | updateButton(): void {
103 | const icon = chrome.runtime.getURL('images/icons/windowed.svg');
104 | const exitIcon = chrome.runtime.getURL(
105 | 'images/icons/windowed_exit.svg',
106 | );
107 |
108 | if (this.windowed) {
109 | this.button.setIcon(exitIcon);
110 | this.button.setTitle(
111 | Language.get('allow-windowed-mode.button-label.fullscreen'),
112 | );
113 | } else {
114 | this.button.setIcon(icon);
115 | this.button.setTitle(
116 | Language.get('allow-windowed-mode.button-label.windowed'),
117 | );
118 | }
119 | }
120 |
121 | // Whether events have been added already or not.
122 | eventsAdded = false;
123 |
124 | /**
125 | * Called once every second, updates component elements and variables
126 | *
127 | * @memberof AllowWindowedMode
128 | */
129 | onUpdate(): void {
130 | // If menu is open and a game is playing.
131 | if (Util.isMenuOpen() && Util.isInGame()) {
132 | // If the button doesn't already exist in the current renderer
133 | if (!this.exists()) {
134 | // Check for new renderers
135 | this.updateRenderer();
136 |
137 | // Create the button instance
138 | this.button.create(() => {
139 | // If events are already added, don't add them again
140 | if (!this.eventsAdded) {
141 | this.button.onPressed(() => {
142 | if (this.windowed) {
143 | this.exitWindowed();
144 | } else {
145 | this.enterWindowed();
146 | }
147 | this.updateButton();
148 | });
149 | this.eventsAdded = true;
150 | }
151 | });
152 | }
153 |
154 | if (!this.button.container?.exists()) {
155 | this.button.container?.create();
156 | }
157 | } else if (this.existsAnywhere()) {
158 | this.button.destroy();
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/components/Ratings.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '../Component';
2 | import Logger from '../Logger';
3 | import Util from '../util/Util';
4 | import './styles/Ratings.scss';
5 | import { Language } from '../Language';
6 | import { StadiaGameDB } from '../StadiaGameDB';
7 |
8 | /**
9 | * A component adding Metacritic ratings to every Stadia game.
10 | *
11 | * @export the Ratings type
12 | * @class Ratings
13 | * @extends {Component}
14 | */
15 | export class Ratings extends Component {
16 | /**
17 | * The component tag, used in language files.
18 | */
19 | tag = 'ratings';
20 |
21 | /**
22 | * The rating element.
23 | */
24 | element: HTMLElement | null = null;
25 |
26 | /**
27 | * The value from each bound in which a game will get 0 or 5 stars.
28 | */
29 | graceAmount = 10;
30 |
31 | /**
32 | * The maximum number of stars to award.
33 | */
34 | maxStars = 5;
35 |
36 | /**
37 | * Creates the rating element.
38 | *
39 | * @memberof Ratings
40 | */
41 | createElement(): void {
42 | this.element = document.createElement('div');
43 | this.element.classList.add('stadiaplus_rating', 'material-icons');
44 | }
45 |
46 | /**
47 | * The current game UUID.
48 | *
49 | * @returns the game UUID as a string.
50 | * @memberof Ratings
51 | */
52 | getUUID(): string {
53 | // eslint-disable-next-line no-restricted-globals
54 | return location.href.substring(
55 | 'https://stadia.google.com/store/details/'.length,
56 | 'https://stadia.google.com/store/details/'.length + 36,
57 | );
58 | }
59 |
60 | /**
61 | * Updates the current rating, fetching it from the database.
62 | *
63 | * @memberof Ratings
64 | */
65 | updateRating(): void {
66 | const uuid = this.getUUID();
67 | const { rating } = StadiaGameDB.get(uuid);
68 | if (rating === undefined) return;
69 |
70 | this.element?.setAttribute('data-rating', rating.toString());
71 | }
72 |
73 | /**
74 | * Calculates how many stars a game should have based on it's rating.
75 | *
76 | * @param {number} rating the game's rating.
77 | * @returns {string[]} an array of icon strings, being either "star",
78 | * "star_half" or "star_outline".
79 | * @memberof Ratings
80 | */
81 | getStars(rating: number): string[] {
82 | const outputStars = [];
83 |
84 | // Clamps the rating to values between 0 and 1,
85 | // where (0 + graceAmount) is 0 and (100 - graceAmount) is 1
86 | const clampedR = (rating / 100)
87 | * (1 + (this.graceAmount / 100) * 2)
88 | - (this.graceAmount / 100);
89 |
90 | for (let i = 0, r = clampedR; i < this.maxStars; i += 1, r -= 1 / this.maxStars) {
91 | if (r >= 1 / this.maxStars) {
92 | outputStars.push('star');
93 | } else if (r >= 0) {
94 | outputStars.push('star_half');
95 | } else {
96 | outputStars.push('star_outline');
97 | }
98 | }
99 |
100 | return outputStars;
101 | }
102 |
103 | /**
104 | * Called on startup, initializes important variables.
105 | *
106 | * @memberof Ratings
107 | */
108 | onStart(): void {
109 | this.active = true;
110 | this.createElement();
111 | if (this.element !== null) {
112 | this.element.id = this.id;
113 | }
114 |
115 | Logger.component(Language.get('component.enabled', { name: this.name }));
116 | }
117 |
118 | /**
119 | * Called on stop, makes sure to dispose of elements and variables.
120 | *
121 | * @memberof Ratings
122 | */
123 | onStop(): void {
124 | this.active = false;
125 | this.element?.remove();
126 | Logger.component(Language.get('component.disabled', { name: this.name }));
127 | }
128 |
129 | /**
130 | * Called every second, updates the rating element
131 | * to make sure it always displays the correct value.
132 | *
133 | * @memberof Ratings
134 | */
135 | onUpdate(): void {
136 | if (Util.isInStoreDetail()) {
137 | if (!this.exists()) {
138 | this.updateRating();
139 | this.updateRenderer();
140 | const rating = parseInt(this.element?.getAttribute('data-rating') as string, 10);
141 | const stars = this.getStars(rating);
142 |
143 | if (rating > 0) {
144 | const nextSibling = this.renderer?.querySelector('.ZzBJSb > .BMUnfd');
145 | if (nextSibling === null || this.element === null) return;
146 |
147 | nextSibling?.parentNode?.insertBefore(this.element, nextSibling);
148 |
149 | this.element.innerHTML = `
150 | ${stars.join(' ')}
151 |
152 |
153 | ${rating} / 100 (${Language.get('ratings.source-name')})
154 |
155 | `;
156 | }
157 | }
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/popup/src/components/Profile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
![]()
6 |
7 |
{{ user.name }}
8 |
#{{ user.tag }}
9 |
10 |
more_vert
11 |
12 | {{ Language.get('popup.main-page.profile.sign-out') }}
13 | {{ Language.get('popup.main-page.profile.wipe-data') }}
14 |
15 |
16 |
17 | person {{ Language.get('popup.main-page.profile.view-profile') }}
18 |
19 |
20 | {{ Language.get('popup.main-page.profile.not-available') }}
21 |
22 |
exit_to_app{{ Language.get('popup.main-page.profile.sign-out') }}
23 |
24 |
34 |
35 |
36 |
37 |
110 |
111 |
175 |
--------------------------------------------------------------------------------
/src/StadiaPlusDB.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 | import axios from 'axios';
3 | import { LocalStorage } from './Storage';
4 | import Logger from './Logger';
5 | import { Language } from './Language';
6 |
7 | export class StadiaPlusDB {
8 | static LFGConnector: LFGConnector;
9 | static ProfileConnector: ProfileConnector;
10 |
11 | static url: string;
12 | static authToken: string;
13 | static connected: boolean;
14 |
15 | static connect(url: string): Promise {
16 | Logger.info(Language.get('stadiaplusdb.connecting', { url }));
17 | StadiaPlusDB.url = url;
18 | StadiaPlusDB.LFGConnector = new LFGConnector();
19 | StadiaPlusDB.ProfileConnector = new ProfileConnector();
20 |
21 | return new Promise((resolve) => {
22 | void this.testConnection()
23 | .then((connected) => {
24 | StadiaPlusDB.connected = connected;
25 |
26 | resolve(connected);
27 | });
28 | });
29 | }
30 |
31 | static testConnection(): Promise {
32 | return new Promise((resolve) => {
33 | axios.get(`${StadiaPlusDB.url}/api/ping`)
34 | .then(() => resolve(true))
35 | .catch(() => resolve(false));
36 | });
37 | }
38 |
39 | static getProfile(): Promise {
40 | return new Promise((resolve, reject) => {
41 | axios.get(`${StadiaPlusDB.url}/api/user?token=${StadiaPlusDB.authToken}`)
42 | .then((res) => {
43 | if (Object.prototype.hasOwnProperty.call(res.data, 'error')) {
44 | reject(res.data);
45 | return;
46 | }
47 | resolve(res.data);
48 | })
49 | .catch(() => reject({ error: 'Could not connect to profile server' }));
50 | });
51 | }
52 |
53 | static isConnected(): boolean {
54 | return StadiaPlusDB.connected && StadiaPlusDB.url != null;
55 | }
56 |
57 | static isAuthenticated(): boolean {
58 | return StadiaPlusDB.isConnected() && StadiaPlusDB.authToken != null;
59 | }
60 |
61 | static authenticate(): Promise {
62 | return new Promise((resolve, reject) => {
63 | if (!StadiaPlusDB.isConnected()) {
64 | reject({
65 | error: 'Not connected to any database',
66 | });
67 | }
68 |
69 | chrome.identity.launchWebAuthFlow(
70 | {
71 | url: `${StadiaPlusDB.url}/auth/google?redirect=${chrome.identity.getRedirectURL('database')}`,
72 | interactive: true,
73 | },
74 | (responseUrl?: string) => {
75 | if (responseUrl == null) {
76 | reject({
77 | error: 'Authentication failed',
78 | });
79 | return;
80 | }
81 |
82 | const url = new URL(responseUrl);
83 | StadiaPlusDB.authToken = url.hash.substring(1);
84 | void LocalStorage.AUTH_TOKEN.set(StadiaPlusDB.authToken)
85 | .then(() => resolve(StadiaPlusDB.authToken));
86 | },
87 | );
88 | });
89 | }
90 |
91 | static signout(): Promise {
92 | return axios({
93 | method: 'post',
94 | url: `${StadiaPlusDB.url}/api/signout`,
95 | data: {
96 | token: StadiaPlusDB.authToken,
97 | },
98 | });
99 | }
100 |
101 | static wipedata(): Promise {
102 | return axios({
103 | method: 'post',
104 | url: `${StadiaPlusDB.url}/api/wipedata`,
105 | data: {
106 | token: StadiaPlusDB.authToken,
107 | },
108 | });
109 | }
110 | }
111 |
112 | export class LFGConnector {
113 | get(game: string): Promise {
114 | return axios.get(`${StadiaPlusDB.url}/api/lfg/${game}`);
115 | }
116 |
117 | post(game: string): Promise {
118 | if (!StadiaPlusDB.isConnected()) {
119 | return new Promise((resolve, reject) => reject({ error: 'Not connected to the StadiaPlusDB database' }));
120 | }
121 | if (!StadiaPlusDB.isAuthenticated()) {
122 | return new Promise((resolve, reject) => reject({ error: 'Not authenticated with StadiaPlusDB' }));
123 | }
124 | return axios({
125 | method: 'post',
126 | url: `${StadiaPlusDB.url}/api/lfg`,
127 | data: {
128 | token: StadiaPlusDB.authToken,
129 | game,
130 | },
131 | });
132 | }
133 | }
134 |
135 | export class ProfileConnector {
136 | async setData(data: unknown): Promise {
137 | if (!StadiaPlusDB.isConnected()) {
138 | return new Promise((resolve, reject) => reject({ error: 'Not connected to the StadiaPlusDB database' }));
139 | }
140 | if (!StadiaPlusDB.isAuthenticated()) {
141 | return new Promise((resolve, reject) => reject({ error: 'Not authenticated with StadiaPlusDB' }));
142 | }
143 |
144 | return axios({
145 | method: 'post',
146 | url: `${StadiaPlusDB.url}/api/update`,
147 | data: {
148 | token: StadiaPlusDB.authToken,
149 | game: data,
150 | },
151 | });
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/lang/gl-ES.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Creado por {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "A extensión está lista. Inicia Stadia e lume! 🎮",
9 | "launch-button": "Inicia Stadia",
10 | "settings-button": "Configuración",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Axuda & FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Configuración",
42 | "language": "Idioma",
43 | "components": "Compoñentes",
44 | "edit-components": "Edita os compoñentes"
45 | },
46 | "developer-page": {
47 | "title": "Avanzadas",
48 | "clear-cache-button": "Limpa a caché",
49 | "storage": "Almacenamento"
50 | },
51 | "component-page": {
52 | "title": "Compoñentes"
53 | }
54 | },
55 | "component": {
56 | "enabled": "Activouse {{name}}.",
57 | "disabled": "Desactivouse {{name}}."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Permite o modo fiestra",
61 | "button-label": {
62 | "windowed": "En fiestra",
63 | "fullscreen": "Pantalla completa"
64 | }
65 | },
66 | "clock": {
67 | "name": "Reloxo"
68 | },
69 | "force-codec": {
70 | "name": "Forza códec",
71 | "4k-tooltip": "O códec seleccionado non está disponíbel ao seleccionar 4K"
72 | },
73 | "force-resolution": {
74 | "name": "Forza resolución",
75 | "note": "Aviso: o valor indicado será o máximo que Stadia intentará acadar. Se o teu computador non é compatíbel coa resolución ou esta supera o uso de datos que teñas configurado na conta, non se activará."
76 | },
77 | "library-filter": {
78 | "name": "Filtrar",
79 | "recent": "Recentes",
80 | "alphabetical": "Alfabeticamente",
81 | "random": "Aleatoriamente",
82 | "show-hidden": "Mostrar agochados",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Monitor de rede",
94 | "heading-visible": "Estatísticas",
95 | "button-label": "Monitor",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "paste-from-clipboard": {
115 | "name": "Pegar do portapapeis"
116 | },
117 | "ratings": {
118 | "name": "Valoracións",
119 | "source-name": "Metacritic"
120 | },
121 | "store-filter": {
122 | "name": "Filtrar"
123 | },
124 | "ui-tab": {
125 | "name": "Pestaña UI de Stadia+",
126 | "button-label": "Stadia+"
127 | },
128 | "popup-fix": {
129 | "name": "Popup Fix"
130 | },
131 | "looking-for-group": {
132 | "name": "Looking For Group",
133 | "toggle-button": {
134 | "start": "Look for a Group",
135 | "stop": "Stop Looking for Groups"
136 | }
137 | },
138 | "stadiaplusdb": {
139 | "name": "Stadia+ DB",
140 | "updating": "Updating {{game}} in Stadia+ DB",
141 | "connecting": "Connecting to Stadia+ DB via {{url}}",
142 | "signed-in": "Logged into Stadia+ DB as {{user}}"
143 | },
144 | "snackbar": {
145 | "reload-to-update": "Recarga a páxina para activar os cambios.",
146 | "hide-game": "Agochóuse un xogo.",
147 | "show-game": "Un xogo deixou de estar agochado."
148 | },
149 | "automatic": "Automático",
150 | "vp9": "VP9",
151 | "h264": "H264",
152 | "apply": "Aplicar",
153 | "loading": "Loading...",
154 | "experimental": "Experimental",
155 | "4k": "4K",
156 | "1440p": "1440p",
157 | "1080p": "1080p",
158 | "720p": "720p",
159 | "lang-config": "Using language configuration {{language}}"
160 | }
161 |
--------------------------------------------------------------------------------
/src/lang/nl-BE.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Ontwikkeld door {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "De extentie is klaar. Je kan nu Stadia openen en beginnen spelen! 🎮",
9 | "launch-button": "Stadia starten",
10 | "settings-button": "Instellingen",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Help & FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Instellingen",
42 | "language": "Taal",
43 | "components": "Componenten",
44 | "edit-components": "Componenten aanpassen"
45 | },
46 | "developer-page": {
47 | "title": "Ontwikkelaar",
48 | "clear-cache-button": "Cache wissen",
49 | "storage": "Opslag"
50 | },
51 | "component-page": {
52 | "title": "Componenten"
53 | }
54 | },
55 | "component": {
56 | "enabled": "Component {{name}} werd ingeschakeld.",
57 | "disabled": "Component {{name}} werd uitgeschakeld."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Spelen in venster toestaan",
61 | "button-label": {
62 | "windowed": "In venster",
63 | "fullscreen": "Fullscreen"
64 | }
65 | },
66 | "clock": {
67 | "name": "Klok"
68 | },
69 | "force-codec": {
70 | "name": "Codec forceren",
71 | "4k-tooltip": "Codec kan niet geforceerd worden wanneer je in 4K of 1440p speelt"
72 | },
73 | "force-resolution": {
74 | "name": "Resolutie forceren",
75 | "note": "Merk op: Deze waarde is de maximale resolutie dat Stadia zal proberen te halen. Dit zal niet werken als je computer deze resolutie niet kan afspelen of als de resolutie niet beschikbaar is voor de dataverbruik selectie."
76 | },
77 | "library-filter": {
78 | "name": "Bibliotheek Filter",
79 | "recent": "Recent",
80 | "alphabetical": "Alfabetisch",
81 | "random": "Willekeurig",
82 | "show-hidden": "Verborgen weergeven",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Netwerk Monitor",
94 | "heading-visible": "Zichtbare Stats",
95 | "button-label": "Monitor",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "ratings": {
115 | "name": "Scores",
116 | "source-name": "Metacritic"
117 | },
118 | "store-filter": {
119 | "name": "Filter bewaren"
120 | },
121 | "ui-tab": {
122 | "name": "Stadia+ UI Tab",
123 | "button-label": "Stadia+"
124 | },
125 | "popup-fix": {
126 | "name": "Popup Fix"
127 | },
128 | "looking-for-group": {
129 | "name": "Looking For Group",
130 | "toggle-button": {
131 | "start": "Look for a Group",
132 | "stop": "Stop Looking for Groups"
133 | }
134 | },
135 | "stadiaplusdb": {
136 | "name": "Stadia+ DB",
137 | "updating": "Updating {{game}} in Stadia+ DB",
138 | "connecting": "Connecting to Stadia+ DB via {{url}}",
139 | "signed-in": "Logged into Stadia+ DB as {{user}}"
140 | },
141 | "snackbar": {
142 | "reload-to-update": "Herlaad de pagina om je aanpassingen te zien.",
143 | "hide-game": "Een spel werd verborgen.",
144 | "show-game": "Een spel is niet langer verborgen."
145 | },
146 | "automatic": "Automatisch",
147 | "vp9": "VP9",
148 | "h264": "H264",
149 | "apply": "Toepassen",
150 | "loading": "Loading...",
151 | "experimental": "Experimental",
152 | "4k": "4K",
153 | "1440p": "1440p",
154 | "1080p": "1080p",
155 | "720p": "720p",
156 | "lang-config": "Using language configuration {{language}}"
157 | }
158 |
--------------------------------------------------------------------------------
/src/lang/eu-ES.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Egilea: {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "Luzapena prest dago. Ireki Stadia eta hasi jolasten! Egurra! 🎮",
9 | "launch-button": "Ireki Stadia",
10 | "settings-button": "Ezarpenak",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Laguntza & FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Ezarpenak",
42 | "language": "Hizkuntza",
43 | "components": "Osagarriak",
44 | "edit-components": "Editatu osagarriak"
45 | },
46 | "developer-page": {
47 | "title": "Aurreratuak",
48 | "clear-cache-button": "Cachea ezabatu",
49 | "storage": "Biltegia"
50 | },
51 | "component-page": {
52 | "title": "Osagarriak"
53 | }
54 | },
55 | "component": {
56 | "enabled": "{{name}} gaitu egin da.",
57 | "disabled": "{{name}} desgaitu egin da."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Onartu lehio-modua",
61 | "button-label": {
62 | "windowed": "Lehioan",
63 | "fullscreen": "Pantaila osoa"
64 | }
65 | },
66 | "clock": {
67 | "name": "Erlojua"
68 | },
69 | "force-codec": {
70 | "name": "Behartu codec",
71 | "4k-tooltip": "Codec hori ezin da gaitu 4K aukerarekin"
72 | },
73 | "force-resolution": {
74 | "name": "Behartu bereizmena",
75 | "note": "Oharra: jarritako balioa izango da Stadia ezartzen saiatuko den handiena. Zure ordenagailuak ezin badu bereizmen hori exekutatu edota ez badator bat Stadiaren zure data-ezarpenekin ezin izango da gauzatu.."
76 | },
77 | "library-filter": {
78 | "name": "Bildumaren iragazkia",
79 | "recent": "Arestikoak",
80 | "alphabetical": "Alfabetikoa",
81 | "random": "Aliritzira",
82 | "show-hidden": "Erakutsi izkutatutakoak",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Sare-monitorea",
94 | "heading-visible": "Ikusteko estatistikak",
95 | "button-label": "Monitorea",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "paste-from-clipboard": {
115 | "name": "Itsatsi arbelean"
116 | },
117 | "ratings": {
118 | "name": "Balorazioak",
119 | "source-name": "Metacritic"
120 | },
121 | "store-filter": {
122 | "name": "Denda-iragazkia"
123 | },
124 | "ui-tab": {
125 | "name": "Stadia+ UI Fitxa",
126 | "button-label": "Stadia+"
127 | },
128 | "popup-fix": {
129 | "name": "Popup Fix"
130 | },
131 | "looking-for-group": {
132 | "name": "Looking For Group",
133 | "toggle-button": {
134 | "start": "Look for a Group",
135 | "stop": "Stop Looking for Groups"
136 | }
137 | },
138 | "stadiaplusdb": {
139 | "name": "Stadia+ DB",
140 | "updating": "Updating {{game}} in Stadia+ DB",
141 | "connecting": "Connecting to Stadia+ DB via {{url}}",
142 | "signed-in": "Logged into Stadia+ DB as {{user}}"
143 | },
144 | "snackbar": {
145 | "reload-to-update": "Orria birkargatu ezarritako aldaketak abiarazteko.",
146 | "hide-game": "Joku bat izkutatu egin da.",
147 | "show-game": "Joku bat jada ikusgai dago."
148 | },
149 | "automatic": "Automatikoa",
150 | "vp9": "VP9",
151 | "h264": "H264",
152 | "apply": "Ezarri",
153 | "loading": "Loading...",
154 | "experimental": "Experimental",
155 | "4k": "4K",
156 | "1440p": "1440p",
157 | "1080p": "1080p",
158 | "720p": "720p",
159 | "lang-config": "Using language configuration {{language}}"
160 | }
161 |
--------------------------------------------------------------------------------
/src/lang/ru-RU.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Разработчик: {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "Расширение готово к работе. Запускай Stadia и начинай играть! 🎮",
9 | "launch-button": "Запустить Stadia",
10 | "settings-button": "Настройки",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Справка и FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Настройки",
42 | "language": "Язык",
43 | "components": "Components",
44 | "edit-components": "Edit Components"
45 | },
46 | "developer-page": {
47 | "title": "Developer",
48 | "clear-cache-button": "Clear Cache",
49 | "storage": "Storage"
50 | },
51 | "component-page": {
52 | "title": "Components"
53 | }
54 | },
55 | "component": {
56 | "enabled": "Элемент {{name}} включён.",
57 | "disabled": "Элемент {{name}} отключен."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Allow Windowed Mode",
61 | "button-label": {
62 | "windowed": "Windowed",
63 | "fullscreen": "Fullscreen"
64 | }
65 | },
66 | "clock": {
67 | "name": "Часы"
68 | },
69 | "force-codec": {
70 | "name": "Принудительный запуск кодека",
71 | "4k-tooltip": "Forced Codec is not available when running in 4K or 1440p"
72 | },
73 | "force-resolution": {
74 | "name": "Принудительная смена разрешения",
75 | "note": "Примечание: установливаемое значение - это максимальное разрешение, которое Stadia может достичь. Если ваш компьютер не может отобразить разрешение или оно не доступно с текущей скоростью передачи данных, оно не будет отображаться."
76 | },
77 | "library-filter": {
78 | "name": "Фильтр",
79 | "recent": "Последние",
80 | "alphabetical": "Алфавитный",
81 | "random": "Случайные",
82 | "show-hidden": "Показать скрытые",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Мониторинг сети",
94 | "heading-visible": "Видимая статистика",
95 | "button-label": "Монитор",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "paste-from-clipboard": {
115 | "name": "Вставить из буфера обмена"
116 | },
117 | "ratings": {
118 | "name": "Рейтинги",
119 | "source-name": "Metacritic"
120 | },
121 | "store-filter": {
122 | "name": "Сохранить фильтр"
123 | },
124 | "ui-tab": {
125 | "name": "Stadia+ UI Вкладка",
126 | "button-label": "Stadia+"
127 | },
128 | "popup-fix": {
129 | "name": "Popup Fix"
130 | },
131 | "looking-for-group": {
132 | "name": "Looking For Group",
133 | "toggle-button": {
134 | "start": "Look for a Group",
135 | "stop": "Stop Looking for Groups"
136 | }
137 | },
138 | "stadiaplusdb": {
139 | "name": "Stadia+ DB",
140 | "updating": "Updating {{game}} in Stadia+ DB",
141 | "connecting": "Connecting to Stadia+ DB via {{url}}",
142 | "signed-in": "Logged into Stadia+ DB as {{user}}"
143 | },
144 | "snackbar": {
145 | "reload-to-update": "Перезагрузите страницу, чтобы увидеть изменения.",
146 | "hide-game": "Игра скрыта.",
147 | "show-game": "Игра теперь отображается."
148 | },
149 | "automatic": "Автоматически",
150 | "vp9": "VP9",
151 | "h264": "H264",
152 | "apply": "Apply",
153 | "loading": "Loading...",
154 | "experimental": "Experimental",
155 | "4k": "4K",
156 | "1440p": "1440p",
157 | "1080p": "1080p",
158 | "720p": "720p",
159 | "lang-config": "Using language configuration {{language}}"
160 | }
161 |
--------------------------------------------------------------------------------
/src/lang/de-DE.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Developed by {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "The extension is all ready to go. Just fire up Stadia and start playing! 🎮",
9 | "launch-button": "Launch Stadia",
10 | "settings-button": "Settings",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Help & FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Settings",
42 | "language": "Language",
43 | "components": "Components",
44 | "edit-components": "Edit Components"
45 | },
46 | "developer-page": {
47 | "title": "Developer",
48 | "clear-cache-button": "Clear Cache",
49 | "storage": "Storage"
50 | },
51 | "component-page": {
52 | "title": "Components"
53 | }
54 | },
55 | "component": {
56 | "enabled": "Komponente {{name}} wurde aktiviert.",
57 | "disabled": "Komponente {{name}} wurde deaktiviert."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Allow Windowed Mode",
61 | "button-label": {
62 | "windowed": "Windowed",
63 | "fullscreen": "Fullscreen"
64 | }
65 | },
66 | "clock": {
67 | "name": "Uhr"
68 | },
69 | "force-codec": {
70 | "name": "Codec erzwingen",
71 | "4k-tooltip": "Forced Codec is not available when running in 4K or 1440p"
72 | },
73 | "force-resolution": {
74 | "name": "Auflösung erzwingen",
75 | "note": "Anmerkung: Die gewählte Auflösung ist die Maximale, die Stadia verweden wird. Falls Ihr Computer diese Auflösung nicht darstellen kann oder nicht genug Bandbreite zu Verfügung steht, wird eine kleinere Auflösung verwendet."
76 | },
77 | "library-filter": {
78 | "name": "Sammlungsfilter",
79 | "recent": "Neuste",
80 | "alphabetical": "Alphabetisch",
81 | "random": "Zufällig",
82 | "show-hidden": "Zeige Versteckte",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Netwerkmonitor",
94 | "heading-visible": "Sichtbare Statistiken",
95 | "button-label": "Monitor",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "paste-from-clipboard": {
115 | "name": "Einfügen aus der Zwischenablage"
116 | },
117 | "ratings": {
118 | "name": "Bewertungen",
119 | "source-name": "Metacritic"
120 | },
121 | "store-filter": {
122 | "name": "Store Filter"
123 | },
124 | "ui-tab": {
125 | "name": "Stadia+ UI Tab",
126 | "button-label": "Stadia+"
127 | },
128 | "popup-fix": {
129 | "name": "Popup Fix"
130 | },
131 | "looking-for-group": {
132 | "name": "Looking For Group",
133 | "toggle-button": {
134 | "start": "Look for a Group",
135 | "stop": "Stop Looking for Groups"
136 | }
137 | },
138 | "stadiaplusdb": {
139 | "name": "Stadia+ DB",
140 | "updating": "Updating {{game}} in Stadia+ DB",
141 | "connecting": "Connecting to Stadia+ DB via {{url}}",
142 | "signed-in": "Logged into Stadia+ DB as {{user}}"
143 | },
144 | "snackbar": {
145 | "reload-to-update": "Seite neu laden um die Änderung anzuzeigen.",
146 | "hide-game": "Ein Spiel wurde versteckt.",
147 | "show-game": "Ein Spiel ist nicht mehr versteckt."
148 | },
149 | "automatic": "Automatisch",
150 | "vp9": "VP9",
151 | "h264": "H264",
152 | "apply": "Anwenden",
153 | "loading": "Loading...",
154 | "experimental": "Experimental",
155 | "4k": "4K",
156 | "1440p": "1440p",
157 | "1080p": "1080p",
158 | "720p": "720p",
159 | "lang-config": "Using language configuration {{language}}"
160 | }
161 |
--------------------------------------------------------------------------------
/src/lang/es-ES.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Developed by {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "The extension is all ready to go. Just fire up Stadia and start playing! 🎮",
9 | "launch-button": "Launch Stadia",
10 | "settings-button": "Settings",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Help & FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Settings",
42 | "language": "Language",
43 | "components": "Components",
44 | "edit-components": "Edit Components"
45 | },
46 | "developer-page": {
47 | "title": "Developer",
48 | "clear-cache-button": "Clear Cache",
49 | "storage": "Storage"
50 | },
51 | "component-page": {
52 | "title": "Components"
53 | }
54 | },
55 | "component": {
56 | "enabled": "El componente {{name}} ha sido habilitado.",
57 | "disabled": "El componente {{name}} ha sido deshabilitado."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Allow Windowed Mode",
61 | "button-label": {
62 | "windowed": "Windowed",
63 | "fullscreen": "Fullscreen"
64 | }
65 | },
66 | "clock": {
67 | "name": "Reloj"
68 | },
69 | "force-codec": {
70 | "name": "Fuerza Códec",
71 | "4k-tooltip": "Forced Codec is not available when running in 4K or 1440p"
72 | },
73 | "force-resolution": {
74 | "name": "Fuerza Resolución",
75 | "note": "Nota: el valor establecido es la resolución máxima que Stadia intentará lograr. Si su computadora no es capaz de procesar la resolución o no está disponible con la opción de uso de datos actual, no se mostrará."
76 | },
77 | "library-filter": {
78 | "name": "Filtro Biblioteca",
79 | "recent": "Reciente",
80 | "alphabetical": "Alfabético",
81 | "random": "Aleatorio",
82 | "show-hidden": "Mostrar oculto",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Monitor de red",
94 | "heading-visible": "Estadísticas visibles",
95 | "button-label": "Monitor",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "paste-from-clipboard": {
115 | "name": "Pegar desde el portapapeles"
116 | },
117 | "ratings": {
118 | "name": "Calificaciones",
119 | "source-name": "Metacrítico"
120 | },
121 | "store-filter": {
122 | "name": "Filtro de tienda"
123 | },
124 | "ui-tab": {
125 | "name": "Stadia+ UI Tab",
126 | "button-label": "Stadia+"
127 | },
128 | "popup-fix": {
129 | "name": "Popup Fix"
130 | },
131 | "looking-for-group": {
132 | "name": "Looking For Group",
133 | "toggle-button": {
134 | "start": "Look for a Group",
135 | "stop": "Stop Looking for Groups"
136 | }
137 | },
138 | "stadiaplusdb": {
139 | "name": "Stadia+ DB",
140 | "updating": "Updating {{game}} in Stadia+ DB",
141 | "connecting": "Connecting to Stadia+ DB via {{url}}",
142 | "signed-in": "Logged into Stadia+ DB as {{user}}"
143 | },
144 | "snackbar": {
145 | "reload-to-update": "Vuelva a cargar la página para ver sus cambios.",
146 | "hide-game": "Un juego ha sido escondido.",
147 | "show-game": "Un juego ya no está oculto."
148 | },
149 | "automatic": "Automático",
150 | "vp9": "VP9",
151 | "h264": "H264",
152 | "apply": "Aplicar",
153 | "loading": "Loading...",
154 | "experimental": "Experimental",
155 | "4k": "4K",
156 | "1440p": "1440p",
157 | "1080p": "1080p",
158 | "720p": "720p",
159 | "lang-config": "Using language configuration {{language}}"
160 | }
161 |
--------------------------------------------------------------------------------
/src/lang/en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "popup": {
3 | "footer": {
4 | "credit": "Developed by {{name}}"
5 | },
6 | "main-page": {
7 | "title": "Stadia+",
8 | "ready-text": "The extension is all ready to go. Just fire up Stadia and start playing! 🎮",
9 | "launch-button": "Launch Stadia",
10 | "settings-button": "Settings",
11 | "patreon-button": "Support on Patreon",
12 | "help-and-faq": "Help & FAQ",
13 | "discord": "Discord",
14 | "reddit": "Reddit",
15 | "github": "Github",
16 | "profile": {
17 | "wipe-data": "Delete user data",
18 | "view-profile": "View profile",
19 | "not-available": "Your profile is not yet available, please check back again after you've played a game or two.",
20 | "heading": "Keep track of your progress",
21 | "text": "Sign in to Stadia+ DB to automagically sync all your achievements and stats to your profile.",
22 | "more": "More about Stadia+ DB",
23 | "login-button":"Sign in with Google",
24 | "sign-out":"Sign out"
25 | }
26 | },
27 | "user-page": {
28 | "title": "Login",
29 | "login-button": "Sign in with Google",
30 | "accept-label": "By signing into Stadia+ DB you accept the",
31 | "privacy-policy": "privacy policy"
32 | },
33 | "wipe-data-page": {
34 | "title": "Wipe Data",
35 | "heading": "Really wipe your data?",
36 | "text": "Wiping your user data from Stadia+ DB means your profile will no longer be available, and your data will be completely gone from the Stadia+ DB database.
This does not effect your Stadia profile, and all your game data will still be available in Stadia.",
37 | "confirm": "Yes, wipe my data",
38 | "cancel": "No, I changed my mind"
39 | },
40 | "settings-page": {
41 | "title": "Settings",
42 | "language": "Language",
43 | "components": "Components",
44 | "edit-components": "Edit Components"
45 | },
46 | "developer-page": {
47 | "title": "Developer",
48 | "clear-cache-button": "Clear Cache",
49 | "storage": "Storage"
50 | },
51 | "component-page": {
52 | "title": "Components"
53 | }
54 | },
55 | "component": {
56 | "enabled": "Component {{name}} has been enabled.",
57 | "disabled": "Component {{name}} has been disabled."
58 | },
59 | "allow-windowed-mode": {
60 | "name": "Allow Windowed Mode",
61 | "button-label": {
62 | "windowed": "Windowed",
63 | "fullscreen": "Fullscreen"
64 | }
65 | },
66 | "clock": {
67 | "name": "Clock"
68 | },
69 | "force-codec": {
70 | "name": "Force Codec",
71 | "4k-tooltip": "Forced Codec is not available when running in 4K or 1440p"
72 | },
73 | "force-resolution": {
74 | "name": "Force Resolution",
75 | "note": "Note: the set value is the maximum resolution Stadia will attempt to achieve. If your computer is not capable of rendering the resolution or it is not available with the current data usage option, it will not be displayed."
76 | },
77 | "library-filter": {
78 | "name": "Library Filter",
79 | "recent": "Recent",
80 | "alphabetical": "Alphabetical",
81 | "random": "Random",
82 | "show-hidden": "Show Hidden",
83 | "get-shortcut": "Get Desktop Shortcut",
84 | "all-visible": "All",
85 | "custom-visible": "Custom",
86 | "your-games": "Your Games",
87 | "your-captures": "Your Captures",
88 | "captures-note": "Your captures are now at the top! Look for the photo_camera icon in the navbar.",
89 | "hide-game": "Hide {{name}}",
90 | "show-game": "Show {{name}}"
91 | },
92 | "network-monitor": {
93 | "name": "Network Monitor",
94 | "heading-visible": "Visible Stats",
95 | "button-label": "Monitor",
96 | "toggle-button": {
97 | "show": "Show Network Monitor",
98 | "hide": "Hide Network Monitor"
99 | },
100 | "stats": {
101 | "time": "Time",
102 | "resolution": "Resolution",
103 | "fps": "FPS",
104 | "latency": "Latency",
105 | "codec": "Codec",
106 | "traffic": "Traffic",
107 | "current-traffic": "Current Traffic",
108 | "average-traffic": "Average Traffic",
109 | "packets-lost": "Packets Lost",
110 | "average-packet-loss": "Average Packet Loss",
111 | "jitter-buffer": "Jitter Buffer"
112 | }
113 | },
114 | "paste-from-clipboard": {
115 | "name": "Paste from Clipboard"
116 | },
117 | "ratings": {
118 | "name": "Ratings",
119 | "source-name": "Metacritic"
120 | },
121 | "store-filter": {
122 | "name": "Store Filter"
123 | },
124 | "ui-tab": {
125 | "name": "Stadia+ UI Tab",
126 | "button-label": "Stadia+"
127 | },
128 | "popup-fix": {
129 | "name": "Popup Fix"
130 | },
131 | "looking-for-group": {
132 | "name": "Looking For Group",
133 | "toggle-button": {
134 | "start": "Look for a Group",
135 | "stop": "Stop Looking for Groups"
136 | }
137 | },
138 | "stadiaplusdb": {
139 | "name": "Stadia+ DB",
140 | "updating": "Updating {{game}} in Stadia+ DB",
141 | "connecting": "Connecting to Stadia+ DB via {{url}}",
142 | "signed-in": "Logged into Stadia+ DB as {{user}}"
143 | },
144 | "snackbar": {
145 | "reload-to-update": "Reload the page to see your changes.",
146 | "hide-game": "A game has been hidden.",
147 | "show-game": "A game is no longer hidden."
148 | },
149 | "profile": {
150 | "name": "Profile"
151 | },
152 | "automatic": "Automatic",
153 | "vp9": "VP9",
154 | "h264": "H264",
155 | "apply": "Apply",
156 | "loading": "Loading...",
157 | "experimental": "Experimental",
158 | "4k": "4K",
159 | "1440p": "1440p",
160 | "1080p": "1080p",
161 | "720p": "720p",
162 | "lang-config": "Using language configuration {{language}}"
163 | }
164 |
--------------------------------------------------------------------------------