├── .eslintrc.cjs ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky └── commit-msg ├── .npmrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── __tests__ ├── chrome.ts └── setup.ts ├── commitlint.config.js ├── illustration ├── 1x │ ├── 1400x560.png │ ├── 440x280.png │ ├── 920x680.png │ ├── GitHub.png │ ├── logo-text.png │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ └── screenshot-4.png ├── cat.png ├── leetcode-night.ai ├── logo.ai ├── logo.png └── text.ai ├── manifest.config.ts ├── package.json ├── pnpm-lock.yaml ├── public └── _locales │ ├── en │ └── messages.json │ ├── ja │ └── messages.json │ ├── ko │ └── messages.json │ ├── zh_CN │ └── messages.json │ └── zh_TW │ └── messages.json ├── scripts ├── PackageJsonReaderWriter.js ├── bump.js ├── openReleasePage.js └── removeUnnecessaryAssets.js ├── src ├── Globals.d.ts ├── assets │ └── img │ │ ├── icon-128.png │ │ ├── logo-text.png │ │ └── logo.png ├── constants.ts ├── options.ts ├── pages │ ├── Content │ │ ├── InsertDislikeCount.ts │ │ ├── InsertYoutubeLink.ts │ │ ├── StyleInjector.ts │ │ ├── darkSide2023.ts │ │ ├── dom.ts │ │ ├── fetchLikesAndDislikes.ts │ │ ├── index.ts │ │ ├── leetcode-version.ts │ │ ├── resetCode.ts │ │ ├── resetCode2023.ts │ │ ├── selector.ts │ │ ├── slug.ts │ │ ├── styles │ │ │ ├── dark-theme │ │ │ │ ├── _ant-modal.sass │ │ │ │ ├── _ant-select-dropdown.sass │ │ │ │ ├── _ant-select.sass │ │ │ │ ├── _colors.sass │ │ │ │ ├── _description.sass │ │ │ │ ├── _editor.sass │ │ │ │ ├── _error-container.sass │ │ │ │ ├── _loading.sass │ │ │ │ ├── _logo.sass │ │ │ │ ├── _navbar.sass │ │ │ │ ├── _question-fast-picker.sass │ │ │ │ ├── _solution.sass │ │ │ │ ├── _submission.sass │ │ │ │ ├── _submit-table.sass │ │ │ │ └── style.module.sass │ │ │ ├── hide-logo-2023 │ │ │ │ └── style.module.sass │ │ │ ├── hide-logo │ │ │ │ └── style.module.sass │ │ │ ├── icon.png │ │ │ ├── invert-image-2023 │ │ │ │ └── style.module.sass │ │ │ ├── invert-image │ │ │ │ └── style.module.sass │ │ │ └── loading-mascot │ │ │ │ ├── _cat.sass │ │ │ │ └── style.module.sass │ │ ├── toggleEnabled.ts │ │ ├── toggleEnabledMascot.ts │ │ ├── toggleHideLogo.ts │ │ ├── toggleHideLogo2023.ts │ │ ├── toggleInvertImageColor.ts │ │ └── toggleInvertImageColor2023.ts │ └── Popup │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ ├── BlockLink.tsx │ │ ├── Chip.tsx │ │ ├── DailyChallengeQuestionCard.tsx │ │ ├── ExtraFeatureOptions.test.tsx │ │ ├── ExtraFeatureOptions.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── HighlightText.tsx │ │ ├── KofiLink.tsx │ │ ├── LanguageSelect.test.tsx │ │ ├── LanguageSelect.tsx │ │ ├── Layout.tsx │ │ ├── LeetcodeVersionBadge.tsx │ │ ├── LeetcodeVersionDescription.test.tsx │ │ ├── LeetcodeVersionDescription.tsx │ │ ├── Link.tsx │ │ ├── Logo.tsx │ │ ├── NoResult.tsx │ │ ├── OptionSwitch.tsx │ │ ├── OptionTitle.tsx │ │ ├── Options.test.tsx │ │ ├── Options.tsx │ │ ├── PremiumBadge.tsx │ │ ├── QuestionCard.tsx │ │ ├── QuestionDateIcon.tsx │ │ ├── QuestionDifficultyText.tsx │ │ ├── QuestionKeywordInput.tsx │ │ ├── QuestionMetaChips.tsx │ │ ├── QuestionStatusIcon.tsx │ │ ├── SearchQuestion.test.tsx │ │ ├── SearchQuestion.tsx │ │ ├── Spacer.tsx │ │ ├── StyleOptions.test.tsx │ │ ├── StyleOptions.tsx │ │ ├── Switch.tsx │ │ └── Tabs.tsx │ │ ├── hooks │ │ ├── useChromeStorage.test.ts │ │ ├── useChromeStorage.ts │ │ ├── useChromeStorageListener.test.ts │ │ ├── useChromeStorageListener.ts │ │ ├── useDailyChallengeQuestion.ts │ │ ├── useEnableDarkTheme.ts │ │ ├── useLeetCodeVersion.ts │ │ ├── useQuestions.ts │ │ ├── useSelectedIndex.test.ts │ │ └── useSelectedIndex.ts │ │ ├── i18n.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── langs │ │ ├── en.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── zh-Hans.json │ │ └── zh-Hant.json │ │ ├── modules │ │ ├── apis.ts │ │ └── themes.ts │ │ ├── popup.html │ │ └── types │ │ ├── DailyChallengeQuestion.ts │ │ ├── Nullish.ts │ │ └── Question.ts ├── storage.ts └── tests │ ├── e2e │ ├── cookie.json.example │ ├── dailyChallenge.test.ts │ ├── darkTheme2023.test.ts │ ├── darkTheme2023DynamicLayout.test.ts │ ├── footer.test.ts │ ├── insertDislikeCount2023DynamicLayout.test.ts │ ├── insertYoutubeLink2023.test.ts │ ├── insertYoutubeLink2023DynamicLayout.test.ts │ ├── resetCode2023.test.ts │ └── resetCode2023DynamicLayout.test.ts │ └── helpers │ ├── mockDailyChallengeQuestion.ts │ ├── mockQuestions.ts │ ├── puppeteer.ts │ ├── sleep.ts │ ├── useOptions.ts │ ├── usePagination2023.ts │ ├── useResetCode.ts │ ├── useYoutubeLink.ts │ └── waitUntil.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.mts └── vitest.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'standard-with-typescript', 8 | 'plugin:react/recommended', 9 | 'plugin:react/jsx-runtime', 10 | ], 11 | overrides: [ 12 | { 13 | files: ['**/?(*.)+(spec|test).[jt]s?(x)'], 14 | extends: ['plugin:testing-library/react'], 15 | }, 16 | ], 17 | parser: '@typescript-eslint/parser', 18 | parserOptions: { 19 | ecmaFeatures: { jsx: true }, 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | }, 24 | plugins: [ 25 | 'react', 26 | '@typescript-eslint', 27 | 'react-hooks', 28 | ], 29 | settings: { 30 | react: { 31 | version: 'detect', 32 | }, 33 | }, 34 | ignorePatterns: ['dist'], 35 | rules: { 36 | '@typescript-eslint/comma-dangle': ['error', { 37 | arrays: 'always-multiline', 38 | objects: 'always-multiline', 39 | imports: 'always-multiline', 40 | exports: 'always-multiline', 41 | functions: 'only-multiline', 42 | generics: 'ignore', 43 | }], 44 | '@typescript-eslint/member-delimiter-style': ['error', { 45 | multiline: { 46 | delimiter: 'comma', 47 | requireLast: true, 48 | }, 49 | singleline: { 50 | delimiter: 'comma', 51 | requireLast: false, 52 | }, 53 | multilineDetection: 'brackets', 54 | }], 55 | '@typescript-eslint/explicit-function-return-type': ['off'], 56 | '@typescript-eslint/promise-function-async': ['off'], 57 | '@typescript-eslint/no-floating-promises': ['off'], 58 | '@typescript-eslint/strict-boolean-expressions': ['off'], 59 | '@typescript-eslint/no-misused-promises': ['error', { 60 | checksVoidReturn: false, 61 | }], 62 | '@typescript-eslint/no-confusing-void-expression': ['off'], 63 | '@typescript-eslint/no-extraneous-class': ['off'], 64 | '@typescript-eslint/no-dynamic-delete': ['off'], 65 | 66 | 'react-hooks/rules-of-hooks': 'error', 67 | 'react-hooks/exhaustive-deps': 'warn', 68 | 69 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 70 | 'no-process-env': ['error'], 71 | 72 | 'react/jsx-indent': ['warn', 2, { 73 | checkAttributes: true, 74 | indentLogicalExpressions: true, 75 | }], 76 | 'react/prop-types': ['off'], 77 | 'react/jsx-indent-props': ['warn', 2], 78 | 'react/jsx-closing-bracket-location': ['warn', 'tag-aligned'], 79 | 'jsx-quotes': ['warn', 'prefer-double'], 80 | 'react/jsx-curly-spacing': ['warn', { when: 'never', children: true }], 81 | 'react/jsx-tag-spacing': ['warn', { 82 | closingSlash: 'never', 83 | beforeSelfClosing: 'always', 84 | afterOpening: 'never', 85 | beforeClosing: 'never', 86 | }], 87 | 'react/self-closing-comp': 'warn', 88 | 'react/jsx-fragments': 'warn', 89 | 'react/jsx-equals-spacing': [2, 'never'], 90 | 'testing-library/no-node-access': ['error', { allowContainerFirstChild: true }], 91 | }, 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | lint-and-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@master 18 | 19 | - uses: pnpm/action-setup@v4.0.0 20 | with: 21 | version: 9.6.0 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '20' 26 | 27 | - name: Install Dependencies 28 | run: pnpm i 29 | 30 | - name: Run Lints 31 | run: pnpm run lint 32 | 33 | - name: Build 34 | run: pnpm run build 35 | 36 | - name: Test 37 | run: pnpm run test:unit 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@master 14 | 15 | - uses: pnpm/action-setup@v4.0.0 16 | with: 17 | version: 9.6.0 18 | 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: '20' 22 | 23 | - name: Install Dependencies 24 | run: pnpm i 25 | 26 | - name: Run Lints 27 | run: pnpm run lint 28 | 29 | - name: Build 30 | run: pnpm run build 31 | 32 | - name: Test 33 | run: pnpm run test:unit 34 | 35 | - name: Compress Dist Folder 36 | run: zip -r build.zip dist 37 | 38 | - name: Get Tag Name 39 | run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 40 | 41 | - name: Create Release 42 | id: create_release 43 | uses: ncipollo/release-action@v1.10.0 44 | with: 45 | tag: ${{ github.ref }} 46 | name: ${{ env.TAG }} 47 | draft: true 48 | omitDraftDuringUpdate: true 49 | artifacts: build.zip 50 | -------------------------------------------------------------------------------- /.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 | /build 11 | dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .history 20 | 21 | # secrets 22 | secrets.*.js 23 | 24 | .vscode/settings.json 25 | src/tests/e2e/cookie.json 26 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | PATH="/usr/local/bin:$PATH" 5 | 6 | npx --no -- commitlint --edit "${1}" 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "streetsidesoftware.code-spell-checker", 5 | "lokalise.i18n-ally" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Michael Xieyang Liu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | A browser extension that enables dark mode & extra features on LeetCode. 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

23 | 24 | # LeetCode Night 25 | 26 | ## Features 27 | 28 | - Dark theme for question page 29 | - Shortcut to Daily LeetCoding challenge 30 | - Quick navigation by question number or keyword 31 | - Automatically reset to default code definition 32 | - Show YouTube link shortcut 33 | - Show Dislike Count 34 | - Invert image color 35 | - Hide logo 36 | 37 | ## Installation 38 | 39 | ### Download from Chrome Web Store 40 | 41 | 42 | 43 | 44 | 45 | ### Manual Download 46 | 47 | Download latest `build.zip` from [releases](https://github.com/ngseke/leetcode-night/releases) and unzip. 48 | 49 |
50 | Installation Steps 51 | 52 | 1. Access [chrome://extensions/](chrome://extensions/) 53 | 2. Check `Developer mode` 54 | 3. Click on `Load unpacked extension` 55 | 4. Select the extracted folder for use 56 |
57 | 58 | 59 | ## Screenshot 60 | 61 | Dark Theme on LeetCode 62 | Daily Challenge Shortcut 63 | Question Navigation with Instant Search And More Extra Features 64 | Question Navigation with Instant Search And More Extra Features 65 | 66 | --- 67 | 68 | ## 安裝 69 | 70 | ### 從 Chrome 線上應用程式商店下載 71 | 72 | 73 | 74 | 75 | 76 | ### 手動下載 77 | 78 | 從 [releases](https://github.com/ngseke/leetcode-night/releases) 下載最新版 `build.zip` 並解壓縮。 79 | 80 |
81 | 安裝步驟 82 | 83 | 1. 進入 [chrome://extensions/](chrome://extensions/) 84 | 2. 勾選右上角的 `開發人員模式` 85 | 3. 點擊左上角的 `載入未封裝項目` 86 | 4. 選擇已解壓縮的 `build/` 目錄 87 |
88 | -------------------------------------------------------------------------------- /__tests__/chrome.ts: -------------------------------------------------------------------------------- 1 | export function mockChrome () { 2 | const mockedChrome = { 3 | storage: { 4 | sync: { 5 | get: vi.fn().mockResolvedValue({}), 6 | set: vi.fn(), 7 | }, 8 | local: { 9 | get: vi.fn().mockResolvedValue({}), 10 | set: vi.fn(), 11 | }, 12 | onChanged: { 13 | addListener: vi.fn(), 14 | removeListener: vi.fn(), 15 | }, 16 | }, 17 | runtime: { 18 | getURL: vi.fn().mockReturnValue(''), 19 | }, 20 | } 21 | vi.stubGlobal('chrome', mockedChrome) 22 | return mockedChrome 23 | } 24 | 25 | beforeEach(() => { 26 | // To ensure that Chrome is mocked in every test 27 | vi.stubGlobal('chrome', undefined) 28 | }) 29 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import '../src/pages/Popup/i18n' 3 | 4 | vi.spyOn(console, 'warn') 5 | vi.spyOn(console, 'error') 6 | 7 | Element.prototype.scrollIntoView = vi.fn() 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /illustration/1x/1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/1400x560.png -------------------------------------------------------------------------------- /illustration/1x/440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/440x280.png -------------------------------------------------------------------------------- /illustration/1x/920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/920x680.png -------------------------------------------------------------------------------- /illustration/1x/GitHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/GitHub.png -------------------------------------------------------------------------------- /illustration/1x/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/logo-text.png -------------------------------------------------------------------------------- /illustration/1x/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/screenshot-1.png -------------------------------------------------------------------------------- /illustration/1x/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/screenshot-2.png -------------------------------------------------------------------------------- /illustration/1x/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/screenshot-3.png -------------------------------------------------------------------------------- /illustration/1x/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/1x/screenshot-4.png -------------------------------------------------------------------------------- /illustration/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/cat.png -------------------------------------------------------------------------------- /illustration/leetcode-night.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/leetcode-night.ai -------------------------------------------------------------------------------- /illustration/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/logo.ai -------------------------------------------------------------------------------- /illustration/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/logo.png -------------------------------------------------------------------------------- /illustration/text.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/illustration/text.ai -------------------------------------------------------------------------------- /manifest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineManifest } from '@crxjs/vite-plugin' 2 | import packageJson from './package.json' 3 | 4 | const { version } = packageJson 5 | 6 | const key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsPcJJyNn/5+SF3R/k2Tns8YYq+tMEBfezf6mjNGfTdV2ukGopBpQRvwrNgoLX6Oaug8r7zpNyeAvDXtKBUC9qr4kxEwNiMU2tuIsV0PZg1H8FzKnQL6msP8yVlZJhKl9Olv3tK7MTTByaR7upP8iQZNBjPest4zRGxkJfOfJRSJpPuwWi1XC2tLeSo6lNz19l5kxfJBxtTHS9Y9P48o2A+9OMZtR64sR2eYxE/E6NHC/vQ4507Ao6x0dhO4eKl9KyV1u8b5ZQjavfVz2zYeF/yQw6VdzysR3W5Yd3BOTDvaDqMdSW+CtrZwTF4lNC+zJzvSIfKMQ4Ow/+Thg2XUe8wIDAQAB' 7 | 8 | export default defineManifest(async () => ({ 9 | manifest_version: 3, 10 | version, 11 | key, 12 | 13 | default_locale: 'en', 14 | name: '__MSG_appName__', 15 | description: '__MSG_appDesc__', 16 | 17 | icons: { 18 | 128: 'src/assets/img/icon-128.png', 19 | }, 20 | action: { 21 | default_popup: 'src/pages/Popup/popup.html', 22 | default_icon: 'src/assets/img/icon-128.png', 23 | }, 24 | content_scripts: [ 25 | { 26 | matches: ['https://leetcode.com/problems/*'], 27 | js: ['src/pages/Content/index.ts'], 28 | run_at: 'document_start', 29 | }, 30 | ], 31 | permissions: ['storage'], 32 | })) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leetcode-night", 3 | "version": "0.6.4", 4 | "description": "A browser extension that enables dark mode & extra features on LeetCode", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ngseke/leetcode-night" 9 | }, 10 | "engines": { 11 | "node": ">=18", 12 | "pnpm": ">=8", 13 | "npm": "please-use-pnpm", 14 | "yarn": "please-use-pnpm" 15 | }, 16 | "scripts": { 17 | "dev": "vite", 18 | "build": "vite build && node scripts/removeUnnecessaryAssets.js", 19 | "lint": "tsc --noEmit && eslint . --ext .ts,.tsx,.js,.cjs,.jsx", 20 | "lint:fix": "eslint --fix . --ext .ts,.tsx,.js,.cjs,.jsx", 21 | "bump": "npm run lint && node ./scripts/bump.js", 22 | "test": "vitest", 23 | "test:unit": "vitest --exclude=src/tests/e2e", 24 | "test:e2e": "vitest --poolOptions.threads.singleThread=true src/tests/e2e", 25 | "prepare": "husky install", 26 | "preversion": "npm run lint" 27 | }, 28 | "dependencies": { 29 | "@types/styled-components": "^5.1.23", 30 | "axios": "^0.26.1", 31 | "clsx": "^1.1.1", 32 | "debounce": "^2.0.0", 33 | "fuzzysort": "^2.0.4", 34 | "graphql": "^16.3.0", 35 | "graphql-request": "^4.2.0", 36 | "i18next": "^21.6.16", 37 | "i18next-browser-languagedetector": "^6.1.4", 38 | "lodash-es": "^4.17.21", 39 | "react": "^18.2.0", 40 | "react-dom": "^18.2.0", 41 | "react-i18next": "^11.16.7", 42 | "react-use-storage-state": "^1.0.3", 43 | "styled-components": "^6.1.8", 44 | "tocas": "^4.0.2", 45 | "url-join": "^5.0.0" 46 | }, 47 | "devDependencies": { 48 | "@commitlint/cli": "^17.0.2", 49 | "@commitlint/config-conventional": "^17.0.2", 50 | "@crxjs/vite-plugin": "2.0.0-beta.26", 51 | "@testing-library/jest-dom": "^6.4.1", 52 | "@testing-library/react": "^14.2.1", 53 | "@testing-library/user-event": "^14.5.2", 54 | "@types/chrome": "^0.0.177", 55 | "@types/fs-extra": "^11.0.4", 56 | "@types/lodash-es": "^4.17.12", 57 | "@types/react": "^18.2.55", 58 | "@types/react-dom": "^18.2.18", 59 | "@typescript-eslint/eslint-plugin": "^6.21.0", 60 | "@typescript-eslint/parser": "6.21.0", 61 | "@vitejs/plugin-react": "^4.2.0", 62 | "eslint": "^8.8.0", 63 | "eslint-config-react-app": "^7.0.0", 64 | "eslint-config-standard-with-typescript": "^40.0.0", 65 | "eslint-plugin-flowtype": "^8.0.3", 66 | "eslint-plugin-import": "^2.25.4", 67 | "eslint-plugin-n": "^15.0.0", 68 | "eslint-plugin-promise": "^6.0.0", 69 | "eslint-plugin-react": "^7.28.0", 70 | "eslint-plugin-react-hooks": "^4.3.0", 71 | "eslint-plugin-testing-library": "^6.2.0", 72 | "fs-extra": "^10.0.0", 73 | "husky": "^8.0.1", 74 | "inquirer": "^8.2.4", 75 | "jsdom": "^24.0.0", 76 | "open": "^8.4.0", 77 | "puppeteer": "^21.10.0", 78 | "sass": "^1.50.0", 79 | "typescript": "^5.2.2", 80 | "vite": "^4.2.0", 81 | "vitest": "^1.2.2" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "LeetCode Night" 4 | }, 5 | "appDesc": { 6 | "message": "Enable dark mode on LeetCode. Additional features include Instant Question Navigation, YouTube Link Shortcut and Auto Reset Code." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "LeetCode Night リートコードナイト" 4 | }, 5 | "appDesc": { 6 | "message": "LeetCode でダークモードと追加機能を有効にしましょう。追加機能には、デイリーチャレンジショートカット、即時問題ナビゲーション、YouTube リンクショートカット、コード自動リセットが含まれます。" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "LeetCode Night" 4 | }, 5 | "appDesc": { 6 | "message": "LeetCode에서 다크 모드와 추가 기능을 활성화하세요. 추가 기능으로는 데일리 챌린지 단축키, 즉시 문제 탐색, YouTube 링크 단축키 및 코드 자동 리셋이 포함됩니다." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "LeetCode Night" 4 | }, 5 | "appDesc": { 6 | "message": "深夜刷题必备良伴 - 在 LeetCode 启用深色模式。包含附加功能如「每日 LeetCoding 挑战快捷」、「极速题目搜索」、「YouTube 链接快捷」和「自动重置代码」。" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "LeetCode Night" 4 | }, 5 | "appDesc": { 6 | "message": "深夜刷題必備良伴 - 在 LeetCode 啟用深色模式。包含附加功能如「每日 LeetCoding 挑戰捷徑」、「極速題目搜尋」、「YouTube 連結捷徑」和「自動重置程式碼」" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/PackageJsonReaderWriter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | module.exports = class PackageJsonReaderWriter { 6 | static fileName = 'package.json' 7 | 8 | static read () { 9 | const content = require(path.join('..', this.fileName)) 10 | return content 11 | } 12 | 13 | static write (content) { 14 | fs.writeFileSync(this.fileName, content) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/bump.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { execSync } = require('child_process') 4 | const openReleasePage = require('./openReleasePage') 5 | 6 | const execWithStdio = (command) => execSync(command, { stdio: 'inherit' }) 7 | 8 | async function bump () { 9 | const commitMessage = 'chore: bump version v%s' 10 | execWithStdio(`npx bumpp --commit "${commitMessage}"`) 11 | 12 | await openReleasePage() 13 | 14 | console.log('\n✅ All Done!') 15 | } 16 | 17 | bump() 18 | -------------------------------------------------------------------------------- /scripts/openReleasePage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const open = require('open') 3 | const PackageJsonReaderWriter = require('./PackageJsonReaderWriter') 4 | 5 | module.exports = async function openReleasePage () { 6 | const { default: urlJoin } = await import('url-join') 7 | const package = PackageJsonReaderWriter.read() 8 | const url = urlJoin(package.repository.url, 'releases') 9 | await open(url) 10 | } 11 | -------------------------------------------------------------------------------- /scripts/removeUnnecessaryAssets.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { execSync } = require('child_process') 4 | 5 | const execWithStdio = (command) => execSync(command, { stdio: 'inherit' }) 6 | 7 | async function removeUnnecessaryAssets () { 8 | execWithStdio('rm dist/assets/*.svg') 9 | console.log('✅ Removed all `dist/assets/*.svg` successfully!') 10 | } 11 | 12 | removeUnnecessaryAssets() 13 | -------------------------------------------------------------------------------- /src/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' 2 | declare module '*.sass' 3 | declare module '*.sass?inline' 4 | -------------------------------------------------------------------------------- /src/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/src/assets/img/icon-128.png -------------------------------------------------------------------------------- /src/assets/img/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/src/assets/img/logo-text.png -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import packageJson from '../package.json' 2 | 3 | export const VERSION = packageJson.version 4 | 5 | export const EXTENSION_ID = 'aaokgipfeeeciodnffigjfiafledhcii' 6 | 7 | export const POWERED_BY_TEXT = '🪄 Powered by LeetCode Night' 8 | 9 | export const TEST_IDS = { 10 | questionsTabButton: 'questionsTabButton', 11 | optionsTabButton: 'optionsTabButton', 12 | questionsTab: 'questionsTab', 13 | optionsTab: 'optionsTab', 14 | dailyChallengeQuestionCard: 'dailyChallengeQuestionCard', 15 | languageSelect: 'languageSelect', 16 | questionCard: 'questionCard', 17 | questionCardHiddenLink: 'questionCardHiddenLink', 18 | } as const 19 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export const OPTIONS = { 2 | INVERT_IMAGE_COLOR: { 3 | key: 'invertImageColor', 4 | icon: 'image', 5 | default: true, 6 | }, 7 | MASCOT: { 8 | key: 'mascot', 9 | icon: 'cat', 10 | default: true, 11 | }, 12 | HIDE_LOGO: { 13 | key: 'hideLogo', 14 | icon: 'eye-slash', 15 | default: false, 16 | }, 17 | } as const 18 | 19 | export type OptionKey = typeof OPTIONS[keyof typeof OPTIONS]['key'] 20 | 21 | export const DEFAULT_OPTIONS = Object.values(OPTIONS) 22 | .reduce((prev, option) => ({ 23 | ...prev, 24 | [option.key]: option.default, 25 | }), {}) as OptionsForm 26 | 27 | export type OptionsForm = Record 28 | -------------------------------------------------------------------------------- /src/pages/Content/InsertDislikeCount.ts: -------------------------------------------------------------------------------- 1 | import { $, $x } from './dom' 2 | import { getSlug } from './slug' 3 | import { type LikesAndDislikes, fetchLikesAndDislikes } from './fetchLikesAndDislikes' 4 | import { POWERED_BY_TEXT } from '../../constants' 5 | 6 | function formatNumber (value: number) { 7 | return new Intl.NumberFormat('en-US').format(value) 8 | } 9 | 10 | export const datasetKey = 'leetcode_night_insert_dislike_count' 11 | 12 | export const containerDatasetValue = 'container' 13 | export const likeTextDatasetValue = 'likeText' 14 | export const customLikeTextDatasetValue = 'customLikeText' 15 | export const customDislikeTextDatasetValue = 'customDislikeText' 16 | 17 | class InsertDislikeCount2023DynamicLayout { 18 | slug: string | null = null 19 | 20 | selectElement (datasetValue: string, parent?: Element) { 21 | const selector = `[data-${datasetKey}=${datasetValue}]` 22 | return ( 23 | parent 24 | ? parent.querySelector(selector) 25 | : $(selector) 26 | ) as HTMLElement 27 | } 28 | 29 | selectHandledContainer () { 30 | return this.selectElement(containerDatasetValue) 31 | } 32 | 33 | selectLikeText () { 34 | const container = this.selectHandledContainer() 35 | return this.selectElement(likeTextDatasetValue, container) 36 | } 37 | 38 | selectCustomLikeText () { 39 | const container = this.selectHandledContainer() 40 | return this.selectElement(customLikeTextDatasetValue, container) 41 | } 42 | 43 | selectCustomDislikeText () { 44 | const container = this.selectHandledContainer() 45 | return this.selectElement(customDislikeTextDatasetValue, container) 46 | } 47 | 48 | handle ({ likes, dislikes }: LikesAndDislikes) { 49 | const container = selectLikeDislikeContainer() 50 | if (!container) return 51 | 52 | container.dataset[datasetKey] = containerDatasetValue 53 | 54 | const [likeButton, dislikeButton] = container.querySelectorAll('button') 55 | if (!(likeButton && dislikeButton)) return 56 | 57 | const likeText = likeButton.querySelector(`${iconSelector} ~ div`) as HTMLElement 58 | if (!likeText) return 59 | 60 | // Create "custom like text" and insert it after the original one 61 | const customLikeText = likeText.cloneNode(true) as HTMLElement 62 | customLikeText.textContent = formatNumber(likes) 63 | 64 | likeText.after(customLikeText) 65 | 66 | // Insert the "custom dislike text" 67 | const customDislikeText = likeText.cloneNode(true) as HTMLElement 68 | customDislikeText.textContent = formatNumber(dislikes) 69 | customDislikeText.title = POWERED_BY_TEXT 70 | customDislikeText.style.lineHeight = '0' // Adjust the style so it won't expand the height of the button 71 | 72 | const dislikeIcon = dislikeButton.querySelector(iconSelector) as HTMLElement 73 | dislikeIcon?.after(customDislikeText) 74 | 75 | // Hide the "original like text" 76 | likeText.style.display = 'none' 77 | 78 | // Mark modified elements with dataset 79 | likeText.dataset[datasetKey] = likeTextDatasetValue 80 | customLikeText.dataset[datasetKey] = customLikeTextDatasetValue 81 | customDislikeText.dataset[datasetKey] = customDislikeTextDatasetValue 82 | } 83 | 84 | /** 85 | * Call this function when the container is modified by LeetCode Night. 86 | * This will edit the text content of existing elements without inserting new 87 | * ones. 88 | */ 89 | modifyHandled ({ likes, dislikes }: LikesAndDislikes) { 90 | const container = this.selectHandledContainer() 91 | if (!container) return 92 | 93 | const customLikeText = this.selectCustomLikeText() 94 | const customDislikeText = this.selectCustomDislikeText() 95 | if (!(customLikeText && customDislikeText)) return 96 | 97 | customLikeText.textContent = formatNumber(likes) 98 | customDislikeText.textContent = formatNumber(dislikes) 99 | } 100 | 101 | async tryHandle () { 102 | const slug = getSlug() 103 | if (!slug) return 104 | 105 | const handledContainer = this.selectHandledContainer() 106 | 107 | const likesAndDislikes = await fetchLikesAndDislikes(slug) 108 | if (handledContainer) { 109 | if (slug !== this.slug) { 110 | this.modifyHandled(likesAndDislikes) 111 | this.slug = slug 112 | } 113 | return 114 | } 115 | 116 | this.handle(likesAndDislikes) 117 | } 118 | 119 | /** Revert all operations done in `handle()` */ 120 | destroy () { 121 | const container = this.selectHandledContainer() 122 | if (!container) return 123 | 124 | // Restore the display of "original like text" 125 | const likeText = this.selectLikeText() 126 | if (likeText) likeText.style.display = '' 127 | 128 | // Remove custom elements 129 | const customLikeText = this.selectCustomLikeText() 130 | customLikeText?.remove() 131 | 132 | const customDislikeText = this.selectCustomDislikeText() 133 | customDislikeText?.remove() 134 | 135 | // Un-mark the container 136 | delete container.dataset[datasetKey] 137 | } 138 | } 139 | 140 | function selectLikeDislikeContainer () { 141 | return $x(` 142 | //*[ 143 | @class="mr-1 flex overflow-hidden rounded-lg bg-fill-secondary dark:bg-fill-secondary" and 144 | .//*[@data-icon="thumbs-up"] 145 | ] 146 | `)[0] 147 | } 148 | 149 | const iconSelector = '.relative.text-\\[14px\\].leading-\\[normal\\].p-\\[1px\\].before\\:block.before\\:h-3\\.5.before\\:w-3\\.5' 150 | 151 | let isStarted = false 152 | 153 | const instances = [ 154 | new InsertDislikeCount2023DynamicLayout(), 155 | ] 156 | 157 | const handleMutation = () => { 158 | if (!isStarted) return 159 | instances.forEach(instance => instance.tryHandle()) 160 | } 161 | 162 | const destroyAll = () => { 163 | instances.forEach(instance => instance.destroy()) 164 | } 165 | 166 | const observer = new MutationObserver(() => handleMutation()) 167 | 168 | export async function startInsertDislikeCountObserver () { 169 | if (isStarted) return 170 | 171 | isStarted = true 172 | handleMutation() 173 | observer.observe(document.documentElement, { 174 | subtree: true, 175 | childList: true, 176 | }) 177 | } 178 | 179 | export async function stopInsertDislikeCountObserver () { 180 | isStarted = false 181 | observer.disconnect() 182 | destroyAll() 183 | } 184 | -------------------------------------------------------------------------------- /src/pages/Content/InsertYoutubeLink.ts: -------------------------------------------------------------------------------- 1 | import { $$, $x, createElement } from './dom' 2 | import { POWERED_BY_TEXT } from '../../constants' 3 | 4 | type SelectContainer = () => Node 5 | type RenderLink = (href: string) => HTMLAnchorElement 6 | type GetTitle = () => string | null 7 | 8 | export const insertYoutubeLinkDatasetKey = 'leetcode_night_insert_youtube_link' 9 | 10 | class InsertYoutubeLink { 11 | selectContainer: SelectContainer 12 | renderLink: RenderLink 13 | getTitle: GetTitle 14 | 15 | currentTitle: string | undefined 16 | 17 | constructor (dependencies: { 18 | selectContainer: SelectContainer, 19 | renderLink: RenderLink, 20 | getTitle: GetTitle, 21 | }) { 22 | this.selectContainer = dependencies.selectContainer 23 | this.renderLink = dependencies.renderLink 24 | this.getTitle = dependencies.getTitle 25 | } 26 | 27 | generateYoutubeUrl (keyword: string) { 28 | return `https://www.youtube.com/results?search_query=${encodeURIComponent(keyword)}` 29 | } 30 | 31 | selectLinks () { 32 | return $$(`[data-${insertYoutubeLinkDatasetKey}]`) 33 | } 34 | 35 | tryInsert () { 36 | const title = this.getTitle() 37 | if (!title) return 38 | 39 | const container = this.selectContainer() 40 | if (!container) return 41 | 42 | if (title === this.currentTitle) return 43 | this.currentTitle = title 44 | 45 | this.removeAll() 46 | const url = this.generateYoutubeUrl(title) 47 | const newLink = this.renderLink(url) 48 | newLink.dataset[insertYoutubeLinkDatasetKey] = '' 49 | container.appendChild(newLink) 50 | } 51 | 52 | removeAll () { 53 | const existedLink = this.selectLinks() 54 | existedLink.forEach(link => link.remove()) 55 | } 56 | 57 | destroy () { 58 | this.currentTitle = undefined 59 | this.removeAll() 60 | } 61 | } 62 | 63 | function renderLink (attributes: { 64 | className?: string, 65 | style?: string, 66 | href?: string, 67 | }) { 68 | function createYoutubeLinkChildren () { 69 | const iconHtml = ` 70 | 71 | ` 72 | const textHtml = 'YouTube' 73 | 74 | return createElement(` 75 | 76 | ${iconHtml} 77 | ${textHtml} 78 | 79 | `) 80 | } 81 | 82 | const link = document.createElement('a') 83 | Object.assign(link, { 84 | target: '_blank', 85 | title: POWERED_BY_TEXT, 86 | ...attributes, 87 | }) 88 | link.style.order = '99' 89 | 90 | link.append(createYoutubeLinkChildren()) 91 | 92 | return link 93 | } 94 | 95 | const insertYoutubeLink2022 = new InsertYoutubeLink({ 96 | selectContainer: () => $x('//*[contains(@class, "css-10o4wqw")]')[0], 97 | renderLink: (href: string) => renderLink({ 98 | className: 'btn__r7r7 css-1rdgofi', 99 | style: 'color: inherit;', 100 | href, 101 | }), 102 | getTitle: () => $x('//*[@data-cy="question-title"]')[0]?.textContent, 103 | }) 104 | 105 | const insertYoutubeLink2023 = new InsertYoutubeLink({ 106 | selectContainer: () => { 107 | const shareIconPath = 'M12.44 7.586a.6.6 0 10-1.2 0h1.2zm-6.734 5.035l.117-.588-.117.588zm-2.628-1.405l.424-.424-.424.424zM1.673 8.588l.588-.117-.588.117zm.292-2.966l.554.23-.554-.23zm1.89-2.304l-.333-.499.334.5zm2.853-.265a.6.6 0 000-1.2v1.2zm.25 3.434a.6.6 0 00.848.849l-.849-.849zm5.154-3.457a.6.6 0 10-.849-.849l.849.849zm-2.82-1.197a.6.6 0 000 1.2v-1.2zm2.546.6h.6a.6.6 0 00-.6-.6v.6zm-.6 2.566a.6.6 0 001.2 0h-1.2zm.003 2.587c0 .897-.266 1.774-.764 2.52l.998.666c.63-.943.966-2.052.966-3.186h-1.2zm-.764 2.52a4.533 4.533 0 01-2.035 1.669l.46 1.108a5.733 5.733 0 002.573-2.111l-.998-.667zm-2.035 1.669a4.534 4.534 0 01-2.619.258L5.59 13.21a5.733 5.733 0 003.313-.327l-.46-1.108zm-2.619.258a4.533 4.533 0 01-2.321-1.241l-.849.849A5.733 5.733 0 005.59 13.21l.234-1.177zm-2.321-1.241a4.533 4.533 0 01-1.24-2.321l-1.178.234a5.734 5.734 0 001.57 2.936l.848-.849zm-1.24-2.321a4.533 4.533 0 01.257-2.62l-1.108-.459a5.733 5.733 0 00-.327 3.313l1.177-.234zm.257-2.62a4.533 4.533 0 011.67-2.034l-.667-.998a5.733 5.733 0 00-2.111 2.573l1.108.46zm1.67-2.034a4.533 4.533 0 012.519-.764v-1.2a5.733 5.733 0 00-3.186.966l.667.998zm3.617 3.519l4.306-4.306-.849-.849-4.306 4.306.849.849zm1.487-4.303h2.545v-1.2H9.293v1.2zm1.945-.6v2.566h1.2V2.433h-1.2z' 108 | 109 | return $x(` 110 | //*[ 111 | @class="mt-3 flex items-center space-x-4" and 112 | .//*[@d="${shareIconPath}"] 113 | ] 114 | `)[0] 115 | }, 116 | renderLink: (href: string) => renderLink({ 117 | className: 'flex cursor-pointer items-center space-x-1 rounded px-1 py-[3px] hover:bg-fill-3 dark:hover:bg-dark-fill-3 text-gray-6 dark:text-dark-gray-6 font-medium text-xs', 118 | href, 119 | }), 120 | getTitle: () => $x(` 121 | //a[ 122 | @class="mr-2 text-label-1 dark:text-dark-label-1 hover:text-label-1 dark:hover:text-dark-label-1 text-lg font-medium" and 123 | contains(@href, "/problems/") 124 | ] 125 | `)[0]?.textContent, 126 | }) 127 | 128 | const insertYoutubeLink2023DynamicLayout = new InsertYoutubeLink({ 129 | selectContainer: () => { 130 | const difficultyXpath = ` 131 | .//*[ 132 | contains(@class, 'text-caption') and ( 133 | contains(@class, 'text-difficulty-easy') or 134 | contains(@class, 'text-difficulty-medium') or 135 | contains(@class, 'text-difficulty-hard') 136 | ) 137 | ` 138 | 139 | return $x(` 140 | //*[@class="flexlayout__tab"] 141 | //*[@class="flex gap-1" and ${difficultyXpath}]] 142 | `)[0] 143 | }, 144 | renderLink: (href: string) => renderLink({ 145 | className: 'relative inline-flex items-center justify-center text-caption px-2 py-1 gap-1 rounded-full bg-fill-secondary cursor-pointer transition-colors hover:bg-fill-primary hover:text-text-primary text-sd-secondary-foreground hover:opacity-80', 146 | href, 147 | }), 148 | getTitle: () => $x(` 149 | //a[ 150 | @class="no-underline hover:text-blue-s dark:hover:text-dark-blue-s truncate cursor-text whitespace-normal hover:!text-[inherit]" and 151 | contains(@href, "/problems/") 152 | ] 153 | `)[0]?.textContent, 154 | }) 155 | 156 | let isStarted = false 157 | 158 | const insertYoutubeLinkInstances = [ 159 | insertYoutubeLink2022, 160 | insertYoutubeLink2023, 161 | insertYoutubeLink2023DynamicLayout, 162 | ] 163 | 164 | const destroyAll = () => { 165 | insertYoutubeLinkInstances 166 | .forEach(instance => instance.destroy()) 167 | } 168 | 169 | function handleMutation () { 170 | insertYoutubeLinkInstances 171 | .forEach(instance => instance.tryInsert()) 172 | } 173 | 174 | const observer = new MutationObserver(handleMutation) 175 | 176 | export async function startInsertYoutubeLinkObserver () { 177 | if (isStarted) return 178 | isStarted = true 179 | 180 | handleMutation() 181 | observer.observe(document.documentElement, { 182 | subtree: true, 183 | childList: true, 184 | }) 185 | } 186 | 187 | export async function stopInsertYoutubeLinkObserver () { 188 | isStarted = false 189 | observer.disconnect() 190 | destroyAll() 191 | } 192 | -------------------------------------------------------------------------------- /src/pages/Content/StyleInjector.ts: -------------------------------------------------------------------------------- 1 | export class StyleInjector { 2 | isInjected = false 3 | content: string 4 | $el: HTMLStyleElement 5 | 6 | constructor (content: unknown) { 7 | this.content = String(content) 8 | this.$el = document.createElement('style') 9 | this.$el.textContent = this.content 10 | } 11 | 12 | get target () { 13 | return document.documentElement 14 | } 15 | 16 | inject () { 17 | if (this.isInjected) return 18 | this.target.appendChild(this.$el) 19 | this.isInjected = true 20 | } 21 | 22 | eject () { 23 | if (!this.isInjected) return 24 | this.target.removeChild(this.$el) 25 | this.isInjected = false 26 | } 27 | 28 | toggle (value: boolean) { 29 | if (value) { 30 | this.inject() 31 | } else { 32 | this.eject() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/Content/darkSide2023.ts: -------------------------------------------------------------------------------- 1 | import { waitForElement } from './selector' 2 | 3 | export const getIsDarkSide2023 = async () => { 4 | await waitForElement('body') 5 | const html = await waitForElement('html') 6 | const isDark = html.classList.contains('dark') 7 | return isDark 8 | } 9 | 10 | export const setIsDarkSide2023 = async (isDark: boolean) => { 11 | const possibleKeys = ['lc-dark-side', 'lc-theme'] 12 | const newValue = isDark ? 'dark' : 'light' 13 | 14 | possibleKeys.forEach(key => { 15 | const event = new StorageEvent('storage', { key, newValue }) 16 | window.dispatchEvent(event) 17 | }) 18 | } 19 | 20 | export const onChangeIsDarkSide2023 = ( 21 | onChange: (isDarkSide: boolean) => void 22 | ) => { 23 | const observer = new MutationObserver(async () => { 24 | onChange(await getIsDarkSide2023()) 25 | }) 26 | 27 | const html = document.querySelector('html') 28 | 29 | if (html) { 30 | observer.observe(html, { 31 | attributes: true, 32 | attributeFilter: ['class'], 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/Content/dom.ts: -------------------------------------------------------------------------------- 1 | export function createElement (html: string) { 2 | const container = document.createElement('div') 3 | container.innerHTML = html.trim() 4 | if (!container.firstChild) throw new Error() 5 | return container.firstChild as HTMLElement 6 | } 7 | 8 | export function $x (xpath: string) { 9 | const snapshot = document.evaluate( 10 | xpath, document, null, 11 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null 12 | ) 13 | return [...Array(snapshot.snapshotLength)] 14 | .map((_, i) => snapshot.snapshotItem(i) as HTMLElement) 15 | } 16 | 17 | export function $ (selector: string): HTMLElement | null { 18 | return document.querySelector(selector) 19 | } 20 | 21 | export function $$ (selector: string) { 22 | return [...document.querySelectorAll(selector)] as HTMLElement[] 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/Content/fetchLikesAndDislikes.ts: -------------------------------------------------------------------------------- 1 | import { request, gql } from 'graphql-request' 2 | 3 | export interface LikesAndDislikes { 4 | likes: number, 5 | dislikes: number, 6 | } 7 | 8 | const cache = new Map() 9 | 10 | export async function fetchLikesAndDislikes (titleSlug: string) { 11 | const cached = cache.get(titleSlug) 12 | if (cached) return cached 13 | 14 | const query = gql` 15 | query questionTitle($titleSlug: String!) { 16 | question(titleSlug: $titleSlug) { 17 | likes 18 | dislikes 19 | } 20 | } 21 | ` 22 | const data = await request( 23 | 'https://leetcode.com/graphql/', 24 | query, 25 | { titleSlug } 26 | ) 27 | 28 | const likesAndDislikes = data.question as LikesAndDislikes 29 | cache.set(titleSlug, likesAndDislikes) 30 | 31 | return likesAndDislikes 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/Content/index.ts: -------------------------------------------------------------------------------- 1 | import { loadIsEnabled, loadOptions, loadIsAutoResetCodeEnabled, saveIsEnabled, loadIsInsertYoutubeLinkEnabled, saveLeetcodeVersion, loadIsInsertDislikeCountEnabled } from '../../storage' 2 | 3 | import { toggleEnabled } from './toggleEnabled' 4 | import { toggleInvertImageColor } from './toggleInvertImageColor' 5 | import { toggleInvertImageColor2023 } from './toggleInvertImageColor2023' 6 | import { toggleEnabledMascot } from './toggleEnabledMascot' 7 | import { toggleHideLogo } from './toggleHideLogo' 8 | import { toggleHideLogo2023 } from './toggleHideLogo2023' 9 | 10 | import { resetCode } from './resetCode' 11 | import { resetCode2023 } from './resetCode2023' 12 | import { detectLeetcodeVersion } from './leetcode-version' 13 | import { onChangeIsDarkSide2023, setIsDarkSide2023 } from './darkSide2023' 14 | import { startInsertYoutubeLinkObserver, stopInsertYoutubeLinkObserver } from './InsertYoutubeLink' 15 | import { startInsertDislikeCountObserver, stopInsertDislikeCountObserver } from './InsertDislikeCount' 16 | 17 | async function toggleInvertImageColorByVersion (value: boolean) { 18 | const version = await detectLeetcodeVersion() 19 | if (version === '2022') { 20 | toggleInvertImageColor(value) 21 | } else { 22 | toggleInvertImageColor2023(value) 23 | } 24 | } 25 | 26 | async function toggleHideLogoByVersion (value: boolean) { 27 | const version = await detectLeetcodeVersion() 28 | if (version === '2022') { 29 | toggleHideLogo(value) 30 | } else { 31 | toggleHideLogo2023(value) 32 | } 33 | } 34 | 35 | async function applyOptions () { 36 | const options = await loadOptions() 37 | toggleInvertImageColorByVersion(options?.invertImageColor ?? false) 38 | toggleEnabledMascot(options?.mascot ?? false) 39 | toggleHideLogoByVersion(options?.hideLogo ?? false) 40 | } 41 | 42 | async function toggleEnabledByVersion () { 43 | const isEnabled = await loadIsEnabled() 44 | const version = await detectLeetcodeVersion() 45 | 46 | if (version === '2022') { 47 | toggleEnabled(isEnabled) 48 | } else { 49 | setIsDarkSide2023(isEnabled) 50 | } 51 | } 52 | 53 | async function tryResetCodeByVersion () { 54 | const isEnabled = await loadIsAutoResetCodeEnabled() 55 | if (!isEnabled) return 56 | 57 | const version = await detectLeetcodeVersion() 58 | if (version === '2022') { 59 | resetCode() 60 | } else { 61 | resetCode2023() 62 | } 63 | } 64 | 65 | async function startOrStopInsertYoutubeLinkObserver () { 66 | const isEnabled = await loadIsInsertYoutubeLinkEnabled() 67 | 68 | if (isEnabled) startInsertYoutubeLinkObserver() 69 | else stopInsertYoutubeLinkObserver() 70 | } 71 | 72 | async function startOrStopInsertDislikeCountObserver () { 73 | const isEnabled = await loadIsInsertDislikeCountEnabled() 74 | 75 | if (isEnabled) startInsertDislikeCountObserver() 76 | else stopInsertDislikeCountObserver() 77 | } 78 | 79 | async function init () { 80 | async function startOrStopTasks () { 81 | toggleEnabledByVersion() 82 | applyOptions() 83 | startOrStopInsertYoutubeLinkObserver() 84 | startOrStopInsertDislikeCountObserver() 85 | } 86 | 87 | chrome.storage.onChanged.addListener(startOrStopTasks) 88 | startOrStopTasks() 89 | 90 | tryResetCodeByVersion() 91 | onChangeIsDarkSide2023(saveIsEnabled) 92 | saveLeetcodeVersion(await detectLeetcodeVersion()) 93 | } 94 | 95 | init() 96 | -------------------------------------------------------------------------------- /src/pages/Content/leetcode-version.ts: -------------------------------------------------------------------------------- 1 | import { waitDOMContentLoaded, waitForElement } from './selector' 2 | 3 | export type LeetcodeVersion = '2022' | '2023' | '2023-dynamic-layout' 4 | 5 | async function waitLeetcode2023PageLoaded () { 6 | await waitForElement('#qd-content') 7 | } 8 | 9 | export async function detectLeetcodeVersion (): Promise { 10 | await waitDOMContentLoaded() 11 | 12 | const isNextApp = Boolean( 13 | document.querySelector('head meta[data-next-head]') 14 | ) 15 | 16 | if (isNextApp) { 17 | await waitLeetcode2023PageLoaded() 18 | const hasDynamicLayoutClass = Boolean( 19 | document.querySelector('.flexlayout__layout') 20 | ) 21 | if (hasDynamicLayoutClass) return '2023-dynamic-layout' 22 | return '2023' 23 | } 24 | return '2022' 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/Content/resetCode.ts: -------------------------------------------------------------------------------- 1 | import { waitForElement } from './selector' 2 | 3 | async function selectResetButton () { 4 | return await waitForElement( 5 | '[data-cy="code-area"] button[icon="return"]' 6 | ) as HTMLButtonElement 7 | } 8 | 9 | async function selectDialogConfirmButton () { 10 | return await waitForElement( 11 | '.ant-modal-body button:nth-child(2)' 12 | ) as HTMLButtonElement 13 | } 14 | 15 | export async function resetCode () { 16 | const resetButton = await selectResetButton() 17 | resetButton.click() 18 | 19 | const confirmButton = await selectDialogConfirmButton() 20 | confirmButton.click() 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/Content/resetCode2023.ts: -------------------------------------------------------------------------------- 1 | import { waitForElementsByXpath } from './selector' 2 | 3 | async function selectResetButton () { 4 | const resetIconPath = 'M5.725 9.255h2.843a1 1 0 110 2H3.2a1 1 0 01-1-1V4.887a1 1 0 012 0v3.056l2.445-2.297a9.053 9.053 0 11-2.142 9.415 1 1 0 011.886-.665 7.053 7.053 0 1010.064-8.515 7.063 7.063 0 00-8.417 1.202L5.725 9.255z' 5 | 6 | const dynamicLayoutResetIconPath = 'M40 224c-13.3 0-24-10.7-24-24V56c0-13.3 10.7-24 24-24s24 10.7 24 24v80.1l20-23.5C125 63.4 186.9 32 256 32c123.7 0 224 100.3 224 224s-100.3 224-224 224c-50.4 0-97-16.7-134.4-44.8c-10.6-8-12.7-23-4.8-33.6s23-12.7 33.6-4.8C179.8 418.9 216.3 432 256 432c97.2 0 176-78.8 176-176s-78.8-176-176-176c-54.3 0-102.9 24.6-135.2 63.4l-.1 .2 0 0L93.1 176H184c13.3 0 24 10.7 24 24s-10.7 24-24 24H40z' 7 | 8 | const [button] = await waitForElementsByXpath(` 9 | //*[@id="editor"] 10 | //button[ 11 | .//*[ 12 | @d="${resetIconPath}" or 13 | @d="${dynamicLayoutResetIconPath}" 14 | ] 15 | ]` 16 | ) 17 | 18 | return button as HTMLButtonElement 19 | } 20 | 21 | async function selectDialogConfirmButton () { 22 | const message = 'Your current code will be discarded and reset to the default code!' 23 | const [button] = await waitForElementsByXpath(` 24 | //*[@role="dialog" and contains(., "${message}")] 25 | //button[contains(., "Confirm")]` 26 | ) 27 | 28 | return button as HTMLButtonElement 29 | } 30 | 31 | const sleep = (ms = 300) => new Promise((resolve) => setTimeout(resolve, ms)) 32 | 33 | export async function resetCode2023 () { 34 | const resetButton = await selectResetButton() 35 | resetButton.click() 36 | 37 | await sleep() 38 | const confirmButton = await selectDialogConfirmButton() 39 | confirmButton.click() 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/Content/selector.ts: -------------------------------------------------------------------------------- 1 | import { $x } from './dom' 2 | 3 | export function waitForElement (selector: string): Promise { 4 | return new Promise(resolve => { 5 | const element = document.querySelector(selector) 6 | if (element) { 7 | return resolve(element) 8 | } 9 | 10 | const observer = new MutationObserver(() => { 11 | const element = document.querySelector(selector) 12 | 13 | if (element) { 14 | resolve(element) 15 | observer.disconnect() 16 | } 17 | }) 18 | 19 | observer.observe(document.documentElement, { 20 | childList: true, 21 | subtree: true, 22 | }) 23 | }) 24 | } 25 | 26 | export function waitForElementsByXpath (xpath: string): Promise { 27 | return new Promise(resolve => { 28 | const elements = $x(xpath) 29 | if (elements.length) { 30 | return resolve(elements) 31 | } 32 | 33 | const observer = new MutationObserver(() => { 34 | const elements = $x(xpath) 35 | if (elements.length) { 36 | resolve(elements) 37 | observer.disconnect() 38 | } 39 | }) 40 | 41 | observer.observe(document.documentElement, { 42 | childList: true, 43 | subtree: true, 44 | }) 45 | }) 46 | } 47 | 48 | export function waitDOMContentLoaded () { 49 | return new Promise((resolve) => { 50 | const eventName = 'DOMContentLoaded' 51 | const handlerLoaded = () => { 52 | document.removeEventListener(eventName, handlerLoaded) 53 | resolve() 54 | } 55 | document.addEventListener(eventName, handlerLoaded) 56 | if (document.readyState !== 'loading') handlerLoaded() 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/Content/slug.ts: -------------------------------------------------------------------------------- 1 | export function getSlug () { 2 | const url = location.href 3 | const pattern = /problems\/([^/]+)/ 4 | const slug = pattern.exec(url)?.[1] 5 | 6 | return slug 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_ant-modal.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | .ant-modal 4 | &, 5 | .ant-modal-title, 6 | [class*="setting-title"] 7 | color: colors.$grey-8 8 | 9 | [class*="setting-description"] 10 | color: colors.$grey-6 11 | 12 | .ant-modal-content, 13 | .ant-modal-header, 14 | [class*="lc-modal-footer"], 15 | [class*="lc-modal-with-type"], 16 | [class*="lc-modal-with-title-header"] 17 | background: colors.$dsw-layer-2 18 | 19 | [class*="modal-close-btn"] 20 | color: colors.$grey-6 21 | &:hover 22 | color: colors.$grey-8 23 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_ant-select-dropdown.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | .ant-select-dropdown 4 | color: colors.$grey-8 5 | background-color: colors.$dsw-overlay-3 6 | ul.ant-select-dropdown-menu 7 | border-color: colors.$grey-4 8 | .ant-select-dropdown-menu-item 9 | color: colors.$grey-8 10 | &[class*="selected"], &[class*="active"] 11 | background-color: colors.$dsw-layer-1 12 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_ant-select.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | .ant-select 4 | background-color: transparent 5 | &, &:hover 6 | border-color: colors.$grey-4 !important 7 | &, &:hover 8 | .ant-select-arrow, 9 | .ant-select-arrow-icon 10 | color: colors.$grey-4 !important 11 | .ant-select-selection-selected-value 12 | color: colors.$grey-8 13 | &:hover 14 | opacity: .8 15 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_colors.sass: -------------------------------------------------------------------------------- 1 | $grey-0: rgb(20,20,20) 2 | $grey-1: rgb(56,56,56) 3 | $grey-2: rgb(60,60,60) 4 | $grey-3: rgb(66,66,66) 5 | $grey-4: rgb(74,74,74) 6 | $grey-5: rgb(92,92,92) 7 | $grey-6: rgb(138,138,138) 8 | $grey-7: rgb(179,179,179) 9 | $grey-8: rgb(219,219,219) 10 | $grey-9: rgb(255,255,255) 11 | $primary-0: rgb(0,15.372,32.13) 12 | $primary-1: rgb(0,20.984,43.86) 13 | $primary-2: rgb(0,37.82,79.05) 14 | $primary-3: rgb(0,60.268,125.97) 15 | $primary-4: rgb(0,88.328,184.62) 16 | $primary-5: rgb(0,122,255) 17 | $primary-6: rgb(51,148.6,255) 18 | $primary-7: rgb(102,175.2,255) 19 | $primary-8: rgb(153,201.8,255) 20 | $primary-9: rgb(204,228.4,255) 21 | $green-0: rgb(12.3923,25.1977,5.23231) 22 | $green-1: rgb(16.4769,33.5031,6.95692) 23 | $green-2: rgb(28.7308,58.4192,12.1308) 24 | $green-3: rgb(45.0692,91.6408,19.0292) 25 | $green-4: rgb(65.4923,133.168,27.6523) 26 | $green-5: rgb(90,183,38) 27 | $green-6: rgb(123,197.4,81.4) 28 | $green-7: rgb(156,211.8,124.8) 29 | $green-8: rgb(189,226.2,168.2) 30 | $green-9: rgb(222,240.6,211.6) 31 | $red-0: rgb(29.81,5.45,4.87) 32 | $red-1: rgb(40.82,8.9,8.14) 33 | $red-2: rgb(73.85,19.25,17.95) 34 | $red-3: rgb(117.89,33.05,31.03) 35 | $red-4: rgb(172.94,50.3,47.38) 36 | $red-5: rgb(239,71,67) 37 | $red-6: rgb(242.2,107.8,104.6) 38 | $red-7: rgb(245.4,144.6,142.2) 39 | $red-8: rgb(248.6,181.4,179.8) 40 | $red-9: rgb(251.8,218.2,217.4) 41 | $blue-0: rgb(3.25,15.41,32.13) 42 | $blue-1: rgb(6.5,22.2305,43.86) 43 | $blue-2: rgb(16.25,42.6921,79.05) 44 | $blue-3: rgb(29.25,69.9742,125.97) 45 | $blue-4: rgb(45.5,104.077,184.62) 46 | $blue-5: rgb(65,145,255) 47 | $blue-6: rgb(103,167,255) 48 | $blue-7: rgb(141,189,255) 49 | $blue-8: rgb(179,211,255) 50 | $blue-9: rgb(217,233,255) 51 | $gold-0: rgb(24.735,19.37,9.945) 52 | $gold-1: rgb(34.17,27.14,14.79) 53 | $gold-2: rgb(62.475,50.45,29.325) 54 | $gold-3: rgb(100.215,81.53,48.705) 55 | $gold-4: rgb(147.39,120.38,72.93) 56 | $gold-5: rgb(204,167,102) 57 | $gold-6: rgb(214.2,184.6,132.6) 58 | $gold-7: rgb(224.4,202.2,163.2) 59 | $gold-8: rgb(234.6,219.8,193.8) 60 | $gold-9: rgb(244.8,237.4,224.4) 61 | $yellow-0: rgb(32.13,19.5095,1.25) 62 | $yellow-1: rgb(43.86,26.9563,2.5) 63 | $yellow-2: rgb(79.05,49.297,6.25) 64 | $yellow-3: rgb(125.97,79.0844,11.25) 65 | $yellow-4: rgb(184.62,116.319,17.5) 66 | $yellow-5: rgb(255,161,25) 67 | $yellow-6: rgb(255,179.8,71) 68 | $yellow-7: rgb(255,198.6,117) 69 | $yellow-8: rgb(255,217.4,163) 70 | $yellow-9: rgb(255,236.2,209) 71 | $light-grey-0: rgb(255,255,255) 72 | $light-grey-1: rgb(247,247,247) 73 | $light-grey-2: rgb(240,240,240) 74 | $light-grey-3: rgb(229,229,229) 75 | $light-grey-4: rgb(223,223,223) 76 | $light-grey-5: rgb(191,191,191) 77 | $light-grey-6: rgb(140,140,140) 78 | $light-grey-7: rgb(89,89,89) 79 | $light-grey-8: rgb(38,38,38) 80 | $light-grey-9: rgb(0,0,0) 81 | $light-primary-0: rgb(242.25,248.35,255) 82 | $light-primary-1: rgb(204,228.4,255) 83 | $light-primary-2: rgb(153,201.8,255) 84 | $light-primary-3: rgb(102,175.2,255) 85 | $light-primary-4: rgb(51,148.6,255) 86 | $light-primary-5: rgb(0,122,255) 87 | $light-primary-6: rgb(0,97.6,204) 88 | $light-primary-7: rgb(0,73.2,153) 89 | $light-primary-8: rgb(0,48.8,102) 90 | $light-primary-9: rgb(0,30.256,63.24) 91 | $light-green-0: rgb(246.75,251.4,244.15) 92 | $light-green-1: rgb(222,240.6,211.6) 93 | $light-green-2: rgb(189,226.2,168.2) 94 | $light-green-3: rgb(156,211.8,124.8) 95 | $light-green-4: rgb(123,197.4,81.4) 96 | $light-green-5: rgb(90,183,38) 97 | $light-green-6: rgb(72.6923,147.808,30.6923) 98 | $light-green-7: rgb(55.3846,112.615,23.3846) 99 | $light-green-8: rgb(38.0769,77.4231,16.0769) 100 | $light-green-9: rgb(24.9231,50.6769,10.5231) 101 | $light-red-0: rgb(254.2,245.8,245.6) 102 | $light-red-1: rgb(251.8,218.2,217.4) 103 | $light-red-2: rgb(248.6,181.4,179.8) 104 | $light-red-3: rgb(245.4,144.6,142.2) 105 | $light-red-4: rgb(242.2,107.8,104.6) 106 | $light-red-5: rgb(239,71,67) 107 | $light-red-6: rgb(191,54.5,51.25) 108 | $light-red-7: rgb(143,38,35.5) 109 | $light-red-8: rgb(95,21.5,19.75) 110 | $light-red-9: rgb(58.52,8.96,7.78) 111 | $light-blue-0: rgb(245.5,249.5,255) 112 | $light-blue-1: rgb(217,233,255) 113 | $light-blue-2: rgb(179,211,255) 114 | $light-blue-3: rgb(141,189,255) 115 | $light-blue-4: rgb(103,167,255) 116 | $light-blue-5: rgb(65,145,255) 117 | $light-blue-6: rgb(48.75,114.118,204) 118 | $light-blue-7: rgb(32.5,83.2368,153) 119 | $light-blue-8: rgb(16.25,52.3553,102) 120 | $light-blue-9: rgb(3.9,28.8853,63.24) 121 | $light-gold-0: rgb(252.45,250.6,247.35) 122 | $light-gold-1: rgb(244.8,237.4,224.4) 123 | $light-gold-2: rgb(234.6,219.8,193.8) 124 | $light-gold-3: rgb(224.4,202.2,163.2) 125 | $light-gold-4: rgb(214.2,184.6,132.6) 126 | $light-gold-5: rgb(204,167,102) 127 | $light-gold-6: rgb(162.563,132.5,79.6875) 128 | $light-gold-7: rgb(121.125,98,57.375) 129 | $light-gold-8: rgb(79.6875,63.5,35.0625) 130 | $light-gold-9: rgb(48.195,37.28,18.105) 131 | $light-yellow-0: rgb(255,250.3,243.5) 132 | $light-yellow-1: rgb(255,236.2,209) 133 | $light-yellow-2: rgb(255,217.4,163) 134 | $light-yellow-3: rgb(255,198.6,117) 135 | $light-yellow-4: rgb(255,179.8,71) 136 | $light-yellow-5: rgb(255,161,25) 137 | $light-yellow-6: rgb(204,128.289,18.75) 138 | $light-yellow-7: rgb(153,95.5783,12.5) 139 | $light-yellow-8: rgb(102,62.8674,6.25) 140 | $light-yellow-9: rgb(63.24,38.0071,1.5) 141 | $brand-orange: rgb(255,161,22) 142 | $brand-black: rgb(0,0,0) 143 | $dsw-black: rgb(255,255,255) 144 | $dsw-black-light: rgb(0,0,0) 145 | $dsw-white: rgb(0,0,0) 146 | $dsw-white-light: rgb(255,255,255) 147 | $dsw-base: rgb(255,255,255) 148 | $dsw-base-light: rgb(33,40,53) 149 | $dsw-paper: rgb(24,26,31) 150 | $dsw-paper-light: rgb(255,255,255) 151 | $dsw-brand-base: rgb(255,255,255) 152 | $dsw-brand-base-light: rgb(0,0,0) 153 | $dsw-brand-orange: rgb(255,161,22) 154 | $dsw-brand-orange-light: rgb(255,161,22) 155 | $dsw-brand-black: rgb(0,0,0) 156 | $dsw-brand-black-light: rgb(0,0,0) 157 | $dsw-brand-grey: rgb(179,179,179) 158 | $dsw-brand-grey-light: rgb(179,179,179) 159 | $dsw-base-grey-1: rgb(56,56,56) 160 | $dsw-base-grey-1-light: rgb(247,247,247) 161 | $dsw-base-grey-2: rgb(60,60,60) 162 | $dsw-base-grey-2-light: rgb(240,240,240) 163 | $dsw-base-grey-3: rgb(66,66,66) 164 | $dsw-base-grey-3-light: rgb(229,229,229) 165 | $dsw-base-grey-4: rgb(74,74,74) 166 | $dsw-base-grey-4-light: rgb(223,223,223) 167 | $dsw-base-grey-5: rgb(92,92,92) 168 | $dsw-base-grey-5-light: rgb(191,191,191) 169 | $dsw-base-grey-6: rgb(138,138,138) 170 | $dsw-base-grey-6-light: rgb(140,140,140) 171 | $dsw-base-grey-7: rgb(179,179,179) 172 | $dsw-base-grey-7-light: rgb(89,89,89) 173 | $dsw-base-grey-8: rgb(219,219,219) 174 | $dsw-base-grey-8-light: rgb(38,38,38) 175 | $dsw-pink: rgb(255,55,95) 176 | $dsw-pink-light: rgb(255,45,85) 177 | $dsw-purple: rgb(191,90,242) 178 | $dsw-purple-light: rgb(175,82,222) 179 | $dsw-olive: rgb(0,184,163) 180 | $dsw-olive-light: rgb(0,175,155) 181 | $dsw-teal: rgb(100,210,255) 182 | $dsw-teal-light: rgb(90,200,250) 183 | $dsw-yellow: rgb(255,192,30) 184 | $dsw-yellow-light: rgb(255,184,0) 185 | $dsw-red-standard: rgb(239,71,67) 186 | $dsw-red-standard-light: rgb(239,71,67) 187 | $dsw-red-4: rgb(243.8,126.2,123.4) 188 | $dsw-red-4-light: rgb(167.3,49.7,46.9) 189 | $dsw-red-3: rgb(241.4,98.6,95.2) 190 | $dsw-red-3-light: rgb(203.15,60.35,56.95) 191 | $dsw-red-2: rgba(239,71,67,0.5) 192 | $dsw-red-2-light: rgba(239,71,67,0.3) 193 | $dsw-red-1: rgba(239,71,67,0.25) 194 | $dsw-red-1-light: rgba(239,71,67,0.15) 195 | $dsw-red-0: rgba(239,71,67,0.18) 196 | $dsw-red-0-light: rgba(239,71,67,0.08) 197 | $dsw-green-standard: rgb(44,187,93) 198 | $dsw-green-standard-light: rgb(45,181,93) 199 | $dsw-green-4: rgb(107.3,207.4,141.6) 200 | $dsw-green-4-light: rgb(31.5,126.7,65.1) 201 | $dsw-green-3: rgb(75.65,197.2,117.3) 202 | $dsw-green-3-light: rgb(38.25,153.85,79.05) 203 | $dsw-green-2: rgba(44,187,93,0.5) 204 | $dsw-green-2-light: rgba(45,181,93,0.3) 205 | $dsw-green-1: rgba(44,187,93,0.25) 206 | $dsw-green-1-light: rgba(45,181,93,0.15) 207 | $dsw-green-0: rgba(44,187,93,0.18) 208 | $dsw-green-0-light: rgba(45,181,93,0.08) 209 | $dsw-blue-standard: rgb(10,132,255) 210 | $dsw-blue-standard-light: rgb(0,122,255) 211 | $dsw-blue-4: rgb(83.5,168.9,255) 212 | $dsw-blue-4-light: rgb(0,85.4,178.5) 213 | $dsw-blue-3: rgb(46.75,150.45,255) 214 | $dsw-blue-3-light: rgb(0,103.7,216.75) 215 | $dsw-blue-2: rgba(10,132,255,0.5) 216 | $dsw-blue-2-light: rgba(0,122,255,0.3) 217 | $dsw-blue-1: rgba(10,132,255,0.25) 218 | $dsw-blue-1-light: rgba(0,122,255,0.15) 219 | $dsw-blue-0: rgba(10,132,255,0.18) 220 | $dsw-blue-0-light: rgba(0,122,255,0.08) 221 | $dsw-primary-standard: rgb(10,132,255) 222 | $dsw-primary-standard-light: rgb(0,122,255) 223 | $dsw-primary-4: rgb(83.5,168.9,255) 224 | $dsw-primary-4-light: rgb(0,85.4,178.5) 225 | $dsw-primary-3: rgb(46.75,150.45,255) 226 | $dsw-primary-3-light: rgb(0,103.7,216.75) 227 | $dsw-primary-2: rgba(10,132,255,0.5) 228 | $dsw-primary-2-light: rgba(0,122,255,0.3) 229 | $dsw-primary-1: rgba(10,132,255,0.25) 230 | $dsw-primary-1-light: rgba(0,122,255,0.15) 231 | $dsw-primary-0: rgba(10,132,255,0.18) 232 | $dsw-primary-0-light: rgba(0,122,255,0.08) 233 | $dsw-label-primary: rgb(255,255,255) 234 | $dsw-label-primary-light: rgb(38,38,38) 235 | $dsw-label-primary-reverse: rgb(38,38,38) 236 | $dsw-label-primary-reverse-light: rgb(255,255,255) 237 | $dsw-label-secondary: rgba(239,241,246,0.75) 238 | $dsw-label-secondary-light: rgba(38,38,38,0.75) 239 | $dsw-label-tertiary: rgba(239,242,246,0.6) 240 | $dsw-label-tertiary-light: rgba(60,60,67,0.6) 241 | $dsw-label-quaternary: rgba(235,235,245,0.3) 242 | $dsw-label-quaternary-light: rgba(60,60,67,0.3) 243 | $dsw-fill-primary: rgba(255,255,255,0.17) 244 | $dsw-fill-primary-light: rgba(0,10,32,0.12) 245 | $dsw-fill-secondary: rgba(255,255,255,0.14) 246 | $dsw-fill-secondary-light: rgba(0,10,32,0.1) 247 | $dsw-fill-tertiary: rgba(255,255,255,0.1) 248 | $dsw-fill-tertiary-light: rgba(0,10,32,0.05) 249 | $dsw-fill-quaternary: rgba(255,255,255,0.07) 250 | $dsw-fill-quaternary-light: rgba(0,10,32,0.03) 251 | $dsw-border: rgba(247,250,255,0.24) 252 | $dsw-border-light: rgba(0,10,32,0.14) 253 | $dsw-border-1: rgba(247,250,255,0.24) 254 | $dsw-border-1-light: rgba(0,10,32,0.14) 255 | $dsw-border-2: rgba(247,250,255,0.18) 256 | $dsw-border-2-light: rgba(0,10,32,0.11) 257 | $dsw-border-3: rgba(247,250,255,0.1) 258 | $dsw-border-3-light: rgba(0,10,32,0.05) 259 | $dsw-divider-primary: rgb(36,36,36) 260 | $dsw-divider-primary-light: rgb(229,229,229) 261 | $dsw-divider-secondary: rgb(61,61,61) 262 | $dsw-divider-secondary-light: rgb(240,240,240) 263 | $dsw-success: rgb(44,187,93) 264 | $dsw-success-light: rgb(45,181,93) 265 | $dsw-danger: rgb(239,71,67) 266 | $dsw-danger-light: rgb(239,71,67) 267 | $dsw-warning: rgb(255,192,30) 268 | $dsw-warning-light: rgb(255,184,0) 269 | $dsw-difficulty-easy: rgb(0,184,163) 270 | $dsw-difficulty-easy-light: rgb(0,175,155) 271 | $dsw-difficulty-medium: rgb(255,192,30) 272 | $dsw-difficulty-medium-light: rgb(255,184,0) 273 | $dsw-difficulty-hard: rgb(255,55,95) 274 | $dsw-difficulty-hard-light: rgb(255,45,85) 275 | $dsw-data-olive-standard: rgb(67,233,118) 276 | $dsw-data-olive-standard-light: rgb(37,227,95) 277 | $dsw-data-olive-tinted: rgb(0,223,210) 278 | $dsw-data-olive-tinted-light: rgb(65,241,230) 279 | $dsw-data-yellow-standard: rgb(255,192,30) 280 | $dsw-data-yellow-standard-light: rgb(255,199,0) 281 | $dsw-data-yellow-tinted: rgb(240,242,108) 282 | $dsw-data-yellow-tinted-light: rgb(254,248,110) 283 | $dsw-data-pink-standard: rgb(234,16,94) 284 | $dsw-data-pink-standard-light: rgb(246,67,99) 285 | $dsw-data-pink-tinted: rgb(239,59,178) 286 | $dsw-data-pink-tinted-light: rgb(255,116,208) 287 | $dsw-overlay: rgba(0,0,0,0.5) 288 | $dsw-overlay-light: rgba(0,0,0,0.5) 289 | $dsw-overlay-1: rgb(26,26,26) 290 | $dsw-overlay-1-light: rgb(247,248,250) 291 | $dsw-overlay-2: rgb(40,40,40) 292 | $dsw-overlay-2-light: rgb(255,255,255) 293 | $dsw-overlay-3: rgb(48,48,48) 294 | $dsw-overlay-3-light: rgb(255,255,255) 295 | $dsw-overlay-4: rgb(54,54,54) 296 | $dsw-overlay-4-light: rgb(255,255,255) 297 | $dsw-layer-bg: rgb(26,26,26) 298 | $dsw-layer-bg-light: rgb(247,248,250) 299 | $dsw-layer-1: rgb(40,40,40) 300 | $dsw-layer-1-light: rgb(255,255,255) 301 | $dsw-layer-2: rgb(48,48,48) 302 | $dsw-layer-2-light: rgb(255,255,255) 303 | $dsw-layer-3: rgb(54,54,54) 304 | $dsw-layer-3-light: rgb(255,255,255) 305 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_description.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | [data-cy="description-content"] 4 | [class*="question-content"] 5 | color: colors.$grey-8 6 | & ~ div > div:nth-child(2) 7 | color: colors.$grey-8 8 | 9 | [class*="question-content"] + div > div > div > div:nth-child(2) 10 | color: colors.$grey-8 11 | 12 | [data-cy="question-title"], 13 | [data-cy="question-title"] ~ div 14 | color: colors.$grey-8 15 | 16 | [class*="css-"] 17 | border-color: colors.$grey-3 18 | 19 | pre, code 20 | background-color: colors.$dsw-fill-tertiary 21 | pre 22 | color: colors.$grey-9 23 | code 24 | color: colors.$grey-7 25 | 26 | button 27 | transition: .2s all 28 | &:hover 29 | color: inherit !important 30 | opacity: .8 31 | 32 | // Menu part (e.g. "Related Topics", "Show Hint") 33 | [class*="description"] 34 | =text-and-icon-except-lock-icon 35 | &, svg:not([class*="lock-"]) 36 | @content 37 | 38 | [class*="header"] 39 | border-color: colors.$grey-2 40 | ~ [class*="css-"] 41 | background: transparent 42 | > [class*="css-"]:first-child 43 | +text-and-icon-except-lock-icon 44 | color: colors.$grey-6 45 | &:hover 46 | +text-and-icon-except-lock-icon 47 | color: colors.$grey-9 48 | 49 | [class*="topic-tag"] > span 50 | color: colors.$grey-8 51 | background-color: colors.$dsw-fill-tertiary 52 | 53 | [class*="question"] [class*="title"] 54 | color: colors.$grey-7 55 | 56 | .css-dgkgi6 57 | color: unset 58 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_editor.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | .CodeMirror-gutters 4 | background-color: colors.$dsw-paper !important 5 | 6 | [class*="resize-bar__"] 7 | color: colors.$grey-5 8 | background: colors.$dsw-layer-2 9 | &::before 10 | background: colors.$grey-4 11 | 12 | [class*="code-area"] 13 | // Top and left border of the code area 14 | > [class*="content__"] > [class*="wrapper__"] > [class*="container__"], 15 | > [class*="result__"] 16 | border-color: colors.$grey-2 17 | box-shadow: inset 0 -1px colors.$grey-2 18 | 19 | // Toolbar 20 | > [class*="content__"] > [class*="wrapper__"] > div:first-child 21 | background-color: colors.$dsw-layer-2 22 | 23 | // Right icon buttons 24 | button[class*="tool-button"] 25 | color: colors.$grey-6 26 | &:hover 27 | color: colors.$grey-5 28 | 29 | // "Autocomplete" button 30 | button[type="ghost"] 31 | & > [class*="-StyledSpan"] 32 | color: colors.$grey-6 33 | &:hover 34 | background-color: rgba(colors.$grey-9, 0.04) 35 | 36 | // Expanded result 37 | > [class*="result__"] > [class*="panel__"] > [class*="header__"] 38 | color: colors.$grey-9 39 | background-color: colors.$dsw-layer-1 40 | border-color: colors.$grey-2 41 | .reset-code-btn 42 | color: rgb(132,136,142) !important 43 | 44 | // Test case textarea 45 | #testcase-editor 46 | color: colors.$grey-9 47 | background-color: transparent 48 | [class*='editor-container'] 49 | background-color: colors.$grey-0 50 | border-color: colors.$grey-3 51 | .ace_cursor 52 | color: colors.$grey-9 53 | // Selected line of tree visualizer 54 | .viewer-data-marker 55 | background-color: colors.$grey-1 56 | 57 | [data-key="runcode-result-content"] 58 | // Skeleton screen of "Run code result" 59 | svg rect 60 | fill: colors.$grey-1 !important 61 | [data-cy="run-code-finished"] 62 | [class*='-ValueContainer'] 63 | background-color: colors.$grey-1 64 | border-color: colors.$grey-3 65 | color: colors.$grey-9 66 | del 67 | background: colors.$red-4 !important 68 | ins 69 | background: colors.$green-4 !important 70 | 71 | // footer 72 | > [class*="container__"] 73 | background-color: colors.$dsw-layer-2 74 | border-top-color: colors.$grey-4 75 | button 76 | color: colors.$grey-8 !important 77 | [class*="contribute__"] 78 | color: colors.$grey-6 79 | button[data-cy="run-code-btn"] 80 | &:hover, &:disabled 81 | background-color: inherit !important 82 | border-color: inherit !important 83 | opacity: .8 84 | &:disabled 85 | opacity: .3 86 | 87 | // "Your previous code was restored from your local storage." buttons 88 | [class*="reset-code-btn__"], 89 | [class*="header__"] > [class*="icon-wrapper"] 90 | color: colors.$grey-6 91 | &:hover 92 | color: colors.$grey-8 !important 93 | 94 | // Tree visualizer 95 | [class*="visualizer__"] 96 | background: colors.$grey-0 97 | [class*="data-structure-viewer"] 98 | circle 99 | fill: colors.$grey-0 100 | stroke: colors.$grey-6 101 | line 102 | stroke: colors.$grey-5 103 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_error-container.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | [class*="ErrorContainer"] 4 | background-color: colors.$red-1 5 | [class*="Error"], 6 | [class*="CollapseTrigger"] 7 | color: colors.$red-5 8 | [class*="Mask"] 9 | background: linear-gradient(rgba(colors.$grey-0, 0), rgba(colors.$grey-0, 0.75)) 10 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_loading.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | #initial-loading 4 | background-color: colors.$dsw-overlay-1 !important 5 | .spinner > .bounce 6 | background-color: colors.$grey-2 7 | 8 | [data-cy="question-loading-container"] 9 | background: colors.$dsw-overlay-1 !important 10 | [class*="wrapper"] 11 | color: colors.$grey-2 12 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_logo.sass: -------------------------------------------------------------------------------- 1 | $logo: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABYCAYAAAC9DArfAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAaVSURBVHgB7ZzPb9xEFMe/46SoN4pEQi8I8xc0e+TUjSpxq7J74QbZ/Qdoc+CI6pyp1PTKZdNy6mlTCkKCQ7aHcuGQrYTEBbSGXmiD6AqBQEnx4721TTdJt+s3Y+8POR/JWmvjdeyPx+OZN28MzDhEdIWXXYp5mqyv4JSjsJQWjeYaTokZIyqlirIjpYaysYMyoxAl9FBWlKLKK8tClLCLssEnvU52rKNM8AnXyI4AZYJPeIXihqaWLZQJPmHfUtQ2ykQiqkd69lAmXETxcg5lwUFUT36LsiClgk5FjScRtUd6erMkysNkaPGii0H9+Qj48t0Ott9YodY5HzOAQcFwyRBRDc1vcPAH8HUd+P374W+7vLctRIf3TbMfYgoUKoviAF0ALV9cOi7qKIbbWv8ebk5aWmG3obWoBx++XNRg56YB75UefbY80WhpISXLWtR3HwM/fAolIaKD1UmUstxLlrWoh9dtRAk+l7Jdai0VPoiRqyyKQyYBtIioh5/AARZm9oq+LXO7DVlUjT/a0OIu6iiGtswH+xsogFxkUTyOJ1FLXd9Nbjupp/Kny/VYPe96zPk2pF7bTtRPd4oSJazE9Vi+jVmnkpV0RUSUr/qhNA2kLSVPMtBd/uwhin7mw+kP/u6ZC7xe4bWL6n0fJzIbpvk4l2ChtSwnUd+818Hf+9yo/K0zbnNqvV7FgncFhBps8RCY959swhErWdaipCR1rzdN5aMOlHDTgBuiuMGHbBnTog53lZou9ZhaFsUBOIlY+tAR8rJqjAlhCbXOcxMhsrlIz4/BoQGrquATUXYlylGUYJq/8sl6q3wgHdghDdg9urVkNYSmKlkUD2pWoSNEDqJOHEtrOeBLbd8ItajHMsuyCrUA8nQTUV0UAFf+V+F5N2BLFK1mecikZLoNWZQcUAN6ChMl8IlyfMt7G3Hp1eMZVckcKyvpGF+FnmaRolL+r8dAFv/LVDVbv1SWdQQB2GBR25gQIsys71cQ0U0UyEhZDqI2WdRUhtdNc/8q10OaTrSqNL5QlnWoJRYVYIrE9RhJVykcv7UuZ+LE09A61DIDoobJ0IDtmvUnFSg4IsuhG3OTRdk8BAqHu0lbfP+sP+8mUZ871zdN80kAF8huxHgbc4CEnXML2fBJN0hPubJaUixKVbmyWlIsSlWvlKIE0iVt9KhMWS3DUJwzdSoqA9IorSq2D/IOtcwTi7ysZdw2ZFG3kAPtdru6uLgo3akqZpc+n+/O4eHhZr1eD+ULKVl+xh/fRQ7cu3fvGouyCSJOGknAa8ix8sX15QuNLOcZWMk/DTBf+MnFHcjK2gTow5GFhYUA84nPd0RVM2DhLIvrgAuYU6IoquaaRTOOeW/Iiqww47ZVOMKy7mN+2dPIck4W8zxvG/NJuLa2dldkPcz4g3XX2+jy5csd/nDOOZgw/WfPnq3KisjKGocWUc4BPhYWsPQmlPHvKSAPtB0WVUkbpSYpLU8VO6jk2eVJG3yzRiroBPT8jRxZKGccK4VPvko6Si+sRzrKK4zsJnaXWtgu6SltLN52kncphL1oRLqBeH6gFmk31WclkspjhHzxztT4DC8OvjDouE6/e2EyG8UvmLDJqgtRQJaflsE8Hs9ICoJ/7E8hImzwaLRVbG5k5t+8CotFSQrCS7KaI6qY5r66BzEyRJMkedj043xedmkKo0CZRAmDFHE9Y3NK56WEZRaVEh28xvWXKqA5Nvg3DyVMLWrAWfXTO1OkdJaF2YkS/lGHyTOHlWdRmLUowrb2FhRUMfhZEmZfoqgP8qwCkOoBi1kQ5iQq4oeOpINb4DKFLsAUnpLuovTtqxTrobCkhDWhx4eUsM8v1aCEWsu1aYkSnMYNk4kBdsLOv9Om20utLHmeg3zQ28ttPtr2tEQJeU0ob0Db+X70FZevRrqHDiSXIqKhkSbvHDx6i1dq2mkjQ0eWmyhhETkgJYyFyWp2YWdeHd5DFTKI6x2/di7XMl9RQm7D9+pb8q9fUBz5ixJyzXVQCfvxDoqhGFFC7okhiTCZ5jG6hSwvv3j8LfKnOFFCIVk0yTzDVRwc61LIS8TkVSrFvPwiLFKUkMvTcBSDyUZvVndxdskfiHr8IBaWP11EXt22ZZ6VQmUJSSw84CfdFRRBhE3nSUsZKVxWSjKlLeBVq9cEnNwht81ooVl0aRpmYrJShqRZvGcmnv7GnztF1k2jmLisYQbvmZGX9JA0Snnc0Rj/6AYkD4guH2VXWvea1woUwX8DS2Flo786hQAAAABJRU5ErkJggg==') 2 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_navbar.sass: -------------------------------------------------------------------------------- 1 | @use 'logo' as * 2 | @use 'colors' 3 | 4 | #leetcode-navbar 5 | background-color: colors.$dsw-layer-1 6 | border-bottom-color: colors.$grey-2 7 | ul:not(#mobile-nav-links, #mobile-nav-links ~ *) 8 | li:not([role="none"], [role="menuitem"]) 9 | > a, #headlessui-menu-button-1 10 | color: colors.$grey-8 !important 11 | 12 | .ant-dropdown-link 13 | svg 14 | fill: colors.$grey-8 !important 15 | 16 | // "Daily Challenge" icon default color 17 | .popover-wrapper a 18 | .text-text-secondary, .text-text-secondary 19 | color: colors.$grey-8 20 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_question-fast-picker.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | [class*="question-fast-picker-wrapper"] 4 | background-color: colors.$dsw-layer-2 5 | border-top-color: colors.$grey-4 6 | button 7 | background-color: transparent !important 8 | color: colors.$grey-8 !important 9 | box-shadow: inset 0 0 0 1px colors.$grey-4 !important 10 | [class*="shuffle-icon"], 11 | [class*="handler-icon"], 12 | [class*="handler-text"] 13 | color: colors.$grey-8 !important 14 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_solution.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | #solution 4 | color: colors.$grey-8 5 | hr 6 | background-color: colors.$grey-2 7 | [class*="nav__"] 8 | border-color: colors.$grey-2 9 | h2, h3, h4 10 | color: colors.$grey-9 11 | *:not(pre) > code 12 | color: colors.$grey-7 13 | background-color: colors.$dsw-fill-quaternary 14 | border-color: colors.$grey-2 15 | .discuss-markdown-container 16 | color: colors.$grey-7 17 | [class*="action__"] 18 | &, & svg 19 | color: colors.$grey-6 20 | transition: none 21 | &:hover 22 | &, & svg 23 | color: colors.$grey-5 !important 24 | [class*="read-more__"] 25 | background: linear-gradient(transparent, colors.$dsw-paper) 26 | [class*="pagination-container__"] 27 | .ant-pagination-item, 28 | .ant-pagination-prev .ant-pagination-item-link, 29 | .ant-pagination-next .ant-pagination-item-link 30 | color: colors.$grey-7 31 | background-color: colors.$grey-0 32 | box-shadow: inset 0 0 0 1px rgba(colors.$grey-8,0.2) 33 | .ant-pagination-item-active 34 | color: colors.$primary-4 35 | [class*="loading-cover__"] 36 | background-color: colors.$dsw-paper 37 | 38 | [class*="header__"] 39 | background-color: rgb(40, 42, 46) !important 40 | border-color: colors.$grey-2 41 | label 42 | color: colors.$grey-8 !important 43 | 44 | [class*="comment__"], 45 | [class*="action-area__"] 46 | border-color: colors.$grey-3 47 | textarea 48 | background-color: transparent 49 | blockquote 50 | border: 0 51 | border-left: 4px solid colors.$grey-4 52 | color: colors.$grey-6 53 | background-color: transparent 54 | border-radius: 0 55 | padding-top: 8px 56 | padding-bottom: 8px 57 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_submission.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | [class*="submissions"] 4 | [class*="result-container__"] 5 | border-color: colors.$grey-2 6 | [class*="container"], span 7 | [class*="info__"], span 8 | color: colors.$grey-8 9 | [class*="container__"] rect 10 | fill: colors.$grey-1 !important 11 | [class*='addl-success-info'] 12 | color: colors.$grey-8 13 | a[class*="detail__"] 14 | color: colors.$grey-6 15 | 16 | // "Last executed input" (when runtime error) 17 | [class*="-Field"] 18 | color: colors.$grey-7 !important 19 | // Code block 20 | & + div 21 | border-color: colors.$grey-3 22 | background-color: colors.$grey-1 23 | [class*="-Value"] 24 | color: colors.$grey-9 25 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/_submit-table.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | .ant-table-body 4 | table 5 | th, td 6 | border-color: colors.$grey-3 !important 7 | th 8 | color: white !important 9 | background: black !important 10 | tr:nth-child(2n) > td 11 | color: colors.$grey-7 !important 12 | background: black !important 13 | tr:nth-child(2n + 1) > td 14 | color: colors.$grey-7 !important 15 | background: colors.$dsw-paper !important 16 | 17 | // "No Data" placeholder 18 | .ant-table-placeholder 19 | background-color: colors.$dsw-base-grey-1 20 | border-color: colors.$grey-3 21 | .ant-empty-normal 22 | color: colors.$grey-6 23 | -------------------------------------------------------------------------------- /src/pages/Content/styles/dark-theme/style.module.sass: -------------------------------------------------------------------------------- 1 | @use 'colors' 2 | 3 | @use 'loading' 4 | @use 'description' 5 | @use 'editor' 6 | @use 'submission' 7 | @use 'submit-table' 8 | @use 'solution' 9 | @use 'question-fast-picker' 10 | @use 'navbar' 11 | @use 'error-container' 12 | @use 'ant-modal' 13 | @use 'ant-select' 14 | @use 'ant-select-dropdown' 15 | 16 | body 17 | color: colors.$grey-8 18 | background-color: colors.$dsw-layer-1 19 | 20 | [class*="TabViewHeader"] 21 | background: colors.$dsw-layer-2 22 | 23 | // Bottom divider 24 | &::after 25 | border-bottom-color: colors.$grey-2 26 | 27 | [class*="TabHeaderContainer"] 28 | color: colors.$grey-6 29 | background-color: colors.$dsw-layer-2 30 | 31 | [data-cy="description"], 32 | [data-cy="solution"], 33 | [data-cy="discuss"], 34 | [data-cy="submissions"], 35 | [data-cy="custom-testcase"], 36 | [data-cy="runcode-result"], 37 | [data-cy="debugger"] 38 | color: inherit 39 | background-color: transparent 40 | 41 | // Short divider 42 | &::before, &::after 43 | background: colors.$grey-2 44 | 45 | // Active tab 46 | // HACK: might be unstable 47 | &.css-19j86kk-TabHeader, 48 | &.css-qndcpp-TabHeader 49 | color: colors.$grey-8 50 | background-color: colors.$dsw-layer-1 51 | 52 | [class*="tab-pane__"] 53 | border-color: colors.$grey-2 54 | -------------------------------------------------------------------------------- /src/pages/Content/styles/hide-logo-2023/style.module.sass: -------------------------------------------------------------------------------- 1 | nav 2 | ul > a[href="/"] 3 | width: 0 4 | margin: 0 5 | opacity: 0 6 | transform: scale(0) 7 | transition: all .2s 8 | -------------------------------------------------------------------------------- /src/pages/Content/styles/hide-logo/style.module.sass: -------------------------------------------------------------------------------- 1 | #leetcode-navbar 2 | div:nth-child(1) > a[href="/"] 3 | width: 0 4 | margin: 0 5 | opacity: 0 6 | transform: scale(0) 7 | transition: all .2s 8 | -------------------------------------------------------------------------------- /src/pages/Content/styles/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngseke/leetcode-night/35f1b4a36475497769a15704b841b64be34f1ba2/src/pages/Content/styles/icon.png -------------------------------------------------------------------------------- /src/pages/Content/styles/invert-image-2023/style.module.sass: -------------------------------------------------------------------------------- 1 | [data-track-load='description_content'] 2 | img:not([alt="company"]) 3 | filter: invert(1) 4 | -------------------------------------------------------------------------------- /src/pages/Content/styles/invert-image/style.module.sass: -------------------------------------------------------------------------------- 1 | [data-cy="description-content"], #solution 2 | img 3 | filter: invert(1) 4 | -------------------------------------------------------------------------------- /src/pages/Content/styles/loading-mascot/style.module.sass: -------------------------------------------------------------------------------- 1 | @use 'cat' as * 2 | 3 | #initial-loading 4 | &::after 5 | $scale: .4 6 | content: '' 7 | background-image: $cat 8 | background-size: contain 9 | background-repeat: no-repeat 10 | position: fixed 11 | width: 400px * $scale 12 | height: 400px * $scale 13 | right: 0 14 | bottom: 0 15 | -------------------------------------------------------------------------------- /src/pages/Content/toggleEnabled.ts: -------------------------------------------------------------------------------- 1 | import styles from './styles/dark-theme/style.module.sass?inline' 2 | import { StyleInjector } from './StyleInjector' 3 | 4 | const styleInjector = new StyleInjector(styles) 5 | 6 | export const toggleEnabled = (value: boolean) => styleInjector.toggle(value) 7 | -------------------------------------------------------------------------------- /src/pages/Content/toggleEnabledMascot.ts: -------------------------------------------------------------------------------- 1 | import styles from './styles/loading-mascot/style.module.sass?inline' 2 | import { StyleInjector } from './StyleInjector' 3 | 4 | const styleInjector = new StyleInjector(styles) 5 | 6 | export const toggleEnabledMascot = (value: boolean) => styleInjector.toggle(value) 7 | -------------------------------------------------------------------------------- /src/pages/Content/toggleHideLogo.ts: -------------------------------------------------------------------------------- 1 | import styles from './styles/hide-logo/style.module.sass?inline' 2 | import { StyleInjector } from './StyleInjector' 3 | 4 | const styleInjector = new StyleInjector(styles) 5 | 6 | export const toggleHideLogo = (value: boolean) => styleInjector.toggle(value) 7 | -------------------------------------------------------------------------------- /src/pages/Content/toggleHideLogo2023.ts: -------------------------------------------------------------------------------- 1 | import styles from './styles/hide-logo-2023/style.module.sass?inline' 2 | import { StyleInjector } from './StyleInjector' 3 | 4 | const styleInjector = new StyleInjector(styles) 5 | 6 | export const toggleHideLogo2023 = (value: boolean) => styleInjector.toggle(value) 7 | -------------------------------------------------------------------------------- /src/pages/Content/toggleInvertImageColor.ts: -------------------------------------------------------------------------------- 1 | import styles from './styles/invert-image/style.module.sass?inline' 2 | import { StyleInjector } from './StyleInjector' 3 | 4 | const styleInjector = new StyleInjector(styles) 5 | 6 | export const toggleInvertImageColor = (value: boolean) => styleInjector.toggle(value) 7 | -------------------------------------------------------------------------------- /src/pages/Content/toggleInvertImageColor2023.ts: -------------------------------------------------------------------------------- 1 | import styles from './styles/invert-image-2023/style.module.sass?inline' 2 | import { StyleInjector } from './StyleInjector' 3 | 4 | const styleInjector = new StyleInjector(styles) 5 | 6 | export const toggleInvertImageColor2023 = (value: boolean) => styleInjector.toggle(value) 7 | -------------------------------------------------------------------------------- /src/pages/Popup/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { App } from './App' 3 | import { mockChrome } from '../../../__tests__/chrome' 4 | import { TEST_IDS } from '../../constants' 5 | import userEvent from '@testing-library/user-event' 6 | import * as useEnableDarkThemeHooks from './hooks/useEnableDarkTheme' 7 | 8 | describe('Popup app', () => { 9 | vi.spyOn(useEnableDarkThemeHooks, 'useEnableDarkTheme') 10 | .mockReturnValue([true, vi.fn()]) 11 | 12 | async function clickQuestionsTabButton () { 13 | await userEvent.click( 14 | screen.getByTestId(TEST_IDS.questionsTabButton) 15 | ) 16 | } 17 | 18 | async function clickOptionsTabButton () { 19 | await userEvent.click( 20 | screen.getByTestId(TEST_IDS.optionsTabButton) 21 | ) 22 | } 23 | 24 | test('should switch to selected tab', async () => { 25 | mockChrome() 26 | render() 27 | 28 | await clickOptionsTabButton() 29 | expect(screen.getByTestId(TEST_IDS.optionsTab)) 30 | 31 | await clickQuestionsTabButton() 32 | expect(screen.getByTestId(TEST_IDS.questionsTab)) 33 | 34 | await clickOptionsTabButton() 35 | expect(screen.getByTestId(TEST_IDS.optionsTab)) 36 | 37 | expect(console.error).toBeCalledTimes(0) 38 | expect(console.warn).toBeCalledTimes(0) 39 | }) 40 | 41 | test('should remember the previously selected tab', async () => { 42 | mockChrome() 43 | const { rerender } = render() 44 | 45 | await clickOptionsTabButton() 46 | rerender() 47 | expect(screen.getByTestId(TEST_IDS.optionsTab)) 48 | 49 | await clickQuestionsTabButton() 50 | rerender() 51 | expect(screen.getByTestId(TEST_IDS.questionsTab)) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/pages/Popup/App.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from './components/Footer' 2 | import { SearchQuestion } from './components/SearchQuestion' 3 | import { useTabs, Tabs } from './components/Tabs' 4 | import { Layout } from './components/Layout' 5 | import { Header } from './components/Header' 6 | import { Options } from './components/Options' 7 | 8 | export function App () { 9 | const { tab, setTab, isTabOptions, isTabQuestions } = useTabs() 10 | 11 | return ( 12 | 14 |
15 | 16 | } 17 | body={<> 18 | {isTabQuestions && } 19 | {isTabOptions && } 20 | } 21 | footer={