├── .gitignore
├── README.md
├── TODO
├── icon128.png
├── icon16.png
├── icon48.png
├── jsconfig.json
├── manifest.json
├── package.json
├── scripts
└── production.js
├── src
├── assets
│ └── images
│ │ ├── Logo.svg
│ │ ├── favicon.png
│ │ ├── icon-dropper.svg
│ │ ├── icon-pencil.svg
│ │ ├── icon-thumbs-down.svg
│ │ ├── icon-thumbs-up.svg
│ │ ├── icon-trash.svg
│ │ ├── play_music.svg
│ │ ├── play_music_logo_dark.png
│ │ ├── play_music_logo_light.png
│ │ └── sprites
│ │ ├── ani_equalizer_white.gif
│ │ ├── ani_loading_white.gif
│ │ └── equalizer_white.png
├── background.js
├── components
│ ├── Button.js
│ ├── Checkbox.js
│ ├── Grid.js
│ ├── Icons
│ │ ├── IconCopy.js
│ │ ├── IconDropper.js
│ │ ├── IconGear.js
│ │ ├── IconPencil.js
│ │ ├── IconTrash.js
│ │ └── SvgIcon.js
│ ├── Modal.container.js
│ ├── Modals
│ │ ├── AlertModal.js
│ │ ├── ColorDeleteModal.js
│ │ ├── ColorPickerModal.js
│ │ ├── ConfirmModal.js
│ │ ├── ModalWrapper.js
│ │ ├── ModalWrapper.statics.js
│ │ ├── ModalWrapper.styled.js
│ │ ├── NotificationModal.js
│ │ ├── NotificationModal.styles.js
│ │ ├── ThemeDeleteModal.js
│ │ └── ThemePickerModal.js
│ ├── Options
│ │ ├── Option
│ │ │ ├── Option.styled.js
│ │ │ ├── OptionCheckbox.js
│ │ │ ├── OptionString.js
│ │ │ ├── OptionThemes.js
│ │ │ └── index.js
│ │ ├── Options.container.js
│ │ ├── Options.js
│ │ ├── Options.styled.js
│ │ ├── Section.js
│ │ └── index.js
│ ├── PlayMidnight.js
│ ├── PlayMidnight.styled.js
│ ├── PlayMidnightLogo.js
│ ├── PlayMusicLogo.js
│ ├── Root.js
│ ├── ThemePreview.js
│ ├── Toast.container.js
│ └── Toasts
│ │ ├── AlertToast.js
│ │ ├── NotificationToast.js
│ │ ├── SuccessToast.js
│ │ └── ToastWrapper.js
├── hoc
│ ├── withOptions.js
│ ├── withPortal.js
│ ├── withStyles.js
│ └── withTheme.js
├── index.html
├── lib
│ ├── api.js
│ └── store.js
├── modules
│ ├── modal.js
│ ├── options.js
│ ├── root.js
│ └── toast.js
├── notifications
│ ├── components
│ │ ├── FootNote.js
│ │ ├── List.js
│ │ ├── ListItem.js
│ │ ├── Text.js
│ │ ├── Title.js
│ │ └── index.js
│ ├── index.js
│ └── templates
│ │ ├── 3.0.0.js
│ │ ├── 3.1.0.js
│ │ ├── 3.2.0.js
│ │ ├── 3.2.1.js
│ │ └── default.js
├── options
│ ├── Components.js
│ ├── components
│ │ ├── AccentsOnly.js
│ │ ├── AccentsOnly.styles.js
│ │ ├── AlbumAccents.js
│ │ ├── Core.js
│ │ ├── Core.observables.js
│ │ ├── Core.styles.js
│ │ ├── Enabled.js
│ │ ├── Enabled.styles.js
│ │ ├── Favicon.js
│ │ ├── LargeTable.js
│ │ ├── LargeTable.styles.js
│ │ ├── Logo.js
│ │ ├── Logo.styles.js
│ │ ├── Menus.js
│ │ ├── Menus.styles.js
│ │ ├── Playlists.js
│ │ ├── Playlists.styles.js
│ │ ├── Queue.js
│ │ ├── Queue.styles.js
│ │ ├── Settings.js
│ │ ├── Settings.styles.js
│ │ ├── SoundSearch.js
│ │ ├── StaticSidebars.js
│ │ └── StaticSidebars.styles.js
│ └── index.js
├── play-midnight.js
├── style
│ ├── global.js
│ ├── sheets
│ │ ├── accents.js
│ │ ├── alerts.js
│ │ ├── buttons.js
│ │ ├── cardGrid.js
│ │ ├── cards.js
│ │ ├── core.js
│ │ ├── forms.js
│ │ ├── index.js
│ │ ├── loading.js
│ │ ├── menus.js
│ │ ├── misc.js
│ │ ├── nav.js
│ │ ├── page.js
│ │ ├── player.js
│ │ ├── queue.js
│ │ └── songTable.js
│ └── theme.js
└── utils
│ ├── array.js
│ ├── awaitElement.js
│ ├── checkEnv.js
│ ├── createStylesheet.js
│ ├── getArrayValue.js
│ ├── getCssString.js
│ ├── injectElement.js
│ ├── removeAllElements.js
│ └── validation.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /deploy
11 | /build
12 | /dist
13 | deploy.zip
14 |
15 | # misc
16 | /.cache
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Play Midnight - Chrome Extension
2 |
3 | [](https://chrome.google.com/webstore/detail/play-midnight-for-google/ljmjmhjkcgfmfdhgplikncgndbdeckci)
4 | [](https://chrome.google.com/webstore/detail/play-midnight-for-google/ljmjmhjkcgfmfdhgplikncgndbdeckci)
5 | [](https://chrome.google.com/webstore/detail/play-midnight-for-google/ljmjmhjkcgfmfdhgplikncgndbdeckci)
6 | |
7 | [](https://www.paypal.me/datducky)
8 |
9 | ### A Nighttime Theme for Google Play Music
10 |
11 | Play Midnight is a different take on the standard theme that is used on Google Play Music. As much as I love the original look of Play, the brightness can hurt the eyes after a while. After noticing there wasn't a dark alternative Play Midnight came to be.
12 |
13 | ### Developing
14 |
15 | Play Midnight is currently running using Node.js/Parcel bundler. You'll have to follow these few steps and you should be up and running.
16 |
17 | 1. Clone Repository
18 | 1. Optionally install `yarn` if you don't have it yet.
19 | * `brew install yarn` if you have homebrew, or `npm install -g yarn`
20 | 1. `cd` into the directory and run `yarn` (or `npm install`)
21 |
22 | ##### Core Updates
23 |
24 | 1. To build work on core changes, run `yarn start` (or `npm run start`) and it should process everything.
25 | 1. In your browser, if you visit `localhost:1234` you'll have a little sandbox you can use for testing core related features
26 | 1. This script will recompile automatically so you can just refresh on save.
27 |
28 | ##### Play Music Updates
29 |
30 | 1. To build changes for Play Music, you'll need to run `yarn dev` after you're ready to test it.
31 | 1. In Chrome, go to `chrome://extensions` and toggle the Developer Mode` option (top right) to "On"
32 | 1. Click `Load Unpacked Extension` and load up your main Play Midnight folder (the one containing `manifest.json`)
33 | 1. Make changes at your leisure! Note: You'll have to click refresh (or Ctrl/Cmd + R) to reload the extension on the `chrome:extensions` page if you rebuild.
34 |
35 | ### About
36 |
37 | The Chrome Extension for Play Midnight uses CSS stored inside the `src/style/sheets/` folder. These files have a theme (from `src/style/theme.js`) injected into them where you have access to the users current Background/Accent colors.
38 |
39 | ### License
40 |
41 | The MIT License (MIT)
42 |
43 | Copyright (c) 2016 Chris Tieman
44 |
45 | Permission is hereby granted, free of charge, to any person obtaining a copy
46 | of this software and associated documentation files (the "Software"), to deal
47 | in the Software without restriction, including without limitation the rights
48 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
49 | copies of the Software, and to permit persons to whom the Software is
50 | furnished to do so, subject to the following conditions:
51 |
52 | The above copyright notice and this permission notice shall be included in all
53 | copies or substantial portions of the Software.
54 |
55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
58 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
59 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
60 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
61 | SOFTWARE.
62 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | - Fix Panel BG on Disable
2 |
3 | - Fix Firefox Issues (Accent Favicon, BG Page)
--------------------------------------------------------------------------------
/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/icon128.png
--------------------------------------------------------------------------------
/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/icon16.png
--------------------------------------------------------------------------------
/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/icon48.png
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | }
5 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "applications": {
3 | "gecko": {
4 | "id": "firefox@playmidnight.com"
5 | }
6 | },
7 |
8 | "manifest_version": 2,
9 | "name": "Play Midnight for Google Play Music™",
10 | "short_name": "Play Midnight",
11 | "version": "3.2.1",
12 |
13 | "description":
14 | "A theme created for Google Play Music™ to give your eyes a break with a darker layout and color options.",
15 |
16 | "icons": {
17 | "16": "icon16.png",
18 | "48": "icon48.png",
19 | "128": "icon128.png"
20 | },
21 |
22 | "permissions": ["storage", "*://play-music.gstatic.com/*"],
23 |
24 | "background": {
25 | "scripts": ["build/background.js"]
26 | },
27 |
28 | "content_scripts": [
29 | {
30 | "matches": ["*://play.google.com/music/listen*"],
31 | "run_at": "document_end",
32 | "js": ["build/play-midnight.js"]
33 | }
34 | ],
35 |
36 | "web_accessible_resources": ["build/*"]
37 | }
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "play-midnight",
3 | "version": "3.2.1",
4 | "private": true,
5 | "dependencies": {
6 | "babel-eslint": "7.2.3",
7 | "babel-plugin-module-resolver": "^3.0.0",
8 | "babel-preset-react-app": "^3.1.0",
9 | "babel-runtime": "^6.26.0",
10 | "chalk": "^2.3.0",
11 | "dotenv": "4.0.0",
12 | "eslint": "4.10.0",
13 | "eslint-config-react-app": "^2.0.1",
14 | "eslint-loader": "1.9.0",
15 | "eslint-plugin-flowtype": "2.39.1",
16 | "eslint-plugin-import": "2.8.0",
17 | "eslint-plugin-jsx-a11y": "5.1.1",
18 | "eslint-plugin-react": "7.4.0",
19 | "fs-extra": "^5.0.0",
20 | "lodash": "^4.17.4",
21 | "node-vibrant": "^3.0.0",
22 | "parcel-bundler": "^1.3.1",
23 | "react": "^16.2.0",
24 | "react-addons-css-transition-group": "^15.6.2",
25 | "react-color": "^2.13.8",
26 | "react-dev-utils": "^4.2.1",
27 | "react-dom": "^16.2.0",
28 | "react-redux": "^5.0.6",
29 | "redux": "^3.7.2",
30 | "redux-actions": "^2.2.1",
31 | "redux-logger": "^3.0.6",
32 | "redux-saga": "^0.16.0",
33 | "reselect": "^3.0.1",
34 | "semver": "^5.5.0",
35 | "styled-components": "^2.4.0",
36 | "stylis": "^3.4.8",
37 | "tinycolor2": "^1.4.1",
38 | "typeface-roboto": "^0.0.45",
39 | "uuid": "^3.1.0"
40 | },
41 | "scripts": {
42 | "start": "parcel build src/background.js --out-dir dist && parcel --no-hmr src/index.html",
43 | "dev":
44 | "parcel build src/play-midnight.js --no-minify --out-dir build && parcel build src/background.js --no-minify --out-dir build",
45 | "prod": "node ./scripts/production.js"
46 | },
47 | "babel": {
48 | "plugins": [
49 | [
50 | "module-resolver",
51 | {
52 | "root": ["./src"]
53 | }
54 | ],
55 | ["transform-decorators-legacy"]
56 | ],
57 | "presets": ["react-app"]
58 | },
59 | "eslintConfig": {
60 | "extends": "react-app",
61 | "globals": {
62 | "browser": true,
63 | "chrome": true
64 | },
65 | "rules": {
66 | "comma-dangle": ["warn", "always-multiline"],
67 | "max-len": ["error", 120]
68 | }
69 | },
70 | "devDependencies": {
71 | "babel-plugin-transform-decorators-legacy": "^1.3.4"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/scripts/production.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs-extra');
3 | const chalk = require('chalk');
4 | const { spawn } = require('child_process');
5 |
6 | const BUILD_DIRECTORY = `deploy`;
7 |
8 | const NORMALIZED_ROOT = path.normalize(`${__dirname}/..`);
9 | const NORMALIZED_DIRECTORY = path.normalize(`${NORMALIZED_ROOT}/${BUILD_DIRECTORY}`);
10 |
11 | const BUILD_COMMAND = `parcel build src/play-midnight.js --no-cache --out-dir ${NORMALIZED_DIRECTORY}/build`;
12 | const BUILD_BG_COMMAND = `parcel build src/background.js --no-cache --out-dir ${NORMALIZED_DIRECTORY}/build`;
13 |
14 | const copyRoot = async fileName => {
15 | try {
16 | console.log(`\n${NORMALIZED_ROOT}/${fileName} -> ${NORMALIZED_DIRECTORY}/${fileName}`);
17 | await fs.copy(`${NORMALIZED_ROOT}/${fileName}`, `${NORMALIZED_DIRECTORY}/${fileName}`);
18 | console.log(chalk.green(`✅ Success!`));
19 | } catch (e) {
20 | console.log(chalk.red(`❌ ERROR - Failed to copy file '${fileName}'`));
21 | }
22 | };
23 |
24 | const exec = (cmd, name = '') => {
25 | const getParts = cmdStr => {
26 | const parts = cmdStr.split(' ');
27 | return {
28 | command: parts[0],
29 | args: parts.slice(1),
30 | };
31 | };
32 |
33 | const { command, args } = getParts(cmd);
34 | const BUILD_PROCESS = spawn(command, args, {
35 | cwd: NORMALIZED_ROOT,
36 | stdio: [process.stdin, process.stdout, 'pipe'],
37 | });
38 |
39 | BUILD_PROCESS.stderr.on('data', data => {
40 | console.log(chalk.red(`❌ ERROR - ${data}`));
41 | });
42 |
43 | BUILD_PROCESS.on('close', data => {
44 | console.log(chalk.green(`✅ ${name} Success!`));
45 | });
46 | };
47 |
48 | const build = async () => {
49 | try {
50 | console.log(chalk.cyan('\n🎉 Starting Bundle Task 🎉 '));
51 |
52 | console.log(chalk.blue(`\nRemoving Deploy Directory '${NORMALIZED_DIRECTORY}'`));
53 | await fs.remove(NORMALIZED_DIRECTORY);
54 | console.log(chalk.green(`✅ Success!`));
55 |
56 | console.log(chalk.blue(`\nCreating Deploy Directory '${NORMALIZED_DIRECTORY}'`));
57 | await fs.ensureDir(NORMALIZED_DIRECTORY);
58 | console.log(chalk.green(`✅ Success!`));
59 |
60 | console.log(chalk.blue(`\nCopying package.json`));
61 | await copyRoot('package.json');
62 |
63 | console.log(chalk.blue(`\nCopying manifest.json`));
64 | await copyRoot('manifest.json');
65 |
66 | console.log(chalk.blue(`\nCopying Icons`));
67 | await copyRoot('icon16.png');
68 | await copyRoot('icon48.png');
69 | await copyRoot('icon128.png');
70 |
71 | console.log(chalk.blue(`\nRunning Build`));
72 | console.log(chalk.blue(BUILD_COMMAND));
73 | exec(BUILD_COMMAND, 'Build');
74 |
75 | console.log(chalk.blue(`\nRunning Background Build`));
76 | console.log(chalk.blue(BUILD_BG_COMMAND));
77 | exec(BUILD_BG_COMMAND, 'Background Build');
78 | } catch (err) {
79 | console.log(chalk.red(`❌ Build Failed!`));
80 | console.log(chalk.red(`❌ ERROR - ${err}`));
81 | }
82 | };
83 |
84 | build();
85 |
--------------------------------------------------------------------------------
/src/assets/images/Logo.svg:
--------------------------------------------------------------------------------
1 | Asset 1
--------------------------------------------------------------------------------
/src/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/src/assets/images/favicon.png
--------------------------------------------------------------------------------
/src/assets/images/icon-dropper.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/icon-pencil.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/images/icon-thumbs-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/icon-thumbs-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/icon-trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/images/play_music_logo_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/src/assets/images/play_music_logo_dark.png
--------------------------------------------------------------------------------
/src/assets/images/play_music_logo_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/src/assets/images/play_music_logo_light.png
--------------------------------------------------------------------------------
/src/assets/images/sprites/ani_equalizer_white.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/src/assets/images/sprites/ani_equalizer_white.gif
--------------------------------------------------------------------------------
/src/assets/images/sprites/ani_loading_white.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/src/assets/images/sprites/ani_loading_white.gif
--------------------------------------------------------------------------------
/src/assets/images/sprites/equalizer_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ducky/play-midnight/3c421b6150c4e476f0ae8fc0ca7ec4b31e9704b2/src/assets/images/sprites/equalizer_white.png
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | const getIcon = ({ accent, url }, sender, cb) => {
2 | const img = new Image();
3 |
4 | img.onload = function() {
5 | const canvas = document.createElement('canvas');
6 |
7 | canvas.width = this.naturalWidth;
8 | canvas.height = this.naturalHeight;
9 | canvas.setAttribute('crossOrigin', 'anonymous');
10 |
11 | const context = canvas.getContext('2d');
12 |
13 | // Create Colored Icon
14 | context.drawImage(this, 0, 0);
15 | context.globalCompositeOperation = 'source-in';
16 | context.fillStyle = accent;
17 | context.fillRect(0, 0, this.naturalWidth, this.naturalHeight);
18 | context.fill();
19 |
20 | const icon = canvas.toDataURL();
21 |
22 | console.log(icon);
23 |
24 | cb({ url: icon });
25 | };
26 |
27 | img.src = url;
28 |
29 | return true;
30 | };
31 |
32 | chrome.runtime.onMessage.addListener(getIcon);
33 |
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import withTheme from 'hoc/withTheme';
5 |
6 | import { isLight, FONT_LIGHT, FONT_DARK, TRANSITION_FAST } from 'style/theme';
7 |
8 | const StyledButton = styled.button`
9 | display: inline-flex;
10 | text-align: center;
11 | justify-content: center;
12 | align-items: center;
13 | text-transform: uppercase;
14 | border-radius: 3px;
15 | font-weight: 500;
16 | padding: 10px 15px;
17 | cursor: pointer;
18 | opacity: 0.9;
19 | border: none;
20 | outline: none;
21 | box-shadow: none;
22 | background: ${props => props.theme.B25};
23 | color: ${props => props.theme.font_primary};
24 | transition: background ${TRANSITION_FAST}, opacity ${TRANSITION_FAST};
25 |
26 | ${props => props.useAccent && `background: ${props.theme.A500}`};
27 | ${props => props.useAccent && `color: ${isLight(props.theme.A500) ? FONT_LIGHT : FONT_DARK}`};
28 |
29 | ${props => props.accent && `background: ${props.accent}`};
30 | ${props => props.accent && `color: ${isLight(props.accent) ? FONT_LIGHT : FONT_DARK}`};
31 |
32 | &:not([disabled]):hover {
33 | opacity: 1;
34 | }
35 |
36 | &[disabled] {
37 | opacity: 0.7;
38 | cursor: not-allowed;
39 | }
40 | `;
41 |
42 | const Button = ({ accent, background, theme, noAccent, children, ...rest }) => (
43 |
44 | {children}
45 |
46 | );
47 |
48 | export default withTheme(Button);
49 |
--------------------------------------------------------------------------------
/src/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import noop from 'lodash/noop';
4 |
5 | import withTheme from 'hoc/withTheme';
6 | import { darken, lighten, TRANSITION_FAST } from 'style/theme';
7 |
8 | const StyledCheckbox = styled.div`
9 | display: inline-flex;
10 |
11 | .Checkbox__container {
12 | position: relative;
13 | width: 36px;
14 | height: 20px;
15 | cursor: pointer;
16 | }
17 |
18 | .Checkbox__track {
19 | position: absolute;
20 | top: 3px;
21 | left: 0;
22 | right: 0;
23 | bottom: 3px;
24 | background: ${props => props.theme.B500};
25 | border: 1px solid ${props => props.theme.B500};
26 | border-radius: 25px;
27 | transition: background ${TRANSITION_FAST}, border-color ${TRANSITION_FAST}, opacity ${TRANSITION_FAST};
28 |
29 | ${props => props.background && `background: ${darken(props.background, 3)}`};
30 | ${props => props.background && `border-color: ${darken(props.background, 3)}`};
31 | }
32 |
33 | .Checkbox__knob {
34 | position: absolute;
35 | left: -2px;
36 | width: 20px;
37 | height: 20px;
38 | background: ${props => props.theme.B200};
39 | border-radius: 50%;
40 | box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.6);
41 | transform: translateX(0);
42 | transition: transform ${TRANSITION_FAST}, background ${TRANSITION_FAST};
43 |
44 | ${props => props.background && `background: ${lighten(props.background, 7)}`};
45 | }
46 |
47 | input:checked + .Checkbox__container .Checkbox__track {
48 | ${props => `background: ${props.theme.A500}`};
49 | ${props => `border-color: ${props.theme.A600}`};
50 | ${props => props.accent && `background: ${props.accent}`};
51 | ${props => props.accent && `border-color: ${darken(props.accent, 3)}`};
52 | opacity: 0.5;
53 | }
54 |
55 | input:checked + .Checkbox__container .Checkbox__knob {
56 | ${props => `background: ${props.theme.A500}`};
57 | ${props => `border-color: ${props.theme.A500}`};
58 | ${props => props.accent && `background: ${props.accent}`};
59 | ${props => props.accent && `border-color: ${props.accent}`};
60 | transform: translateX(100%);
61 | }
62 |
63 | input:disabled + .Checkbox__container .Checkbox__track {
64 | cursor: not-allowed;
65 | }
66 |
67 | input:disabled + .Checkbox__container .Checkbox__knob {
68 | cursor: not-allowed;
69 | }
70 |
71 | input {
72 | display: none;
73 | }
74 | `;
75 |
76 | const Checkbox = ({
77 | accent,
78 | background,
79 | dispatch,
80 | style,
81 | theme,
82 | checked,
83 | disabled,
84 | defaultChecked,
85 | onChange,
86 | ...rest
87 | }) => (
88 |
89 |
90 |
98 |
102 |
103 |
104 | );
105 |
106 | export default withTheme(Checkbox);
107 |
--------------------------------------------------------------------------------
/src/components/Grid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Grid = styled.div`
4 | display: grid;
5 | grid-template-columns: repeat(${props => props.span || 1}, 1fr);
6 | grid-gap: 25px;
7 | `;
8 |
9 | export default Grid;
10 |
--------------------------------------------------------------------------------
/src/components/Icons/IconCopy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SvgIcon from './SvgIcon';
4 |
5 | const IconCopy = props => (
6 |
7 |
8 |
9 | );
10 |
11 | export default IconCopy;
12 |
--------------------------------------------------------------------------------
/src/components/Icons/IconDropper.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SvgIcon from './SvgIcon';
4 |
5 | const IconDropper = props => (
6 |
7 |
8 |
9 | );
10 |
11 | export default IconDropper;
12 |
--------------------------------------------------------------------------------
/src/components/Icons/IconGear.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SvgIcon from './SvgIcon';
4 |
5 | const IconGear = props => (
6 |
7 |
8 |
9 | );
10 |
11 | export default IconGear;
12 |
--------------------------------------------------------------------------------
/src/components/Icons/IconPencil.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SvgIcon from './SvgIcon';
4 |
5 | const IconPencil = props => (
6 |
7 |
8 |
9 | );
10 |
11 | export default IconPencil;
12 |
--------------------------------------------------------------------------------
/src/components/Icons/IconTrash.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import SvgIcon from './SvgIcon';
4 |
5 | const IconTrash = props => (
6 |
7 |
8 |
9 | );
10 |
11 | export default IconTrash;
12 |
--------------------------------------------------------------------------------
/src/components/Icons/SvgIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledSvg = styled.svg`
5 | display: inline-flex;
6 | fill: currentColor;
7 | height: 24px;
8 | width: 24px;
9 | `;
10 |
11 | const SvgIcon = ({ children, viewBox, color, ...props }) => (
12 |
13 | {children}
14 |
15 | );
16 |
17 | SvgIcon.defaultProps = {
18 | color: 'currentColor',
19 | viewBox: '0 0 24 24',
20 | };
21 |
22 | export default SvgIcon;
23 |
--------------------------------------------------------------------------------
/src/components/Modal.container.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
3 | import { connect } from 'react-redux';
4 |
5 | import { selectors } from 'modules/modal';
6 |
7 | import { stripTransition, TRANSITION_LIGHTNING, TRANSITION_FAST } from 'style/theme';
8 |
9 | import AlertModal from 'components/Modals/AlertModal';
10 | import ColorDeleteModal from 'components/Modals/ColorDeleteModal';
11 | import ColorPickerModal from 'components/Modals/ColorPickerModal';
12 | import ConfirmModal from 'components/Modals/ConfirmModal';
13 | import NotificationModal from 'components/Modals/NotificationModal';
14 | import ThemeDeleteModal from 'components/Modals/ThemeDeleteModal';
15 | import ThemePickerModal from 'components/Modals/ThemePickerModal';
16 |
17 | const types = {
18 | alert: ,
19 | confirm: ,
20 | colorDelete: ,
21 | colorPicker: ,
22 | notification: ,
23 | themeDelete: ,
24 | themePicker: ,
25 | };
26 |
27 | const ModalConductor = ({ modals = [] }) => {
28 | const getModalComponent = type => types[type] || null;
29 |
30 | return (
31 |
32 |
37 | {modals.map(modal => {
38 | const Modal = getModalComponent(modal.type);
39 |
40 | return Modal
41 | ? React.cloneElement(Modal, {
42 | key: modal.id,
43 | id: modal.id,
44 | transitionEnter: TRANSITION_FAST,
45 | transitionLeave: TRANSITION_LIGHTNING,
46 | ...modal.options,
47 | })
48 | : null;
49 | })}
50 |
51 |
52 | );
53 | };
54 |
55 | const stateToProps = state => ({
56 | modals: selectors.modals(state),
57 | });
58 |
59 | export default connect(stateToProps)(ModalConductor);
60 |
--------------------------------------------------------------------------------
/src/components/Modals/AlertModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ModalWrapper from './ModalWrapper';
4 |
5 | const AlertModal = ({ ...props }) => {
6 | return (
7 |
13 | {props.message}
14 |
15 | );
16 | };
17 |
18 | export default AlertModal;
19 |
--------------------------------------------------------------------------------
/src/components/Modals/ColorDeleteModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import ModalWrapper from './ModalWrapper';
5 |
6 | export const PrettyColor = styled.div`
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | height: 100px;
11 | border-radius: 5px;
12 | background: ${props => props.color};
13 | box-shadow: 0 11px 7px 0 rgba(0, 0, 0, 0.19);
14 | margin: 25px 0 8px;
15 |
16 | strong {
17 | font-weight: 900;
18 | }
19 |
20 | &:before {
21 | content: '😎';
22 | font-size: 36px;
23 | }
24 | `;
25 |
26 | const ColorDeleteModal = ({ ...props }) => {
27 | const name = props.details.name ? props.details.name : props.details.color;
28 |
29 | return (
30 |
31 |
32 | Whoa there, friend! You sure you wanna delete the lovely {name} color?
33 |
34 |
35 | This action{' '}
36 |
37 | cannot
38 | {' '}
39 | be undone, so just be sure about this.
40 |
41 |
42 |
43 | Take a second look. Beautiful, innit?
44 |
45 |
46 | );
47 | };
48 |
49 | export default ColorDeleteModal;
50 |
--------------------------------------------------------------------------------
/src/components/Modals/ColorPickerModal.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { PhotoshopPicker } from 'react-color';
3 | import { connect } from 'react-redux';
4 | import styled from 'styled-components';
5 |
6 | import withTheme from 'hoc/withTheme';
7 |
8 | import { DEFAULT_ACCENT } from 'style/theme';
9 | import { actions } from 'modules/modal';
10 |
11 | import ModalWrapper from './ModalWrapper';
12 |
13 | const FancyInput = styled.input`
14 | width: 100%;
15 | text-align: center;
16 | background: transparent;
17 | outline: none;
18 | box-shadow: none;
19 | color: ${props => props.theme.font_primary};
20 | text-shadow: 0px 3px ${props => props.theme.B500};
21 | border: none;
22 | font-size: 24px;
23 | font-weight: 700;
24 | margin: 0 0 25px;
25 | `;
26 |
27 | const PickerWrapper = styled.div`
28 | box-shadow: 0 11px 7px 0 rgba(0, 0, 0, 0.19), 0 13px 25px 0 rgba(0, 0, 0, 0.3);
29 | color: ${props => props.theme.black};
30 | margin: 0 0 10px;
31 | `;
32 |
33 | @withTheme
34 | @connect(null, { close: actions.closeModal })
35 | class ColorPickerModal extends PureComponent {
36 | constructor(props) {
37 | super(props);
38 |
39 | const details = props.details || {};
40 | this.state = {
41 | id: undefined,
42 | color: DEFAULT_ACCENT,
43 | name: '',
44 | ...details,
45 | };
46 | }
47 |
48 | onTitleChange = ({ target }) => {
49 | const value = target.type === 'checkbox' ? target.checked : target.value;
50 | const id = target.name;
51 | this.setState(state => ({ [id]: value }));
52 | };
53 |
54 | onColorChange = color => {
55 | this.setState(state => ({
56 | color: color.hex,
57 | }));
58 | };
59 |
60 | closeModal = acceptValue => {
61 | const { id, color, name } = this.state;
62 | const { id: modalId, onClose, close } = this.props;
63 |
64 | if (acceptValue) {
65 | onClose({ id, color, name });
66 | }
67 |
68 | close(modalId);
69 | };
70 |
71 | render() {
72 | const { color, name } = this.state;
73 | const { theme } = this.props;
74 |
75 | return (
76 |
77 |
88 |
89 | this.closeModal(true)}
94 | onCancel={() => this.closeModal(false)}
95 | />
96 |
97 |
98 |
99 | Play around until you find the perfect color. Your eyes will thank you.
100 |
101 |
102 |
103 | );
104 | }
105 | }
106 |
107 | export default ColorPickerModal;
108 |
--------------------------------------------------------------------------------
/src/components/Modals/ConfirmModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ModalWrapper from './ModalWrapper';
4 |
5 | const ConfirmModal = ({ ...props }) => {
6 | return (
7 |
8 | {props.message}
9 |
10 | );
11 | };
12 |
13 | export default ConfirmModal;
14 |
--------------------------------------------------------------------------------
/src/components/Modals/ModalWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 | import isFunction from 'lodash/isFunction';
4 | import noop from 'lodash/noop';
5 |
6 | import withTheme from 'hoc/withTheme';
7 |
8 | import { actions } from 'modules/modal';
9 |
10 | import Button from 'components/Button';
11 | import { propTypes, defaultProps } from './ModalWrapper.statics';
12 | import StyledModal, { ModalActions, ModalBackdrop } from './ModalWrapper.styled';
13 |
14 | @withTheme
15 | @connect(null, {
16 | close: actions.closeModal,
17 | })
18 | class ModalWrapper extends PureComponent {
19 | static defaultProps = defaultProps;
20 | static propTypes = propTypes;
21 |
22 | // Wrap Action with close event
23 | withClose = (action, useReduxCloseAction = false) => () => {
24 | const { id, close } = this.props;
25 | const modalAction = isFunction(action) ? action : noop;
26 |
27 | if (useReduxCloseAction) {
28 | modalAction();
29 | } else {
30 | modalAction();
31 | close(id);
32 | }
33 | };
34 |
35 | getCloseEvent = () => {
36 | const { cancelButton, onClose, onCancel, useCloseAction, useCancelAction } = this.props;
37 | return cancelButton ? this.withClose(onCancel, useCloseAction) : this.withClose(onClose, useCancelAction);
38 | };
39 |
40 | handleBackgroundClick = e => {
41 | if (e.target === e.currentTarget) {
42 | const closeEvent = this.getCloseEvent();
43 | closeEvent();
44 | }
45 | };
46 |
47 | handleKeyPress = ({ keyCode }) => {
48 | if (keyCode === 27) {
49 | const closeEvent = this.getCloseEvent();
50 | closeEvent();
51 | }
52 | };
53 |
54 | getButtonType = (type = '') => ({
55 | alert: type === 'alert',
56 | info: type === 'info',
57 | success: type === 'success',
58 | noAccent: type === 'noAccent',
59 | });
60 |
61 | componentDidMount() {
62 | document.addEventListener('keyup', this.handleKeyPress);
63 | }
64 |
65 | componentWillUnmount() {
66 | document.removeEventListener('keyup', this.handleKeyPress);
67 | }
68 |
69 | render() {
70 | const {
71 | id,
72 | buttons,
73 | children,
74 | collapse,
75 | cancelText,
76 | closeText,
77 | cancelButton,
78 | closeButton,
79 | footNote,
80 | useCloseAction,
81 | useCancelAction,
82 | locked,
83 | theme,
84 | title,
85 | type,
86 | width,
87 | valid,
88 | onCancel,
89 | onClose,
90 | transitionEnter,
91 | transitionLeave,
92 | } = this.props;
93 |
94 | const modalButtons = [];
95 |
96 | if (closeButton) {
97 | modalButtons.push({
98 | action: onClose,
99 | useReduxAction: useCloseAction,
100 | text: closeText,
101 | type: type || 'info',
102 | disabled: !valid,
103 | });
104 | }
105 |
106 | if (cancelButton) {
107 | modalButtons.push({
108 | action: onCancel,
109 | useReduxAction: useCancelAction,
110 | type: 'noAccent',
111 | text: cancelText,
112 | });
113 | }
114 |
115 | const modalActions = [...buttons, ...modalButtons];
116 |
117 | return (
118 |
125 |
134 | {title && {title}
}
135 | {children}
136 | {modalActions.length > 0 && (
137 |
138 |
139 | {modalActions.map(({ action, type, text, useReduxAction, ...props }, i) => (
140 |
147 | {text}
148 |
149 | ))}
150 |
151 |
152 | )}
153 | {footNote && {footNote}
}
154 |
155 |
156 | );
157 | }
158 | }
159 |
160 | export default ModalWrapper;
161 |
--------------------------------------------------------------------------------
/src/components/Modals/ModalWrapper.statics.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import noop from 'lodash/noop';
3 |
4 | export const propTypes = {
5 | buttons: PropTypes.arrayOf(
6 | PropTypes.shape({
7 | action: PropTypes.func.isRequired,
8 | text: PropTypes.string.isRequired,
9 | type: PropTypes.string.isRequired,
10 | })
11 | ),
12 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.element, PropTypes.string]).isRequired,
13 | collapse: PropTypes.bool,
14 | cancelText: PropTypes.string,
15 | closeText: PropTypes.string,
16 | cancelButton: PropTypes.bool,
17 | closeButton: PropTypes.bool,
18 | footNote: PropTypes.string,
19 | locked: PropTypes.bool,
20 | title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
21 | type: PropTypes.string,
22 | width: PropTypes.number,
23 | valid: PropTypes.bool,
24 | edited: PropTypes.bool,
25 |
26 | // Methods
27 | close: PropTypes.func.isRequired,
28 | showModal: PropTypes.func.isRequired,
29 | onCancel: PropTypes.func,
30 | onClose: PropTypes.func,
31 | };
32 |
33 | export const defaultProps = {
34 | buttons: [],
35 | cancelText: 'Cancel',
36 | closeText: 'OK',
37 | collapse: false,
38 | cancelButton: true,
39 | closeButton: true,
40 | locked: false,
41 | title: '',
42 | type: '',
43 | useCloseAction: false,
44 | useCancelAction: false,
45 | valid: true,
46 | edited: false,
47 |
48 | // Methods
49 | close: noop,
50 | showModal: noop,
51 | onCancel: noop,
52 | onClose: noop,
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/Modals/ModalWrapper.styled.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import { transparentize } from 'style/theme';
4 |
5 | export const ModalBackdrop = styled.div`
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | bottom: 0;
10 | right: 0;
11 | z-index: 209;
12 | padding: 100px;
13 | overflow: auto;
14 | background: ${props => transparentize(props.theme.B800, 0.7)};
15 |
16 | .modal-enter & {
17 | opacity: 0.01;
18 | }
19 |
20 | .modal-enter.modal-enter-active & {
21 | opacity: 1;
22 | transition: opacity ${props => props.transitionEnter};
23 | }
24 |
25 | .modal-leave & {
26 | opacity: 1;
27 | }
28 |
29 | .modal-leave.modal-leave-active & {
30 | opacity: 0.01;
31 | transition: opacity ${props => props.transitionLeave};
32 | }
33 | `;
34 |
35 | const Modal = styled.div`
36 | background: ${props => props.theme.B300};
37 | color: ${props => props.theme.font_primary};
38 | border-radius: 5px;
39 | box-shadow: 0 11px 7px 0 rgba(0, 0, 0, 0.19), 0 13px 25px 0 rgba(0, 0, 0, 0.3);
40 | padding: ${props => (props.collapse ? '0' : '36px')};
41 | max-width: 1024px;
42 | margin: 0 auto;
43 | transform-origin: top center;
44 |
45 | .Modal__header {
46 | font-size: 20px;
47 | font-weight: 500;
48 | margin: 0 0 25px;
49 | }
50 |
51 | .Modal__content {
52 | font-size: 14px;
53 | font-weight: 400;
54 | line-height: 20px;
55 | margin: 0 0 25px;
56 |
57 | p {
58 | margin: 0 0 15px;
59 |
60 | &:last-child {
61 | margin: 0;
62 | }
63 | }
64 |
65 | &:last-child {
66 | margin: 0;
67 | }
68 | }
69 |
70 | .Modal__footNote {
71 | position: absolute;
72 | left: 50%;
73 | font-size: 12px;
74 | font-weight: 300;
75 | font-style: italic;
76 | text-align: center;
77 | transform: translateX(-50%) translateY(100%);
78 | }
79 |
80 | .modal-enter & {
81 | transform: scale(0.9);
82 | opacity: 0.01;
83 | }
84 |
85 | .modal-enter.modal-enter-active & {
86 | transform: scale(1);
87 | opacity: 1;
88 | transition: transform ${props => props.transitionEnter}, opacity ${props => props.transitionEnter};
89 | }
90 |
91 | .modal-leave & {
92 | transform: scale(1);
93 | opacity: 0.1;
94 | }
95 |
96 | .modal-leave.modal-leave-active & {
97 | transform: scale(0.7);
98 | opacity: 0.01;
99 | transition: transform ${props => props.transitionLeave}, opacity ${props => props.transitionLeave};
100 | }
101 | `;
102 |
103 | export const ModalActions = styled.div`
104 | display: grid;
105 | grid-template-columns: repeat(${props => props.length || 2}, 1fr);
106 | grid-gap: 0 6px;
107 | `;
108 |
109 | export default Modal;
110 |
--------------------------------------------------------------------------------
/src/components/Modals/NotificationModal.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import ModalWrapper from './ModalWrapper';
4 | import StyledNotification from './NotificationModal.styles';
5 |
6 | import PlayMidnightLogo from 'components/PlayMidnightLogo';
7 |
8 | const NotificationModal = ({ details, ...props }) => {
9 | const { DETAILS, Template } = details.notification;
10 | const title = (
11 |
12 |
15 |
16 | Play Midnight Material
17 |
18 |
24 |
25 | );
26 |
27 | return (
28 |
29 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default NotificationModal;
48 |
--------------------------------------------------------------------------------
/src/components/Modals/NotificationModal.styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import withTheme from 'hoc/withTheme';
4 |
5 | const NotificationModal = styled.div`
6 | a {
7 | color: ${props => props.theme.font_primary};
8 | }
9 |
10 | .Modal {
11 | max-width: 750px;
12 | }
13 |
14 | .Modal__header {
15 | position: relative;
16 | text-align: center;
17 | background: ${props => props.theme.B200};
18 | border-bottom: 1px solid ${props => props.theme.B400};
19 | box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.3);
20 | color: ${props => props.font_primary};
21 | z-index: 1;
22 | flex: 0 0 auto;
23 | padding: 15px 25px;
24 | margin: 0;
25 |
26 | .Modal__header-title {
27 | background: none;
28 | padding: 0;
29 | font-weight: 500;
30 | margin: 0 0 5px;
31 |
32 | em {
33 | font-weight: 300;
34 | }
35 | }
36 |
37 | .Modal__header-version {
38 | font-weight: 300;
39 | background: none;
40 | margin: 0;
41 | }
42 |
43 | a {
44 | color: ${props => props.font_primary};
45 | text-decoration: none;
46 |
47 | &:hover {
48 | text-decoration: underline;
49 | }
50 | }
51 | }
52 |
53 | .Modal__content {
54 | position: relative;
55 | height: 375px;
56 | line-height: 1.6;
57 | overflow: hidden;
58 | flex: 1 1 auto;
59 | margin: 0;
60 |
61 | &-container {
62 | position: absolute;
63 | top: 0;
64 | left: 0;
65 | right: 0;
66 | bottom: 0;
67 | padding: 25px 0;
68 | overflow: auto;
69 | }
70 | }
71 |
72 | .Modal__footer {
73 | position: relative;
74 | padding: 15px 25px;
75 | background: ${props => props.theme.B200};
76 | box-shadow: 0 -5px 25px 0 rgba(0, 0, 0, 0.3);
77 | z-index: 1;
78 | }
79 |
80 | .Modal__actions {
81 | display: flex;
82 | justify-content: center;
83 | margin: 0 0 5px;
84 |
85 | &:last-child {
86 | margin: 0;
87 | }
88 | }
89 | `;
90 |
91 | export default withTheme(NotificationModal);
92 |
--------------------------------------------------------------------------------
/src/components/Modals/ThemeDeleteModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ModalWrapper from './ModalWrapper';
4 | import ThemePreview from 'components/ThemePreview';
5 |
6 | const ThemeDeleteModal = ({ ...props }) => {
7 | const name = props.details.name ? props.details.name : 'Anonymous';
8 | const { accent, background } = props.details;
9 |
10 | return (
11 |
12 |
13 | Whoa there, friend! You sure you wanna delete the lovely {name} theme?
14 |
15 |
16 | This action{' '}
17 |
18 | cannot
19 | {' '}
20 | be undone, so just be sure about this.
21 |
22 |
23 |
24 | Take a second look. Beautiful, innit?
25 |
26 |
27 | );
28 | };
29 |
30 | export default ThemeDeleteModal;
31 |
--------------------------------------------------------------------------------
/src/components/Modals/ThemePickerModal.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { ChromePicker } from 'react-color';
3 | import { connect } from 'react-redux';
4 | import styled from 'styled-components';
5 |
6 | import withTheme from 'hoc/withTheme';
7 |
8 | import { DEFAULT_ACCENT, DEFAULT_BACKGROUND, TRANSITION_FAST } from 'style/theme';
9 | import { actions } from 'modules/modal';
10 |
11 | import Grid from 'components/Grid';
12 | import ModalWrapper from './ModalWrapper';
13 | import ThemePreview from 'components/ThemePreview';
14 |
15 | const FancyInput = styled.input`
16 | width: 100%;
17 | text-align: center;
18 | background: transparent;
19 | outline: none;
20 | box-shadow: none;
21 | box-sizing: border-box;
22 | color: ${props => props.theme.font_primary};
23 | border: none;
24 | font-size: 20px;
25 | padding: 10px;
26 | font-weight: 700;
27 | margin: 0 0 10px;
28 | cursor: pointer;
29 | transition: background ${TRANSITION_FAST}, box-shadow ${TRANSITION_FAST};
30 |
31 | &:hover,
32 | &:focus,
33 | &:active {
34 | background: ${props => props.theme.B400};
35 | box-shadow: inset 0 0 3px 0 rgba(0, 0, 0, 0.3);
36 | }
37 |
38 | &::placeholder {
39 | color: ${props => props.theme.font_secondary};
40 | }
41 | `;
42 |
43 | const PickerWrapper = styled.div`
44 | box-shadow: 0 11px 7px 0 rgba(0, 0, 0, 0.19), 0 13px 25px 0 rgba(0, 0, 0, 0.3);
45 | color: ${props => props.theme.black};
46 | margin: 0 0 20px;
47 |
48 | .chrome-picker {
49 | background: ${props => props.theme.B200} !important;
50 | color: ${props => props.theme.font_primary} !important;
51 |
52 | input {
53 | background: ${props => props.theme.B50} !important;
54 | border: 1px solid ${props => props.theme.B300} !important;
55 | color: ${props => props.theme.font_primary} !important;
56 | box-shadow: none !important;
57 | }
58 |
59 | svg {
60 | background: ${props => props.theme.B200} !important;
61 | transition: background ${TRANSITION_FAST};
62 |
63 | &:hover {
64 | background: ${props => props.theme.B100} !important;
65 | }
66 |
67 | path {
68 | fill: ${props => props.theme.font_primary} !important;
69 | }
70 | }
71 |
72 | span {
73 | color: ${props => props.theme.font_primary} !important;
74 | }
75 | }
76 | `;
77 |
78 | @withTheme
79 | @connect(null, { close: actions.closeModal })
80 | class ColorPickerModal extends PureComponent {
81 | constructor(props) {
82 | super(props);
83 |
84 | const details = props.details || {};
85 | this.state = {
86 | id: undefined,
87 | accent: DEFAULT_ACCENT,
88 | background: DEFAULT_BACKGROUND,
89 | name: '',
90 | ...details,
91 | };
92 | }
93 |
94 | onTitleChange = ({ target }) => {
95 | const value = target.type === 'checkbox' ? target.checked : target.value;
96 | const id = target.name;
97 | this.setState(state => ({ [id]: value }));
98 | };
99 |
100 | onColorChange = type => color => {
101 | this.setState(state => ({
102 | [type]: color.hex,
103 | }));
104 | };
105 |
106 | closeModal = acceptValue => {
107 | const { id, accent, background, name } = this.state;
108 | const { id: modalId, onClose, close } = this.props;
109 |
110 | if (acceptValue) {
111 | onClose({ id, accent, background, name });
112 | }
113 |
114 | close(modalId);
115 | };
116 |
117 | render() {
118 | const { accent, background, name } = this.state;
119 | const { theme } = this.props;
120 |
121 | return (
122 | 0}
126 | onClose={() => this.closeModal(true)}
127 | closeText="Save, It's Beautiful"
128 | cancelText="Cancel, Nevermind"
129 | width={475}
130 | locked
131 | >
132 |
143 |
144 |
Theme Preview
145 |
146 |
147 |
148 |
149 |
Background Color
150 |
151 |
152 |
153 |
154 |
155 |
Accent Color
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | Play around until you find the perfect color. Your eyes will thank you.
164 |
165 |
166 |
167 | );
168 | }
169 | }
170 |
171 | export default ColorPickerModal;
172 |
--------------------------------------------------------------------------------
/src/components/Options/Option/Option.styled.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import withTheme from 'hoc/withTheme';
4 |
5 | import { TRANSITION_FAST } from 'style/theme';
6 |
7 | export const CollectionItem = styled.label`
8 | position: relative;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | cursor: pointer;
13 | flex: 1;
14 | min-width: 33.33%;
15 | height: 85px;
16 | transition: all ${TRANSITION_FAST};
17 | padding: 8px 32px;
18 | cursor: pointer;
19 | overflow: hidden;
20 | background: ${props => props.background || 'transparent'};
21 | ${props => props.selected && `padding-bottom: 18px`};
22 |
23 | input {
24 | display: none;
25 | }
26 |
27 | .CollectionItem__fields {
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | position: absolute;
32 | content: '';
33 | top: 0;
34 | bottom: 0;
35 | left: 0;
36 | right: 0;
37 | background: rgba(0, 0, 0, 0.5);
38 | visibility: hidden;
39 | opacity: 0;
40 | transform: scale3d(1.3, 1.3, 1.3);
41 | text-shadow: 1px 1px 0 ${props => props.theme.black};
42 | transition: all ${TRANSITION_FAST};
43 | color: ${props => props.theme.white};
44 | }
45 |
46 | .CollectionItem__field {
47 | font-size: 12px;
48 | margin: 0 0 3px;
49 |
50 | &.CollectionItem__field--title {
51 | font-weight: 700;
52 | font-size: 14px;
53 | }
54 |
55 | &:last-child {
56 | margin: 0;
57 | }
58 | }
59 |
60 | &:before {
61 | position: absolute;
62 | content: '';
63 | top: 0;
64 | bottom: 0;
65 | left: 0;
66 | right: 0;
67 | background: ${props => props.accent || 'transparent'};
68 | clip-path: polygon(100% 0, 0% 100%, 100% 100%);
69 | }
70 |
71 | &:after {
72 | position: absolute;
73 | content: 'ACTIVE SELECTION';
74 | bottom: 0;
75 | left: 0;
76 | right: 0;
77 | background: rgba(0, 0, 0, 0.25);
78 | color: ${props => props.theme.white};
79 | text-shadow: 1px 1px 0 ${props => props.theme.black};
80 | padding: 3px 10px;
81 | font-size: 10px;
82 | text-align: center;
83 | transform: scale3d(1.1, 1.1, 1.1);
84 | opacity: 0;
85 | transition: all ${TRANSITION_FAST};
86 |
87 | ${props => props.selected && `opacity: 1`};
88 | ${props => props.selected && `transform: scale3d(1, 1, 1)`};
89 | }
90 |
91 | &:hover {
92 | .CollectionItem__fields {
93 | transform: scale3d(1, 1, 1);
94 | visibility: visible;
95 | opacity: 1;
96 | }
97 |
98 | .CollectionItem__icon {
99 | opacity: 0.8;
100 | transform: scale3d(1, 1, 1);
101 | }
102 | }
103 |
104 | .CollectionItem__icon {
105 | position: absolute;
106 | top: 4px;
107 | display: flex;
108 | align-items: center;
109 | justify-content: center;
110 | width: 24px;
111 | height: 24px;
112 | color: ${props => props.theme.white};
113 | background-color: rgba(0, 0, 0, 0.25);
114 | cursor: pointer;
115 | background-position: center center;
116 | background-repeat: no-repeat;
117 | opacity: 0;
118 | transform: scale3d(0.75, 0.75, 0.75);
119 | transition: all ${TRANSITION_FAST};
120 |
121 | &:hover {
122 | transform: scale3d(1.15, 1.15, 1.15);
123 | }
124 | }
125 |
126 | .CollectionItem__edit {
127 | left: 4px;
128 | }
129 |
130 | .CollectionItem__copy {
131 | left: 4px;
132 | top: 32px;
133 | }
134 |
135 | .CollectionItem__remove {
136 | right: 4px;
137 | }
138 |
139 | &:active,
140 | &:focus {
141 | opacity: 0.9;
142 | }
143 | `;
144 |
145 | const StyledOption = styled.div`
146 | background: ${props => props.theme.B300};
147 | border-bottom: 1px solid ${props => props.theme.B500};
148 | transition: background ${TRANSITION_FAST}, color ${TRANSITION_FAST}, border-color ${TRANSITION_FAST};
149 |
150 | &:last-child {
151 | border: none;
152 | }
153 |
154 | .Option__header {
155 | display: flex;
156 | align-items: center;
157 | padding: 15px 20px;
158 | }
159 |
160 | .Option__content {
161 | margin-right: 15px;
162 | }
163 |
164 | .Option__action {
165 | margin-left: auto;
166 | }
167 |
168 | .Option__action-button {
169 | font-size: 10px;
170 | line-height: 1.2;
171 | align-self: stretch;
172 | }
173 |
174 | .Option__title {
175 | font-size: 16px;
176 | font-weight: 700;
177 | margin: 0 0 8px;
178 | }
179 |
180 | .Option__description {
181 | font-size: 13px;
182 | line-height: 1.6;
183 | font-weight: 300;
184 | }
185 |
186 | .Option__color-slider {
187 | padding: 25px;
188 | }
189 |
190 | .Option__collection {
191 | display: flex;
192 | justify-content: center;
193 | flex-flow: row wrap;
194 | perspective: 1000;
195 | }
196 | }
197 | `;
198 |
199 | export default withTheme(StyledOption);
200 |
--------------------------------------------------------------------------------
/src/components/Options/Option/OptionCheckbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Checkbox from 'components/Checkbox';
4 | import StyledOption from './Option.styled';
5 |
6 | const Option = ({ id, title, description, reliesOn, values, onTargetedChange }) => {
7 | let disabled = false;
8 | let value = values[id];
9 |
10 | if (reliesOn) {
11 | disabled = values[reliesOn] === false;
12 | value = values[reliesOn] === true ? value : false;
13 | }
14 |
15 | return (
16 |
17 |
18 |
19 |
{title}
20 |
{description}
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Option;
31 |
--------------------------------------------------------------------------------
/src/components/Options/Option/OptionString.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import StyledOption from './Option.styled';
4 |
5 | const Option = ({ title, children }) => (
6 |
7 |
8 |
9 |
{title}
10 |
{children}
11 |
12 |
13 |
14 | );
15 |
16 | export default Option;
17 |
--------------------------------------------------------------------------------
/src/components/Options/Option/OptionThemes.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { actions } from 'modules/modal';
5 | import { insertAt, replaceItem, removeItem } from 'utils/array';
6 | import { validateId, validateTitle } from 'utils/validation';
7 |
8 | import Button from 'components/Button';
9 | import StyledOption, { CollectionItem } from './Option.styled';
10 | import IconCopy from 'components/Icons/IconCopy';
11 | import IconDropper from 'components/Icons/IconDropper';
12 | import IconPencil from 'components/Icons/IconPencil';
13 | import IconTrash from 'components/Icons/IconTrash';
14 |
15 | // TODO - Update to be dynamic and allow for createable - false
16 | @connect(null, { showModal: actions.showModal })
17 | class Option extends PureComponent {
18 | removeColor = (e, { id: colorId, name, accent, background }) => {
19 | const { id, defaultValues, plural, values, onChange, showModal } = this.props;
20 | const singleValue = values[id];
21 | const arrayValue = values[plural];
22 |
23 | e.preventDefault();
24 |
25 | const remove = () => {
26 | // Remove Item or Reset Default if Empty
27 | const updatedValues = arrayValue.length > 1 ? removeItem(arrayValue, { id: colorId }) : [...defaultValues];
28 |
29 | // Deleting Current Accent, reset to first in array
30 | if (singleValue === colorId) {
31 | onChange({
32 | id,
33 | value: updatedValues[0].id,
34 | });
35 | }
36 |
37 | onChange({
38 | id: plural,
39 | value: updatedValues,
40 | });
41 | };
42 |
43 | showModal('themeDelete', {
44 | type: 'alert',
45 | closeText: `Delete, It's Pure Garbage`,
46 | cancelText: `Cancel, Buyer's Remorse`,
47 | details: { name, accent, background },
48 | onClose: remove,
49 | });
50 | };
51 |
52 | saveColor = ({ id: existingColorId, name: rawName, accent, background }) => {
53 | const { id, plural, values, onChange } = this.props;
54 | const arrayValue = values[plural];
55 |
56 | const name = validateTitle(rawName);
57 | const colorId = existingColorId ? existingColorId : validateId(name);
58 |
59 | onChange({
60 | id: plural,
61 | value: replaceItem(arrayValue, { id: colorId, name, accent, background }),
62 | });
63 |
64 | onChange({
65 | id,
66 | value: colorId,
67 | });
68 | };
69 |
70 | duplicateColor = (e, { name: rawName, accent, background }, index) => {
71 | e.preventDefault();
72 |
73 | const { plural, values, onChange, showModal } = this.props;
74 | const arrayValue = values[plural];
75 |
76 | const name = validateTitle(`${rawName} Copy`);
77 | const id = validateId(name);
78 |
79 | onChange({
80 | id: plural,
81 | value: insertAt(arrayValue, { id, name, accent, background }, index),
82 | });
83 |
84 | showModal('themePicker', {
85 | details: { id, name, accent, background },
86 | onClose: this.saveColor,
87 | });
88 | };
89 |
90 | editColor = (e, details) => {
91 | e.preventDefault();
92 |
93 | const { showModal } = this.props;
94 |
95 | showModal('themePicker', {
96 | details,
97 | onClose: this.saveColor,
98 | });
99 | };
100 |
101 | render() {
102 | const { id, plural, title, description, theme, values, onTargetedChange } = this.props;
103 | const singleValue = values[id];
104 | const arrayValues = values[plural];
105 |
106 | const renderItems = items =>
107 | items.map((item, i) => {
108 | const { id: colorId, name, accent, background } = item;
109 |
110 | return (
111 |
119 |
126 |
129 | this.editColor(e, { id: colorId, name, accent, background })}
134 | >
135 |
136 |
137 | this.duplicateColor(e, { name, accent, background }, i)}
142 | >
143 |
144 |
145 | this.removeColor(e, { id: colorId, name, accent, background })}
150 | >
151 |
152 |
153 |
154 | );
155 | });
156 |
157 | return (
158 |
159 |
160 |
161 |
{title}
162 |
{description}
163 |
164 |
165 |
166 |
167 | NEW THEME
168 |
169 |
170 |
171 |
172 | {renderItems(arrayValues)}
173 |
174 | );
175 | }
176 | }
177 |
178 | export default Option;
179 |
--------------------------------------------------------------------------------
/src/components/Options/Option/index.js:
--------------------------------------------------------------------------------
1 | export { default as OptionCheckbox } from './OptionCheckbox';
2 | export { default as OptionString } from './OptionString';
3 | export { default as OptionThemes } from './OptionThemes';
4 |
--------------------------------------------------------------------------------
/src/components/Options/Options.container.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import withTheme from 'hoc/withTheme';
5 |
6 | import { actions, selectors } from 'modules/options';
7 |
8 | import Options from './Options';
9 |
10 | const mapStateToProps = state => ({
11 | sections: selectors.sortedOptions(state),
12 | values: selectors.optionsValues(state),
13 | version: selectors.version(state),
14 | menuVisible: state.options.menuVisible,
15 | });
16 |
17 | @withTheme
18 | @connect(mapStateToProps, {
19 | onSave: actions.saveOptions,
20 | onToggle: actions.toggleMenu,
21 | onUpdate: actions.updateOption,
22 | })
23 | class OptionsContainer extends PureComponent {
24 | closeOptions = () => {
25 | const { onToggle } = this.props;
26 | onToggle(false);
27 | };
28 |
29 | saveOptions = () => {
30 | const { values, onSave } = this.props;
31 | onSave(values);
32 | };
33 |
34 | updateTargetedOption = ({ target }) => {
35 | const { onUpdate } = this.props;
36 | const value = target.type === 'checkbox' ? target.checked : target.value;
37 | const id = target.name;
38 | onUpdate({ id, value });
39 | };
40 |
41 | updateOption = ({ id, value }) => {
42 | const { onUpdate } = this.props;
43 | onUpdate({ id, value });
44 | };
45 |
46 | render() {
47 | const { sections, theme, menuVisible, values, version } = this.props;
48 |
49 | return (
50 |
61 | );
62 | }
63 | }
64 |
65 | export default OptionsContainer;
66 |
--------------------------------------------------------------------------------
/src/components/Options/Options.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
3 |
4 | import { stripTransition, TRANSITION_FAST } from 'style/theme';
5 | import Button from 'components/Button';
6 |
7 | import StyledOptions, { Backdrop } from './Options.styled';
8 | import { OptionArray, OptionCheckbox, OptionString, OptionThemes } from './Option';
9 | import PlayMidnightLogo from 'components/PlayMidnightLogo';
10 | import Section from './Section';
11 |
12 | const OPTION_TYPES = {
13 | array: OptionArray,
14 | boolean: OptionCheckbox,
15 | string: OptionString,
16 | themes: OptionThemes,
17 | };
18 |
19 | const Options = ({
20 | theme,
21 | visible,
22 | sections,
23 | values,
24 | version,
25 | onArrayChange,
26 | onOptionChange,
27 | onTargetedChange,
28 | onClose,
29 | onSave,
30 | }) => {
31 | const handleBackgroundClick = e => {
32 | if (e.target === e.currentTarget) onClose();
33 | };
34 |
35 | const renderSections = sections =>
36 | sections.map(({ id, title, options }) => {
37 | return options.length > 0 ? (
38 |
39 | {options.map(option => {
40 | const Option = OPTION_TYPES[option.type];
41 | return (
42 |
52 | );
53 | })}
54 |
55 | ) : null;
56 | });
57 |
58 | return (
59 |
64 | {visible ? (
65 |
66 |
67 |
81 |
82 |
83 |
84 | {renderSections(sections)}
85 |
86 |
87 |
88 | Why hello! Just wanted to take another minute to thank all of you for checking out Play Midnight.
89 | This project has been something that has grown with me over time and I can't begin to express how
90 | much I've enjoyed hearing from all of you. There have been and will be bugs, and I appreciate that
91 | you guys have been kind enough not to tear me apart while I fix them. I've poured as much of
92 | myself into this as I can and I hope that you enjoy it as much as I do. :)
93 |
94 |
95 | Again, if you notice any issues, or have any suggestions don't hesitate to shoot that over to me.
96 | The best place to reach me is through the subreddit at{' '}
97 |
103 | /r/PlayMidnight
104 | {' '}
105 | or through{' '}
106 |
112 | email
113 | . If you'd like to shoot me a donation or buy me some coffee, you can do so via{' '}
114 |
120 | PayPal
121 | {' '}
122 | or swing by my{' '}
123 |
129 | Etsy
130 | {' '}
131 | shop. Once again, thanks again for all the feedback and I hope you enjoy Play Midnight, peace!
132 |
133 |
134 |
135 |
136 |
137 |
138 |
141 |
142 |
143 | ) : null}
144 |
145 | );
146 | };
147 |
148 | export default Options;
149 |
--------------------------------------------------------------------------------
/src/components/Options/Options.styled.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import { TRANSITION_FAST } from 'style/theme';
4 |
5 | export const Backdrop = styled.div`
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | bottom: 0;
10 | right: 0;
11 | z-index: 109;
12 | `;
13 |
14 | const StyledOptions = styled.div`
15 | display: flex;
16 | flex-flow: column;
17 | position: fixed;
18 | left: calc(50vw - 300px);
19 | bottom: 118px;
20 | width: 600px;
21 | height: calc(100vh - 110px - 137px);
22 | min-height: 360px;
23 | max-height: 750px;
24 | z-index: 110;
25 | border-radius: 3px;
26 | color: ${props => props.theme.font_primary};
27 | background: ${props => props.theme.B300};
28 | box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12),
29 | 0 8px 10px -5px rgba(0, 0, 0, 0.4);
30 | transform-origin: center bottom 0;
31 | transition: background ${TRANSITION_FAST}, color ${TRANSITION_FAST};
32 |
33 | &:after {
34 | content: '';
35 | position: absolute;
36 | width: 0;
37 | height: 0;
38 | box-sizing: border-box;
39 | left: calc(100% / 2 - 8px);
40 | bottom: -8px;
41 | transform-origin: 50% 50%;
42 | transform: rotate(-45deg);
43 | border: 8px solid transparent;
44 | border-color: ${props => `transparent transparent ${props.theme.B400} ${props.theme.B400}`};
45 | box-shadow: -12px 12px 15px 0px rgba(0, 0, 0, 0.24);
46 | transition: border-color ${TRANSITION_FAST};
47 | }
48 |
49 | .animate-enter & {
50 | transform: scale(0.2);
51 | opacity: 0.01;
52 | }
53 |
54 | .animate-enter.animate-enter-active & {
55 | transform: scale(1);
56 | opacity: 1;
57 | transition: transform ${props => props.transitionEnter}, opacity ${props => props.transitionEnter};
58 | }
59 |
60 | .animate-leave & {
61 | transform: scale(1);
62 | opacity: 1;
63 | }
64 |
65 | .animate-leave.animate-leave-active & {
66 | transform: scale(0.2);
67 | opacity: 0.01;
68 | transition: transform ${props => props.transitionLeave}, opacity ${props => props.transitionLeave};
69 | }
70 |
71 | .Options__header {
72 | position: relative;
73 | text-align: center;
74 | flex: 0 0 auto;
75 | border-radius: 3px 3px 0 0;
76 | background: ${props => props.theme.B400};
77 | border-bottom: 1px solid ${props => props.theme.B600};
78 | padding: 15px 25px;
79 | box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.3);
80 | z-index: 1;
81 | transition: background ${TRANSITION_FAST}, color ${TRANSITION_FAST}, border-color ${TRANSITION_FAST};
82 |
83 | .Options__header-title {
84 | font-weight: 300;
85 | font-size: 28px;
86 | margin: 0 0 5px;
87 |
88 | span {
89 | font-weight: 100;
90 | }
91 | }
92 |
93 | .Options__header-version {
94 | font-size: 10px;
95 | font-weight: 700;
96 | }
97 | }
98 |
99 | .Options__options {
100 | position: relative;
101 | flex: 1 1 auto;
102 | overflow: hidden;
103 |
104 | .Options__options-container {
105 | position: absolute;
106 | top: 0;
107 | right: 0;
108 | left: 0;
109 | bottom: 0;
110 | overflow: auto;
111 | }
112 | }
113 |
114 | .Options__footer {
115 | position: relative;
116 | flex: 0 0 auto;
117 | border-radius: 0 0 3px 3px;
118 | background: ${props => props.theme.B400};
119 | border-top: 1px solid ${props => props.theme.B500};
120 | box-shadow: 0 -5px 25px 0 rgba(0, 0, 0, 0.3);
121 | z-index: 1;
122 | padding: 15px 25px;
123 | text-align: center;
124 | transition: background ${TRANSITION_FAST}, color ${TRANSITION_FAST}, border-color ${TRANSITION_FAST};
125 | }
126 |
127 | a {
128 | color: ${props => props.theme.font_primary};
129 | text-decoration: none;
130 | transition: color ${TRANSITION_FAST};
131 |
132 | &:hover {
133 | text-decoration: underline;
134 | }
135 | }
136 | `;
137 |
138 | export default StyledOptions;
139 |
--------------------------------------------------------------------------------
/src/components/Options/Section.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import styled from 'styled-components';
3 |
4 | import withTheme from 'hoc/withTheme';
5 |
6 | import { TRANSITION_FAST } from 'style/theme';
7 |
8 | const StyledSection = styled.div`
9 | &:last-child .Options__section-options {
10 | border: none;
11 | }
12 |
13 | .Section__title {
14 | display: flex;
15 | align-items: center;
16 | background: ${props => props.theme.B400};
17 | margin: 0;
18 | padding: 15px 20px;
19 | font-size: 16px;
20 | font-weight: 700;
21 | border-bottom: 1px solid ${props => props.theme.B500};
22 | cursor: pointer;
23 | transition: background ${TRANSITION_FAST}, color ${TRANSITION_FAST}, border-color ${TRANSITION_FAST};
24 |
25 | &:last-child {
26 | border: none;
27 | }
28 | }
29 |
30 | .Section__text {
31 | margin-right: 15px;
32 | }
33 |
34 | .Section__toggle {
35 | margin-left: auto;
36 |
37 | .Section__toggle-icon {
38 | width: 8px;
39 | height: 8px;
40 | transform: rotate(45deg);
41 | border-right: 2px solid ${props => props.theme.font_primary};
42 | border-bottom: 2px solid ${props => props.theme.font_primary};
43 | transition: transform ${TRANSITION_FAST};
44 |
45 | ${props => !props.toggled && `transform: rotate(-45deg)`};
46 | }
47 | }
48 |
49 | .Section__options {
50 | border-bottom: 1px solid ${props => props.theme.B500};
51 | transition: border-color ${TRANSITION_FAST};
52 |
53 | ${props => !props.toggled && `display: none`};
54 | ${props => !props.toggled && `border-bottom-color: transparent`};
55 |
56 | &:empty {
57 | border: none;
58 | }
59 | }
60 | `;
61 |
62 | @withTheme
63 | class Section extends PureComponent {
64 | state = {
65 | toggled: this.props.closed ? false : true,
66 | };
67 |
68 | onToggle = () => {
69 | this.setState(state => ({
70 | toggled: !state.toggled,
71 | }));
72 | };
73 |
74 | render() {
75 | const { toggled } = this.state;
76 | const { children, title, theme } = this.props;
77 |
78 | return (
79 |
80 | {title && (
81 |
87 | )}
88 | {children}
89 |
90 | );
91 | }
92 | }
93 |
94 | export default Section;
95 |
--------------------------------------------------------------------------------
/src/components/Options/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Options.container';
2 |
--------------------------------------------------------------------------------
/src/components/PlayMidnight.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { actions } from 'modules/options';
5 | import Components from 'options/Components';
6 |
7 | import StyledPlayMidnight from './PlayMidnight.styled';
8 | import Options from './Options';
9 |
10 | @connect(null, { fetchOptions: actions.fetchOptions })
11 | class PlayMidnight extends PureComponent {
12 | componentDidMount() {
13 | this.props.fetchOptions();
14 | }
15 |
16 | render() {
17 | const renderComponents = () => {
18 | return Components.map((Component, i) => );
19 | };
20 |
21 | return (
22 |
23 |
24 | {renderComponents()}
25 |
26 | );
27 | }
28 | }
29 |
30 | export default PlayMidnight;
31 |
--------------------------------------------------------------------------------
/src/components/PlayMidnight.styled.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const PlayMidnight = styled.div`
4 | font-family: 'Roboto', Helvetica, sans-serif;
5 |
6 | * {
7 | box-sizing: border-box;
8 | }
9 | `;
10 |
11 | export default PlayMidnight;
12 |
--------------------------------------------------------------------------------
/src/components/PlayMidnightLogo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import { TRANSITION_FAST } from 'style/theme';
5 |
6 | const StyledLogo = styled.svg`
7 | width: 45px;
8 | height: 45px;
9 | fill: currentColor;
10 | transition: fill ${TRANSITION_FAST};
11 | `;
12 |
13 | const PlayMidnightLogo = props => (
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default PlayMidnightLogo;
21 |
--------------------------------------------------------------------------------
/src/components/PlayMusicLogo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import withTheme from 'hoc/withTheme';
5 |
6 | import { TRANSITION_FAST } from 'style/theme';
7 |
8 | const StyledLogo = styled.svg`
9 | margin-top: -6px;
10 | margin-left: 2px;
11 | width: 170px;
12 | height: 60px;
13 |
14 | .Logo__brand,
15 | .Logo__product {
16 | transition: fill ${TRANSITION_FAST};
17 | }
18 |
19 | .Logo__brand {
20 | fill: ${props => (props.enabled ? props.theme.font_primary : props.theme.font_google)};
21 | }
22 |
23 | .Logo__product {
24 | fill: ${props => props.theme.A500};
25 | }
26 | `;
27 |
28 | const PlayMusicLogo = ({ enabled, theme }) => (
29 |
30 |
34 |
38 |
42 |
46 |
50 |
54 |
55 | );
56 |
57 | export default withTheme(PlayMusicLogo);
58 |
--------------------------------------------------------------------------------
/src/components/Root.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 |
4 | import store from 'lib/store';
5 |
6 | import PlayMidnight from 'components/PlayMidnight';
7 | import ModalContainer from 'components/Modal.container';
8 | import ToastContainer from 'components/Toast.container';
9 |
10 | const Root = () => (
11 |
12 |
17 |
18 | );
19 |
20 | export default Root;
21 |
--------------------------------------------------------------------------------
/src/components/ThemePreview.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import { isLight, FONT_LIGHT, FONT_DARK } from 'style/theme';
5 |
6 | import Button from 'components/Button';
7 | import Checkbox from 'components/Checkbox';
8 |
9 | const PrettyColor = styled.div`
10 | position: relative;
11 | display: flex;
12 | flex-flow: column nowrap;
13 | align-items: center;
14 | justify-content: center;
15 | padding: 20px;
16 | border-radius: 5px;
17 | background: ${props => props.background};
18 | box-shadow: 0 11px 7px 0 rgba(0, 0, 0, 0.19), 0 13px 25px 0 rgba(0, 0, 0, 0.3);
19 | margin: 0 0 15px;
20 | `;
21 |
22 | const ThemePreview = ({ accent, background }) => (
23 |
24 |
25 |
26 | Such Button, Wow!
27 |
28 | Some Text!
29 |
30 | );
31 |
32 | export default ThemePreview;
33 |
--------------------------------------------------------------------------------
/src/components/Toast.container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
3 | import { connect } from 'react-redux';
4 | import styled from 'styled-components';
5 |
6 | import { selectors as optionSelectors } from 'modules/options';
7 | import { selectors } from 'modules/toast';
8 |
9 | import { stripTransition, TRANSITION_FAST, TRANSITION_MEDIUM } from 'style/theme';
10 | import AlertToast from 'components/Toasts/AlertToast';
11 | import NotificationToast from 'components/Toasts/NotificationToast';
12 | import SuccessToast from 'components/Toasts/SuccessToast';
13 |
14 | const types = {
15 | alert: ,
16 | notification: ,
17 | success: ,
18 | };
19 |
20 | const ToastContainer = styled.div`
21 | position: fixed;
22 | top: 79px;
23 | right: 26px;
24 | z-index: 119;
25 |
26 | ${props => props.hasSidebar && `right: 326px`};
27 | `;
28 |
29 | const ToastConductor = ({ options = {}, toasts = [] }) => {
30 | const getToastComponent = type => types[type] || null;
31 |
32 | return (
33 |
34 |
39 | {toasts.map(toast => {
40 | const Toast = getToastComponent(toast.type);
41 |
42 | return Toast
43 | ? React.cloneElement(Toast, {
44 | key: toast.id,
45 | id: toast.id,
46 | transitionEnter: TRANSITION_MEDIUM,
47 | transitionLeave: TRANSITION_FAST,
48 | ...toast.options,
49 | })
50 | : null;
51 | })}
52 |
53 |
54 | );
55 | };
56 |
57 | const stateToProps = state => ({
58 | options: optionSelectors.optionsValues(state),
59 | toasts: selectors.toasts(state),
60 | });
61 |
62 | export default connect(stateToProps)(ToastConductor);
63 |
--------------------------------------------------------------------------------
/src/components/Toasts/AlertToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ToastWrapper from './ToastWrapper';
4 |
5 | const AlertToast = ({ ...props }) => {
6 | return (
7 |
8 | {props.message || 'This is an alert.'}
9 |
10 | );
11 | };
12 |
13 | export default AlertToast;
14 |
--------------------------------------------------------------------------------
/src/components/Toasts/NotificationToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ToastWrapper from './ToastWrapper';
4 |
5 | const NotificationToast = ({ details, ...props }) => {
6 | const { DETAILS, Template } = details.notification;
7 |
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default NotificationToast;
16 |
--------------------------------------------------------------------------------
/src/components/Toasts/SuccessToast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ToastWrapper from './ToastWrapper';
4 |
5 | const SuccessToast = ({ ...props }) => {
6 | return (
7 |
8 | {props.message || 'This is a success.'}
9 |
10 | );
11 | };
12 |
13 | export default SuccessToast;
14 |
--------------------------------------------------------------------------------
/src/components/Toasts/ToastWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import styled from 'styled-components';
3 | import { connect } from 'react-redux';
4 | import PropTypes from 'prop-types';
5 | import isNaN from 'lodash/isNaN';
6 | import noop from 'lodash/noop';
7 |
8 | import withTheme from 'hoc/withTheme';
9 |
10 | import { actions } from 'modules/toast';
11 |
12 | const DEFAULT_TIMEOUT = 4000;
13 |
14 | const Toast = styled.div`
15 | position: relative;
16 | width: 300px;
17 | border-radius: 5px;
18 | background: ${props => props.theme.B300};
19 | color: ${props => props.theme.font_primary};
20 | padding: 20px 20px 35px;
21 | margin: 0 0 15px;
22 | cursor: pointer;
23 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
24 |
25 | ${props => props.type === 'success' && `border-top: 10px solid ${props.theme.A500}`};
26 | ${props => props.type === 'alert' && `border-top: 10px solid ${props.theme.red}`};
27 |
28 | &:after {
29 | position: absolute;
30 | bottom: 0;
31 | left: 0;
32 | right: 0;
33 | content: 'click to close';
34 | text-align: center;
35 | padding: 8px;
36 | font-size: 10px;
37 | font-style: italic;
38 | color: ${props => props.theme.font_secondary};
39 | }
40 |
41 | &:last-child {
42 | margin: 0;
43 | }
44 |
45 | &.toast-enter {
46 | transform: scale(0.8);
47 | opacity: 0.01;
48 | }
49 |
50 | &.toast-enter.toast-enter-active {
51 | transform: scale(1);
52 | opacity: 1;
53 | transition: transform ${props => props.transitionEnter}, opacity ${props => props.transitionEnter};
54 | }
55 |
56 | &.toast-leave {
57 | transform: scale(0.8);
58 | opacity: 1;
59 | }
60 |
61 | &.toast-leave.toast-leave-active {
62 | transform: scale(0);
63 | opacity: 0.01;
64 | transition: transform ${props => props.transitionLeave}, opacity ${props => props.transitionLeave};
65 | }
66 |
67 | .Toast__header {
68 | font-size: 16px;
69 | font-weight: 500;
70 | margin: 0 0 15px;
71 | }
72 |
73 | .Toast__content {
74 | font-size: 14px;
75 | font-weight: 400;
76 | line-height: 1.5;
77 | }
78 | `;
79 |
80 | @withTheme
81 | class ToastWrapper extends PureComponent {
82 | static propTypes = {
83 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.element, PropTypes.string]).isRequired,
84 | title: PropTypes.string,
85 | type: PropTypes.string,
86 | timeout: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
87 |
88 | // Methods
89 | close: PropTypes.func.isRequired,
90 | onClose: PropTypes.func.isRequired,
91 | };
92 |
93 | static defaultProps = {
94 | title: '',
95 | type: 'info',
96 | timeout: DEFAULT_TIMEOUT,
97 |
98 | // Methods
99 | close: noop,
100 | onClose: noop,
101 | };
102 |
103 | startTimer = () => {
104 | const { onClose, timeout } = this.props;
105 |
106 | if (timeout !== false) {
107 | const parseTimeout = parseInt(timeout, 10);
108 | const computedTimeout = !isNaN(parseTimeout) ? parseTimeout : DEFAULT_TIMEOUT;
109 | this.timer = setTimeout(this.withClose(onClose), computedTimeout);
110 | }
111 | };
112 |
113 | stopTimer = () => {
114 | if (this.timer) {
115 | clearTimeout(this.timer);
116 | }
117 | };
118 |
119 | withClose = (action = noop) => ({ target }) => {
120 | const { id, close } = this.props;
121 |
122 | // Prevent close when clicking link
123 | if (target.tagName.toLowerCase() === 'a') return;
124 |
125 | action();
126 | close(id);
127 | };
128 |
129 | componentDidMount() {
130 | this.startTimer();
131 | }
132 |
133 | componentWillUnmount() {
134 | this.stopTimer();
135 | }
136 |
137 | render() {
138 | const { children, theme, title, type, onClose, transitionEnter, transitionLeave } = this.props;
139 |
140 | return (
141 |
150 | {title}
151 | {children}
152 |
153 | );
154 | }
155 | }
156 |
157 | export default connect(null, { close: actions.closeToast })(ToastWrapper);
158 |
--------------------------------------------------------------------------------
/src/hoc/withOptions.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { selectors } from 'modules/options';
5 |
6 | const mapStateToProps = state => ({
7 | options: selectors.optionsValues(state),
8 | });
9 |
10 | const withOptions = Component => {
11 | @connect(mapStateToProps)
12 | class OptionsComponent extends PureComponent {
13 | render() {
14 | return ;
15 | }
16 | }
17 |
18 | return OptionsComponent;
19 | };
20 |
21 | export default withOptions;
22 |
--------------------------------------------------------------------------------
/src/hoc/withPortal.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { createPortal } from 'react-dom';
3 |
4 | import awaitElement from 'utils/awaitElement';
5 |
6 | const withPortal = where => Component => {
7 | class PortalComponent extends PureComponent {
8 | state = {
9 | render: false,
10 | };
11 |
12 | constructor(props) {
13 | super(props);
14 | this.el = document.querySelector(where);
15 | }
16 |
17 | async componentDidMount() {
18 | if (this.el) {
19 | this.setState({ render: true });
20 | } else {
21 | this.el = await awaitElement(where);
22 | this.setState({ render: true });
23 | }
24 | }
25 |
26 | render() {
27 | const { render } = this.state;
28 | return render ? createPortal( , this.el) : null;
29 | }
30 | }
31 |
32 | return PortalComponent;
33 | };
34 |
35 | export default withPortal;
36 |
--------------------------------------------------------------------------------
/src/hoc/withStyles.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 | import noop from 'lodash/noop';
4 |
5 | import { selectors } from 'modules/options';
6 |
7 | const mapStateToProps = state => ({
8 | theme: selectors.theme(state),
9 | });
10 |
11 | const withStyles = (styleGenerator = noop) => Component => {
12 | @connect(mapStateToProps)
13 | class StyleComponent extends PureComponent {
14 | generateStylesheet = () => {
15 | const { theme } = this.props;
16 | return styleGenerator(theme);
17 | };
18 |
19 | render() {
20 | return ;
21 | }
22 | }
23 |
24 | return StyleComponent;
25 | };
26 |
27 | export default withStyles;
28 |
--------------------------------------------------------------------------------
/src/hoc/withTheme.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { selectors } from 'modules/options';
5 |
6 | const mapStateToProps = state => ({
7 | theme: selectors.theme(state),
8 | });
9 |
10 | const withTheme = Component => {
11 | @connect(mapStateToProps)
12 | class StyleComponent extends PureComponent {
13 | render() {
14 | const { theme } = this.props;
15 | return ;
16 | }
17 | }
18 |
19 | return StyleComponent;
20 | };
21 |
22 | export default withTheme;
23 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Play Midnight Sandbox
9 |
67 |
68 |
69 |
70 |
77 |
78 |
😉
79 |
Hello, Beautiful
80 |
81 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/lib/api.js:
--------------------------------------------------------------------------------
1 | const LOCAL_STORAGE_KEY = 'PLAY_MIDNIGHT';
2 | const IS_EXTENSION = window.chrome && chrome.runtime && chrome.runtime.id;
3 |
4 | const urlCache = {};
5 |
6 | export const getUrl = url => {
7 | // Cache url to prevent hitting Chrome all the time
8 | if (urlCache[url]) return urlCache[url];
9 |
10 | if (IS_EXTENSION) {
11 | urlCache[url] = chrome.runtime.getURL(url);
12 | } else {
13 | urlCache[url] = url;
14 | }
15 |
16 | return urlCache[url];
17 | };
18 |
19 | export const load = async () => {
20 | return new Promise(resolve => {
21 | if (IS_EXTENSION) {
22 | return chrome.storage.sync.get(null, resolve);
23 | } else {
24 | const storageContents = localStorage.getItem(LOCAL_STORAGE_KEY);
25 | const storageData = storageContents ? JSON.parse(storageContents) : {};
26 | console.log('Fetching from Local Storage!', storageData);
27 | resolve(storageData);
28 | }
29 | });
30 | };
31 |
32 | export const loadBackground = async data => {
33 | return new Promise((resolve, reject) => {
34 | if (IS_EXTENSION) {
35 | try {
36 | return chrome.runtime.sendMessage(chrome.runtime.id, data, resolve);
37 | } catch (e) {
38 | // Extension reloaded and can't reach background page
39 | return reject(e);
40 | }
41 | } else {
42 | console.log('Load Background Fallback!', data);
43 | resolve(data);
44 | }
45 | });
46 | };
47 |
48 | export const save = async data => {
49 | return new Promise(resolve => {
50 | if (IS_EXTENSION) {
51 | return chrome.storage.sync.set(data, resolve);
52 | } else {
53 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data));
54 | console.log('Saving to Local Storage!', data);
55 | resolve(data);
56 | }
57 | });
58 | };
59 |
--------------------------------------------------------------------------------
/src/lib/store.js:
--------------------------------------------------------------------------------
1 | import createSagaMiddleware from 'redux-saga';
2 | import { createLogger } from 'redux-logger';
3 | import { createStore, applyMiddleware, compose } from 'redux';
4 |
5 | import { rootReducer, rootSaga } from 'modules/root';
6 | import checkEnv from 'utils/checkEnv';
7 |
8 | const DEV = checkEnv('development');
9 |
10 | /*
11 | * Middleware & enhancers
12 | */
13 | const middleware = [];
14 |
15 | // middleware: Redux Saga
16 | const sagaMiddleware = createSagaMiddleware();
17 | middleware.push(sagaMiddleware);
18 |
19 | // middleware: Redux Logger
20 | if (DEV) middleware.push(createLogger({ collapsed: true }));
21 |
22 | const enhancers = [];
23 |
24 | // enhancer: middleware chain
25 | enhancers.push(applyMiddleware(...middleware));
26 |
27 | // enhancer: Redux DevTools
28 | if (DEV) {
29 | enhancers.push(
30 | window.devToolsExtension ? window.devToolsExtension() : f => f
31 | );
32 | }
33 |
34 | // export the created store
35 | const store = createStore(rootReducer, compose(...enhancers));
36 |
37 | // run the root saga
38 | sagaMiddleware.run(rootSaga);
39 |
40 | export default store;
41 |
--------------------------------------------------------------------------------
/src/modules/modal.js:
--------------------------------------------------------------------------------
1 | import { createActions, handleActions } from 'redux-actions';
2 | import uuid from 'uuid/v4';
3 |
4 | import { replaceItem, removeItem } from 'utils/array';
5 |
6 | // State
7 | const defaultState = {
8 | modals: [],
9 | };
10 |
11 | // Actios
12 | export const actions = createActions(
13 | {
14 | SHOW_MODAL: (type, options) => ({
15 | type,
16 | options,
17 | }),
18 | },
19 | 'CLOSE_MODAL'
20 | );
21 |
22 | // Selectors
23 | export const selectors = {
24 | modals: state => state.modal.modals,
25 | };
26 |
27 | // Helpers
28 | const updateOrCreateModal = (modals, modal) => {
29 | const id = modal.id ? modal.id : uuid();
30 | return replaceItem(modals, { ...modal, id });
31 | };
32 |
33 | // Reducer
34 | export default handleActions(
35 | {
36 | [actions.showModal](state, { payload: { type, options: { id, ...options } } }) {
37 | return {
38 | ...state,
39 | modals: updateOrCreateModal(state.modals, { id, type, options }),
40 | };
41 | },
42 | [actions.closeModal](state, { payload: id }) {
43 | return { ...state, modals: removeItem(state.modals, { id }) };
44 | },
45 | },
46 | defaultState
47 | );
48 |
--------------------------------------------------------------------------------
/src/modules/root.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { all } from 'redux-saga/effects';
3 |
4 | // Reducers/Sagas
5 | import options, { optionsSaga } from 'modules/options';
6 | import modal from 'modules/modal';
7 | import toast from 'modules/toast';
8 |
9 | // Root Saga
10 | export function* rootSaga() {
11 | yield all([optionsSaga()]);
12 | }
13 |
14 | // Root Reducer
15 | export const rootReducer = combineReducers({
16 | modal,
17 | options,
18 | toast,
19 | });
20 |
--------------------------------------------------------------------------------
/src/modules/toast.js:
--------------------------------------------------------------------------------
1 | import { createActions, handleActions } from 'redux-actions';
2 | import uuid from 'uuid/v4';
3 |
4 | import { replaceItem, removeItem } from 'utils/array';
5 |
6 | export const TOAST_STANDARD = 'PLAY_MIDNIGHT_TOAST';
7 |
8 | // State
9 | const defaultState = {
10 | toasts: [],
11 | };
12 |
13 | // Selectors
14 | export const selectors = {
15 | toasts: state => state.toast.toasts,
16 | };
17 |
18 | // Actions
19 | export const actions = createActions(
20 | {
21 | CREATE_TOAST: (type, { id, ...options }) => ({
22 | id,
23 | type,
24 | options,
25 | }),
26 | },
27 | 'CLOSE_TOAST'
28 | );
29 |
30 | // Helpers
31 | const updateOrCreateToast = (toasts, toast) => {
32 | const id = toast.id ? toast.id : uuid();
33 | return replaceItem(toasts, { ...toast, id });
34 | };
35 |
36 | // Reducer
37 | export default handleActions(
38 | {
39 | [actions.createToast](state, { payload: toast }) {
40 | return {
41 | ...state,
42 | toasts: updateOrCreateToast(state.toasts, toast),
43 | };
44 | },
45 | [actions.closeToast](state, { payload: id }) {
46 | return { ...state, toasts: removeItem(state.toasts, { id }) };
47 | },
48 | },
49 | defaultState
50 | );
51 |
--------------------------------------------------------------------------------
/src/notifications/components/FootNote.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import { Text, Title } from 'notifications/components';
4 |
5 | const FootNote = () => (
6 |
7 | Feedback
8 |
9 |
10 | As always, I'm sure there will be bugs/issues. Please let me know, preferably via{' '}
11 |
12 | Reddit
13 | ,{' '}
14 |
15 | email
16 | , or{' '}
17 |
22 | Chrome Webstore
23 | {' '}
24 | if you happen to encounter any. Screenshots are definitely the most helpful, or even submitting a Pull Request on{' '}
25 |
26 | Github
27 | {' '}
28 | if you're into that kind of thing. I'll get to these as quickly as I can.
29 |
30 |
31 | Personal Message
32 |
33 |
34 | If you'd like to help support this project in any way, please consider spreading the word via friends,{' '}
35 |
36 | Reddit
37 | , buying me some much needed coffee via{' '}
38 |
39 | PayPal
40 | , or checking out my work on{' '}
41 |
42 | Etsy
43 | .
44 |
45 |
46 |
47 | Best, 🐤 Ducky (Chris Tieman)
48 |
49 |
50 | );
51 |
52 | export default FootNote;
53 |
--------------------------------------------------------------------------------
/src/notifications/components/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import Title from './Title';
5 |
6 | const StyledList = styled.div`
7 | .List__title {
8 | margin: 0;
9 | }
10 | `;
11 |
12 | const List = ({ title, children }) => (
13 |
14 | {title}
15 | {children}
16 |
17 | );
18 |
19 | export default List;
20 |
--------------------------------------------------------------------------------
/src/notifications/components/ListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import withTheme from 'hoc/withTheme';
5 |
6 | const StyledListItem = withTheme(styled.div`
7 | padding: 15px 25px;
8 | margin: 0;
9 | border-bottom: 1px solid ${props => props.theme.B500};
10 |
11 | .ListItem__title {
12 | font-weight: 700;
13 | margin: 0 0 5px;
14 | }
15 |
16 | &:last-child {
17 | border: none;
18 | }
19 | `);
20 |
21 | const ListItem = ({ title, children }) => (
22 |
23 | {title}
24 | {children}
25 |
26 | );
27 |
28 | export default ListItem;
29 |
--------------------------------------------------------------------------------
/src/notifications/components/Text.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Text = styled.div`
4 | padding: 0 25px;
5 | margin: 0 0 25px;
6 |
7 | ${props => props.nopad && `padding: 0`};
8 |
9 | &:last-child {
10 | margin: 0;
11 | }
12 | `;
13 |
14 | export default Text;
15 |
--------------------------------------------------------------------------------
/src/notifications/components/Title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import withTheme from 'hoc/withTheme';
4 |
5 | const Title = styled.div`
6 | background: ${props => props.theme.B200};
7 | font-size: 16px;
8 | font-weight: 700;
9 | padding: 15px 25px;
10 | margin: 0 0 25px;
11 | border-bottom: 1px solid ${props => props.theme.B500};
12 |
13 | &:last-child {
14 | margin: 0;
15 | }
16 | `;
17 |
18 | export default withTheme(Title);
19 |
--------------------------------------------------------------------------------
/src/notifications/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as FootNote } from './FootNote';
2 | export { default as List } from './List';
3 | export { default as ListItem } from './ListItem';
4 | export { default as Text } from './Text';
5 | export { default as Title } from './Title';
6 |
--------------------------------------------------------------------------------
/src/notifications/index.js:
--------------------------------------------------------------------------------
1 | import * as defaultNotification from './templates/default';
2 | import * as v3_0_0 from './templates/3.0.0';
3 | import * as v3_1_0 from './templates/3.1.0';
4 | import * as v3_2_0 from './templates/3.2.0';
5 | import * as v3_2_1 from './templates/3.2.1';
6 |
7 | export default {
8 | default: defaultNotification,
9 | '3.0.0': v3_0_0,
10 | '3.1.0': v3_1_0,
11 | '3.2.0': v3_2_0,
12 | '3.2.1': v3_2_1,
13 | };
14 |
--------------------------------------------------------------------------------
/src/notifications/templates/3.0.0.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import { FootNote, List, ListItem, Text } from 'notifications/components';
4 |
5 | export const NOTIFICATION_TYPE = 'MODAL';
6 |
7 | export const DETAILS = {
8 | buttonText: 'Allons-y!',
9 | };
10 |
11 | export const Template = () => (
12 |
13 |
14 | Welcome to the all new Play Midnight!
15 |
16 |
17 | I took the old version and threw it straight into the garbage where it belonged. Regardless, I think you'll enjoy
18 | your stay here.
19 |
20 |
21 | For those that are interested in development, I rewrote the entire extension from the ground up with React which
22 | made life so much easier on my end. For those that don't know what React is, you should{' '}
23 |
24 | check it out
25 | . Or if you don't care, meh.
26 |
27 |
28 | TLDR; Updates on my end are much, much easier to maintain.
29 |
30 |
31 |
32 | Basically everything is brand spanking new and was rewritten from the ground up. So far in my testing everything
33 | has been running much smoother, but please let me know if you encounter any issues.
34 |
35 |
36 |
37 | You can now create a theme with a background color and accent to tweak the entire site to be your own personal
38 | playground! Not a fan of straight black? How about a nice cool blue, or even purple? This feature has been
39 | something I've wanted to do for a long time, but wasn't going to be very easy to implement in the older code.
40 | The feature you never knew you wanted is finally here!
41 |
42 |
43 | Note - All your old accent colors have been converted to themes, so don't worry you haven't
44 | lost them. Your accent colors will show up at the top of the themes list, followed by the default themes. Feel
45 | free to delete any you don't like!
46 |
47 |
48 | I really want to see what you guys come up with, so if you post theme combos on{' '}
49 |
50 | Reddit
51 | {' '}
52 | or{' '}
53 |
54 | email
55 | {' '}
56 | me about them, I can totally add them as future defaults. Show me what chu got!
57 |
58 |
59 |
60 |
61 | This is probably my second favorite part about the new setup (after those sick themes!). All changes you make
62 | in the options now take effect instantly . However, you'll still need to click save {' '}
63 | in order to persist them in Chrome.
64 |
65 |
66 | The best part? No more page refreshes.{' '}
67 |
68 | 😌
69 |
70 |
71 |
72 | Note - I'm aware of a small issue that exists when you disable/enable Play Midnight where the
73 | home screen stays dark/light. For now if you just switch to your library and back to home it should be okay.
74 |
75 |
76 |
77 | One of the most jarring changes will probably be the new location of the settings.
78 |
79 | You should now see a little settings cog right next to your queue/volume buttons on the player. There were
80 | some issues with the old floating button that were annoying to fix. Plus I feel like it fits it much better on
81 | the player.
82 |
83 |
84 |
85 | Yay! You can now finally use a color picker rather than just guessing with your sick HEX/RGB skills. You can
86 | also edit existing colors and their names! You can even duplicate an existing color to make minor changes, wow!
87 |
88 |
89 | I did go ahead and remove some no longer relevant options, or ones that I felt didn't fit well with how Play
90 | Midnight was set up. If I removed something you absolutely loved, let me know and I'll see what I can do.
91 |
92 |
93 |
94 |
95 |
96 | );
97 |
--------------------------------------------------------------------------------
/src/notifications/templates/3.1.0.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import { FootNote, List, ListItem, Text } from 'notifications/components';
4 |
5 | export const NOTIFICATION_TYPE = 'MODAL';
6 |
7 | export const DETAILS = {
8 | buttonText: 'Onward Friend!',
9 | };
10 |
11 | export const Template = () => (
12 |
13 |
14 | You spoke, I listened!
15 |
16 |
17 | Firstly, I just want to say thanks to all the people that contacted me with kind words about the new version. I've
18 | really enjoyed hearing your thoughts/suggestions! Keep them coming. :)
19 |
20 |
21 | I took your guys feedback about some of the removed options and went ahead and reimplemented them. Also cleaned up
22 | my codebase and fixed up a few bugs while I was at it.
23 |
24 |
25 | TLDR; Thanks for the feedback! Here's some updates:
26 |
27 |
28 |
29 | In case anyone saw this new permission request in Chrome, it's because I added a property that was listing Play
30 | Music as an allowed url for offloading the accented favicon to a background page. After toying with it, I now
31 | realize I didn't actually need this option and was able to remove it. Sorry if I spooked anyone in the process.
32 | You can read more about why I added it{' '}
33 |
38 | here
39 | .
40 |
41 |
42 |
43 | After multiple requests I've now added this option back. It allows you to keep the right playlists sidebar
44 | open permanently.
45 |
46 |
47 |
48 | As with Static Playlists, this allows you to keep the main left sidebar open permanently.
49 |
50 | Note - Using both static options together can cause some bugginess with the content since
51 | Google has set widths on a few things. I'd stick to one or the other unless you have a huge screen.
52 |
53 |
54 |
55 |
56 | Another requested feature to bring back is the "Larger Song Table". Tables are much larger (height and
57 | text-wise) if you prefer a more spacious option.
58 |
59 |
60 |
61 |
62 | I made a minor change to the theme editor to show actual buttons/sliders to give you a more accurate
63 | representation of what a theme will look like.
64 |
65 |
66 |
67 |
68 | - Fixed up a styling issue on the browse stations section
69 | - Cleaned up some of my codebase
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 |
--------------------------------------------------------------------------------
/src/notifications/templates/3.2.0.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | export const NOTIFICATION_TYPE = 'TOAST';
5 |
6 | export const DETAILS = {
7 | title: 'Update v3.2.0',
8 | };
9 |
10 | const Wrapper = styled.div`
11 | h2 {
12 | font-size: 16px;
13 | }
14 |
15 | p {
16 | margin: 0 0 10px;
17 | font-size: 14px;
18 | }
19 |
20 | ul {
21 | margin: 0 0 10px;
22 | list-style-type: none;
23 | padding: 0;
24 |
25 | li {
26 | margin: 0 0 8px;
27 | }
28 | }
29 | `;
30 |
31 | export const Template = () => (
32 |
33 |
34 | Hello again! I decided to go the route of using notifications like this for smaller updates so it's not so{' '}
35 | in your face . However, I still wanted to make sure you'd be aware of newer features as they appear!
36 |
37 | New Feature(s)
38 |
39 |
40 | Album Accents - Changes your accent color based on the currently playing song! This color comes
41 | from the more vibrant colors in the album art, similar to Android Oreo. Props to /u/DankCool for the suggestion!{' '}
42 |
43 | 👍
44 |
45 |
46 |
47 | Miscellaneous - Fixed some colors in the player; Fixed version number bug in Play Midnight
48 | Options; Some smaller optimizations.
49 |
50 |
51 |
52 | That's all for now! Sorry for the interruption!{' '}
53 |
54 | 😭
55 |
56 |
57 |
58 | );
59 |
--------------------------------------------------------------------------------
/src/notifications/templates/3.2.1.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | export const NOTIFICATION_TYPE = 'TOAST';
5 |
6 | export const DETAILS = {
7 | title: 'Update v3.2.1',
8 | };
9 |
10 | const Wrapper = styled.div`
11 | h2 {
12 | font-size: 16px;
13 | }
14 |
15 | p {
16 | margin: 0 0 10px;
17 | font-size: 14px;
18 | }
19 |
20 | ul {
21 | margin: 0 0 10px;
22 | list-style-type: none;
23 | padding: 0;
24 |
25 | li {
26 | margin: 0 0 8px;
27 | }
28 | }
29 | `;
30 |
31 | export const Template = () => (
32 |
33 | Hello! Just a tiny little update these days.
34 |
35 |
36 | New Logo - Our friends over at Google weren't too keen on me making a black version of the
37 | Google Play logo (as one would expect), so Play Midnight now has a new logo designed by my{' '}
38 |
39 | lovely girlfriend
40 | .
41 |
42 |
43 | I'm Feeling Lucky Loader - Looks like I missed one of the loading modals, so this should be all
44 | fixed up now. Thanks for looking out /u/Digger412!
45 |
46 |
47 | Keyboard Shortcuts Dialog - Some things changed on Google's end, so the keyboard shortcuts (?
48 | on Keyboard) were somewhat hard to read. These should be all fixed up now. Thanks /u/NIGHTFIRE777!
49 |
50 |
51 | That's all for now! Have a swell day (or evening)!
52 |
53 | );
54 |
--------------------------------------------------------------------------------
/src/notifications/templates/default.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import styled from 'styled-components';
3 |
4 | import withTheme from 'hoc/withTheme';
5 |
6 | import { TRANSITION_FAST } from 'style/theme';
7 |
8 | import { FootNote, List, ListItem, Text } from 'notifications/components';
9 | import IconGear from 'components/Icons/IconGear';
10 |
11 | const FAB = withTheme(styled.div`
12 | display: inline-flex;
13 | cursor: pointer;
14 | opacity: 0.9;
15 | color: ${props => props.theme.font_primary};
16 | transition: color ${TRANSITION_FAST}, opacity ${TRANSITION_FAST}, transform ${TRANSITION_FAST};
17 |
18 | &:hover {
19 | opacity: 1;
20 | transform: scale3d(1.1, 1.1, 1.1);
21 | }
22 |
23 | &:active,
24 | &:focus {
25 | opacity: 0.8;
26 | transform: scale3d(0.95, 0.95, 0.95);
27 | }
28 | `);
29 |
30 | export const NOTIFICATION_TYPE = 'MODAL';
31 |
32 | export const DETAILS = {
33 | buttonText: 'Cool Beans!',
34 | };
35 |
36 | export const Template = () => (
37 |
38 |
39 | Hello, friend. I really appreciate you taking the time to check out Play Midnight. This has been an ongoing
40 | project for me for the last few years and it holds a dear place in my heart. I hope it makes your day slightly
41 | better, or at least saves your eyes some straining.
42 |
43 |
44 | I just wanted to give you a quick overview of how to tweak Play Midnight to your liking and then I'll be out of
45 | your way, since I know these interruptions are terrible.
46 |
47 |
48 | Play Midnight activates itself automatically when you visit Google Play Music, so you won't have to mess with it
49 | too much. You'll also be able to easily change things via the Play Midnight options. To access the options, look
50 | for this little guy on the right side of your player bar (near the volume and queue buttons):
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | (Go ahead, you can play with it. It's pretty fun.)
59 |
60 |
61 | This little guy will live in his own little world in the bottom right corner of your screen to give you easy
62 | access to all the customizations you'd like to make to Play Midnight.
63 |
64 |
65 |
66 | Allows you to temporarily enable/disable Play Midnight if you're not feeling it.
67 |
68 | Note - I'm aware of a small issue that exists when you disable/enable Play Midnight where the
69 | home screen stays dark/light. For now if you just switch to your library and back to home it should be okay.
70 |
71 |
72 |
73 |
74 | This enables you to keep your accent color while Play Midnight is disabled.
75 |
76 |
77 |
78 | Changes your accent color based on the currently playing song! This color comes from the dominant colors in the
79 | album art, similar to Android Oreo.
80 |
81 |
82 |
83 | Customize the look and feel of Play Music to look exactly how you want. Allows you to update background and
84 | accent colors.
85 |
86 |
87 |
88 | Modifies the Orange headphone favicon on your url tab to be dark.
89 |
90 |
91 |
92 | Update the headphones favicon to be the same as your chosen accent color.
93 |
94 |
95 |
96 | This extends the now playing popup queue to be wider so your songs don't get cut off.
97 |
98 |
99 |
100 | Makes the song table rows and text much larger if you prefer it to be less cramped.
101 |
102 |
103 |
104 | Allows you to lock the playlists sidebar to the right side of the screen.
105 |
106 |
107 |
108 | Allows you to lock the main left sidebar to the left side of the screen.
109 |
110 | Note - Using both static options together can cause some bugginess with the content since
111 | Google has set widths on a few things. I'd stick to one or the other unless you have a huge screen.
112 |
113 |
114 |
115 | Allows you to customize which menus are shown on the left-side menu.
116 |
117 |
118 | Allows you to customize which auto-playlists are shown on the right-side menu.
119 |
120 |
121 |
122 |
123 |
124 | );
125 |
--------------------------------------------------------------------------------
/src/options/Components.js:
--------------------------------------------------------------------------------
1 | import AccentsOnly from './components/AccentsOnly';
2 | import AlbumAccents from './components/AlbumAccents';
3 | import Core from './components/Core';
4 | import Enabled from './components/Enabled';
5 | import Favicon from './components/Favicon';
6 | import LargeTable from './components/LargeTable';
7 | import Logo from './components/Logo';
8 | import Menus from './components/Menus';
9 | import Playlists from './components/Playlists';
10 | import Queue from './components/Queue';
11 | import Settings from './components/Settings';
12 | import SoundSearch from './components/SoundSearch';
13 | import StaticSidebars from './components/StaticSidebars';
14 |
15 | export default [
16 | // Core
17 | Core,
18 | Logo,
19 | Settings,
20 |
21 | // General
22 | AccentsOnly,
23 | Enabled,
24 |
25 | // Colorize
26 | AlbumAccents,
27 |
28 | // Customize
29 | Favicon,
30 | Queue,
31 | LargeTable,
32 | StaticSidebars,
33 |
34 | // Menus
35 | Menus,
36 |
37 | // Playlists
38 | Playlists,
39 | SoundSearch,
40 | ];
41 |
--------------------------------------------------------------------------------
/src/options/components/AccentsOnly.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './AccentsOnly.styles';
7 |
8 | const OPTION_ID = 'accentsOnly';
9 |
10 | @withOptions
11 | @withStyles(styles)
12 | class AccentsOnly extends PureComponent {
13 | render() {
14 | const { options, Stylesheet } = this.props;
15 | return !options.enabled && options[OPTION_ID] ? : null;
16 | }
17 | }
18 |
19 | export default AccentsOnly;
20 |
--------------------------------------------------------------------------------
/src/options/components/AccentsOnly.styles.js:
--------------------------------------------------------------------------------
1 | import accents from 'style/sheets/accents';
2 | import createStylesheet from 'utils/createStylesheet';
3 |
4 | export default createStylesheet(accents);
5 |
--------------------------------------------------------------------------------
/src/options/components/AlbumAccents.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import Vibrant from 'node-vibrant';
4 | import forIn from 'lodash/forIn';
5 |
6 | import withOptions from 'hoc/withOptions';
7 |
8 | import awaitElement from 'utils/awaitElement';
9 | import { actions, selectors } from 'modules/options';
10 |
11 | const colorDebug = (swatches, src) => {
12 | let str = '';
13 | let colors = [];
14 |
15 | forIn(swatches, (swatch, key) => {
16 | if (swatch) {
17 | str += `%c${key} `;
18 | colors.push(`color: ${swatch.getHex()}; font-weight: 700; font-family: Helvetica;`);
19 | }
20 | });
21 |
22 | console.log('%c ', `font-size: 80px; background: url(${src}) no-repeat; background-size: 90px 90px;`);
23 | console.log(str, ...colors);
24 | };
25 |
26 | const mapStateToProps = state => ({
27 | alternateAccent: selectors.alternateAccent(state),
28 | });
29 |
30 | @withOptions
31 | @connect(mapStateToProps, { updateAlternateAccent: actions.updateAlternateAccent })
32 | class Core extends Component {
33 | // Use Vibrant to generate a palette from image src
34 | getImageAccent = async src => {
35 | const swatches = await Vibrant.from(src).getPalette();
36 |
37 | // colorDebug(swatches, src);
38 |
39 | if (!swatches) {
40 | return null;
41 | } else if (swatches.Vibrant) {
42 | return swatches.Vibrant.getHex();
43 | } else if (swatches.LightVibrant) {
44 | return swatches.LightVibrant.getHex();
45 | } else if (swatches.Muted) {
46 | return swatches.Muted.getHex();
47 | } else if (swatches.DarkVibrant) {
48 | return swatches.DarkVibrant.getHex();
49 | }
50 |
51 | return null;
52 | };
53 |
54 | // Watch img src, use cached accent or regenerate if new image
55 | observe = async () => {
56 | this.albumArt = await awaitElement('img#playerBarArt');
57 |
58 | if (this.src !== this.albumArt.src) {
59 | this.src = this.albumArt.src;
60 | this.accent = await this.getImageAccent(this.albumArt.src);
61 | this.props.updateAlternateAccent(this.accent);
62 | } else if (this.accent) {
63 | if (this.accent === this.props.alternateAccent) return;
64 | this.props.updateAlternateAccent(this.accent);
65 | }
66 | };
67 |
68 | // Setup observer for the song info section of player
69 | enable = async () => {
70 | if (this.songInfoObserver) return;
71 |
72 | this.songInfo = await awaitElement('#playerSongInfo');
73 | this.songInfoObserver = new MutationObserver(this.observe);
74 | this.songInfoObserver.observe(this.songInfo, { childList: true, subtree: true });
75 | this.observe();
76 | };
77 |
78 | // Kill observer, set accent to null
79 | disable = () => {
80 | if (this.songInfoObserver) this.songInfoObserver.disconnect();
81 |
82 | this.props.updateAlternateAccent(null);
83 | this.songInfoObserver = null;
84 | };
85 |
86 | // Only update when option changes
87 | shouldComponentUpdate({ options: nextOptions }) {
88 | const { options } = this.props;
89 | return options.albumAccents !== nextOptions.albumAccents;
90 | }
91 |
92 | // Invoke Enable/Disable on options change
93 | componentWillReceiveProps({ options: nextOptions }) {
94 | const { options } = this.props;
95 |
96 | // Prevent unnecessary action on disable
97 | if (options.albumAccents === nextOptions.albumAccents) return;
98 |
99 | if (!nextOptions.albumAccents) {
100 | this.disable();
101 | } else {
102 | this.enable();
103 | }
104 | }
105 |
106 | render() {
107 | return null;
108 | }
109 | }
110 |
111 | export default Core;
112 |
--------------------------------------------------------------------------------
/src/options/components/Core.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 | import withTheme from 'hoc/withTheme';
6 |
7 | import observables from './Core.observables';
8 | import styles from './Core.styles';
9 |
10 | @withTheme
11 | @withOptions
12 | @withStyles(styles)
13 | class Core extends Component {
14 | observe = () => {
15 | const { options, theme } = this.props;
16 | observables.forEach(observable => observable(options.enabled, theme));
17 | };
18 |
19 | componentWillUnmount() {
20 | this.bodyObserver.disconnect();
21 | }
22 |
23 | componentDidMount() {
24 | this.bodyObserver = new MutationObserver(this.observe);
25 | this.bodyObserver.observe(document.body, { attributes: true, childList: true, subtree: true });
26 | }
27 |
28 | render() {
29 | const { Stylesheet } = this.props;
30 | this.observe();
31 | return ;
32 | }
33 | }
34 |
35 | export default Core;
36 |
--------------------------------------------------------------------------------
/src/options/components/Core.observables.js:
--------------------------------------------------------------------------------
1 | import color from 'tinycolor2';
2 |
3 | // Methods that need to run anytime body changes
4 |
5 | export const recentActivity = () => {
6 | const urlRegex = /(=s90)/;
7 | const gridItems = document.querySelectorAll('sj-scrolling-module[module-token="CLIENT_SIDE_RECENTS"] sj-card');
8 |
9 | for (const item of gridItems) {
10 | if (!item) continue;
11 |
12 | const img = item.querySelector('img');
13 |
14 | if (img && urlRegex.test(img.src)) {
15 | img.setAttribute('src', img.src.replace('=s90', '=s150'));
16 | }
17 | }
18 | };
19 |
20 | export const paneBackgrounds = (enabled, theme) => {
21 | const panes = document.querySelectorAll('#gpm-home-module-0, #gpm-home-module-1');
22 | const backgroundColor = enabled
23 | ? color(theme.B500)
24 | .setAlpha(0.99)
25 | .toRgbString()
26 | : 'rgba(250, 250, 250, 1)';
27 |
28 | for (const pane of panes) {
29 | if (pane.getAttribute('background-color') !== backgroundColor) {
30 | pane.setAttribute('background-color', backgroundColor);
31 | }
32 | }
33 | };
34 |
35 | export default [recentActivity, paneBackgrounds];
36 |
--------------------------------------------------------------------------------
/src/options/components/Core.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import createStylesheet from 'utils/createStylesheet';
4 |
5 | const styles = theme => css`
6 | /* Recent Activity Images */
7 | sj-scrolling-module[module-token='CLIENT_SIDE_RECENTS'] sj-card {
8 | width: 140px !important;
9 | }
10 |
11 | sj-scrolling-module[module-token='CLIENT_SIDE_RECENTS'] .details {
12 | margin-bottom: 20px !important;
13 | }
14 | `;
15 |
16 | export default createStylesheet(styles);
17 |
--------------------------------------------------------------------------------
/src/options/components/Enabled.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './Enabled.styles';
7 |
8 | const OPTION_ID = 'enabled';
9 |
10 | @withOptions
11 | @withStyles(styles)
12 | class Enabled extends PureComponent {
13 | render() {
14 | const { options, Stylesheet } = this.props;
15 | return options[OPTION_ID] ? : null;
16 | }
17 | }
18 |
19 | export default Enabled;
20 |
--------------------------------------------------------------------------------
/src/options/components/Enabled.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import * as style from 'style/sheets';
4 | import createStylesheet from 'utils/createStylesheet';
5 |
6 | const styles = theme => css`
7 | ${style.core(theme)};
8 | ${style.accents(theme)};
9 | ${style.alerts(theme)};
10 | ${style.buttons(theme)};
11 | ${style.cardGrid(theme)};
12 | ${style.cards(theme)};
13 | ${style.forms(theme)};
14 | ${style.loading(theme)};
15 | ${style.menus(theme)};
16 | ${style.misc(theme)};
17 | ${style.nav(theme)};
18 | ${style.page(theme)};
19 | ${style.player(theme)};
20 | ${style.queue(theme)};
21 | ${style.songTable(theme)};
22 | `;
23 |
24 | export default createStylesheet(styles);
25 |
--------------------------------------------------------------------------------
/src/options/components/Favicon.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import filter from 'lodash/filter';
3 | import isEqual from 'lodash/isEqual';
4 |
5 | import withOptions from 'hoc/withOptions';
6 | import withTheme from 'hoc/withTheme';
7 |
8 | import { getUrl, loadBackground } from 'lib/api';
9 | import removeAllElements from 'utils/removeAllElements';
10 |
11 | import FaviconImage from 'assets/images/favicon.png';
12 | import injectElement from 'utils/injectElement';
13 |
14 | const OPTION_ID = 'favicon';
15 | const ICON_STORAGE = 'PM_ICON';
16 |
17 | @withTheme
18 | @withOptions
19 | class Favicon extends Component {
20 | updateFavicon = async (accent, useAccent) => {
21 | const stored = localStorage.getItem(ICON_STORAGE) ? JSON.parse(localStorage.getItem(ICON_STORAGE)) : undefined;
22 |
23 | const cached = stored && stored.accent === accent;
24 | const data = {
25 | url: getUrl(FaviconImage),
26 | accent,
27 | };
28 |
29 | const createIcon = href => {
30 | // Remove Old Favicon
31 | removeAllElements('link[rel="SHORTCUT ICON"], link[rel="shortcut icon"], link[rel="icon"], link[href $= ".ico"]');
32 |
33 | // Create Link Element
34 | injectElement('link', { id: `play-midnight-${OPTION_ID}`, rel: 'icon', type: 'image/png', href }, 'head');
35 | };
36 |
37 | if (!useAccent) {
38 | return createIcon(data.url);
39 | } else {
40 | if (cached) {
41 | createIcon(stored.url);
42 | } else {
43 | try {
44 | const { url } = await loadBackground(data);
45 | localStorage.setItem(
46 | ICON_STORAGE,
47 | JSON.stringify({
48 | url,
49 | accent,
50 | })
51 | );
52 | createIcon(url);
53 | } catch (e) {
54 | // Console error for now, Modal seems too intrusive for favicon
55 | console.error(
56 | `Play Midnight - Issue communcating with background page \
57 | to update favicon. Refreshing page should fix this.`
58 | );
59 | }
60 | }
61 | }
62 | };
63 |
64 | shouldComponentUpdate({ theme: prevTheme, options: prevOptions }) {
65 | const { theme, options } = this.props;
66 | const prevFavicon = [prevOptions.favicon, prevOptions.faviconAccent];
67 | const favicon = [options.favicon, options.faviconAccent];
68 |
69 | return !isEqual(prevTheme.A500, theme.A500) || !isEqual(prevFavicon, favicon);
70 | }
71 |
72 | render() {
73 | const { theme, options } = this.props;
74 |
75 | if (options[OPTION_ID]) this.updateFavicon(theme.A500, options.faviconAccent);
76 |
77 | return null;
78 | }
79 | }
80 |
81 | export default Favicon;
82 |
--------------------------------------------------------------------------------
/src/options/components/LargeTable.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './LargeTable.styles';
7 |
8 | const OPTION_ID = 'largeTable';
9 |
10 | @withOptions
11 | @withStyles(styles)
12 | class LargeTable extends PureComponent {
13 | render() {
14 | const { options, Stylesheet } = this.props;
15 | return options[OPTION_ID] ? : null;
16 | }
17 | }
18 |
19 | export default LargeTable;
20 |
--------------------------------------------------------------------------------
/src/options/components/LargeTable.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { getUrl } from 'lib/api';
4 |
5 | import createStylesheet from 'utils/createStylesheet';
6 | import ThumbsUp from 'assets/images/icon-thumbs-up.svg';
7 | import ThumbsDown from 'assets/images/icon-thumbs-down.svg';
8 |
9 | const styles = theme => css`
10 | .song-table.tight {
11 | .song-row {
12 | td {
13 | height: 64px;
14 | line-height: 64px;
15 | max-height: 64px;
16 | min-height: 64px;
17 | font-size: 16px;
18 | }
19 |
20 | .column-content {
21 | height: 64px;
22 | }
23 |
24 | .song-indicator {
25 | margin-top: 10px !important;
26 | width: 44px !important;
27 | height: 44px !important;
28 | padding: 0 !important;
29 | background-size: 24px 24px !important;
30 | }
31 |
32 | .hover-button[data-id='play'] {
33 | top: 10px !important;
34 | width: 44px !important;
35 | height: 44px !important;
36 | }
37 |
38 | [data-col='song-details'] img,
39 | [data-col='title'] img {
40 | height: 44px !important;
41 | width: 44px !important;
42 | padding: 0 !important;
43 | margin-top: 10px !important;
44 | }
45 |
46 | [data-col='title'] img {
47 | margin-right: 16px;
48 | }
49 |
50 | .song-details-wrapper {
51 | margin-top: 16px !important;
52 | left: 60px !important;
53 |
54 | .song-title {
55 | height: 20px !important;
56 | font-size: 16px !important;
57 | line-height: 20px !important;
58 | }
59 |
60 | .song-artist-album {
61 | height: 16px !important;
62 | font-size: 14px !important;
63 | line-height: 16px !important;
64 | }
65 | }
66 |
67 | .title-right-items {
68 | line-height: 64px !important;
69 | }
70 |
71 | paper-icon-button[data-id='menu'] {
72 | margin: 12px 0 0 0 !important;
73 | }
74 |
75 | .rating-container.thumbs {
76 | margin-top: 20px;
77 |
78 | li[data-rating='5'] {
79 | margin-top: 0;
80 | margin-left: 25px;
81 | }
82 |
83 | li[data-rating='1'] {
84 | margin-top: 0;
85 | margin-left: 8px;
86 | }
87 | }
88 |
89 | [data-col='rating'][data-rating='4'],
90 | [data-col='rating'][data-rating='5'] {
91 | background-color: inherit;
92 | background-image: url(${getUrl(ThumbsUp)}) !important;
93 | background-repeat: no-repeat !important;
94 | background-position: 25px 21px !important;
95 | background-size: 24px 24px !important;
96 | }
97 |
98 | [data-col='rating'][data-rating='1'],
99 | [data-col='rating'][data-rating='2'] {
100 | background-color: inherit;
101 | background-image: url(${getUrl(ThumbsDown)}) !important;
102 | background-repeat: no-repeat !important;
103 | background-position: 57px 20px !important;
104 | background-size: 24px 24px !important;
105 | }
106 | }
107 | }
108 | `;
109 |
110 | export default createStylesheet(styles);
111 |
--------------------------------------------------------------------------------
/src/options/components/Logo.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, PureComponent } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withPortal from 'hoc/withPortal';
5 | import withStyles from 'hoc/withStyles';
6 |
7 | import PlayMusicLogo from 'components/PlayMusicLogo';
8 | import styles from './Logo.styles';
9 |
10 | @withOptions
11 | @withPortal('#topBar .music-logo-link')
12 | class TopLogo extends PureComponent {
13 | render() {
14 | const { options } = this.props;
15 | return ;
16 | }
17 | }
18 |
19 | @withOptions
20 | @withPortal('#drawer .music-logo-link')
21 | class MenuLogo extends PureComponent {
22 | render() {
23 | const { options } = this.props;
24 | return ;
25 | }
26 | }
27 |
28 | @withOptions
29 | @withStyles(styles)
30 | class Logo extends PureComponent {
31 | render() {
32 | const { options, Stylesheet } = this.props;
33 | const enabled = options.enabled || options.accentsOnly;
34 |
35 | return enabled ? (
36 |
37 |
38 |
39 |
40 |
41 | ) : null;
42 | }
43 | }
44 |
45 | export default Logo;
46 |
--------------------------------------------------------------------------------
/src/options/components/Logo.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import createStylesheet from 'utils/createStylesheet';
4 |
5 | const styles = theme => css`
6 | body #material-app-bar #material-one-left,
7 | body.qp #material-app-bar #material-one-left,
8 | body #drawer,
9 | body.qp #drawer {
10 | .music-logo-link {
11 | height: 48px !important;
12 | }
13 |
14 | .music-logo,
15 | .menu-logo {
16 | display: none !important;
17 | }
18 | }
19 | `;
20 |
21 | export default createStylesheet(styles);
22 |
--------------------------------------------------------------------------------
/src/options/components/Menus.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './Menus.styles';
7 |
8 | @withOptions
9 | @withStyles(styles)
10 | class Menus extends Component {
11 | relatedOptions = [
12 | 'myLibrary',
13 | 'recent',
14 | 'topCharts',
15 | 'newReleases',
16 | 'browseStations',
17 | 'podcasts',
18 | 'shop',
19 | 'subscribe',
20 | ];
21 |
22 | render() {
23 | const { options, Stylesheet: StylesheetList } = this.props;
24 |
25 | const enabledList = this.relatedOptions
26 | .filter(id => !options[id])
27 | .map(id => ({ id, Stylesheet: StylesheetList[id] }));
28 |
29 | const renderAll = (enabledList, relatedOptions) => {
30 | const Stylesheet = StylesheetList['all'];
31 | return enabledList.length === relatedOptions.length && Stylesheet ? : null;
32 | };
33 |
34 | return (
35 |
36 | {enabledList.map(({ id, Stylesheet }) => (Stylesheet ? : null))}
37 | {renderAll(enabledList, this.relatedOptions)}
38 |
39 | );
40 | }
41 | }
42 |
43 | export default Menus;
44 |
--------------------------------------------------------------------------------
/src/options/components/Menus.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import createStylesheet from 'utils/createStylesheet';
4 |
5 | const styles = () => ({
6 | myLibrary: css`
7 | #nav_collections a[data-type='mylibrary'],
8 | #quickNavContainer gpm-quick-nav-item[view-type='mylibrary'],
9 | #quick-nav-container gpm-quick-nav-item[view-type='mylibrary'] {
10 | display: none;
11 | }
12 | `,
13 | recent: css`
14 | #nav_collections a[data-type='recents'],
15 | #quickNavContainer gpm-quick-nav-item[view-type='recents'],
16 | #quick-nav-container gpm-quick-nav-item[view-type='recents'] {
17 | display: none;
18 | }
19 | `,
20 | topCharts: css`
21 | #nav_collections a[data-type='wtc'],
22 | #quickNavContainer gpm-quick-nav-item[view-type='wtc'],
23 | #quick-nav-container gpm-quick-nav-item[view-type='wtc'] {
24 | display: none;
25 | }
26 | `,
27 | newReleases: css`
28 | #nav_collections a[data-type='wnr'],
29 | #quickNavContainer gpm-quick-nav-item[view-type='wnr'],
30 | #quick-nav-container gpm-quick-nav-item[view-type='wnr'] {
31 | display: none;
32 | }
33 | `,
34 | browseStations: css`
35 | #nav_collections a[data-type='wbs'],
36 | #quickNavContainer gpm-quick-nav-item[view-type='wbs'],
37 | #quick-nav-container gpm-quick-nav-item[view-type='wbs'] {
38 | display: none;
39 | }
40 | `,
41 | podcasts: css`
42 | #nav_collections a[data-type='tps'],
43 | #quickNavContainer gpm-quick-nav-item[view-type='tps'],
44 | #quick-nav-container gpm-quick-nav-item[view-type='tps'] {
45 | display: none;
46 | }
47 | `,
48 | shop: css`
49 | #nav_collections a[data-type='shop'] {
50 | display: none;
51 | }
52 | `,
53 | subscribe: css`
54 | #nav_collections a[data-type='sub'] {
55 | display: none;
56 | }
57 | `,
58 | });
59 |
60 | export default createStylesheet(styles);
61 |
--------------------------------------------------------------------------------
/src/options/components/Playlists.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './Playlists.styles';
7 |
8 | @withOptions
9 | @withStyles(styles)
10 | class Playlists extends Component {
11 | relatedOptions = ['thumbsUp', 'soundSearch', 'lastAdded', 'freePurchased'];
12 |
13 | render() {
14 | const { options, Stylesheet: StylesheetList } = this.props;
15 |
16 | const enabledList = this.relatedOptions
17 | .filter(id => !options[id])
18 | .map(id => ({ id, Stylesheet: StylesheetList[id] }));
19 |
20 | const renderAll = (enabledList, relatedOptions) => {
21 | const Stylesheet = StylesheetList['all'];
22 | return enabledList.length === relatedOptions.length && Stylesheet ? : null;
23 | };
24 |
25 | return (
26 |
27 | {enabledList.map(({ id, Stylesheet }) => (Stylesheet ? : null))}
28 | {renderAll(enabledList, this.relatedOptions)}
29 |
30 | );
31 | }
32 | }
33 |
34 | export default Playlists;
35 |
--------------------------------------------------------------------------------
/src/options/components/Playlists.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import createStylesheet from 'utils/createStylesheet';
4 |
5 | const styles = () => ({
6 | thumbsUp: css`
7 | .sj-right-drawer .autoplaylist-section [data-id = 'auto-playlist-thumbs-up'] {
8 | display: none;
9 | }
10 | `,
11 | lastAdded: css`
12 | .sj-right-drawer .autoplaylist-section [data-id = 'auto-playlist-recent'] {
13 | display: none;
14 | }
15 | `,
16 | freePurchased: css`
17 | .sj-right-drawer .autoplaylist-section [data-id = 'auto-playlist-promo'] {
18 | display: none;
19 | }
20 | `,
21 | all: css`
22 | #auto-playlists-header,
23 | .autoplaylist-section {
24 | display: none;
25 | }
26 | `,
27 | });
28 |
29 | export default createStylesheet(styles);
30 |
--------------------------------------------------------------------------------
/src/options/components/Queue.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './Queue.styles';
7 |
8 | const OPTION_ID = 'queue';
9 |
10 | @withOptions
11 | @withStyles(styles)
12 | class Queue extends PureComponent {
13 | render() {
14 | const { options, Stylesheet } = this.props;
15 | return options[OPTION_ID] ? : null;
16 | }
17 | }
18 |
19 | export default Queue;
20 |
--------------------------------------------------------------------------------
/src/options/components/Queue.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import createStylesheet from 'utils/createStylesheet';
4 |
5 | const styles = theme => css`
6 | #queue-overlay {
7 | width: calc(100vw - 64px);
8 | max-width: 1000px;
9 | transform-origin: calc(100% - 64px) calc(100% + 26px);
10 | }
11 | `;
12 |
13 | export default createStylesheet(styles);
14 |
--------------------------------------------------------------------------------
/src/options/components/Settings.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, PureComponent } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import withOptions from 'hoc/withOptions';
5 | import withPortal from 'hoc/withPortal';
6 | import withStyles from 'hoc/withStyles';
7 | import withTheme from 'hoc/withTheme';
8 |
9 | import { actions } from 'modules/options';
10 |
11 | import styles, { StyledSettings } from './Settings.styles';
12 | import IconGear from 'components/Icons/IconGear';
13 |
14 | const mapStateToProps = ({ options }) => ({
15 | menuVisible: options.menuVisible,
16 | });
17 |
18 | @withTheme
19 | @withOptions
20 | @withStyles(styles)
21 | @withPortal('#material-player-right-wrapper')
22 | @connect(mapStateToProps, { toggleMenu: actions.toggleMenu })
23 | class Settings extends PureComponent {
24 | render() {
25 | const { options, theme, menuVisible, toggleMenu, Stylesheet } = this.props;
26 |
27 | return (
28 |
29 | toggleMenu()}>
30 |
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default Settings;
39 |
--------------------------------------------------------------------------------
/src/options/components/Settings.styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | import { TRANSITION_FAST } from 'style/theme';
4 | import createStylesheet from 'utils/createStylesheet';
5 |
6 | export const StyledSettings = styled.div`
7 | display: inline-flex;
8 | margin: 4px;
9 | margin-right: 32px;
10 | padding: 8px;
11 | cursor: pointer;
12 | visibility: visible;
13 | opacity: 0.9;
14 | color: ${props =>
15 | props.useAccent ? props.theme.A500 : props.enabled ? props.theme.font_primary : props.theme.font_google};
16 | border-radius: 50%;
17 | transition: color ${TRANSITION_FAST}, opacity ${TRANSITION_FAST}, transform ${TRANSITION_FAST};
18 |
19 | &:hover {
20 | opacity: 1;
21 | transform: scale3d(1.1, 1.1, 1.1);
22 | }
23 |
24 | &:active,
25 | &:focus {
26 | opacity: 0.8;
27 | transform: scale3d(0.95, 0.95, 0.95);
28 | }
29 | `;
30 |
31 | const styles = theme => css`
32 | #player #material-player-right-wrapper paper-icon-button[data-id='queue'] {
33 | margin: 8px;
34 | }
35 |
36 | #queue-overlay:after {
37 | right: 72px;
38 | }
39 | `;
40 |
41 | export default createStylesheet(styles);
42 |
--------------------------------------------------------------------------------
/src/options/components/SoundSearch.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withPortal from 'hoc/withPortal';
5 |
6 | const OPTION_ID = 'soundSearch';
7 |
8 | @withOptions
9 | @withPortal('#playlist-drawer .autoplaylist-section')
10 | class soundSearch extends PureComponent {
11 | render() {
12 | const { options } = this.props;
13 |
14 | return options[OPTION_ID] ? (
15 |
32 | ) : null;
33 | }
34 | }
35 |
36 | export default soundSearch;
37 |
--------------------------------------------------------------------------------
/src/options/components/StaticSidebars.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 |
3 | import withOptions from 'hoc/withOptions';
4 | import withStyles from 'hoc/withStyles';
5 |
6 | import styles from './StaticSidebars.styles';
7 |
8 | @withOptions
9 | @withStyles(styles)
10 | class StaticSidebars extends Component {
11 | relatedOptions = ['staticPlaylists', 'staticSidebar'];
12 |
13 | render() {
14 | const { options, Stylesheet: StylesheetList } = this.props;
15 |
16 | const enabledList = this.relatedOptions
17 | .filter(id => options[id])
18 | .map(id => ({ id, Stylesheet: StylesheetList[id] }));
19 |
20 | return (
21 | {enabledList.map(({ id, Stylesheet }) => (Stylesheet ? : null))}
22 | );
23 | }
24 | }
25 |
26 | export default StaticSidebars;
27 |
--------------------------------------------------------------------------------
/src/options/components/StaticSidebars.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import createStylesheet from 'utils/createStylesheet';
4 |
5 | const styles = theme => ({
6 | staticPlaylists: css`
7 | #playlist-drawer-button {
8 | display: none;
9 | }
10 |
11 | #playlist-drawer {
12 | height: calc(100% - 90px);
13 |
14 | #topBar {
15 | background: ${theme.enabled ? theme.B300 : '#fff'} !important;
16 | }
17 |
18 | #drawer {
19 | z-index: 2 !important;
20 | bottom: 90px !important;
21 | height: auto !important;
22 | visibility: visible !important;
23 | transform: translateX(0) !important;
24 | transition: none !important;
25 | }
26 |
27 | #scrim {
28 | display: none;
29 | }
30 | }
31 |
32 | sj-right-drawer #drawer.sj-right-drawer {
33 | box-shadow: none !important;
34 | }
35 |
36 | #pageIndicatorContainer.sj-home {
37 | right: 324px !important;
38 | }
39 |
40 | #main paper-header-panel#content-container {
41 | width: auto !important;
42 | margin-right: 300px;
43 | }
44 |
45 | .play-midnight-fab {
46 | right: 324px;
47 | }
48 |
49 | #queue-overlay {
50 | right: 324px;
51 |
52 | &:after {
53 | display: none;
54 | }
55 | }
56 |
57 | sj-home #backgroundContainer.sj-home {
58 | right: 300px !important;
59 | }
60 | `,
61 | staticSidebar: css`
62 | #left-nav-close-button,
63 | #material-one-left #left-nav-open-button,
64 | #material-one-left .music-logo-link,
65 | #quick-nav-container {
66 | display: none !important;
67 | }
68 |
69 | #drawer paper-toolbar {
70 | background: ${theme.enabled ? theme.B300 : '#fff'} !important;
71 | }
72 |
73 | paper-drawer-panel .left-drawer.narrow-layout.paper-drawer-panel {
74 | & > #drawer.paper-drawer-panel {
75 | z-index: 1 !important;
76 | bottom: 90px !important;
77 | height: auto !important;
78 | visibility: visible !important;
79 | transform: translateX(0) !important;
80 | box-shadow: inset 0 -8px 8px -8px rgba(0, 0, 0, 0.4) !important;
81 | transition: none !important;
82 |
83 | #topBar {
84 | padding: 0 !important;
85 | }
86 | }
87 | }
88 |
89 | .new-playlist-drawer #nav-container #nav {
90 | border: none !important;
91 | }
92 |
93 | #material-breadcrumbs {
94 | margin-left: 0;
95 | }
96 |
97 | .material-transfer-status-indicator-container {
98 | left: 295px !important;
99 | }
100 |
101 | #main paper-header-panel#content-container {
102 | width: auto !important;
103 | margin-left: 280px !important;
104 | }
105 |
106 | sj-home #backgroundContainer.sj-home {
107 | left: 280px !important;
108 | }
109 | `,
110 | });
111 |
112 | export default createStylesheet(styles);
113 |
--------------------------------------------------------------------------------
/src/play-midnight.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import 'typeface-roboto';
5 | import 'style/global';
6 |
7 | import Root from 'components/Root';
8 | import injectElement from 'utils/injectElement';
9 |
10 | const entry = injectElement('div', {
11 | id: 'play-midnight',
12 | });
13 |
14 | ReactDOM.render( , entry);
15 |
--------------------------------------------------------------------------------
/src/style/global.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'styled-components';
2 |
3 | import { getUrl } from 'lib/api';
4 | import PlayMidnightLogo from 'assets/images/Logo.svg';
5 |
6 | injectGlobal`
7 | body{
8 | -webkit-font-smoothing: antialiased;
9 | }
10 |
11 | #splash-screen {
12 | #loading-progress-content {
13 | position: relative;
14 |
15 | &:before {
16 | position: absolute;
17 | top: 90px;
18 | left: 50%;
19 | transform: translateX(-50%);
20 | content: '';
21 | width: 228px;
22 | height: 228px;
23 | background: url(${getUrl(PlayMidnightLogo)});
24 | background-size: 228px 228px;
25 | }
26 |
27 | svg {
28 | visibility: hidden;
29 | opacity: 0;
30 | }
31 | }
32 | `;
33 |
--------------------------------------------------------------------------------
/src/style/sheets/alerts.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | const styles = theme => css`
4 | sj-callout {
5 | /* Saved */
6 | }
7 |
8 | gpm-bottom-sheet {
9 | background: ${theme.B300} !important;
10 |
11 | h2,
12 | h2 span {
13 | color: ${theme.font_primary} !important;
14 | }
15 |
16 | p,
17 | p span {
18 | color: ${theme.font_secondary} !important;
19 | }
20 | }
21 | `;
22 |
23 | export default styles;
24 |
--------------------------------------------------------------------------------
/src/style/sheets/buttons.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | const styles = theme => css`
4 | /* Radio Button */
5 | paper-button .toggle-bar.paper-toggle-button {
6 | background-color: ${theme.B500};
7 | }
8 |
9 | paper-button .paper-toggle-button.paper-toggle-button {
10 | background-color: ${theme.font_primary};
11 | }
12 |
13 | /* Toggle Button */
14 | paper-toggle-button {
15 | &[disabled] {
16 | cursor: not-allowed !important;
17 | }
18 |
19 | .toggle-bar.paper-toggle-button,
20 | .toggle-button.paper-toggle-button {
21 | background: ${theme.B500} !important;
22 | border: 1px solid ${theme.B500} !important;
23 | }
24 |
25 | .toggle-ink.paper-toggle-button {
26 | color: ${theme.B200} !important;
27 | }
28 | }
29 |
30 | /* Checkbox */
31 | paper-checkbox #checkboxLabel.paper-checkbox {
32 | color: ${theme.font_primary} !important;
33 | }
34 |
35 | /* Buttons */
36 | .button.primary,
37 | .simple-dialog-buttons button.goog-buttonset-default,
38 | paper-button,
39 | paper-button.material-primary,
40 | sj-paper-button.material-primary {
41 | opacity: 0.9;
42 |
43 | &[disabled] {
44 | background: transparent !important;
45 | color: ${theme.font_secondary} !important;
46 | cursor: not-allowed !important;
47 | opacity: 0.3;
48 | }
49 |
50 | &:hover {
51 | opacity: 1;
52 | }
53 | }
54 |
55 | /* Top Colored Bar icons */
56 | core-header-panel#content-container core-icon,
57 | core-header-panel#content-container sj-icon-button {
58 | color: ${theme.font_primary} !important;
59 | }
60 |
61 | /* Playlist button */
62 | #unsubscribe-playlist-button,
63 | paper-button#unsubscribe-playlist-button sj-paper-button#unsubscribe-playlist-button {
64 | .playlist-unsubscribe {
65 | color: ${theme.font_primary};
66 | }
67 | }
68 |
69 | /* Play Button */
70 | sj-play-button {
71 | background: transparent !important;
72 |
73 | &:hover #pulse.sj-play-button {
74 | transform: scale(1.15) !important;
75 | }
76 |
77 | #pulse.sj-play-button {
78 | opacity: 0.3;
79 | transform: scale(0.95) !important;
80 | }
81 | }
82 | `;
83 |
84 | export default styles;
85 |
--------------------------------------------------------------------------------
/src/style/sheets/cardGrid.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | gpm-card-grid,
7 | gpm-now-card-grid {
8 | sj-play-button {
9 | #buttonContent.sj-play-button {
10 | background: ${theme.B25} !important;
11 | }
12 |
13 | #icon.sj-play-button {
14 | fill: ${theme.font_primary} !important;
15 | }
16 | }
17 |
18 | [slot='title'],
19 | #gridTitle {
20 | color: ${theme.font_primary} !important;
21 | }
22 |
23 | sj-card {
24 | &[card-aspect-ratio='wide'] {
25 | background-color: ${theme.B400} !important;
26 |
27 | [slot='title'],
28 | .card-title {
29 | color: ${transparentize(theme.white, 0.9)} !important;
30 | text-shadow: 1px 1px 3px ${theme.B400} !important;
31 | }
32 |
33 | [slot='reason'],
34 | .card-reason {
35 | color: ${transparentize(theme.white, 0.7)} !important;
36 | text-shadow: 1px 1px 3px ${theme.B400} !important;
37 | }
38 |
39 | [slot='subtitle'],
40 | .card-subtitle {
41 | color: ${theme.font_secondary} !important;
42 | text-shadow: 1px 1px 3px ${theme.B400} !important;
43 | }
44 |
45 | [slot='description'],
46 | .card-description {
47 | color: ${theme.font_secondary} !important;
48 | text-shadow: 1px 1px 3px ${theme.B400} !important;
49 | }
50 | }
51 |
52 | &:not([card-aspect-ratio='wide']) {
53 | [slot='title'],
54 | .card-title {
55 | color: ${transparentize(theme.white, 0.9)} !important;
56 | text-shadow: 1px 1px 3px ${theme.B400} !important;
57 | }
58 |
59 | [slot='subtitle'],
60 | .card-subtitle {
61 | color: ${transparentize(theme.white, 0.7)} !important;
62 | text-shadow: 1px 1px 3px ${theme.B400} !important;
63 | }
64 |
65 | [slot='reason'],
66 | .card-reason {
67 | color: ${transparentize(theme.white, 0.7)} !important;
68 | text-shadow: 1px 1px 3px ${theme.B400} !important;
69 | }
70 |
71 | [slot='description'],
72 | .card-description {
73 | color: ${transparentize(theme.white, 0.6)} !important;
74 | text-shadow: 1px 1px 3px ${theme.B400} !important;
75 | padding-bottom: 3px !important;
76 | }
77 | }
78 | }
79 |
80 | gpm-colored-now-card {
81 | #textProtection {
82 | background: linear-gradient(
83 | to top,
84 | ${transparentize(theme.B400, 0.999)},
85 | ${transparentize(theme.B400, 0.15)}
86 | ) !important;
87 | }
88 |
89 | [slot='title'],
90 | [slot='header'],
91 | .card-header {
92 | color: ${transparentize(theme.white, 0.9)} !important;
93 | text-shadow: 1px 1px 3px ${theme.B400} !important;
94 | }
95 |
96 | [slot='reason'],
97 | .card-reason {
98 | color: ${transparentize(theme.white, 0.7)} !important;
99 | text-shadow: 1px 1px 3px ${theme.B400} !important;
100 | }
101 |
102 | #textSeparator {
103 | background: ${transparentize(theme.white, 0.7)} !important;
104 | box-shadow: 1px 1px 3px ${theme.B400} !important;
105 | }
106 |
107 | .card-title {
108 | color: ${transparentize(theme.white, 0.9)} !important;
109 | text-shadow: 1px 1px 3px ${theme.B400} !important;
110 | }
111 |
112 | [slot='description'],
113 | [slot='subtitle'],
114 | .card-description {
115 | color: ${transparentize(theme.white, 0.6)} !important;
116 | text-shadow: 1px 1px 3px ${theme.B400} !important;
117 | padding-bottom: 3px !important;
118 | }
119 | }
120 | }
121 | `;
122 |
123 | export default styles;
124 |
--------------------------------------------------------------------------------
/src/style/sheets/cards.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | .info-card,
7 | .material-card,
8 | .settings-card {
9 | background: ${theme.B400};
10 | color: ${theme.font_primary};
11 |
12 | .title {
13 | color: ${theme.font_primary} !important;
14 | }
15 |
16 | .sub-title {
17 | color: ${theme.font_secondary} !important;
18 | }
19 |
20 | &[data-is-listen-now='true'] .reason .reason-text {
21 | color: ${theme.font_primary};
22 | }
23 |
24 | &[data-type='situations'][data-is-podlist-situation='true'] .podcast-badge {
25 | background: ${theme.B400};
26 | }
27 |
28 | &[data-type='album'] .details .fade-out:after {
29 | background: linear-gradient(
30 | to right,
31 | ${transparentize(theme.B400, 0)} 0%,
32 | ${transparentize(theme.B400, 0.999)} 100%
33 | );
34 | }
35 |
36 | &[data-type='artist'] .details .fade-out:after {
37 | background: linear-gradient(
38 | to right,
39 | ${transparentize(theme.B500, 0)} 0%,
40 | ${transparentize(theme.B500, 0.999)} 100%
41 | );
42 | }
43 |
44 | .details {
45 | background: ${theme.B400};
46 | }
47 |
48 | &.entity-card {
49 | background: transparent;
50 |
51 | .image-wrapper {
52 | background: ${theme.B200};
53 | }
54 |
55 | .details {
56 | background: transparent;
57 | }
58 | }
59 |
60 | .details sj-icon-button.menu-anchor {
61 | color: ${theme.font_primary};
62 | }
63 |
64 | /* Feeling Lucky */
65 | &[data-size='small'][data-type='imfl'] .title {
66 | color: ${theme.font_primary};
67 | }
68 |
69 | &[data-size='small'][data-type='imfl'] .sub-title {
70 | color: ${theme.font_secondary};
71 | }
72 | }
73 |
74 | /* Info Card */
75 | #music-content .info-card {
76 | background-color: ${theme.B400};
77 |
78 | .title {
79 | color: ${theme.font_primary};
80 | }
81 |
82 | .sub-title {
83 | color: ${theme.font_secondary};
84 | }
85 | }
86 |
87 | /* Songza cards */
88 | .material-card[data-type='situations'][data-is-podlist-situation='true'] iron-icon.podcast-badge {
89 | background-color: ${theme.B400};
90 | }
91 | `;
92 |
93 | export default styles;
94 |
--------------------------------------------------------------------------------
/src/style/sheets/forms.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | const styles = theme => css`
4 | /* Popup Dialog */
5 | paper-action-dialog {
6 | background: ${theme.B400};
7 | color: ${theme.font_primary};
8 |
9 | &::shadow h1 {
10 | color: ${theme.font_primary};
11 | }
12 |
13 | input {
14 | color: ${theme.font_secondary};
15 | }
16 |
17 | .unfocused-underline {
18 | background: ${theme.B500};
19 | }
20 |
21 | sj-paper-button {
22 | &[disabled] {
23 | background: ${theme.B500};
24 | }
25 | }
26 | }
27 |
28 | paper-input-container,
29 | .paper-input-container,
30 | .paper-input-container-1 {
31 | .input-content.paper-input-container .paper-input-input,
32 | .input-content.paper-input-container input,
33 | .input-content.paper-input-container iron-autogrow-textarea,
34 | .input-content.paper-input-container textarea {
35 | color: ${theme.font_primary} !important;
36 | }
37 | }
38 |
39 | .material-share-options #sharing-option-label {
40 | color: ${theme.font_primary} !important;
41 | }
42 |
43 | .error,
44 | .label-text,
45 | .material-share-options #sharing-option-description {
46 | color: ${theme.font_secondary} !important;
47 | }
48 | `;
49 |
50 | export default styles;
51 |
--------------------------------------------------------------------------------
/src/style/sheets/index.js:
--------------------------------------------------------------------------------
1 | export { default as accents } from './accents';
2 | export { default as alerts } from './alerts';
3 | export { default as buttons } from './buttons';
4 | export { default as cardGrid } from './cardGrid';
5 | export { default as cards } from './cards';
6 | export { default as core } from './core';
7 | export { default as forms } from './forms';
8 | export { default as loading } from './loading';
9 | export { default as menus } from './menus';
10 | export { default as misc } from './misc';
11 | export { default as nav } from './nav';
12 | export { default as page } from './page';
13 | export { default as player } from './player';
14 | export { default as queue } from './queue';
15 | export { default as songTable } from './songTable';
16 |
--------------------------------------------------------------------------------
/src/style/sheets/loading.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | const styles = theme => css`
4 | /* Initial Loading Screen */
5 | #loading-progress {
6 | background-color: ${theme.B500};
7 | }
8 |
9 | #loading-progress-message {
10 | color: ${theme.font_secondary};
11 | }
12 |
13 | #loading-progress-bar,
14 | #loading-progress-content #progressContainer,
15 | #loading-progress-content > div:nth-child(2) {
16 | background: ${theme.B300} !important;
17 | border: none;
18 | }
19 |
20 | #loading-progress-content #progressContainer #secondaryProgress {
21 | background: ${theme.B100};
22 | }
23 |
24 | #loading-progress-content div:last-child {
25 | color: ${theme.font_secondary} !important;
26 | }
27 |
28 | gpm-loading-indicator #contentWrapper.gpm-loading-indicator {
29 | background: ${theme.B300} !important;
30 | color: ${theme.font_primary} !important;
31 | }
32 | `;
33 |
34 | export default styles;
35 |
--------------------------------------------------------------------------------
/src/style/sheets/menus.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { getUrl } from 'lib/api';
4 | import { lighten, transparentize } from 'style/theme';
5 |
6 | import LoadingWhite from 'assets/images/sprites/ani_loading_white.gif';
7 |
8 | const styles = theme => css`
9 | .paper-dialog-0,
10 | paper-dialog {
11 | &::shadow {
12 | .content,
13 | .description,
14 | .dialog-header .episode-title,
15 | .dialog-header a[data-id='series-navigate'],
16 | h2,
17 | p {
18 | color: ${theme.font_primary} !important;
19 | }
20 | }
21 |
22 | .content,
23 | .description,
24 | .dialog-header .episode-title,
25 | .dialog-header a[data-id='series-navigate'],
26 | h2,
27 | p {
28 | color: ${theme.font_primary} !important;
29 | }
30 | }
31 |
32 | /* Tabs */
33 | #material-app-bar .material-header-bar paper-tabs.tab-container,
34 | #material-app-bar .material-header-bar sj-tabs.tab-container {
35 | color: ${theme.font_primary};
36 | }
37 |
38 | .simple-dialog-bg {
39 | background: ${transparentize(theme.B25, 0.7)};
40 | }
41 |
42 | .simple-dialog {
43 | background: ${theme.B400};
44 | border: 1px solid ${theme.B700};
45 |
46 | .simple-dialog-title {
47 | background: ${theme.B400};
48 | color: ${theme.font_primary};
49 | }
50 |
51 | .simple-dialog-content {
52 | background: ${theme.B400};
53 | color: ${theme.font_primary};
54 |
55 | .browseSubtext {
56 | color: ${theme.font_primary};
57 | margin-bottom: 5px;
58 | }
59 | }
60 |
61 | .edit-section {
62 | div:first-child {
63 | margin-bottom: 5px;
64 | }
65 | }
66 |
67 | input[type='text'],
68 | textarea {
69 | color: ${theme.font_primary};
70 | background: transparent;
71 | border: none;
72 | border-bottom: 1px solid ${theme.font_secondary};
73 |
74 | &:focus {
75 | outline: none;
76 | }
77 | }
78 | }
79 |
80 | /* Context Menus */
81 | .goog-menu,
82 | .goog-menuitem,
83 | .now-playing-menu.goog-menu,
84 | .now-playing-menu.goog-menu .goog-menuitem,
85 | .now-playing-menu.goog-menu .goog-submenu,
86 | .now-playing-menu.goog-menu .goog-submenu .goog-submenu-arrow {
87 | background-color: ${theme.B300} !important;
88 | color: ${theme.font_primary} !important;
89 | }
90 |
91 | .goog-menu,
92 | .now-playing-menu.goog-menu {
93 | border-color: transparent !important;
94 | }
95 |
96 | .goog-menuitem {
97 | &:hover {
98 | background: ${theme.B400} !important;
99 | }
100 | }
101 |
102 | .goog-menuheader {
103 | color: ${theme.font_secondary} !important;
104 |
105 | /* Loading Icon */
106 | .spinner {
107 | background: transparent url(${getUrl(LoadingWhite)}) no-repeat center center;
108 | background-size: 20px 20px;
109 | width: auto;
110 | min-width: 20px;
111 | }
112 | }
113 |
114 | .extra-links-menu .goog-menuitem-content,
115 | .goog-menuitem,
116 | .goog-menuitem-content,
117 | .now-playing-menu .goog-menuitem .goog-menuitem-content {
118 | color: ${theme.font_primary} !important;
119 | }
120 |
121 | .goog-menuitem-highlight .goog-menuitem-content,
122 | .goog-menuitem-highlight .goog-menuitem-content .goog-submenu-arrow {
123 | color: ${theme.font_primary} !important;
124 | }
125 |
126 | .goog-menu {
127 | .goog-menuitem {
128 | .goog-menuitem-content {
129 | color: ${theme.font_primary} !important;
130 | }
131 |
132 | &:hover {
133 | background: ${theme.B200} !important;
134 |
135 | .goog-menuitem-content {
136 | color: ${lighten(theme.font_primary, 3)} !important;
137 | }
138 | }
139 | }
140 |
141 | .goog-submenu {
142 | iron-icon {
143 | color: ${theme.font_primary} !important;
144 | }
145 | }
146 |
147 | .goog-menuseparator {
148 | background: ${theme.B200} !important;
149 | }
150 | }
151 |
152 | .goog-menuitem-content .goog-submenu-arrow,
153 | .now-playing-menu .goog-submenu .goog-submenu-arrow {
154 | color: ${theme.font_primary} !important;
155 | }
156 |
157 | .goog-menuseparator {
158 | border-top: 1px solid ${theme.B500} !important;
159 | }
160 |
161 | /* Paper Menu */
162 | paper-menu {
163 | background-color: ${theme.B300} !important;
164 | color: ${theme.font_primary} !important;
165 |
166 | [secondary] {
167 | color: ${theme.font_secondary} !important;
168 | }
169 | }
170 | `;
171 |
172 | export default styles;
173 |
--------------------------------------------------------------------------------
/src/style/sheets/misc.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | /* Scrollables */
7 | paper-dialog-scrollable {
8 | button.suggestion {
9 | color: ${theme.font_secondary} !important;
10 | }
11 |
12 | .download-dialog {
13 | color: ${theme.font_primary} !important;
14 |
15 | .limit-text {
16 | color: ${theme.font_secondary} !important;
17 | }
18 | }
19 |
20 | &.is-scrolled:not(:first-child)::before,
21 | &.can-scroll:not([style-scope]):not(.style-scope):not(.scrolled-to-bottom)::after {
22 | background: ${theme.B700} !important;
23 | }
24 | }
25 |
26 | /* Visualizer Card */
27 | .visualizercard {
28 | background: ${theme.B400};
29 | color: ${theme.font_primary};
30 |
31 | .label {
32 | color: ${theme.font_primary};
33 | }
34 | }
35 |
36 | /* Google Labs */
37 | .labs-card {
38 | .lab-list-item {
39 | .lab-name {
40 | font-weight: 700;
41 | }
42 |
43 | .lab-description {
44 | color: ${theme.font_primary} !important;
45 | }
46 |
47 | &:not(:last-child) {
48 | border-bottom: 1px solid ${theme.B500} !important;
49 | }
50 | }
51 | }
52 |
53 | /* Shortcuts Table */
54 | .shortcuts-table {
55 | color: ${theme.font_primary};
56 |
57 | td,
58 | th {
59 | border-bottom: 1px solid ${theme.B700};
60 | color: ${theme.font_primary};
61 | }
62 | }
63 |
64 | .shortcuts-dialog {
65 | tr {
66 | border-bottom: 1px solid ${theme.B100};
67 |
68 | &:first-child {
69 | border-top: 1px solid ${theme.B100};
70 | }
71 | }
72 |
73 | caption,
74 | td,
75 | td:nth-child(2) {
76 | color: ${theme.font_primary};
77 | }
78 | }
79 |
80 | /* Suggested Query */
81 | .suggested-query {
82 | color: ${theme.font_primary};
83 | }
84 |
85 | /* Album Art */
86 | .albumImage {
87 | border: 1px solid ${theme.B700};
88 | background: ${theme.B400};
89 | }
90 |
91 | /* Loading Overlay */
92 | #loading-overlay,
93 | #loading-overlay[data-type='full-loading-overlay'],
94 | #loading-overlay[data-type='regular-loading-overlay'],
95 | #loading-overlay[data-type='ifl-loading-overlay'],
96 | .core-overlay-backdrop.core-opened,
97 | .zoomable-image-dialog-bg {
98 | background-color: ${transparentize(theme.B25, 0.8)};
99 | opacity: 1;
100 | }
101 |
102 | .iron-overlay-backdrop-0 {
103 | background-color: ${transparentize(theme.B25, 0.8)};
104 | }
105 |
106 | /* Upload Music Dialog */
107 | .simple-dialog-bg,
108 | .upload-dialog-bg,
109 | .zoomable-image-dialog-bg {
110 | background: ${transparentize(theme.B25, 0.8)};
111 | }
112 |
113 | .upload-dialog {
114 | background: ${theme.B400};
115 |
116 | .upload-dialog-title-close {
117 | cursor: pointer;
118 | }
119 |
120 | .upload-dialog-title {
121 | color: ${theme.font_primary};
122 | }
123 |
124 | .upload-dialog-content {
125 | color: ${theme.font_primary};
126 | }
127 | }
128 |
129 | .upload-progress-finished-label,
130 | .upload-progress-upload-label {
131 | color: ${theme.font_primary};
132 | }
133 |
134 | .upload-dialog-dragover {
135 | .upload-dialog-graphic {
136 | filter: grayscale(100%);
137 | }
138 | }
139 |
140 | /* Explicit */
141 | .album-view .material-container-details .info .title .explicit,
142 | .explicit,
143 | .material .song-row .explicit,
144 | .material-card .explicit,
145 | .podcast-series-view .material-container-details .info .title .explicit {
146 | color: ${theme.B400};
147 | }
148 |
149 | /* Share Buttons */
150 | .share-buttons {
151 | border-bottom: 1px solid ${theme.B500};
152 |
153 | .share-button .button-label {
154 | color: ${theme.font_primary};
155 | }
156 | }
157 |
158 | /* Fade Gradient */
159 | .fade-out:after,
160 | .material .fade-out.gray:after,
161 | .material .fade-out:after {
162 | background: linear-gradient(
163 | to right,
164 | ${transparentize(theme.B400, 0)} 0%,
165 | ${transparentize(theme.B400, 0.999)} 100%
166 | );
167 | background-image: linear-gradient(
168 | to right,
169 | ${transparentize(theme.B400, 0)} 0%,
170 | ${transparentize(theme.B400, 0.999)} 100%
171 | );
172 | }
173 |
174 | core-icon[icon='sj:unsubscribe'] svg,
175 | iron-icon[icon='sj:unsubscribe'] svg {
176 | path:nth-child(2) {
177 | fill: ${theme.font_primary};
178 | }
179 | }
180 |
181 | /* Default album art & radio station background */
182 | .card[data-zoomable-image-url$='default_album.svg'] svg,
183 | .material-card[data-type='album'] svg.image,
184 | .material-card[data-type='wst'] .quilted-radio-fallback,
185 | .material-card[data-type='wst'] .quilted-radio-fallback > *,
186 | img[src$='default_album.svg'],
187 | img[src$='default_album_art_song_row.png'],
188 | img[src$='default_album_med.png'],
189 | img[src$='default_playlist.svg'] {
190 | filter: invert(100%);
191 | }
192 |
193 | /* Improve Recommendations */
194 | paper-header-panel #music-content .g-content {
195 | padding: 30px 0;
196 | }
197 |
198 | .nuq-view {
199 | & > h2 {
200 | color: ${theme.font_primary};
201 | margin-bottom: 25px;
202 | }
203 |
204 | .quiz-item-list {
205 | padding: 0 25px;
206 | }
207 |
208 | button.quiz-block {
209 | cursor: pointer;
210 | margin-bottom: 15px;
211 |
212 | .name {
213 | color: ${theme.font_primary};
214 | }
215 | }
216 | }
217 |
218 | .button-bar {
219 | background: ${theme.B300};
220 | }
221 | `;
222 |
223 | export default styles;
224 |
--------------------------------------------------------------------------------
/src/style/sheets/nav.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize, isDark, TRANSITION_FAST } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | #material-app-bar {
7 | background: ${theme.B300};
8 | color: ${theme.font_primary};
9 | border-bottom-color: ${theme.B400} !important;
10 | transition: background ${TRANSITION_FAST}, border-color ${TRANSITION_FAST};
11 | }
12 |
13 | #nav_collections .nav-item-container,
14 | .nav-item-container,
15 | .playlists-container .nav-item-container {
16 | .owner-name {
17 | color: ${theme.font_secondary};
18 | }
19 |
20 | paper-icon-button iron-icon {
21 | color: ${theme.font_primary};
22 | }
23 |
24 | &.selected,
25 | &.selected:focus,
26 | &.selected:hover,
27 | &:focus,
28 | &:hover {
29 | background: ${theme.B300};
30 |
31 | .fade-out:after {
32 | background: linear-gradient(
33 | to right,
34 | ${transparentize(theme.B300, 0)} 0%,
35 | ${transparentize(theme.B300, 0.999)} 100%
36 | );
37 | }
38 | }
39 |
40 | &.playlist-drag-target {
41 | background: ${theme.B200};
42 |
43 | .fade-out:after {
44 | background: linear-gradient(
45 | to right,
46 | ${transparentize(theme.B200, 0)} 0%,
47 | ${transparentize(theme.B200, 0.999)} 100%
48 | );
49 | }
50 | }
51 |
52 | .fade-out:after {
53 | background: linear-gradient(
54 | to right,
55 | ${transparentize(theme.B400, 0)} 0%,
56 | ${transparentize(theme.B400, 0.999)} 100%
57 | );
58 | }
59 | }
60 |
61 | .nav-section-header {
62 | color: ${theme.font_secondary};
63 | }
64 |
65 | .nav-section-divider {
66 | border-bottom: 1px solid ${theme.B500} !important;
67 | }
68 |
69 | .new-playlist-drawer {
70 | #nav-container {
71 | background: ${theme.B400};
72 |
73 | .nav-toolbar {
74 | background: ${theme.B400};
75 |
76 | .toolbar-tools {
77 | padding: 0 10px;
78 | }
79 | }
80 |
81 | #nav {
82 | background: ${theme.B400};
83 | border-top: 1px solid ${theme.B700};
84 | }
85 | }
86 | }
87 |
88 | /* Playlist */
89 | sj-right-drawer #drawer.sj-right-drawer {
90 | background: ${theme.B400};
91 | }
92 |
93 | #playlist-drawer {
94 | .playlist-title,
95 | #mainPanel iron-icon,
96 | #playlist-drawer-header {
97 | color: ${theme.font_primary} !important;
98 | }
99 |
100 | .playlist-owner {
101 | color: ${theme.font_secondary} !important;
102 | }
103 |
104 | paper-header-panel[at-top] paper-toolbar,
105 | #recent-playlists-container,
106 | .autoplaylist-section {
107 | border-bottom-color: ${theme.B700} !important;
108 | }
109 |
110 | .playlist-drawer-item {
111 | color: ${theme.font_primary} !important;
112 |
113 | .playlist-wrapper {
114 | &:active,
115 | &:focus,
116 | &:hover {
117 | background: ${theme.B300};
118 | }
119 | }
120 | }
121 | }
122 |
123 | #playlist-drawer .playlist-drawer-item .playlist-wrapper:focus,
124 | #playlist-drawer .playlist-drawer-item .playlist-wrapper:hover,
125 | #playlist-drawer .playlist-drawer-item iron-icon:hover ~ .playlist-wrapper,
126 | #playlist-drawer .playlist-drawer-item sj-play-button:hover ~ .playlist-wrapper,
127 | #playlist-drawer .playlist-drawer-item.playlist-drop-target:not(.subscribed) .playlist-wrapper {
128 | background: ${theme.B300};
129 | }
130 |
131 | #playlist-drawer paper-header-panel paper-toolbar:not([style-scope]):not(.style-scope) {
132 | background: ${theme.B400};
133 | }
134 |
135 | #playlist-drawer paper-header-panel[at-top] paper-toolbar:not([style-scope]):not(.style-scope) {
136 | border-bottom-color: ${theme.B700};
137 | }
138 |
139 | .nav-toolbar {
140 | background: ${theme.B300};
141 | }
142 |
143 | .my-devices-card .device-list-item:not(:last-child) {
144 | border-bottom: 1px solid ${theme.B700};
145 | }
146 |
147 | /* Quick Nav */
148 | gpm-quick-nav #label {
149 | color: ${theme.font_primary};
150 | }
151 |
152 | /* Google Stuff */
153 | paper-toolbar #material-one-right #gb {
154 | a {
155 | color: ${theme.black} !important;
156 | }
157 |
158 | .gb_dc {
159 | filter: ${isDark(theme.B500) ? 'invert(100%)' : 'none'};
160 | }
161 | }
162 | `;
163 |
164 | export default styles;
165 |
--------------------------------------------------------------------------------
/src/style/sheets/page.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize, TRANSITION_FAST } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | #drawer-panel #material-hero-image {
7 | background: ${theme.B500};
8 | }
9 |
10 | .material-detail-view .material-container-details .read-more-button {
11 | &:hover {
12 | background: ${theme.B200};
13 | }
14 | }
15 |
16 | #material-app-bar .header-tab-title,
17 | .cluster .header,
18 | .cluster .header .cluster-title,
19 | .cluster .header .title,
20 | .cluster.material-cluster .header h2.title,
21 | .material-detail-view .artist-details .bio-wrapper .bio,
22 | .material-detail-view .material-container-details .info .description,
23 | .recommended-header,
24 | .section-header,
25 | .settings-cluster .header,
26 | .song-table [data-col='index'],
27 | .song-table [data-col='track'] {
28 | color: ${theme.font_primary};
29 | }
30 |
31 | .cluster .header .subtitle,
32 | .cluster.material-cluster .header span.subtitle {
33 | color: ${theme.font_secondary};
34 | }
35 |
36 | .material-album-container,
37 | .material-detail-view .artist-details,
38 | .material-playlist-container,
39 | .situations-filter,
40 | .songlist-container,
41 | .song-table,
42 | .top-tracks-info,
43 | .more-songs-container {
44 | background: ${theme.B400};
45 | }
46 |
47 | .playlist-view .editable:hover,
48 | .situations-content.material .situations-filter sj-item:focus,
49 | .situations-content.material .situations-filter sj-item:hover {
50 | background: ${theme.B200};
51 | }
52 |
53 | gpm-vertical-list {
54 | #items.gpm-vertical-list {
55 | background-color: ${theme.B400} !important;
56 | color: ${theme.font_primary} !important;
57 |
58 | a {
59 | color: ${theme.font_primary} !important;
60 | }
61 |
62 | & > *:hover,
63 | & > *[focused] {
64 | background-color: ${theme.B200} !important;
65 | }
66 | }
67 | }
68 |
69 | .situations-content.material .situations-filter sj-item:not(:last-child) .material-filter {
70 | border-bottom: 1px solid ${theme.B300};
71 | }
72 |
73 | .material-detail-view .material-container-details .info .container-stats-container .container-stats span {
74 | color: ${theme.font_secondary};
75 | }
76 |
77 | .material-detail-view .material-container-details .actions {
78 | border-top: 1px solid ${theme.B200};
79 | }
80 |
81 | .material-detail-view .top-tracks {
82 | background: ${theme.B400};
83 | }
84 |
85 | /* Subcategories */
86 | .subcategories-list {
87 | background: ${theme.B400};
88 |
89 | .subcategory {
90 | &:not(:last-child) {
91 | border-bottom: 1px solid ${theme.B300};
92 | }
93 |
94 | &:focus,
95 | &:hover {
96 | background: ${theme.B200};
97 | }
98 | }
99 |
100 | li {
101 | &:not(:last-child) {
102 | .li-content {
103 | border-bottom: 1px solid ${theme.B300};
104 | }
105 | }
106 |
107 | a:focus,
108 | a:hover {
109 | background: ${theme.B200};
110 | }
111 | }
112 | }
113 |
114 | /* Page Play Button (Top Right) */
115 | .material-container-details sj-fab {
116 | &::shadow core-icon {
117 | g {
118 | path:nth-child(1) {
119 | fill: ${theme.B300};
120 | }
121 | }
122 | }
123 | }
124 |
125 | /* Banner */
126 | #music-content .material-banner.banner.new-user-quiz-card,
127 | #music-content .material-banner.banner.ws-search-banner,
128 | #music-content .material-banner.banner.ws-subscriber-card {
129 | background: ${theme.B400};
130 |
131 | .title {
132 | color: ${theme.font_primary};
133 | }
134 | }
135 |
136 | /* Homepage Scrolling Module */
137 | sj-scrolling-module {
138 | h2 {
139 | color: ${transparentize(theme.white, 0.9)} !important;
140 | text-shadow: 1px 1px 3px ${theme.B400};
141 | }
142 |
143 | .module-subtitle {
144 | color: ${transparentize(theme.white, 0.7)};
145 | text-shadow: 1px 1px 3px ${theme.B400};
146 | }
147 | }
148 |
149 | /* More Songs */
150 | .more-songs-container {
151 | border-top: 1px solid ${theme.B300};
152 | }
153 |
154 | /* Playlist/Artist/Albums Header */
155 | .gpm-detail-page-header div.gpm-detail-page-header [slot='title'] {
156 | color: ${transparentize(theme.font_primary, 0.87)} !important;
157 | }
158 |
159 | gpm-detail-page-header div.gpm-detail-page-header [slot='subtitle'],
160 | gpm-detail-page-header div.gpm-detail-page-header [slot='description'],
161 | gpm-detail-page-header div.gpm-detail-page-header [slot='metadata'] {
162 | color: ${transparentize(theme.font_primary, 0.54)} !important;
163 | }
164 |
165 | gpm-detail-page-header[description-overflows] #descriptionWrapper.gpm-detail-page-header {
166 | background: transparent !important;
167 | border-color: transparent;
168 | margin-bottom: 4px;
169 | transition: border ${TRANSITION_FAST}, padding ${TRANSITION_FAST};
170 | cursor: pointer;
171 |
172 | & > [slot='description'] {
173 | margin: 0 !important;
174 | }
175 | }
176 |
177 | gpm-detail-page-header[description-overflows] #descriptionWrapper.gpm-detail-page-header:hover {
178 | background: transparent !important;
179 | padding-left: 8px;
180 | border-left: 3px solid ${theme.A500};
181 | }
182 |
183 | .gpm-detail-page-header div.gpm-detail-page-header [slot='buttons'] {
184 | color: ${transparentize(theme.font_primary, 0.87)} !important;
185 | }
186 |
187 | .station-container-content-wrapper .material-container-details {
188 | background: ${theme.B400};
189 | }
190 |
191 | gpm-action-buttons {
192 | border-top: 1px solid ${theme.B300};
193 | }
194 | `;
195 |
196 | export default styles;
197 |
--------------------------------------------------------------------------------
/src/style/sheets/player.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { TRANSITION_FAST } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | #player {
7 | background: ${theme.B300};
8 | color: ${theme.font_primary};
9 | transition: background ${TRANSITION_FAST};
10 | }
11 |
12 | #player.material .player-rating-container {
13 | background: ${theme.B300};
14 | }
15 |
16 | paper-slider #sliderBar #progressContainer {
17 | background: ${theme.B200} !important;
18 | transition: background ${TRANSITION_FAST};
19 | }
20 |
21 | #player.material:hover #material-player-progress #sliderContainer:not(.disabled) #sliderBar #progressContainer {
22 | background: ${theme.B100} !important;
23 | }
24 |
25 | #material-player-left-wrapper #playerSongInfo #player-artist,
26 | #material-player-left-wrapper #playerSongInfo .player-album,
27 | #material-player-left-wrapper #playerSongInfo .player-dash {
28 | color: ${theme.font_secondary};
29 | }
30 |
31 | paper-slider .ring > #sliderKnob > #sliderKnobInner {
32 | background: ${theme.B300};
33 | border: 2px solid ${theme.B200};
34 | }
35 |
36 | #player.material .material-player-middle paper-icon-button[data-id='play-pause'][disabled],
37 | #player.material .material-player-middle sj-icon-button[data-id='play-pause'][disabled] {
38 | opacity: 0.35 !important;
39 | cursor: not-allowed !important;
40 | }
41 |
42 | #player.material .material-player-middle paper-icon-button[data-id='play-pause'][title='Play'],
43 | #player.material .material-player-middle sj-icon-button[data-id='play-pause'][title='Play'] {
44 | path:nth-child(2) {
45 | fill: ${theme.B300};
46 | }
47 | }
48 |
49 | #player paper-icon-button[data-id='show-miniplayer'] {
50 | z-index: 1;
51 | }
52 |
53 | #time_container_current,
54 | #time_container_duration {
55 | color: ${theme.font_secondary};
56 | }
57 |
58 | #sliderKnob {
59 | display: none;
60 | }
61 |
62 | #player:hover #sliderKnob {
63 | display: block;
64 | }
65 | `;
66 |
67 | export default styles;
68 |
--------------------------------------------------------------------------------
/src/style/sheets/queue.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize } from 'style/theme';
4 |
5 | const styles = theme => css`
6 | #queue-overlay {
7 | z-index: 501 !important;
8 | }
9 |
10 | #queue-overlay,
11 | paper-dialog {
12 | background: ${theme.B300} !important;
13 |
14 | &::shadow #scroller {
15 | margin-bottom: 0;
16 | }
17 |
18 | #mini-queue-details img.large {
19 | width: 100%;
20 | height: auto;
21 | }
22 |
23 | .song-row {
24 | .column-content,
25 | .content,
26 | .title-right-items,
27 | td {
28 | background-color: ${theme.B300} !important;
29 | }
30 |
31 | .song-indicator {
32 | background-color: ${theme.B300} !important;
33 | }
34 |
35 | .fade-out:after {
36 | background: linear-gradient(
37 | to right,
38 | ${transparentize(theme.B300, 0)} 0%,
39 | ${transparentize(theme.B300, 0.999)} 100%
40 | );
41 | }
42 |
43 | &:nth-child(odd) {
44 | .column-content,
45 | .content,
46 | .title-right-items,
47 | td {
48 | background-color: ${theme.B200} !important;
49 | }
50 |
51 | .song-indicator {
52 | background-color: ${theme.B200} !important;
53 | }
54 |
55 | .fade-out:after {
56 | background: linear-gradient(
57 | to right,
58 | ${transparentize(theme.B200, 0)} 0%,
59 | ${transparentize(theme.B200, 0.999)} 100%
60 | );
61 | }
62 | }
63 |
64 | &.hover,
65 | &.selected-song-row,
66 | &:hover {
67 | .column-content,
68 | .content,
69 | .title-right-items,
70 | td {
71 | background-color: ${theme.B100} !important;
72 | }
73 |
74 | .song-indicator {
75 | background-color: ${theme.B100} !important;
76 | }
77 |
78 | .fade-out:after {
79 | background: linear-gradient(
80 | to right,
81 | ${transparentize(theme.B100, 0)} 0%,
82 | ${transparentize(theme.B100, 0.999)} 100%
83 | );
84 | }
85 | }
86 | }
87 |
88 | .upload-progress-row {
89 | td {
90 | background-color: ${theme.B300};
91 | color: ${theme.font_primary};
92 | }
93 |
94 | .fade-out:after {
95 | background: linear-gradient(
96 | to right,
97 | ${transparentize(theme.B300, 0)} 0%,
98 | ${transparentize(theme.B300, 0.999)} 100%
99 | );
100 | }
101 | }
102 |
103 | &:after {
104 | border-color: transparent transparent ${theme.B300} ${theme.B300};
105 | }
106 |
107 | #mini-queue-details .imfl-image {
108 | filter: grayscale(100%);
109 | }
110 | }
111 |
112 | .material-empty .empty-message,
113 | .material-empty .empty-submessage {
114 | color: ${theme.font_secondary};
115 | }
116 | `;
117 |
118 | export default styles;
119 |
--------------------------------------------------------------------------------
/src/style/sheets/songTable.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | import { transparentize } from 'style/theme';
4 | import { getUrl } from 'lib/api';
5 |
6 | import { TRANSITION_FAST } from 'style/theme';
7 |
8 | import ThumbsUp from 'assets/images/icon-thumbs-up.svg';
9 | import ThumbsDown from 'assets/images/icon-thumbs-down.svg';
10 | import LoadingWhite from 'assets/images/sprites/ani_loading_white.gif';
11 | import EqualWhite from 'assets/images/sprites/ani_equalizer_white.gif';
12 | import EqualWhiteStatic from 'assets/images/sprites/equalizer_white.png';
13 |
14 | const styles = theme => css`
15 | .song-table {
16 | &,
17 | .detail-view & {
18 | padding: 5px 0 0 0;
19 | }
20 |
21 | &[data-type='srbm'] {
22 | border-top: 1px solid ${theme.B600};
23 | }
24 |
25 | .song-row {
26 | background: ${theme.B400};
27 | color: ${theme.font_primary} !important;
28 |
29 | td a {
30 | color: ${theme.font_secondary} !important;
31 | }
32 |
33 | [data-col='index'] .hover-button[data-id='play'],
34 | [data-col='track'] .hover-button[data-id='play'] {
35 | background-color: transparent;
36 | }
37 |
38 | .rating-container.thumbs li {
39 | background-color: transparent;
40 | filter: invert(100%);
41 | }
42 |
43 | .rating-container.thumbs {
44 | li[data-rating='5'] {
45 | margin-top: 0;
46 | margin-left: 25px;
47 | }
48 |
49 | li[data-rating='1'] {
50 | margin-top: 0;
51 | margin-left: 8px;
52 | }
53 | }
54 |
55 | [data-col='rating'][data-rating='4'],
56 | [data-col='rating'][data-rating='5'] {
57 | background-color: inherit;
58 | background-image: url(${getUrl(ThumbsUp)}) !important;
59 | background-repeat: no-repeat !important;
60 | background-position: 26px 8px !important;
61 | background-size: 24px 24px !important;
62 | }
63 |
64 | [data-col='rating'][data-rating='1'],
65 | [data-col='rating'][data-rating='2'] {
66 | background-color: inherit;
67 | background-image: url(${getUrl(ThumbsDown)}) !important;
68 | background-repeat: no-repeat !important;
69 | background-position: 58px 8px !important;
70 | background-size: 24px 24px !important;
71 | }
72 |
73 | td,
74 | .content,
75 | .column-content,
76 | .song-indicator,
77 | .title-right-items {
78 | background-color: ${theme.B400} !important;
79 | color: ${theme.font_primary} !important;
80 | transition: background ${TRANSITION_FAST};
81 | }
82 |
83 | .title-right-items {
84 | line-height: 39px;
85 | }
86 |
87 | &.currently-playing {
88 | td[data-col='title'] {
89 | color: ${theme.font_primary} !important;
90 | }
91 | }
92 |
93 | .song-details-wrapper .song-artist-album,
94 | .song-details-wrapper .song-artist-album a {
95 | color: ${theme.font_secondary} !important;
96 | }
97 |
98 | .song-indicator {
99 | background-size: 24px 24px !important;
100 | }
101 |
102 | .song-indicator[data-playback-status='loading'] {
103 | background-image: url(${getUrl(LoadingWhite)});
104 | }
105 |
106 | .song-indicator[data-playback-status='playing'] {
107 | background-image: url(${getUrl(EqualWhite)});
108 | }
109 |
110 | .song-indicator[data-playback-status='paused'] {
111 | background-image: url(${getUrl(EqualWhiteStatic)}) !important;
112 | background-repeat: no-repeat !important;
113 | background-position: center center !important;
114 | }
115 |
116 | .fade-out:after {
117 | background: linear-gradient(
118 | to right,
119 | ${transparentize(theme.B400, 0)} 0%,
120 | ${transparentize(theme.B400, 0.999)} 100%
121 | );
122 | }
123 |
124 | &:nth-child(odd) {
125 | td,
126 | .content,
127 | .column-content,
128 | .song-indicator,
129 | .title-right-items {
130 | background-color: ${theme.B300} !important;
131 | }
132 |
133 | .fade-out:after {
134 | background: linear-gradient(
135 | to right,
136 | ${transparentize(theme.B300, 0)} 0%,
137 | ${transparentize(theme.B300, 0.999)} 100%
138 | );
139 | }
140 | }
141 |
142 | &.hover,
143 | &.selected-song-row,
144 | &:hover {
145 | td,
146 | .content,
147 | .column-content,
148 | .song-indicator,
149 | .title-right-items {
150 | background-color: ${theme.B200} !important;
151 | }
152 |
153 | .title-right-items {
154 | line-height: 39px;
155 | }
156 |
157 | .fade-out:after {
158 | background: linear-gradient(
159 | to right,
160 | ${transparentize(theme.B200, 0)} 0%,
161 | ${transparentize(theme.B200, 0.999)} 100%
162 | );
163 | }
164 | }
165 |
166 | &.selected-song-row {
167 | td[data-col='song-details'] {
168 | border-left: 2px solid ${theme.A500};
169 | }
170 | }
171 |
172 | &.currently-playing,
173 | &.hover,
174 | &:hover {
175 | [data-col='index'],
176 | [data-col='track'] {
177 | .column-content,
178 | .content {
179 | font-size: 0;
180 | }
181 | }
182 | }
183 | }
184 |
185 | &.mini [data-col='song-details'] .song-details-wrapper .song-title,
186 | [data-col='title'],
187 | th {
188 | color: ${theme.font_primary};
189 | }
190 | }
191 |
192 | .top-tracks {
193 | .song-row {
194 | .hover-button[data-id='play'] {
195 | background: ${theme.B200} !important;
196 | }
197 | }
198 | }
199 |
200 | .upload-progress-row td {
201 | background: ${theme.B400};
202 | color: ${theme.font_primary};
203 | border-top: 1px solid ${theme.B600};
204 | }
205 |
206 | .upload-progress-bar-thumb {
207 | background: lighten(${theme.B400}, 8%);
208 | color: ${theme.font_primary};
209 | }
210 |
211 | .upload-progress-upload-icon-arrow,
212 | .upload-progress-upload-icon-bar {
213 | filter: grayscale(100%);
214 | }
215 |
216 | .cluster-text-protection {
217 | background: ${theme.B500};
218 |
219 | &:before {
220 | width: 100%;
221 | background: ${theme.B500};
222 | }
223 | }
224 |
225 | /* Download Styles */
226 | .song-transfer-table-container {
227 | .song-transfer-table-row {
228 | border-bottom: 1px solid ${theme.B600};
229 | }
230 | }
231 |
232 | .progress-bar-vertical,
233 | .progress-bar-horizontal {
234 | background: lighten(${theme.B400}, 8%);
235 | border: 1px solid ${theme.B600};
236 | }
237 | `;
238 |
239 | export default styles;
240 |
--------------------------------------------------------------------------------
/src/style/theme.js:
--------------------------------------------------------------------------------
1 | import color from 'tinycolor2';
2 |
3 | export const transparentize = (c, amount) => {
4 | return color(c)
5 | .setAlpha(amount)
6 | .toString();
7 | };
8 |
9 | export const isDark = c => {
10 | return color(c).getBrightness() <= 165;
11 | };
12 |
13 | export const isLight = c => {
14 | return color(c).getBrightness() > 165;
15 | };
16 |
17 | export const darken = (c, amount) => {
18 | return color(c)
19 | .darken(amount)
20 | .toHexString();
21 | };
22 |
23 | export const lighten = (c, amount) => {
24 | return color(c)
25 | .lighten(amount)
26 | .toHexString();
27 | };
28 |
29 | export const DEFAULT_ACCENT = '#ec4e28';
30 | export const DEFAULT_BACKGROUND = '#141517';
31 | export const FONT_LIGHT = '#212121';
32 | export const FONT_DARK = '#ececec';
33 | export const TRANSITION_LIGHTNING = '200ms';
34 | export const TRANSITION_FAST = '300ms';
35 | export const TRANSITION_MEDIUM = '500ms';
36 | export const TRANSITION_SLOW = '700ms';
37 |
38 | export const stripTransition = transition => parseInt(transition.replace('ms', ''), 10);
39 |
40 | export const createTheme = (enabled = false, background = DEFAULT_BACKGROUND, accent = DEFAULT_ACCENT) => ({
41 | enabled,
42 | black: '#000',
43 | white: '#fff',
44 | font_google: '#212121',
45 | font_primary: isLight(background) ? FONT_LIGHT : FONT_DARK,
46 | font_secondary: isLight(background) ? lighten(FONT_LIGHT, 25) : darken(FONT_DARK, 25),
47 | red: '#9d0000',
48 |
49 | B25: lighten(background, 13),
50 | B50: lighten(background, 11),
51 | B100: lighten(background, 9),
52 | B200: lighten(background, 7),
53 | B300: lighten(background, 5),
54 | B400: lighten(background, 3),
55 | B500: background,
56 | B600: darken(background, 3),
57 | B700: darken(background, 5),
58 | B800: darken(background, 7),
59 | B900: darken(background, 9),
60 |
61 | A50: lighten(accent, 11),
62 | A100: lighten(accent, 9),
63 | A200: lighten(accent, 7),
64 | A300: lighten(accent, 5),
65 | A400: lighten(accent, 3),
66 | A500: accent,
67 | A600: darken(accent, 3),
68 | A700: darken(accent, 5),
69 | A800: darken(accent, 7),
70 | A900: darken(accent, 9),
71 | });
72 |
--------------------------------------------------------------------------------
/src/utils/array.js:
--------------------------------------------------------------------------------
1 | export const getIndex = (arr = [], item = {}, key = 'id') =>
2 | arr.findIndex(current => {
3 | return current[key] === item[key] || current[key] === item[key];
4 | });
5 |
6 | export const findItem = (arr = [], item = {}, key = 'id') =>
7 | arr.find(current => {
8 | return current[key] === item[key] || current[key] === item[key];
9 | });
10 |
11 | export const insertAt = (arr = [], item = {}, index = -1) => {
12 | return index > -1
13 | ? [...arr.slice(0, index + 1), item, ...arr.slice(index + 1)]
14 | : [...arr, item];
15 | };
16 |
17 | export const replaceItem = (arr = [], item = {}, key = 'id') => {
18 | const index = getIndex(arr, item, key);
19 | return index > -1
20 | ? [...arr.slice(0, index), item, ...arr.slice(index + 1)]
21 | : [...arr, item];
22 | };
23 |
24 | export const removeItem = (arr = [], item = {}, key = 'id') => {
25 | const index = getIndex(arr, item, key);
26 | return index > -1 ? [...arr.slice(0, index), ...arr.slice(index + 1)] : arr;
27 | };
28 |
29 | export const updateItem = (arr = [], item = {}, key = 'id') => {
30 | const index = getIndex(arr, item, key);
31 | return index > -1
32 | ? [
33 | ...arr.slice(0, index),
34 | { ...arr[index], ...item },
35 | ...arr.slice(index + 1)
36 | ]
37 | : [...arr, item];
38 | };
39 |
40 | export default {};
41 |
--------------------------------------------------------------------------------
/src/utils/awaitElement.js:
--------------------------------------------------------------------------------
1 | const awaitElement = async (where = 'body') => {
2 | return new Promise(resolve => {
3 | // Already Exists
4 | const target = document.querySelector(where);
5 | if (target) return resolve(target);
6 |
7 | // Target Not Found
8 | const observer = new MutationObserver(mutations => {
9 | const target = document.querySelector(where);
10 | if (target) {
11 | observer.disconnect();
12 | return resolve(target);
13 | }
14 | });
15 |
16 | observer.observe(document.body, { childList: true, subtree: true });
17 | });
18 | };
19 |
20 | export default awaitElement;
21 |
--------------------------------------------------------------------------------
/src/utils/checkEnv.js:
--------------------------------------------------------------------------------
1 | const checkEnv = env => process.env.NODE_ENV === env;
2 | export default checkEnv;
3 |
--------------------------------------------------------------------------------
/src/utils/createStylesheet.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import isPlainObject from 'lodash/isPlainObject';
3 |
4 | import getCssString from 'utils/getCssString';
5 |
6 | const createStylesheet = creator => accentColor => {
7 | const styles = creator(accentColor);
8 |
9 | // Array, assuming single stylesheet
10 | if (!isPlainObject(styles)) {
11 | return () => ;
12 | }
13 |
14 | // Object, assuming multiple sheets
15 | const stylesheets = {};
16 |
17 | Object.keys(styles).forEach(id => {
18 | const style = styles[id];
19 | stylesheets[id] = () => ;
20 | });
21 |
22 | return stylesheets;
23 | };
24 |
25 | export default createStylesheet;
26 |
--------------------------------------------------------------------------------
/src/utils/getArrayValue.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 |
3 | const getArrayValue = (arr = [], id = '', defaultValue, useValue = true) => {
4 | const foundOptions = find(arr, { id });
5 |
6 | if (foundOptions) {
7 | const foundValue = find(foundOptions.values, { id: foundOptions.value });
8 | return foundValue ? (useValue ? foundValue.value : foundValue) : defaultValue;
9 | }
10 |
11 | return defaultValue;
12 | };
13 |
14 | export default getArrayValue;
15 |
--------------------------------------------------------------------------------
/src/utils/getCssString.js:
--------------------------------------------------------------------------------
1 | import stylis from 'stylis';
2 |
3 | export default (css = []) => {
4 | const flatCSS = css.join('').replace(/^\s*\/\/.*$/gm, ''); // replace JS comments
5 | return stylis('', flatCSS);
6 | };
7 |
--------------------------------------------------------------------------------
/src/utils/injectElement.js:
--------------------------------------------------------------------------------
1 | const createElement = (type, attrs) => {
2 | const element = document.createElement(type);
3 |
4 | Object.keys(attrs).forEach(key => {
5 | const attribute = attrs[key];
6 | element[key] = attribute;
7 | });
8 |
9 | return element;
10 | };
11 |
12 | const injectElement = (element, where, prepend = false) => {
13 | const container = document.querySelector(where);
14 |
15 | if (container) {
16 | if (prepend) {
17 | container.insertAdjacentElement('afterbegin', element);
18 | } else {
19 | container.appendChild(element);
20 | }
21 |
22 | return element;
23 | }
24 |
25 | return undefined;
26 | };
27 |
28 | export const injectElementAsync = async (type, attributes, where = 'body') => {
29 | const { prepend, ...attrs } = attributes;
30 |
31 | return new Promise(resolve => {
32 | // Already Exists
33 | const element = document.querySelector(`${type}#${attrs.id}`);
34 | if (element) return resolve(element);
35 |
36 | // New Element
37 | const create = createElement(type, attrs);
38 | const injected = injectElement(create, where, prepend);
39 | if (injected) return resolve(create);
40 |
41 | // Inject Target Not Found
42 | const observer = new MutationObserver(mutations => {
43 | const injected = injectElement(create, where, prepend);
44 | if (injected) {
45 | observer.disconnect();
46 | return resolve(create);
47 | }
48 | });
49 |
50 | observer.observe(document.body, { childList: true });
51 | });
52 | };
53 |
54 | export default (type, attributes, where = 'body') => {
55 | const { prepend, ...attrs } = attributes;
56 |
57 | // Already Exists
58 | const element = document.querySelector(`${type}#${attrs.id}`);
59 | if (element) return element;
60 |
61 | // New Element
62 | const create = createElement(type, attrs);
63 | const injected = injectElement(create, where, prepend);
64 | if (!injected) throw new Error(`Error Injecting ${attrs.id} into ${where}`);
65 |
66 | return create;
67 | };
68 |
--------------------------------------------------------------------------------
/src/utils/removeAllElements.js:
--------------------------------------------------------------------------------
1 | const removeAllElements = selectors => {
2 | const elements = document.querySelectorAll(selectors);
3 | for (let i = 0, len = elements.length; i < len; i++) {
4 | const element = elements[i];
5 | element.remove();
6 | }
7 | };
8 |
9 | export default removeAllElements;
10 |
--------------------------------------------------------------------------------
/src/utils/validation.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid/v1';
2 |
3 | export const validateTitle = (title = '') => {
4 | return title.replace(/[^a-zA-Z0-9.',"()\s]/g, '');
5 | };
6 |
7 | export const validateId = (id = '') => {
8 | const cleansed = id
9 | .replace(/[^a-zA-Z0-9\s]/g, '')
10 | .replace(/\s/g, '-')
11 | .toLowerCase();
12 |
13 | return `${cleansed}-${uuid()}`;
14 | };
15 |
--------------------------------------------------------------------------------