├── .github
└── workflows
│ └── build-and-deploy.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── package.json
├── public
├── assets
│ └── images
│ │ ├── github-product-logo-scaled-resolutions.png
│ │ ├── opengraph-github-scaled-resolutions.png
│ │ ├── opengraph.png
│ │ └── opengraph.sketch
├── favicon.ico
├── index.html
├── manifest.json
└── sitemap.xml
├── src
├── common
│ └── components
│ │ ├── App.jsx
│ │ ├── App.module.css
│ │ ├── AspectRatioCalculator.jsx
│ │ ├── AspectRatioCalculator.module.css
│ │ ├── CharsetAndEncodingHelper.js
│ │ ├── CharsetAndEncodingHelper.spec.js
│ │ ├── PlistConstants.js
│ │ ├── PlistContainer.jsx
│ │ ├── PlistContainer.module.css
│ │ ├── PlistForm.jsx
│ │ ├── PlistForm.module.css
│ │ ├── PlistParser.jsx
│ │ ├── XmlParser.js
│ │ ├── XmlParserError.js
│ │ └── img
│ │ ├── comsysto-logo.png
│ │ └── logo-scaled-resolutions.svg
├── index.css
├── index.js
├── serviceWorker.js
└── setupTests.js
└── yarn.lock
/.github/workflows/build-and-deploy.yml:
--------------------------------------------------------------------------------
1 | name: build and dploy main branch
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: '12.x'
16 | - name: install dependencies
17 | run: yarn
18 | - name: run build
19 | run: yarn build
20 | - name: run tests
21 | run: yarn test
22 | - name: Deploy 🚀
23 | uses: JamesIves/github-pages-deploy-action@v4
24 | with:
25 | folder: build # The folder the action should deploy.
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "trailingComma": "all",
4 | "tabWidth": 2,
5 | "semi": true,
6 | "singleQuote": true
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Bernhard Grünewaldt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scaled Resolutions
2 |
3 | for your MacBooks external monitor. Display Override PropertyList File Parser and Generator with HiDPI Support on macOS.
4 |
5 | [](https://codeclou.github.io/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/)
6 |
7 | ---
8 |
9 |
10 |
11 | ### License
12 |
13 | [MIT](./LICENSE) © [Bernhard Grünewaldt](https://github.com/clouless)
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "display-overrides-plist-parser-hidpi",
3 | "version": "0.0.1",
4 | "description": "display-overrides-plist-parser-hidpi",
5 | "private": true,
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions.git"
9 | },
10 | "author": "Bernhard Grünewaldt",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/codeclou"
14 | },
15 | "homepage": "https://codeclou.github.io/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/",
16 | "dependencies": {
17 | "@testing-library/jest-dom": "^4.2.4",
18 | "@testing-library/react": "^9.3.2",
19 | "@testing-library/user-event": "^7.1.2",
20 | "alertify.js": "1.0.12",
21 | "bootstrap": "^4.5.2",
22 | "classnames": "2.2.5",
23 | "codemirror": "^5.56.0",
24 | "gh-pages": "^3.1.0",
25 | "prop-types": "^15.7.2",
26 | "react": "^16.13.1",
27 | "react-ace": "^9.1.3",
28 | "react-codemirror2": "^7.2.1",
29 | "react-dom": "^16.13.1",
30 | "react-icons": "^3.10.0",
31 | "react-scripts": "3.4.3",
32 | "redux": "4.0.5",
33 | "whatwg-fetch": "1.0.0"
34 | },
35 | "scripts": {
36 | "predeploy": "npm run build",
37 | "deploy": "gh-pages -d build",
38 | "start": "react-scripts start",
39 | "build": "react-scripts build",
40 | "test": "react-scripts test",
41 | "eject": "react-scripts eject"
42 | },
43 | "eslintConfig": {
44 | "extends": "react-app"
45 | },
46 | "browserslist": {
47 | "production": [
48 | ">0.2%",
49 | "not dead",
50 | "not op_mini all"
51 | ],
52 | "development": [
53 | "last 1 chrome version",
54 | "last 1 firefox version",
55 | "last 1 safari version"
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/public/assets/images/github-product-logo-scaled-resolutions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/85a05e03de9a3fde197c980d2f30e4e5309a9ff7/public/assets/images/github-product-logo-scaled-resolutions.png
--------------------------------------------------------------------------------
/public/assets/images/opengraph-github-scaled-resolutions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/85a05e03de9a3fde197c980d2f30e4e5309a9ff7/public/assets/images/opengraph-github-scaled-resolutions.png
--------------------------------------------------------------------------------
/public/assets/images/opengraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/85a05e03de9a3fde197c980d2f30e4e5309a9ff7/public/assets/images/opengraph.png
--------------------------------------------------------------------------------
/public/assets/images/opengraph.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/85a05e03de9a3fde197c980d2f30e4e5309a9ff7/public/assets/images/opengraph.sketch
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/85a05e03de9a3fde197c980d2f30e4e5309a9ff7/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Scaled Resolutions for your MacBooks external monitor | by codeclou
9 |
10 |
11 |
12 |
13 |
14 |
15 | You need to enable JavaScript to run this app.
16 |
17 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Scaled Resolutions for your MacBooks external monitor",
3 | "name": "Scaled Resolutions for your MacBooks external monitor | by codeclou",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | https://codeclou.github.io/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/
7 |
8 | https://codeclou.github.io/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/assets/images/opengraph-github-scaled-resolutions.png
9 | Scaled Resolutions for your MacBooks external monitor by codeclou
10 |
11 | 2019-07-21
12 | monthly
13 | 0.8
14 |
15 |
--------------------------------------------------------------------------------
/src/common/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PlistContainer from './PlistContainer';
3 | import AspectRatioCalculator from './AspectRatioCalculator';
4 | import appStyles from './App.module.css';
5 | import logo from './img/logo-scaled-resolutions.svg';
6 | import { IoIosStarOutline, IoIosHeart } from 'react-icons/io';
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.foo = 'bar';
12 | }
13 |
14 | render() {
15 | return (
16 | <>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
Scaled Resolutions
34 |
for your MacBooks external Monitor
35 |
36 | Display Override PropertyList File Parser and Generator with HiDPI support
37 |
38 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | This solution might not work with macOS Big Sur. We do not provide end user support.
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
Aspect Ratio Calculator
63 |
64 |
65 | Use this calculator to calculcate horizontal and vertical resolution based on
66 | given Aspect Ratio.
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
We like code!
75 |
...
76 |
77 |
78 |
79 |
80 |
81 |
82 |
111 | >
112 | );
113 | }
114 | }
115 |
116 | export default App;
117 |
--------------------------------------------------------------------------------
/src/common/components/App.module.css:
--------------------------------------------------------------------------------
1 | .app {
2 | margin: 100px 15px;
3 | }
4 | .warning {
5 | display: flex;
6 | color: #000;
7 | text-align: left;
8 | margin-top: 20px;
9 | }
10 | .warningIcon {
11 | padding-top: 10px;
12 | }
13 | .numberedList {
14 | position: relative;
15 | padding-left: 35px;
16 | }
17 | .howtoHeaderH3 {
18 | font-size: 2rem;
19 | margin-bottom: 30px;
20 | text-align: center;
21 | }
22 | .howtoHeaderH5 {
23 | margin-top: 50px;
24 | font-size: 1.4rem;
25 | }
26 | .numberedList::after {
27 | padding: 3px;
28 | text-align: center;
29 | font-size: 1rem;
30 | color: #fff;
31 | position: absolute;
32 | top: 0px;
33 | left: 0px;
34 | display: block;
35 | width: 24px;
36 | height: 24px;
37 | background-color: #61dafb;
38 | border-radius: 50%;
39 | z-index: 3;
40 | }
41 | .numberedListFirst::after {
42 | content: '1';
43 | }
44 | .numberedListSecond::after {
45 | content: '2';
46 | }
47 | .numberedListThird::after {
48 | content: '3';
49 | }
50 | .numberedListFourth::after {
51 | content: '4';
52 | }
53 | .numberedListFifth::after {
54 | content: '5';
55 | }
56 | .csBox {
57 | padding: 60px;
58 | }
59 | .howTo {
60 | margin-top: 100px;
61 | }
62 | .codeBox {
63 | padding: 3px 8px;
64 | border-radius: 2px;
65 | border: 1px solid rgba(0, 0, 0, 0.05);
66 | color: #555;
67 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
68 | background-color: #f4f5f7;
69 | overflow: auto;
70 | font-size: 0.9rem;
71 | margin-bottom: 0.5rem;
72 | white-space: nowrap;
73 | }
74 | @media (max-width: 768px) {
75 | .codeBox {
76 | white-space: normal;
77 | word-break: break-word;
78 | }
79 | }
80 | .bigSurMessage {
81 | color: #fff;
82 | background-color: rgb(208, 47, 47);
83 | padding: 20px;
84 | text-align: center;
85 | }
86 |
--------------------------------------------------------------------------------
/src/common/components/AspectRatioCalculator.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ratioStyles from './AspectRatioCalculator.module.css';
3 | import classNames from 'classnames';
4 |
5 | export default class AspectRatioCalculator extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.recalculate = this.recalculate.bind(this);
9 | this.state = {
10 | width: 1920,
11 | height: 1080,
12 | ratioWidth: 16,
13 | ratioHeight: 9,
14 | };
15 | }
16 |
17 | recalculate(event) {
18 | this.setState({ [`${event.target.name}`]: event.target.value });
19 | const ratio = this.state.ratioWidth / this.state.ratioHeight;
20 | if (event.target.name === 'width') {
21 | this.setState({ height: event.target.value / ratio });
22 | }
23 | if (event.target.name === 'height') {
24 | this.setState({ width: event.target.value * ratio });
25 | }
26 | if (event.target.name === 'ratioWidth') {
27 | this.setState({ width: this.state.height * (event.target.value / this.state.ratioHeight) });
28 | }
29 | if (event.target.name === 'ratioHeight') {
30 | this.setState({ height: this.state.width / (this.state.ratioWidth / event.target.value) });
31 | }
32 | }
33 |
34 | render() {
35 | return (
36 |
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/common/components/AspectRatioCalculator.module.css:
--------------------------------------------------------------------------------
1 | .field {
2 | width: 100px;
3 | padding: 0px 3px;
4 | margin: 5px;
5 | border: none;
6 | }
7 |
--------------------------------------------------------------------------------
/src/common/components/CharsetAndEncodingHelper.js:
--------------------------------------------------------------------------------
1 | export default class CharsetAndEncodingHelper {
2 |
3 | deepCopyOrReturnEmptyObject(object) {
4 | if (object === undefined || object === null) {
5 | return {};
6 | }
7 | try {
8 | return JSON.parse(JSON.stringify(object));
9 | } catch (err) {
10 | return {};
11 | }
12 | }
13 |
14 | intToHex(intNumber) {
15 | if (intNumber !== undefined && intNumber !== null) {
16 | return Number(intNumber).toString(16);
17 | }
18 | return '';
19 | }
20 |
21 | intToHexPrefixedWithZerosTo8Bit(intNumber) {
22 | const hexString = this.intToHex(intNumber);
23 | const len = 8 - hexString.length;
24 | let hexWithZeroPrefixed = '';
25 | for (let i = 0; i < len; i++) {
26 | hexWithZeroPrefixed = `0${hexWithZeroPrefixed}`;
27 | }
28 | return `${hexWithZeroPrefixed}${hexString}`;
29 | }
30 |
31 | hexToInt(hexString) {
32 | return parseInt(hexString, 16);
33 | }
34 |
35 | base64ToHexStringSpaced(base64String) {
36 | if (base64String === undefined || base64String === null || base64String === '') {
37 | return '';
38 | }
39 | return new Buffer(base64String, 'base64').toString('hex').replace(/(.{8})/g, '$1 ')
40 | .trim();
41 | }
42 |
43 | base64ToDecimalStringSpaced(base64String) {
44 | if (base64String === undefined || base64String === null || base64String === '') {
45 | return '';
46 | }
47 | const hexString = new Buffer(base64String, 'base64').toString('hex').replace(/(.{8})/g, '$1 ')
48 | .trim();
49 | const splitted = hexString.split(' ');
50 | let ret = '';
51 | for (let i = 0; i < splitted.length; i++) {
52 | ret = `${ret}${this.hexToInt(splitted[i])} `;
53 | }
54 | return ret.trim();
55 | }
56 |
57 | hexToBase64(hexStringToEncode) {
58 | if (hexStringToEncode === undefined || hexStringToEncode === null || hexStringToEncode === '') {
59 | return '';
60 | }
61 | return new Buffer(hexStringToEncode, 'hex').toString('base64');
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/common/components/CharsetAndEncodingHelper.spec.js:
--------------------------------------------------------------------------------
1 | import CharsetAndEncodingHelper from './CharsetAndEncodingHelper';
2 |
3 | describe('CharsetAndEncodingHelper test', () => {
4 | it('deepCopyOrReturnEmptyObject() works', () => {
5 | const helper = new CharsetAndEncodingHelper();
6 | expect(helper.deepCopyOrReturnEmptyObject({ foo: 'bar' })).toMatchObject({ foo: 'bar' });
7 | });
8 | // FIXME: Add more testcsases
9 | });
10 |
--------------------------------------------------------------------------------
/src/common/components/PlistConstants.js:
--------------------------------------------------------------------------------
1 | const plistConstants = {
2 | plistXmlHeader: `
3 |
4 | `,
5 | plistXmlFooter: " ",
6 | plistXmlString: `
7 |
8 |
9 |
10 | DisplayProductName
11 | DELL U2515H
12 | DisplayProductID
13 | 53358
14 | DisplayVendorID
15 | 4268
16 | scale-resolutions
17 |
18 | AAAKAAAABaAAAAABACAAAA==
19 | AAAFAAAAAtAAAAABACAAAA==
20 | AAAPAAAACHAAAAABACAAAA==
21 | AAAHgAAABDgAAAABACAAAA==
22 | AAAMgAAABwgAAAABACAAAA==
23 | AAAGQAAAA4QAAAABACAAAA==
24 | AAAKAgAABaAAAAABACAAAA==
25 | AAAKrAAABgAAAAABACAAAA==
26 | AAAFVgAAAwAAAAABACAAAA==
27 |
28 |
29 | `,
30 | defaultResolutionForAddNew: {
31 | base64String: "AAAFAAAAAtAAAAABACAAAA==",
32 | decoded: {
33 | decimalStringSpaced: "1280 720 1",
34 | hexStringSpaced: "00000500 000002d0 00000001 00200000"
35 | },
36 | decimal: {
37 | width: "1280",
38 | height: "720",
39 | hidpi: true
40 | }
41 | }
42 | };
43 |
44 | export default plistConstants;
45 |
--------------------------------------------------------------------------------
/src/common/components/PlistContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import alertify from 'alertify.js';
3 | import PlistParser from './PlistParser';
4 | import plistConstants from './PlistConstants';
5 | import PlistForm from './PlistForm';
6 | import XmlParser from './XmlParser';
7 | import CharsetAndEncodingHelper from './CharsetAndEncodingHelper';
8 | import plistContainerStyles from './PlistContainer.module.css';
9 | import classNames from 'classnames';
10 | import appStyles from './App.module.css';
11 | import {
12 | IoIosCloudDownload,
13 | IoMdTrash,
14 | IoIosCloseCircle,
15 | IoIosCheckmarkCircle,
16 | IoIosWarning,
17 | } from 'react-icons/io';
18 |
19 | export default class PlistContainer extends Component {
20 | constructor(props) {
21 | super(props);
22 | this.handlePlistParserChange = this.handlePlistParserChange.bind(this);
23 | this.handlePlistFormChange = this.handlePlistFormChange.bind(this);
24 | this.downloadPlistAsFile = this.downloadPlistAsFile.bind(this);
25 | this.parser = new XmlParser();
26 | this.encHelp = new CharsetAndEncodingHelper();
27 | /* initial state */
28 | this.state = {
29 | plistXmlString: plistConstants.plistXmlString,
30 | plist: this.parser.parsePlistXml(plistConstants.plistXmlString),
31 | xmlParseError: false,
32 | };
33 | alertify.logPosition('top right');
34 | }
35 |
36 | handlePlistParserChange(newPlistXmlString) {
37 | let plistJson = {};
38 | try {
39 | plistJson = this.parser.parsePlistXml(newPlistXmlString);
40 | this.setState({
41 | plistXmlString: newPlistXmlString,
42 | plist: plistJson,
43 | xmlParseError: false,
44 | });
45 | } catch (err) {
46 | this.setState({
47 | plistXmlString: newPlistXmlString,
48 | xmlParseError: true,
49 | });
50 | }
51 | }
52 |
53 | handlePlistFormChange(newPlistJsonObject) {
54 | const xml = this.parser.generatePlistXml(newPlistJsonObject);
55 | this.setState({
56 | plistXmlString: xml,
57 | plist: newPlistJsonObject,
58 | xmlParseError: false,
59 | });
60 | }
61 | getPlistFilename() {
62 | return `DisplayProductID-${this.encHelp.intToHex(this.state.plist.displayProductId)}.plist`;
63 | }
64 | getFullPlistFilename() {
65 | const filePrefix = '/System/Library/Displays/Contents/Resources/Overrides/DisplayVendorID';
66 | return `${filePrefix}-${this.encHelp.intToHex(
67 | this.state.plist.displayVendorId,
68 | )}/DisplayProductID-${this.encHelp.intToHex(this.state.plist.displayProductId)}`;
69 | }
70 | downloadPlistAsFile() {
71 | const textFileAsBlob = new Blob([this.state.plistXmlString], { type: 'text/plain' });
72 | const fileNameToSaveAs = this.getPlistFilename();
73 |
74 | const downloadLink = document.createElement('a');
75 | downloadLink.download = fileNameToSaveAs;
76 | downloadLink.innerHTML = 'Download File';
77 | if (window.webkitURL != null) {
78 | /* CHROME */
79 | downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob);
80 | } else {
81 | /* FIREFOX */
82 | downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
83 | downloadLink.onclick = (event) => document.body.removeChild(event.target);
84 | downloadLink.style.display = 'none';
85 | document.body.appendChild(downloadLink);
86 | }
87 | downloadLink.click();
88 | }
89 |
90 | render() {
91 | const xmlNoticeClassNames = classNames({
92 | [`${plistContainerStyles.xmlValid}`]: !this.state.xmlParseError,
93 | [`${plistContainerStyles.xmlInvalid}`]: this.state.xmlParseError,
94 | });
95 | let xmlNoticeMessage;
96 | if (this.state.xmlParseError) {
97 | xmlNoticeMessage = (
98 |
99 | xml invalid
100 |
101 | );
102 | } else {
103 | xmlNoticeMessage = (
104 |
105 | xml valid
106 |
107 | );
108 | }
109 | return (
110 | <>
111 |
112 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | Display PropertyList Filename
122 |
123 |
124 |
125 |
126 |
127 |
128 | {this.getFullPlistFilename()}
129 |
130 |
131 |
132 |
133 |
{xmlNoticeMessage}
134 |
138 |
139 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
How to use Scale Resolutions
171 |
172 |
173 | Follow the steps below to learn how to use Scale Resolutions. See the awesome{' '}
174 |
175 | tonymacx86.com forum
176 | {' '}
177 | for more details.
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | This tool is provided without warranty . codeclou is not
186 | responsible for damages to your system. It is heavily discouraged to edit
187 | files in /System/ directory. You are doing it at your own risk.
188 |
189 |
190 |
191 |
192 |
199 | Enable HiDPI Mode
200 |
201 |
Open your terminal and copy/paste the following command to enable HiDPI mode:
202 |
203 | sudo defaults write /Library/Preferences/com.apple.windowserver.plist
204 | DisplayResolutionEnabled -bool true
205 |
206 |
213 | Detect your Display
214 |
215 |
To detect your display copy/paste the following command into your terminal:
216 |
ioreg -lw0 | grep IODisplayPrefsKey
217 |
The output on a Mac should look like so:
218 |
219 | "IODisplayPrefsKey" =
220 | "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/IGPU@2/AppleIntelFramebuffer@0/display0/AppleBacklightDisplay-610-a019"
221 |
222 | "IODisplayPrefsKey" =
223 | "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/IGPU@2/AppleIntelFramebuffer@2/display0/AppleDisplay-10ac-d06e"
224 |
225 |
226 | All external monitors are identified by AppleDisplay and internal monitors
227 | by AppleBacklightDisplay . So if you want to set scale resolutions for your
228 | external monitor you will need to look at the second line. Identify at the end of
229 | the line your DisplayVendorId as{' '}
230 | {this.encHelp.intToHex(this.state.plist.displayVendorId)}
and your{' '}
231 | DisplayProductID as{' '}
232 | {this.encHelp.intToHex(this.state.plist.displayProductId)}
and note
233 | these values. Now insert them into the corresponding input fields on the top left
234 | and the generator will generate a basic plist file for you.
235 |
236 |
243 | Customize your Resolutions
244 |
245 |
246 | Now you can customize the Scale Resolutions . For example you could
247 | enter 2560x1440 and enable HiDPI . Be aware that
248 | the generator does not do any sanity checks for you, what you enter is just getting
249 | encoded. Please be aware that depending on your monitor and internal mac-checkups
250 | during startup some resolutions might not work.
251 |
252 |
259 | Download plist and copy to System folder
260 |
261 |
262 | Once you have configured all your resolutions click the download button. A file
263 | called DisplayProductID-*.plist
will download.
264 |
265 |
266 | Now copy this file to the destination that has been calculated on the top right
267 | under Display PropertyList Filename . Example for a DELL Monitor:
268 |
269 |
270 | {`sudo cp ~/Downloads/DisplayProductID-${this.encHelp.intToHex(
271 | this.state.plist.displayProductId,
272 | )}.plist `}
273 | {`/System/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-${this.encHelp.intToHex(
274 | this.state.plist.displayVendorId,
275 | )}/DisplayProductID-${this.encHelp.intToHex(this.state.plist.displayProductId)}`}
276 |
277 |
278 | NOTE: That you might need to{' '}
279 |
280 | disable System Integrity Protection
281 | {' '}
282 | (at your own risk) to copy files to /System/
. If you are running macOS
283 | Catalina or above, see the note below.
284 |
285 |
286 | NOTE: On macOS Catalina systems (or{' '}
287 | above ), the{' '}
288 | /System/
folder is mounted read-only, so instead you should copy the
289 | file to{' '}
290 |
291 | {`/Library/Displays/Contents/Resources/Overrides/DisplayVendorID-${this.encHelp.intToHex(
292 | this.state.plist.displayVendorId,
293 | )}/DisplayProductID-${this.encHelp.intToHex(this.state.plist.displayProductId)}`}
294 |
295 | . This can be done without disabling System Integrity Protection. If the file path
296 | does not exist, create it with{' '}
297 |
298 | {`sudo mkdir -p /Library/Displays/Contents/Resources/Overrides/DisplayVendorID-${this.encHelp.intToHex(
299 | this.state.plist.displayVendorId,
300 | )}`}
301 |
302 |
303 |
310 | Restart Mac and use Resolutions
311 |
312 |
313 | Now you can change your resolution to the ones you entered (if macOS did not disable
314 | them during boot check-ups)
315 |
316 |
317 | A very handy{' '}
318 | tool to enable resolutions is RDM
319 | .
320 |
321 | If you are more into commandline tools you might like{' '}
322 | screenresolutions
323 | .
324 |
325 | And if you are willing to pay a small amount{' '}
326 | SwitchResX is also a great tool.
327 |
328 |
329 |
330 |
331 |
332 | >
333 | );
334 | }
335 | }
336 |
--------------------------------------------------------------------------------
/src/common/components/PlistContainer.module.css:
--------------------------------------------------------------------------------
1 | .xmlValid,
2 | .xmlInvalid {
3 | font-weight: bold;
4 | color: #70c75a;
5 | padding: 8px;
6 | margin-top: 1.5rem;
7 | background-color: #f4f5f7;
8 | }
9 | .xmlInvalid {
10 | color: #fa493b;
11 | background-color: #f4f5f7;
12 | }
13 | .buttonBarFooter {
14 | margin-top: 15px;
15 | }
16 | .resetButton,
17 | .downloadButton {
18 | display: inline-block;
19 | color: #272c34;
20 | padding: 3px 8px;
21 | margin-left: 10px;
22 | cursor: pointer;
23 | background-color: #f4f5f7;
24 | }
25 | /*#61dafb*/
26 | .resetButton:hover,
27 | .downloadButton:hover {
28 | text-decoration: none;
29 | color: #272c34;
30 | background-color: #d6dbe4;
31 | }
32 | .downloadNote {
33 | color: #ccc;
34 | font-size: 0.8rem;
35 | }
36 | .resolutionSettingsHeading {
37 | margin-top: 20px;
38 | margin-bottom: 0rem;
39 | font-weight: 500;
40 | font-size: 1rem;
41 | color: #272c34;
42 | }
43 | .filename {
44 | -ms-word-break: break-all;
45 | word-break: break-all;
46 | color: #555;
47 | font-weight: 400;
48 | font-size: 0.9rem;
49 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
50 | padding-top: 5px;
51 | }
52 |
--------------------------------------------------------------------------------
/src/common/components/PlistForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import CharsetAndEncodingHelper from './CharsetAndEncodingHelper';
4 | import XmlParser from './XmlParser';
5 | import plistConstants from './PlistConstants';
6 | import plistStyles from './PlistForm.module.css';
7 | import classNames from 'classnames';
8 | import { IoMdTrash, IoMdAddCircle } from 'react-icons/io';
9 | import alertify from 'alertify.js';
10 | alertify.logPosition('top right');
11 |
12 | const encHelp = new CharsetAndEncodingHelper();
13 | const xmlParser = new XmlParser();
14 |
15 | const PlistForm = (props) => {
16 | const isValidHexString = (hexStringToCheck) => {
17 | if (hexStringToCheck.match(/^[a-f0-9]+$/)) {
18 | return true;
19 | }
20 | alertify.error('Please enter only HEX values [a-f0-9]+');
21 | return false;
22 | };
23 |
24 | const isValidDecimalString = (decimalStringToCheck) => {
25 | if (decimalStringToCheck.match(/^[0-9]+$/)) {
26 | return true;
27 | }
28 | alertify.error('Please enter only digits [0-9]+');
29 | return false;
30 | };
31 |
32 | const handleResolutionInputChange = (event) => {
33 | const index = event.target.getAttribute('data-index');
34 | const newState = Array.from(props.plist.scaleResolutions);
35 | const scaleResolution = encHelp.deepCopyOrReturnEmptyObject(newState[index]);
36 | if (event.target.name === 'decimal_width') {
37 | if (!isValidDecimalString(event.target.value)) {
38 | return;
39 | }
40 | scaleResolution.decimal.width = event.target.value;
41 | }
42 | if (event.target.name === 'decimal_height') {
43 | if (!isValidDecimalString(event.target.value)) {
44 | return;
45 | }
46 | scaleResolution.decimal.height = event.target.value;
47 | }
48 | if (event.target.name === 'decimal_hidpi') {
49 | scaleResolution.decimal.hidpi = event.target.checked;
50 | }
51 | try {
52 | const base64String = xmlParser.generateBase64String(scaleResolution.decimal);
53 | newState[index] = {
54 | base64String,
55 | decoded: xmlParser.generateDecodedScaleResolutionObject(
56 | scaleResolution.decimal,
57 | base64String,
58 | ),
59 | decimal: encHelp.deepCopyOrReturnEmptyObject(scaleResolution.decimal),
60 | };
61 | props.handleChange({
62 | ...props.plist,
63 | ...{ scaleResolutions: newState },
64 | });
65 | } catch (err) {
66 | alertify.error(`Invalid Input. ${err}`);
67 | }
68 | };
69 |
70 | const handleInputChange = (event) => {
71 | if (event.target.name === 'displayProductName') {
72 | props.handleChange({
73 | ...props.plist,
74 | ...{ displayProductName: event.target.value },
75 | });
76 | }
77 | if (event.target.name === 'displayProductIdHex') {
78 | if (isValidHexString(event.target.value)) {
79 | props.handleChange({
80 | ...props.plist,
81 | ...{ displayProductId: encHelp.hexToInt(event.target.value) },
82 | });
83 | }
84 | }
85 | if (event.target.name === 'displayVendorIdHex') {
86 | if (isValidHexString(event.target.value)) {
87 | props.handleChange({
88 | ...props.plist,
89 | ...{ displayVendorId: encHelp.hexToInt(event.target.value) },
90 | });
91 | }
92 | }
93 | };
94 |
95 | const addResolution = () => {
96 | const newState = Array.from(props.plist.scaleResolutions);
97 | /* deep clone default object without any shared references! */
98 | newState.push(encHelp.deepCopyOrReturnEmptyObject(plistConstants.defaultResolutionForAddNew));
99 | props.handleChange({
100 | ...props.plist,
101 | ...{ scaleResolutions: newState },
102 | });
103 | };
104 |
105 | const deleteResolution = (index) => {
106 | props.handleChange({
107 | ...props.plist,
108 | ...{ scaleResolutions: props.plist.scaleResolutions.filter((_, i) => i !== index) },
109 | });
110 | };
111 |
112 | return (
113 |
114 |
115 |
116 |
DisplayProductName
117 |
118 |
119 |
130 |
131 |
132 |
DisplayProductID
133 |
134 |
135 |
136 |
137 |
138 | (0x
139 |
149 | )16 = ({props.plist.displayProductId})10
150 |
151 |
152 |
153 |
154 |
155 |
DisplayVendorID
156 |
157 |
158 |
159 |
160 |
161 | (0x
162 |
172 | )16 = ({props.plist.displayVendorId})10
173 |
174 |
175 |
176 |
177 |
178 |
Scale Resolutions
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | HiDPI
189 |
190 |
191 | Resolution
192 |
193 |
196 | Remove
197 |
198 |
199 | {props.plist.scaleResolutions.map((scaleResolution, scaleResolutionIndex) => (
200 |
204 |
251 |
252 |
253 |
254 |
255 | <{scaleResolution.decoded.hexStringSpaced}>
256 |
257 |
258 |
259 |
260 | ))}
261 |
277 |
278 |
279 |
280 |
281 |
282 |
283 | );
284 | };
285 |
286 | PlistForm.propTypes = {
287 | plist: PropTypes.object.isRequired,
288 | handleChange: PropTypes.func.isRequired,
289 | };
290 | export default PlistForm;
291 |
--------------------------------------------------------------------------------
/src/common/components/PlistForm.module.css:
--------------------------------------------------------------------------------
1 | .filename {
2 | -ms-word-break: break-all;
3 | word-break: break-all;
4 | word-break: break-word;
5 | color: #555;
6 | font-weight: 400;
7 | font-size: 16px;
8 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
9 | }
10 | .field {
11 | width: 100%;
12 | padding: 3px;
13 | margin-top: 5px;
14 | border: 0px;
15 | color: #555;
16 | font-size: 0.9rem;
17 | background-color: #fff;
18 | }
19 |
20 | .hexField {
21 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
22 | width: 50px;
23 | border: 0px;
24 | color: #555;
25 | font-size: 0.9rem;
26 | background-color: #fff;
27 | }
28 |
29 | .hexColumn {
30 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
31 | color: #555;
32 | font-size: 0.9rem;
33 | margin-top: 5px;
34 | }
35 | .resolutionBox {
36 | background-color: #f4f5f7;
37 | padding: 0px 20px 20px 20px;
38 | }
39 | .resolutionSettingsHeading {
40 | margin-top: 20px;
41 | margin-bottom: 0rem;
42 | font-weight: 500;
43 | font-size: 1rem;
44 | color: #272c34;
45 | }
46 | .resolutionListHeading {
47 | text-transform: uppercase;
48 | margin-top: 5px;
49 | font-size: 0.7rem;
50 | margin-bottom: 5px;
51 | font-weight: 600;
52 | color: #999;
53 | }
54 | .resolutionListTd {
55 | padding: 0 !important;
56 | }
57 | .resolutionList {
58 | list-style-type: none;
59 | padding: 0;
60 | margin: 0;
61 | }
62 | .resolutionField {
63 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
64 | width: 73px;
65 | border: 0px;
66 | color: #555;
67 | font-size: 0.9rem;
68 | background-color: #fff;
69 | }
70 | .resolutionButton {
71 | display: inline-block;
72 | color: #555 !important;
73 | padding: 0px 6px;
74 | cursor: pointer;
75 | }
76 | .resolutionButtonAdd {
77 | display: block;
78 | font-size: 0.9rem;
79 | background-color: #fff;
80 | color: #555 !important;
81 | padding: 4px 8px 4px 8px;
82 | cursor: pointer;
83 | }
84 | .resolutionButtonAdd:hover {
85 | text-decoration: none;
86 | color: #fff;
87 | background-color: #d6dbe4;
88 | }
89 | .resolutionButtonRemove {
90 | margin-left: 0px;
91 | }
92 | .resolutionButton:hover {
93 | text-decoration: none;
94 | color: #fff;
95 | background-color: #d6dbe4;
96 | }
97 | .hexCode {
98 | display: inline-block;
99 | color: #555;
100 | font-size: 0.8rem;
101 | border: none;
102 | padding: 0px;
103 | margin-bottom: 10px;
104 | }
105 |
--------------------------------------------------------------------------------
/src/common/components/PlistParser.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import alertify from 'alertify.js';
3 | import PropTypes from 'prop-types';
4 | import { Controlled as CodeMirror } from 'react-codemirror2';
5 |
6 | alertify.logPosition('top right');
7 |
8 | const PlistParser = ({ plistXmlString, updatePlistXmlString }) => {
9 | return (
10 |
11 | {
19 | updatePlistXmlString(value);
20 | }}
21 | onChange={(editor, data, value) => {}}
22 | />
23 |
24 | );
25 | };
26 | PlistParser.propTypes = {
27 | plistXmlString: PropTypes.string.isRequired,
28 | updatePlistXmlString: PropTypes.func.isRequired,
29 | };
30 |
31 | export default PlistParser;
32 |
--------------------------------------------------------------------------------
/src/common/components/XmlParser.js:
--------------------------------------------------------------------------------
1 | import XmlParserError from "./XmlParserError";
2 | import plistConstants from "./PlistConstants";
3 | import CharsetAndEncodingHelper from "./CharsetAndEncodingHelper";
4 |
5 | export default class XmlParser {
6 | constructor() {
7 | this.encHelp = new CharsetAndEncodingHelper();
8 | if (typeof window.DOMParser !== "undefined") {
9 | this.parseXmlIntern = xmlStr =>
10 | new window.DOMParser().parseFromString(xmlStr, "text/xml");
11 | } else if (
12 | typeof window.ActiveXObject !== "undefined" &&
13 | new window.ActiveXObject("Microsoft.XMLDOM")
14 | ) {
15 | this.parseXmlIntern = xmlStr => {
16 | const xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
17 | xmlDoc.async = "false";
18 | xmlDoc.loadXML(xmlStr);
19 | return xmlDoc;
20 | };
21 | } else {
22 | throw new XmlParserError("No XML parser found");
23 | }
24 | }
25 |
26 | parseXml(xmlString) {
27 | return this.parseXmlIntern(xmlString);
28 | }
29 |
30 | generateBase64String(decimalScaleResolutionObject) {
31 | const hexString = [];
32 | hexString.push(
33 | this.encHelp.intToHexPrefixedWithZerosTo8Bit(
34 | decimalScaleResolutionObject.width
35 | )
36 | );
37 | hexString.push(
38 | this.encHelp.intToHexPrefixedWithZerosTo8Bit(
39 | decimalScaleResolutionObject.height
40 | )
41 | );
42 | if (decimalScaleResolutionObject.hidpi === true) {
43 | hexString.push(this.encHelp.intToHexPrefixedWithZerosTo8Bit(1));
44 | hexString.push(this.encHelp.intToHexPrefixedWithZerosTo8Bit(2097152));
45 | }
46 | const joinedHexString = hexString.join("");
47 | return this.encHelp.hexToBase64(joinedHexString);
48 | }
49 |
50 | generateDecodedScaleResolutionObject(
51 | decimalScaleResolutionObject,
52 | base64String
53 | ) {
54 | const decodedObject = {};
55 | if (decimalScaleResolutionObject.hidpi === true) {
56 | decodedObject.decimalStringSpaced = `${
57 | decimalScaleResolutionObject.width
58 | } ${decimalScaleResolutionObject.height} ${
59 | decimalScaleResolutionObject.hidpi
60 | }`;
61 | } else {
62 | decodedObject.decimalStringSpaced = `${
63 | decimalScaleResolutionObject.width
64 | } ${decimalScaleResolutionObject.height}`;
65 | }
66 | decodedObject.hexStringSpaced = this.encHelp.base64ToHexStringSpaced(
67 | base64String
68 | );
69 | return decodedObject;
70 | }
71 |
72 | generateDecimalScaleResolutionObject(base64String) {
73 | const decimalStringSpaced = this.encHelp.base64ToDecimalStringSpaced(
74 | base64String
75 | );
76 | const decimalItems = decimalStringSpaced.split(" ");
77 | const resolution = {
78 | width: decimalItems[0],
79 | height: decimalItems[1],
80 | hidpi: false
81 | };
82 | if (decimalItems.length > 2) {
83 | if (Number(decimalItems[2]) === 1) {
84 | resolution.hidpi = true;
85 | }
86 | }
87 | return resolution;
88 | }
89 |
90 | /**
91 | * reverse version of parsePlistXml
92 | * @param plistObject
93 | */
94 | generatePlistXml(plistObject) {
95 | const xml = [];
96 | xml.push(plistConstants.plistXmlHeader);
97 | xml.push("");
98 | xml.push(" DisplayProductName ");
99 | xml.push(` ${plistObject.displayProductName} `);
100 | xml.push(" DisplayProductID ");
101 | xml.push(` ${plistObject.displayProductId} `);
102 | xml.push(" DisplayVendorID ");
103 | xml.push(` ${plistObject.displayVendorId} `);
104 | xml.push(" scale-resolutions ");
105 | xml.push(" ");
106 | plistObject.scaleResolutions.forEach(scaleResolution => {
107 | xml.push(` ${scaleResolution.base64String} `);
108 | //
109 | });
110 | xml.push(" ");
111 | xml.push(" ");
112 | xml.push(plistConstants.plistXmlFooter);
113 | return xml.join("\n");
114 | }
115 |
116 | parsePlistXml(xmlString) {
117 | let xml = {};
118 | try {
119 | xml = this.parseXml(xmlString);
120 | } catch (err) {
121 | throw new XmlParserError("invalid xml 001");
122 | }
123 |
124 | if (xml.getElementsByTagName("parsererror").length > 0) {
125 | /* xml.getElementsByTagName("parsererror")[0] */
126 | throw new XmlParserError("invalid xml 002");
127 | }
128 |
129 | const plistJson = {
130 | displayProductName: "",
131 | displayProductId: "",
132 | displayVendorId: "",
133 | scaleResolutions: []
134 | };
135 |
136 | try {
137 | const children = xml.documentElement.childNodes;
138 | for (let i = 0; i < children.length; i++) {
139 | if (children[i].nodeName === "dict") {
140 | const dictChildren = children[i].childNodes;
141 | for (let j = 0; j < dictChildren.length; j++) {
142 | /* nextSibling */
143 | const dictChildNodeName = dictChildren[j].nodeName;
144 | let dictChildNodeValue = "";
145 | if (dictChildren[j].childNodes[0] !== undefined) {
146 | dictChildNodeValue = dictChildren[j].childNodes[0].nodeValue;
147 | }
148 | if (
149 | dictChildNodeName === "key" &&
150 | dictChildNodeValue === "DisplayProductName"
151 | ) {
152 | plistJson.displayProductName =
153 | dictChildren[j + 2].childNodes[0].nodeValue;
154 | }
155 | if (
156 | dictChildNodeName === "key" &&
157 | dictChildNodeValue === "DisplayProductID"
158 | ) {
159 | plistJson.displayProductId =
160 | dictChildren[j + 2].childNodes[0].nodeValue;
161 | }
162 | if (
163 | dictChildNodeName === "key" &&
164 | dictChildNodeValue === "DisplayVendorID"
165 | ) {
166 | plistJson.displayVendorId =
167 | dictChildren[j + 2].childNodes[0].nodeValue;
168 | }
169 | if (
170 | dictChildNodeName === "key" &&
171 | dictChildNodeValue === "scale-resolutions"
172 | ) {
173 | const scaleResolutionsChildren = dictChildren[j + 2].childNodes;
174 | for (let l = 0; l < scaleResolutionsChildren.length; l++) {
175 | if (scaleResolutionsChildren[l].nodeName === "data") {
176 | const scaleResolution = {};
177 | scaleResolution.base64String =
178 | scaleResolutionsChildren[l].childNodes[0].nodeValue;
179 | scaleResolution.decimal = this.generateDecimalScaleResolutionObject(
180 | scaleResolution.base64String
181 | );
182 | scaleResolution.decoded = this.generateDecodedScaleResolutionObject(
183 | scaleResolution.decimal,
184 | scaleResolution.base64String
185 | );
186 | plistJson.scaleResolutions.push(scaleResolution);
187 | }
188 | }
189 | }
190 | }
191 | }
192 | }
193 | } catch (err) {
194 | throw new XmlParserError("invalid xml 003");
195 | }
196 | return plistJson;
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/common/components/XmlParserError.js:
--------------------------------------------------------------------------------
1 | export default class XmlParserError extends Error {
2 | constructor(message) {
3 | super();
4 | this.message = message;
5 | this.stack = (new Error()).stack;
6 | this.name = this.constructor.name;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/common/components/img/comsysto-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codeclou/Display-Override-PropertyList-File-Parser-and-Generator-with-HiDPI-Support-For-Scaled-Resolutions/85a05e03de9a3fde197c980d2f30e4e5309a9ff7/src/common/components/img/comsysto-logo.png
--------------------------------------------------------------------------------
/src/common/components/img/logo-scaled-resolutions.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | /* ReactCodeMirror overrides */
2 | .ReactCodeMirror {
3 | border: 1px solid #efefef;
4 | }
5 | .CodeMirror {
6 | height: 100% !important;
7 | }
8 | .CodeMirror-linenumbers {
9 | background-color: #f4f5f7;
10 | }
11 | .cm-tag,
12 | .cm-bracket {
13 | color: #4eadc7 !important;
14 | }
15 | .cm-attribute {
16 | color: #764abc !important;
17 | }
18 | .cm-string {
19 | color: #c72c84 !important;
20 | }
21 | .cm-error {
22 | color: #dd4a68 !important;
23 | }
24 | .cm-tag .cm-bracket .cm-error {
25 | color: #dd4a68 !important;
26 | }
27 | /* bootstrap overrides */
28 | p {
29 | color: #555;
30 | margin-bottom: 0.5rem;
31 | }
32 | code {
33 | color: #555;
34 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
35 | background-color: #f4f5f7;
36 | padding: 3px 8px;
37 | border-radius: 2px;
38 | border: 1px solid rgba(0, 0, 0, 0.05);
39 | word-break: break-word;
40 | }
41 |
42 | /* cs Styles */
43 | body {
44 | background-color: #fff;
45 | }
46 | .cs-header {
47 | background-color: #272c34;
48 | color: #fff;
49 | }
50 | .cs-header-inner {
51 | max-width: 1400px;
52 | margin: 0 auto;
53 | padding: 100px 0px;
54 | }
55 | .cs-footer-inner {
56 | max-width: 1400px;
57 | margin: 0 auto;
58 | padding: 50px 0px;
59 | }
60 | .cs-header__logo {
61 | width: 100%;
62 | margin-bottom: 20px;
63 | text-align: center;
64 | }
65 | @media (min-width: 768px) {
66 | .cs-header__h1 {
67 | text-align: left !important;
68 | }
69 | .cs-header__h2 {
70 | text-align: left !important;
71 | }
72 | .cs-header__h4 {
73 | text-align: left !important;
74 | }
75 | .cs-header__star {
76 | text-align: left !important;
77 | }
78 | }
79 |
80 | .cs-header__h1 {
81 | font-family: Roboto, sans-serif;
82 | text-transform: uppercase;
83 | font-weight: 600;
84 | margin-bottom: 0;
85 | margin-top: 0;
86 | color: #61dafb;
87 | text-align: center;
88 | }
89 | .cs-header__h2 {
90 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
91 | 'Helvetica Neue', sans-serif;
92 | font-weight: 300;
93 | text-align: center;
94 | }
95 | .cs-header__h4 {
96 | margin-top: 10px;
97 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
98 | 'Helvetica Neue', sans-serif;
99 | font-weight: 300;
100 | font-size: 1.3rem;
101 | text-align: center;
102 | }
103 | .cs-header__star {
104 | margin-top: 25px;
105 | text-align: center;
106 | }
107 | .cs-header__starlink {
108 | color: #fff;
109 | border: 1px solid #fff;
110 | padding: 10px;
111 | }
112 | .cs-header__starlink:hover {
113 | color: #272c34;
114 | text-decoration: none;
115 | background-color: #61dafb;
116 | border: 1px solid #61dafb;
117 | }
118 | .cs-body {
119 | background-color: #fff;
120 | max-width: 1400px;
121 | margin: 0 auto;
122 | }
123 | .cs-bodyHowTo {
124 | padding: 0px 0px 100px 0px;
125 | background: linear-gradient(#f8f8f8, #fff);
126 | max-width: 1400px;
127 | margin: 0 auto;
128 | }
129 | .cs-footer {
130 | background-color: #272c34;
131 | color: #fff;
132 | }
133 | .cs-footer a {
134 | color: #fff;
135 | }
136 | .cs-footer a:hover {
137 | color: #efefef;
138 | text-decoration: underline;
139 | }
140 | .cs-footer__privacy {
141 | padding: 5px 30px 0px 30px;
142 | font-size: 13px;
143 | line-height: 16px;
144 | text-align: justify;
145 | display: inline-block;
146 | }
147 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // ///////////////////////////////////////////
2 | // Libraries
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import alertify from 'alertify.js';
6 |
7 | // ///////////////////////////////////////////
8 | // Global CSS
9 | import 'bootstrap/dist/css/bootstrap.css';
10 | import 'codemirror/lib/codemirror.css';
11 | import 'codemirror/theme/material.css';
12 | import './index.css';
13 |
14 | // ///////////////////////////////////////////
15 | // App
16 | import App from './common/components/App';
17 | import * as serviceWorker from './serviceWorker';
18 |
19 | alertify.logPosition('top right');
20 | // ///////////////////////////////////////////
21 | // Render the router
22 | ReactDOM.render( , document.getElementById('root'));
23 |
24 | // If you want your app to work offline and load faster, you can change
25 | // unregister() to register() below. Note this comes with some pitfalls.
26 | // Learn more about service workers: https://bit.ly/CRA-PWA
27 | serviceWorker.unregister();
28 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------