├── .github └── workflows │ └── auto-release.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── CNAME ├── apple-touch-icon.png ├── assets │ ├── icons │ │ └── logo_32.png │ ├── images │ │ ├── add_friend_button.png │ │ ├── chrome_webstore.png │ │ ├── company_problems.png │ │ ├── contest_company_tags.png │ │ ├── friends_contest_rating.png │ │ ├── friends_import_export.png │ │ ├── friends_list.png │ │ ├── friends_tab_icon.png │ │ ├── per_contest_rating.png │ │ └── problem_editorial.png │ └── svg │ │ └── get-the-addon-fx-apr-2020.svg ├── css │ └── index.css ├── favicon.ico ├── googleecc5c3bfdf8850e6.html ├── index.html ├── js │ └── index.js └── p │ └── privacy-policy.html ├── package.json ├── src ├── components │ ├── companyTagsPremium.ts │ ├── friendsTable.ts │ └── navbarFriendsIcon.ts ├── core │ ├── app.ts │ ├── defines │ │ ├── browserType.ts │ │ ├── buildModes.ts │ │ ├── pageType.ts │ │ ├── requestMethod.ts │ │ ├── responseType.ts │ │ ├── result.ts │ │ └── sortType.ts │ ├── interfaces │ │ └── module.ts │ ├── manager.ts │ ├── modules │ │ ├── contestRank.ts │ │ ├── friendsPage.ts │ │ ├── index.ts │ │ ├── navbarFriendsButton.ts │ │ ├── problemCompanyTagsPremium.ts │ │ ├── problemsetCompaniesPremium.ts │ │ └── profileAddFriendButton.ts │ └── utils │ │ ├── friendManager.ts │ │ ├── helpers.ts │ │ ├── leetcodeManager.ts │ │ ├── logManager.ts │ │ ├── metaManager.ts │ │ └── storageManager.ts ├── entries │ ├── background │ │ ├── index.ts │ │ ├── migrate.ts │ │ └── service.ts │ ├── content.ts │ ├── import_friends │ │ ├── index.html │ │ └── main.ts │ └── popup │ │ ├── index.html │ │ ├── main.ts │ │ └── style.css ├── public │ └── icons │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-24.png │ │ ├── icon-48.png │ │ └── icon-96.png └── values │ ├── config │ ├── app.ts │ ├── colors.ts │ ├── index.ts │ └── strings.ts │ ├── html │ ├── company_tag.html │ ├── contest_friend_table.html │ ├── contest_friend_table_column.html │ ├── friend_row.html │ ├── friend_table.html │ ├── premium_company_tags_body.html │ ├── ps_frequency_column.html │ └── ps_problem_row.html │ ├── selectors │ ├── index.ts │ └── lc │ │ ├── contest.ts │ │ ├── friend.ts │ │ ├── index.ts │ │ ├── navbar.ts │ │ ├── problem.ts │ │ ├── profile.ts │ │ └── static_dom.ts │ └── svg │ ├── friend_down_arrow.svg │ ├── friend_up_arrow.svg │ ├── friend_updown_arrow.svg │ ├── people_dark.svg │ ├── people_icon.svg │ ├── people_light.svg │ ├── ps_notac.svg │ ├── ps_video_solution.svg │ └── star_icon.svg ├── tsconfig.json └── wxt.config.ts /.github/workflows/auto-release.yml: -------------------------------------------------------------------------------- 1 | name: Auto Release Workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | # Add permissions block to grant necessary access 10 | permissions: 11 | contents: write # This grants write access to repository contents including releases 12 | 13 | jobs: 14 | check-and-release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '18' 26 | 27 | - name: Get current version 28 | id: current_version 29 | run: | 30 | CURRENT_VERSION=$(node -p "require('./package.json').version") 31 | echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT 32 | 33 | - name: Check if version changed 34 | id: check_version 35 | run: | 36 | # Get the previous commit hash 37 | PREV_COMMIT=$(git rev-parse HEAD^) 38 | 39 | # Check if package.json existed in the previous commit 40 | if git ls-tree $PREV_COMMIT --name-only | grep -q "package.json"; then 41 | # Get previous version 42 | PREV_VERSION=$(git show $PREV_COMMIT:package.json | node -p "JSON.parse(require('fs').readFileSync(0)).version") 43 | 44 | # Compare versions 45 | if [ "$PREV_VERSION" != "${{ steps.current_version.outputs.version }}" ]; then 46 | echo "changed=true" >> $GITHUB_OUTPUT 47 | echo "Version changed from $PREV_VERSION to ${{ steps.current_version.outputs.version }}" 48 | else 49 | echo "changed=false" >> $GITHUB_OUTPUT 50 | echo "Version unchanged: ${{ steps.current_version.outputs.version }}" 51 | fi 52 | else 53 | # If package.json didn't exist before, consider it a new version 54 | echo "changed=true" >> $GITHUB_OUTPUT 55 | echo "New package.json detected with version ${{ steps.current_version.outputs.version }}" 56 | fi 57 | 58 | - name: Get commit message 59 | id: commit_message 60 | run: | 61 | COMMIT_MSG=$(git log -1 --pretty=%B) 62 | echo "message<> $GITHUB_OUTPUT 63 | echo "$COMMIT_MSG" >> $GITHUB_OUTPUT 64 | echo "EOF" >> $GITHUB_OUTPUT 65 | 66 | - name: Check if release exists 67 | id: check_release 68 | if: ${{ steps.check_version.outputs.changed == 'false' }} 69 | run: | 70 | # Using gh API directly to avoid parsing issues 71 | if gh api repos/${{ github.repository }}/releases/tags/v${{ steps.current_version.outputs.version }} --silent 2>/dev/null; then 72 | echo "exists=true" >> $GITHUB_OUTPUT 73 | else 74 | echo "exists=false" >> $GITHUB_OUTPUT 75 | fi 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | 79 | - name: Create new release 80 | if: ${{ steps.check_version.outputs.changed == 'true' }} 81 | run: | 82 | # Get the last 1 commit with short hashes 83 | CHANGELOG=$(git log -1 --pretty=format:"- %h %s" --reverse) 84 | 85 | # Create release with changelog 86 | gh release create v${{ steps.current_version.outputs.version }} \ 87 | --title "${{ steps.current_version.outputs.version }}" \ 88 | --notes "## Published version $CHANGELOG _($(date +%Y-%m-%d))_" \ 89 | --target ${{ github.ref_name }} 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | 93 | - name: Update existing release 94 | if: ${{ steps.check_version.outputs.changed == 'false' && steps.check_release.outputs.exists == 'true' }} 95 | run: | 96 | # Get current release notes 97 | CURRENT_NOTES=$(gh release view v${{ steps.current_version.outputs.version }} --json body -q .body) 98 | 99 | # Get latest commit info (short hash and message) 100 | LATEST_COMMIT=$(git log -1 --pretty=format:"- %h %s") 101 | 102 | # Append new commit message with short hash 103 | NEW_NOTES="${CURRENT_NOTES} 104 | 105 | ${LATEST_COMMIT} _($(date +%Y-%m-%d))_" 106 | 107 | # Update release notes 108 | gh release edit v${{ steps.current_version.outputs.version }} --notes "${NEW_NOTES}" 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | 112 | - name: Create release for unchanged version with no existing release 113 | if: ${{ steps.check_version.outputs.changed == 'false' && steps.check_release.outputs.exists == 'false' }} 114 | run: | 115 | # Get the last 1 commits with short hashes 116 | CHANGELOG=$(git log -1 --pretty=format:"- %h %s" --reverse) 117 | 118 | # Create release with changelog 119 | gh release create v${{ steps.current_version.outputs.version }} \ 120 | --title "${{ steps.current_version.outputs.version }}" \ 121 | --notes "## Published version $CHANGELOG _($(date +%Y-%m-%d))_" \ 122 | --target ${{ github.ref_name }} 123 | env: 124 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .output 12 | stats.html 13 | stats-*.json 14 | .wxt 15 | web-ext.config.ts 16 | _ 17 | *.lock 18 | dist/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 binbard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leet Xt - Extend Your LeetCode 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | [![TypeScript](https://img.shields.io/badge/TypeScript-4.9%2B-blue)](https://www.typescriptlang.org/) 5 | [![WXT](https://img.shields.io/badge/Framework-WXT-purple)](https://wxt.dev/) 6 | 7 | A feature-rich browser extension that enhances your LeetCode experience with friends management, contest predictions, and premium features. 8 | 9 | ## ✨ Features 10 | 11 | - **Friends Management** 12 | - Add users to Friends Page from their Profile Page (⭐ star button) 13 | - View all friends and their data from one place 14 | - Import/Export friends list 15 | 16 | - **Contest Enhancements** 17 | - Predict your rating before official results 18 | - View friends' participation and predicted ratings 19 | 20 | - **Problem Insights** 21 | - View all problems by company (with duration + frequency) 22 | - See problem ratings, contest, and company tags 23 | 24 | ## 🏗️ Project Structure 25 | 26 | Built with **WXT Framework** (WebExtensions + TypeScript) for robust extension development. 27 | 28 | ``` 29 | src/ 30 | ├── components/ # Reusable UI components 31 | │ 32 | ├── core/ # Core application logic 33 | │ ├── app.ts # Main application class 34 | │ ├── manager.ts # Base manager class 35 | │ ├── defines/ # Mostly enums 36 | │ ├── interfaces/ # TypeScript interfaces 37 | │ ├── modules/ # Feature modules 38 | │ └── utils/ # Sub-managers and helpers 39 | │ 40 | ├── entries/ # Extension entry points 41 | │ ├── content.ts # Content script entry 42 | │ ├── background/ # Background service worker 43 | │ ├── import_friends/ # Firefox fix for file import 44 | │ └── popup/ # Extension popup 45 | │ 46 | ├── public/ # Static assets 47 | │ └── icons/ # Extension icons 48 | │ 49 | └── values/ # Configuration values 50 | ├── config/ # App configuration 51 | ├── html/ # HTML components 52 | ├── selectors/ # DOM selectors 53 | └── svg/ # SVG assets 54 | ``` 55 | 56 | ### Key Architectural Decisions 57 | 58 | 1. **Modular Design** 59 | - Features are split into independent modules in `core/modules/` 60 | - Each module implements `IModule` interface for consistent lifecycle 61 | - Enables easy feature toggling and maintenance 62 | 63 | 2. **Separate Selectors** 64 | - All DOM selectors are centralized in `values/selectors/` 65 | - Organized by LeetCode page type (contest, problem, profile) 66 | - Makes UI changes easier to manage 67 | 68 | 3. **HTML Components** 69 | - Templates stored in `values/html/` as separate files 70 | - Loaded dynamically to keep TSX/JSX minimal 71 | - Promotes separation of concerns 72 | 73 | 4. **Type Safety** 74 | - Comprehensive type definitions 75 | - Strict TypeScript configuration 76 | 77 | ## � Installation 78 | 79 | ### Development 80 | 81 | Note: Alternatively npm can be used instead of bun 82 | 83 | 1. Fork this repository 84 | 85 | Fork 86 | 87 | 3. Clone the repository 88 | ```bash 89 | git clone https://github.com//leet-xt.git 90 | cd leet-xt 91 | ``` 92 | 93 | 4. Install dependencies 94 | ```bash 95 | bun install 96 | ``` 97 | 98 | 5. Start development server 99 | ```bash 100 | bun run dev 101 | ``` 102 | 103 | ### Production Build 104 | 105 | ```bash 106 | bun run build 107 | ``` 108 | 109 | ## � Contributing 110 | 111 | We welcome contributions! Please follow these steps: 112 | 113 | 1. Make sure you have forked and cloned the repository 114 | 2. Create a feature branch (`git checkout -b feature/AmazingFeature`) 115 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 116 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 117 | 5. Open a Pull Request 118 | 119 | ## 📜 License 120 | 121 | Distributed under the MIT License. See `LICENSE` for more information. 122 | 123 | ## 📬 Contact 124 | 125 | Leet Xt - leet-xt@binbard.org 126 | 127 | Project Link: [https://leet-xt.js.org/](https://leet-xt.js.org/) 128 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | leet-xt.js.org 2 | -------------------------------------------------------------------------------- /docs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/assets/icons/logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/icons/logo_32.png -------------------------------------------------------------------------------- /docs/assets/images/add_friend_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/add_friend_button.png -------------------------------------------------------------------------------- /docs/assets/images/chrome_webstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/chrome_webstore.png -------------------------------------------------------------------------------- /docs/assets/images/company_problems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/company_problems.png -------------------------------------------------------------------------------- /docs/assets/images/contest_company_tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/contest_company_tags.png -------------------------------------------------------------------------------- /docs/assets/images/friends_contest_rating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/friends_contest_rating.png -------------------------------------------------------------------------------- /docs/assets/images/friends_import_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/friends_import_export.png -------------------------------------------------------------------------------- /docs/assets/images/friends_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/friends_list.png -------------------------------------------------------------------------------- /docs/assets/images/friends_tab_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/friends_tab_icon.png -------------------------------------------------------------------------------- /docs/assets/images/per_contest_rating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/per_contest_rating.png -------------------------------------------------------------------------------- /docs/assets/images/problem_editorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/assets/images/problem_editorial.png -------------------------------------------------------------------------------- /docs/assets/svg/get-the-addon-fx-apr-2020.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /docs/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Arial, sans-serif; 5 | background-color: #f8f8f8; 6 | display: flex; 7 | flex-direction: column; 8 | height: 100vh; 9 | overflow: hidden; 10 | } 11 | 12 | header { 13 | background-color: #333; 14 | color: #fff; 15 | padding: 20px; 16 | text-align: center; 17 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 18 | } 19 | 20 | .logo{ 21 | float: left; 22 | } 23 | 24 | .title{ 25 | display: inline; 26 | color: #ff8c00; 27 | } 28 | 29 | .github-a svg{ 30 | display: inline; 31 | position: relative; 32 | top: 0.2em; 33 | left: 1em; 34 | border-radius: 50%; 35 | } 36 | 37 | .github-a svg:hover{ 38 | color: #ff8c00; 39 | } 40 | 41 | .desc{ 42 | position: relative; 43 | left: 0.5em; 44 | color: #cbcbcb; 45 | } 46 | 47 | /* e0e0de */ 48 | 49 | .github-svg rect{ 50 | fill: #fea014; 51 | } 52 | 53 | .container { 54 | flex: 1; 55 | display: flex; 56 | overflow: hidden; 57 | } 58 | 59 | .sidebar { 60 | background-color: #444; 61 | color: #fff; 62 | padding: 0px; 63 | overflow-y: auto; 64 | width: 0; 65 | overflow: hidden; 66 | transition: 0.3s; 67 | } 68 | 69 | .sidebar-toggle-button { 70 | background-color: #333; 71 | color: white; 72 | border: none; 73 | padding: 6px 12px; 74 | cursor: pointer; 75 | z-index: 1; 76 | margin: 10px; 77 | position: relative; 78 | left: -20px; 79 | top: -20px; 80 | } 81 | 82 | .sidebar.open { 83 | width: 250px; 84 | } 85 | 86 | /**** pc devices ****/ 87 | @media only screen and (min-width: 768px) { 88 | .sidebar { 89 | width: 250px; 90 | } 91 | .sidebar-toggle-button { 92 | display: none; 93 | } 94 | } 95 | 96 | 97 | .feature-list { 98 | list-style: none; 99 | padding: 0; 100 | } 101 | 102 | .feature-list li { 103 | margin-bottom: 10px; 104 | cursor: pointer; 105 | transition: background-color 0.2s; 106 | padding: 10px; 107 | } 108 | 109 | .feature-list li:hover { 110 | background-color: #555; 111 | border-left: 5px solid #ff8c00; 112 | } 113 | 114 | .content { 115 | flex: 1; 116 | padding: 20px; 117 | overflow-y: auto; 118 | background-color: #fff; 119 | box-shadow: -5px 0 10px rgba(0, 0, 0, 0.1); 120 | } 121 | 122 | .feature-title { 123 | font-size: 24px; 124 | margin-bottom: 10px; 125 | color: #333; 126 | } 127 | 128 | .feature-screenshot { 129 | max-width: 100%; 130 | height: auto; 131 | border: 1px solid #ddd; 132 | border-radius: 5px; 133 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 134 | } 135 | 136 | .feature-description { 137 | font-size: 16px; 138 | color: #444; 139 | line-height: 1.5; 140 | } 141 | 142 | 143 | .button { 144 | display: inline-block; 145 | padding: 10px 20px; 146 | background-color: #ff8c00; 147 | color: #fff; 148 | border: none; 149 | border-radius: 5px; 150 | cursor: pointer; 151 | font-weight: bold; 152 | transition: background-color 0.2s; 153 | } 154 | 155 | .button:hover { 156 | background-color: #ff6600; 157 | } 158 | 159 | .hidden { 160 | display: none; 161 | } 162 | 163 | 164 | .download_webstore{ 165 | position: absolute; 166 | bottom: 10vh; 167 | right: 5vw; 168 | width: 250px; 169 | opacity: 0.4; 170 | } 171 | 172 | .download_webstore.firefox{ 173 | /* bottom: 20vh; 174 | max-width: 150px; */ 175 | opacity: 0.2; 176 | } 177 | 178 | .download_webstore:hover{ 179 | opacity: 1; 180 | } 181 | 182 | .download_webstore.firefox:hover{ 183 | opacity: 0.6; 184 | } 185 | 186 | /**** mobile devices ****/ 187 | @media only screen and (max-width: 768px) { 188 | .download_webstore{ 189 | bottom: 5vh; 190 | right: 5vw; 191 | width: 150px; 192 | } 193 | .content{ 194 | padding-bottom: 20vw; 195 | } 196 | } -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/docs/favicon.ico -------------------------------------------------------------------------------- /docs/googleecc5c3bfdf8850e6.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googleecc5c3bfdf8850e6.html -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leet Xt 6 | 7 | 8 | 9 | 11 | 12 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 29 |

All Features

30 | 91 | 92 |
93 | 94 | 97 | 100 | 101 |
102 | 118 |
119 | 120 |
121 |
Friends
122 |

123 | 1) Add Users to Friends Page from their Profile Page (using the star button).
124 |

125 | Add Friend Button

127 | 128 |

129 | 2) Goto Friends Page from Anywhere (using the friends icon).
130 |

131 | Friends Tab Icon

132 | 133 |

134 | 3) View Users on Friends Page (sortable).
135 |

136 | Friends List View

137 | 138 |

139 | 4) Import / Export of Friends is supported (from extension options).
140 |

141 | Friends Import Export

142 | 143 |
144 | 162 | 177 | 187 |
188 |
189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /docs/js/index.js: -------------------------------------------------------------------------------- 1 | function showFeature(tabname) { 2 | const features = document.querySelectorAll('.feature'); 3 | features.forEach(feature => { 4 | feature.classList.add('hidden'); 5 | }); 6 | 7 | const selectedFeature = document.getElementById(tabname); 8 | if (selectedFeature) { 9 | selectedFeature.classList.remove('hidden'); 10 | document.querySelector('.sidebar').classList.toggle('open'); 11 | } 12 | } 13 | 14 | function addClickHandlers() { 15 | document.querySelectorAll('ul.feature-list li').forEach((li) => { 16 | li.addEventListener('click', () => { 17 | showFeature(li.getAttribute('name')); 18 | }); 19 | }); 20 | } 21 | 22 | function initHandler() { 23 | if (window.location.href.startsWith('http')) { 24 | document.querySelector('h1.title').innerHTML = document.title; 25 | document.querySelectorAll('.web').forEach((el) => { 26 | el.classList.remove('hidden'); 27 | }); 28 | } 29 | 30 | if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1){ 31 | document.querySelector('.firefox').classList.remove('hidden'); 32 | } else if(navigator.userAgent.toLowerCase().indexOf('chrome') > -1){ 33 | document.querySelector('.chrome').classList.remove('hidden'); 34 | } 35 | 36 | const sidebar = document.querySelector('.sidebar'); 37 | const sidebarToggle = document.getElementById('sidebar-toggle'); 38 | 39 | sidebarToggle.addEventListener('click', () => { 40 | sidebar.classList.toggle('open'); 41 | }); 42 | } 43 | 44 | 45 | initHandler(); 46 | 47 | addClickHandlers(); -------------------------------------------------------------------------------- /docs/p/privacy-policy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Leet Xt - Privacy Policy 8 | 122 | 123 | 124 | 125 | 126 | 127 |

Leet Xt - Privacy Policy

128 | 129 |

Last updated: 17/01/2024

130 | 131 |

Thank you for using Leet Xt, a browser extension for enhancing your LeetCode experience. This privacy policy explains how we handle user data.

132 | 133 |

Information Collection

134 | 135 |

We does not collect or store any personally identifiable information. We prioritizes user privacy by refraining from collecting or storing any personally identifiable information, ensuring the security of your account credentials and sensitive data is never compromised. Your account credentials and any sensitive information remain entirely secure, as we intentionally do not have access to such details. We understand the sensitivity of personal data and are dedicated to ensuring that your experience with us is not only enhanced but also secure and respectful of your privacy. Our commitment extends to the principle of data minimization, meaning we only collect the information necessary for the core functionality of our product. We prioritize your trust and confidence, assuring you that your privacy is paramount throughout your usage our product. If you have any concerns or questions about the information we collect, we encourage you to reach out for further clarification. Your privacy is not just a policy for us but a fundamental value that shapes every aspect of your experience with us.

136 | 137 |

Usage Data

138 | 139 |

In our commitment to continuous improvement and delivering a top-notch user experience, We may collect anonymized usage data. This information is carefully analyzed to gain insights into how users interact with the extension. By understanding usage patterns, we can identify areas for enhancement, refine features, and optimize overall performance. Rest assured, this usage data is entirely anonymized, meaning it is devoid of any personally identifiable information. We prioritize the privacy and security of our users, ensuring that the collected data serves only analytical purposes, contributing to the ongoing refinement and evolution. Your participation in this process is instrumental in shaping a more intuitive and user-friendly extension for the entire community.

140 | 141 |

Cookies

142 | 143 |

We utilize cookies for local storage and session management, playing an essential role in optimizing your user experience. These cookies are specifically designed to be stored locally on your device and are crucial for the proper functioning of the product. One important aspect involves the use of local storage for better functionality, ensuring seamless access and retrieval of this information to enhance your overall product experience. It's crucial to note that these cookies are not meant to be shared remotely or any 3rd-party platform, and are solely geared towards providing a personalized and efficient product experience.

144 | 145 |

Information Sharing

146 | 147 |

For us, safeguarding your privacy is paramount. We want to assure you that we do not share any user data with third parties under any circumstances. Your information is treated with the utmost confidentiality and is strictly used within the confines of our ecosystem. We operate on a principle of trust, and our commitment extends to refraining from engaging in the sale or exchange of user information. This policy is rooted in our dedication to providing a secure and trustworthy environment for our users. When you use our product, you can be confident that your data is handled responsibly and that your privacy remains a top priority throughout your interaction with our extension. If you have any concerns or questions about how your information is handled, please do not hesitate to reach out to us for clarification and reassurance. Your peace of mind is essential to us, and we are committed to maintaining a transparent and user-centric approach to data management.

148 | 149 |

Security

150 | 151 |

While we take reasonable measures to protect user data, please be aware that no method of internet transmission is entirely secure. We strive to maintain the security of any information you provide but cannot guarantee its absolute security.

152 | 153 |

Changes to this Privacy Policy

154 | 155 |

In our commitment to transparency and keeping you informed, We may periodically update this privacy policy to reflect changes in our practices, services, or to comply with legal requirements. We encourage you to check this page regularly for any updates. The latest version of the privacy policy will be prominently displayed with the effective date, ensuring you have access to the most current information regarding how your data is handled. We value your trust and want to be as transparent as possible about any adjustments we make to this policy, ensuring that you are always aware of how your privacy is safeguarded within our ecosystem. If you have any questions or concerns about specific changes, please don't hesitate to reach out to us for clarification. Your understanding and trust are crucial to the ongoing success of the product, and we appreciate your continued support.

156 | 157 |

Contact Us

158 | 159 |

If you have any questions or concerns regarding this privacy policy, please contact us at:   binbardx [at] gmail [dot] com.

160 | 161 |
162 | 2024 Leet Xt. All rights reserved. 163 |
164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leet-xt", 3 | "description": "Extend your Leetcode", 4 | "version": "1.0.5", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "wxt", 8 | "dev:firefox": "wxt -b firefox", 9 | "build": "wxt build", 10 | "build:firefox": "wxt build -b firefox", 11 | "zip": "wxt zip", 12 | "zip:firefox": "wxt zip -b firefox", 13 | "compile": "tsc --noEmit", 14 | "postinstall": "wxt prepare" 15 | }, 16 | "devDependencies": { 17 | "@types/chrome": "^0.0.280", 18 | "typescript": "^5.6.3", 19 | "wxt": "^0.19.13" 20 | }, 21 | "dependencies": { 22 | "@webext-core/proxy-service": "^1.2.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/companyTagsPremium.ts: -------------------------------------------------------------------------------- 1 | import PREMIUM_COMPANY_TAGS_BODY_HTML from "@/values/html/premium_company_tags_body.html?raw"; 2 | 3 | import { docFind, parseHTML } from "@/core/utils/helpers"; 4 | 5 | function getCompanyTagsModalContentDiv(message: string): HTMLDivElement { 6 | const div = parseHTML(PREMIUM_COMPANY_TAGS_BODY_HTML); 7 | const span = docFind("span", div); 8 | 9 | span.textContent = message; 10 | 11 | return div as HTMLDivElement; 12 | } 13 | 14 | function getCompanyTagsModalATag(link: string, message: string): HTMLAnchorElement { 15 | const a = document.createElement("a"); 16 | a.classList.add("text-blue"); 17 | a.classList.add("dark:text-dark-blue"); 18 | 19 | a.setAttribute("href", link); 20 | 21 | a.setAttribute("target", "_blank"); 22 | 23 | a.textContent = message; 24 | 25 | return a as HTMLAnchorElement; 26 | } 27 | 28 | export { getCompanyTagsModalContentDiv, getCompanyTagsModalATag } -------------------------------------------------------------------------------- /src/components/friendsTable.ts: -------------------------------------------------------------------------------- 1 | import FRIEND_ROW_HTML from "@/values/html/friend_row.html?raw" 2 | 3 | import { IFriendData } from "@/core/utils/leetcodeManager"; 4 | import { docFind } from "@/core/utils/helpers"; 5 | import Selectors from "@/values/selectors"; 6 | import Manager from "@/core/manager"; 7 | import Config from "@/values/config"; 8 | 9 | function getFriendsTableRow(friendData: IFriendData): HTMLDivElement { 10 | const div = document.createElement('div'); 11 | div.innerHTML = FRIEND_ROW_HTML; 12 | const row = docFind('div', div) as HTMLDivElement; 13 | 14 | const favatar = docFind(Selectors.lc.friend.table.row_group.row.avatar, row) as HTMLImageElement; 15 | const fname = docFind(Selectors.lc.friend.table.row_group.row.name, row) as HTMLAnchorElement; 16 | 17 | favatar.src = friendData.avatar; 18 | fname.innerHTML = friendData.displayName; 19 | fname.href = `/u/${friendData.username}`; 20 | 21 | const hoverCard = document.createElement('iframe'); 22 | hoverCard.setAttribute('class', 'absolute hidden p-0 m-0 rounded-xl w-[300px] h-[120px]'); 23 | 24 | fname.appendChild(hoverCard); 25 | fname.addEventListener('mouseenter', function () { 26 | hoverCard.classList.remove('hidden'); 27 | hoverCard.style.opacity = "0"; 28 | let theme; 29 | 30 | if (Manager.Leetcode.isDarkTheme()) { 31 | theme = 'nord'; 32 | hoverCard.style.border = "none"; 33 | } else { 34 | theme = 'light'; 35 | hoverCard.style.border = "1px solid #E5E7EB"; 36 | } 37 | hoverCard.src = `${Config.App.LEETCARD_BASE_URL}/${friendData.username}?theme=${theme}&border=0&radius=10&sheets=https://bit.ly/M3NpQr7`; 38 | }); 39 | 40 | fname.addEventListener('mouseleave', function () { 41 | hoverCard.classList.add('hidden'); 42 | }); 43 | 44 | hoverCard.addEventListener('load', function () { 45 | hoverCard.style.opacity = "1"; 46 | }); 47 | 48 | const rowSelector = Selectors.lc.friend.table.row_group.row; 49 | 50 | docFind(rowSelector.rating, row).innerHTML = friendData.rating.toString() === "-" ? friendData.rating.toString() : Math.round(friendData.rating).toString(); 51 | docFind(rowSelector.numcontest, row).innerHTML = friendData.contests == "-" ? "" : "(" + friendData.contests + ")"; 52 | docFind(rowSelector.problems_solved, row).innerHTML = friendData.problems_solved.toString(); 53 | docFind(rowSelector.easy, row).innerHTML = friendData.easy.toString(); 54 | docFind(rowSelector.medium, row).innerHTML = friendData.medium.toString(); 55 | docFind(rowSelector.hard, row).innerHTML = friendData.hard.toString(); 56 | docFind(rowSelector.top, row).innerHTML = friendData.top == "-" ? friendData.top : friendData.top + "%"; 57 | 58 | return row; 59 | } 60 | 61 | export { getFriendsTableRow } -------------------------------------------------------------------------------- /src/components/navbarFriendsIcon.ts: -------------------------------------------------------------------------------- 1 | import PEOPLE_ICON_SVG from "@/values/svg/people_icon.svg?raw"; 2 | 3 | function getNavbarFriendsIcon(url: string): HTMLAnchorElement { 4 | let a = document.createElement('a'); 5 | a.href = url; 6 | a.setAttribute("class", "group relative flex h-8 p-1 items-center justify-center rounded hover:bg-fill-3 dark:hover:bg-dark-fill-3"); 7 | 8 | let parser = new DOMParser(); 9 | let doc = parser.parseFromString(PEOPLE_ICON_SVG, 'image/svg+xml'); 10 | let svg = doc.querySelector('svg'); 11 | svg?.setAttribute('fill', 'currentColor'); 12 | if (svg) a.appendChild(svg); 13 | 14 | return a; 15 | } 16 | 17 | export { getNavbarFriendsIcon } -------------------------------------------------------------------------------- /src/core/app.ts: -------------------------------------------------------------------------------- 1 | import Manager from "./manager"; 2 | import * as modules from "./modules"; 3 | 4 | class App { 5 | constructor() { 6 | Manager.Logger.log(App.name, 'LeetXt initialized'); 7 | } 8 | 9 | async applyModules() { 10 | if (await Manager.Meta.getActivated() === false) { 11 | return; 12 | } 13 | 14 | const path = window.location.pathname; 15 | // Manager.Logger.log(path); 16 | 17 | for (const module of Object.values(modules)) { 18 | const mod = new module(); 19 | 20 | if ('blacklist_pages' in mod && mod.blacklist_pages) { 21 | for (const blacklistString of mod.blacklist_pages!!) { 22 | const blacklist = new RegExp(blacklistString); 23 | 24 | if (blacklist.test(path)) { 25 | Manager.Logger.log(App.name, `Blacklisted module: ${module.name}`); 26 | break; 27 | } 28 | } 29 | } 30 | 31 | for (const regexString of mod.pages) { 32 | const regex = new RegExp(regexString); 33 | 34 | // Manager.Logger.log(regex); 35 | 36 | if (regex.test(path)) { 37 | Manager.Logger.log(App.name, `Applying module: ${module.name}`); 38 | 39 | mod.apply(); 40 | break; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default App; -------------------------------------------------------------------------------- /src/core/defines/browserType.ts: -------------------------------------------------------------------------------- 1 | export enum BrowserType { 2 | Chrome = "Chrome", 3 | Firefox = "Firefox", 4 | Other = "Other", 5 | } -------------------------------------------------------------------------------- /src/core/defines/buildModes.ts: -------------------------------------------------------------------------------- 1 | export enum BuildMode { 2 | DEV = "development", 3 | PROD = "production", 4 | TEST = "testing", 5 | STAGE = "staging", 6 | } -------------------------------------------------------------------------------- /src/core/defines/pageType.ts: -------------------------------------------------------------------------------- 1 | export enum PageType { 2 | Explore = "/explore/", 3 | Problems = "/problemset/", 4 | Contest = "/contest/", 5 | Discuss = "/discuss/", 6 | Friends = "/friends/", 7 | 8 | PROFILE = "/u/[\\w-]+/", 9 | PROBLEM = "/problems/.*", 10 | CONTEST = "/contest/(weekly|biweekly)-contest-\\d+/(?:ranking/)?", 11 | FRIENDS = "/(u/)?friends/", 12 | 13 | ALL = ".*" 14 | } -------------------------------------------------------------------------------- /src/core/defines/requestMethod.ts: -------------------------------------------------------------------------------- 1 | export enum RequestMethod { 2 | GET = 'GET', 3 | POST = 'POST', 4 | } -------------------------------------------------------------------------------- /src/core/defines/responseType.ts: -------------------------------------------------------------------------------- 1 | export enum ResponseType { 2 | JSON = 'json', 3 | TEXT = 'text', 4 | BLOB = 'blob', 5 | FORM_DATA = 'formData', 6 | ARRAY_BUFFER = 'arrayBuffer', 7 | DOCUMENT = 'document', 8 | DEFAULT = 'default' 9 | } -------------------------------------------------------------------------------- /src/core/defines/result.ts: -------------------------------------------------------------------------------- 1 | export enum Result { 2 | OK = "Sucess", 3 | ERROR = "Error", 4 | TIMEOUT = "Timeout", 5 | NO_DATA = "No data available", 6 | INVALID = "Invalid response", 7 | UNKNOWN = "Unknown error" 8 | } -------------------------------------------------------------------------------- /src/core/defines/sortType.ts: -------------------------------------------------------------------------------- 1 | export enum SortType { 2 | ASC = "asc", 3 | DESC = "desc" 4 | } -------------------------------------------------------------------------------- /src/core/interfaces/module.ts: -------------------------------------------------------------------------------- 1 | import { PageType } from "../defines/pageType"; 2 | 3 | export interface IModule { 4 | 5 | // apply is a method that is called when the module is loaded 6 | apply(): void; 7 | 8 | // action is a method that is called when the module is executed 9 | action(): void; 10 | 11 | // enabled is a property that can be used to enable or disable the module 12 | enabled?: boolean; 13 | 14 | // pages is a property that can be used to specify the pages on which the module should be loaded 15 | pages: PageType[] 16 | 17 | // blacklist_pages is a property that can be used to specify the pages on which the module should not be loaded 18 | blacklist_pages?: PageType[] 19 | 20 | } -------------------------------------------------------------------------------- /src/core/manager.ts: -------------------------------------------------------------------------------- 1 | import { FriendManager } from "./utils/friendManager"; 2 | import { LeetcodeManager } from "./utils/leetcodeManager"; 3 | import { LogManager } from "./utils/logManager"; 4 | import { MetaManager } from "./utils/metaManager"; 5 | import { StorageManager } from "./utils/storageManager"; 6 | 7 | class Manager { 8 | constructor() { 9 | Manager.Logger.log(Manager.name, 'This Manager will never be hired'); 10 | } 11 | 12 | static Meta = MetaManager 13 | static Friend = FriendManager 14 | static Leetcode = LeetcodeManager 15 | static Logger = LogManager 16 | static Storage = StorageManager 17 | } 18 | 19 | export default Manager; -------------------------------------------------------------------------------- /src/core/modules/contestRank.ts: -------------------------------------------------------------------------------- 1 | import CONTEST_FRIEND_TABLE_HTML from "@/values/html/contest_friend_table.html?raw" 2 | import CONSTEST_FRIEND_TABLE_COLUMN_HTML from "@/values/html/contest_friend_table_column.html?raw" 3 | 4 | import PEOPLE_DARK_SVG from "@/values/svg/people_dark.svg?raw" 5 | import PEOPLE_LIGHT_SVG from "@/values/svg/people_light.svg?raw" 6 | 7 | import { IModule } from "@/core/interfaces/module"; 8 | import { PageType } from "@/core/defines/pageType"; 9 | import { checkDone, docFind, getStringValue, mutObserve } from "@/core/utils/helpers"; 10 | import { Result } from "../defines/result"; 11 | import { IUserContestDetails } from "../utils/leetcodeManager"; 12 | 13 | import Manager from "@/core/manager"; 14 | import Selectors from "@/values/selectors"; 15 | 16 | export class ContestRank implements IModule { 17 | 18 | static async toggleFriendMode() { 19 | try { 20 | let table_container = docFind(Selectors.lc.contest.ranking.container.table_container); 21 | 22 | let original_table = docFind(Selectors.lc.contest.ranking.container.table_container.original_table, table_container) as HTMLTableElement; 23 | let friend_table = table_container.querySelector(Selectors.lc.contest.ranking.container.table_container.friend_table.self) as HTMLTableElement; 24 | 25 | let pagination_nav = document.querySelector(Selectors.lc.contest.ranking.container.pagination_nav) as HTMLElement; 26 | 27 | if (friend_table == null) { // Initially friend_table will be null 28 | original_table.style.display = "none"; 29 | 30 | table_container.innerHTML += CONTEST_FRIEND_TABLE_HTML; 31 | friend_table = docFind(Selectors.lc.contest.ranking.container.table_container.friend_table) as HTMLTableElement; 32 | friend_table.style.display = "none"; 33 | } 34 | 35 | let div = docFind(Selectors.lc.contest.ranking.container.people_icon.mode); 36 | if (div.querySelector(Selectors.lc.contest.ranking.container.people_icon.dark) == null) { 37 | div.innerHTML = PEOPLE_DARK_SVG; 38 | original_table.style.display = "none"; 39 | friend_table.style.display = "none"; 40 | friend_table.style.display = "table"; 41 | pagination_nav.style.display = "none"; 42 | 43 | if (checkDone(friend_table)) return; 44 | await ContestRank.setContestFriends(); 45 | } else { 46 | div.innerHTML = PEOPLE_LIGHT_SVG; 47 | original_table.style.display = "table"; 48 | friend_table.style.display = "none"; 49 | pagination_nav.style.display = "flex"; 50 | } 51 | } catch (e: any) { 52 | Manager.Logger.warn(ContestRank.name, e); 53 | } 54 | } 55 | 56 | static async setContestFriends() { 57 | try { 58 | let friend_table = docFind(Selectors.lc.contest.ranking.container.table_container.friend_table); 59 | let friend_table_body = docFind(Selectors.lc.contest.ranking.container.table_container.friend_table.body, friend_table); 60 | friend_table_body.innerHTML = friend_table_body.children[0].outerHTML; 61 | let friend_list = []; 62 | 63 | const contestName = window.location.pathname.split("/")[2]; 64 | const friendList = await Manager.Friend.getFriends(); 65 | 66 | const totalFriends = friendList.length; 67 | let friendsParticipated = 0; 68 | 69 | let contestNotFound = false; 70 | let errorLoadingData = false; 71 | 72 | let promises = friendList.map(async (friend) => { 73 | if (contestNotFound) return; 74 | 75 | try { 76 | let userContestDetails = await Manager.Leetcode.getUserContestDetails(friend); 77 | if (userContestDetails === null) return; 78 | 79 | userContestDetails.username = friend; 80 | friend_list.push(userContestDetails); 81 | 82 | friendsParticipated++; 83 | 84 | friend_list.sort((a, b) => { 85 | return a.rank - b.rank; 86 | }); 87 | 88 | friend_table_body.innerHTML = friend_table_body.children[0].outerHTML; 89 | 90 | for (const friend of friend_list) { 91 | const row = this.getRankingTableRow(friend, contestName); 92 | if (!row) return; 93 | friend_table_body.appendChild(row); 94 | } 95 | } catch (e: any) { 96 | if (e === Result.NO_DATA) { 97 | contestNotFound = true; 98 | } 99 | else if (e == Result.ERROR) { 100 | errorLoadingData = true; 101 | } 102 | } 103 | }); 104 | 105 | try { 106 | await Promise.all(promises); 107 | 108 | if (totalFriends == 0) { 109 | const row = this.getRankingTableMessage('No Friends Added'); 110 | if (!row) return; 111 | friend_table_body.innerHTML = ""; 112 | friend_table_body.appendChild(row); 113 | } 114 | else if(friendsParticipated == 0) { 115 | const row = this.getRankingTableMessage(`${friendsParticipated}/${totalFriends} Friends participated`); 116 | if (!row) return; 117 | friend_table_body.innerHTML = friend_table_body.children[0].outerHTML; 118 | friend_table_body.appendChild(row); 119 | } 120 | else if (contestNotFound) { 121 | const row = this.getRankingTableMessage('Contest Not Found'); 122 | if (!row) return; 123 | friend_table_body.innerHTML = ""; 124 | friend_table_body.appendChild(row); 125 | } 126 | else if (errorLoadingData) { 127 | const row = this.getRankingTableMessage('Error Loading Data'); 128 | if (!row) return; 129 | friend_table_body.innerHTML = ""; 130 | friend_table_body.appendChild(row); 131 | } 132 | 133 | } catch (e: any) { 134 | Manager.Logger.warn(ContestRank.name, e); 135 | throw e; 136 | } 137 | } 138 | 139 | catch (e: any) { 140 | Manager.Logger.error(ContestRank.name, e); 141 | } 142 | } 143 | 144 | static getRankingTableMessage(message: string): Element | null { 145 | try { 146 | const row = docFind(Selectors.lc.contest.ranking.container.table_container.friend_table.body.row); 147 | const rowClone = row.cloneNode(true) as Element; 148 | if (!rowClone) return null; 149 | 150 | rowClone.innerHTML = CONSTEST_FRIEND_TABLE_COLUMN_HTML; 151 | const column = rowClone.firstChild as HTMLDivElement; 152 | if (!column) return null; 153 | column.innerText = message; 154 | 155 | return rowClone; 156 | } catch (e: any) { 157 | Manager.Logger.warn(ContestRank.name, e); 158 | return null; 159 | } 160 | } 161 | 162 | static getRankingTableRow(userContestData: IUserContestDetails, contestName: string): Element | null { 163 | try { 164 | const rowSelector = Selectors.lc.contest.ranking.container.table_container.friend_table.body.row; 165 | const row = docFind(rowSelector); 166 | const rowClone = row.cloneNode(true) as Element; 167 | if (!rowClone) return null; 168 | 169 | docFind(rowSelector.rank, rowClone).innerHTML = userContestData.rank !== -1 ? 170 | `${userContestData.rank}` : userContestData.rank.toString(); 171 | docFind(rowSelector.name, rowClone).innerHTML = 172 | `${userContestData.username}`; 173 | docFind(rowSelector.score, rowClone).innerText = getStringValue(userContestData.score); 174 | docFind(rowSelector.old_rating, rowClone).innerText = getStringValue(userContestData.old_rating); 175 | docFind(rowSelector.delta_rating, rowClone).innerText = getStringValue(userContestData.delta_rating); 176 | docFind(rowSelector.new_rating, rowClone).innerText = getStringValue(userContestData.new_rating); 177 | return rowClone; 178 | } catch (e: any) { 179 | Manager.Logger.warn(ContestRank.name, e); 180 | return null; 181 | } 182 | } 183 | 184 | async action(_?: MutationRecord[], observer?: MutationObserver): Promise { 185 | try { 186 | let contestHeader = document.querySelectorAll('[href^="/contest"]')[2]; // on ranking page 187 | if (!contestHeader) return; 188 | if (checkDone(contestHeader)) return; 189 | 190 | let div = document.createElement('div'); 191 | div.id = "lx-people-mode"; 192 | 193 | div.style = "cursor: pointer; margin-left: 10px; padding-top: 2px;display:inline;"; 194 | div.innerHTML = PEOPLE_LIGHT_SVG; 195 | contestHeader?.parentElement?.appendChild(div); 196 | div.addEventListener('click', ContestRank.toggleFriendMode); 197 | 198 | observer?.disconnect(); 199 | 200 | Manager.Logger.log("Completed", ContestRank.name); 201 | } catch (e: any) { 202 | Manager.Logger.error(ContestRank.name, e); 203 | } 204 | } 205 | 206 | apply(): void { 207 | try { 208 | this.action(); 209 | const rankingContainer = docFind(Selectors.lc.contest.ranking.container); 210 | mutObserve(rankingContainer, this.action); 211 | mutObserve(Selectors.lc.static_dom.next, this.action); 212 | } catch (e: any) { 213 | Manager.Logger.error(ContestRank.name, e); 214 | } 215 | } 216 | 217 | pages = [PageType.CONTEST]; 218 | 219 | } -------------------------------------------------------------------------------- /src/core/modules/friendsPage.ts: -------------------------------------------------------------------------------- 1 | import FRIEND_TABLE_HTML from "@/values/html/friend_table.html?raw" 2 | 3 | import FRIEND_UPDOWN_ARROW from "@/values/svg/friend_updown_arrow.svg?raw" 4 | import FRIEND_UP_ARROW from "@/values/svg/friend_up_arrow.svg?raw" 5 | import FRIEND_DOWN_ARROW from "@/values/svg/friend_down_arrow.svg?raw" 6 | 7 | import { IModule } from "@/core/interfaces/module"; 8 | import { PageType } from "@/core/defines/pageType"; 9 | import { clearAllChildren, docFind, parseHTML } from "@/core/utils/helpers"; 10 | 11 | import Manager from "@/core/manager"; 12 | import Config from "@/values/config" 13 | import { IFriendData } from "../utils/leetcodeManager" 14 | import { SortType } from "@/core/defines/sortType"; 15 | import { getFriendsTableRow } from "@/components/friendsTable"; 16 | import Selectors from "@/values/selectors" 17 | 18 | type SortField = 'username' | 'rating' | 'problems_solved' | 'top'; 19 | 20 | export class FriendsPage implements IModule { 21 | private friendsData: IFriendData[] = []; 22 | private currentSortField: SortField = 'username'; 23 | private currentSortDirection = SortType.ASC; 24 | private fx_headers = [ 25 | Selectors.lc.friend.table.row_group.head_row.name, 26 | Selectors.lc.friend.table.row_group.head_row.rating, 27 | Selectors.lc.friend.table.row_group.head_row.problems_solved, 28 | Selectors.lc.friend.table.row_group.head_row.top 29 | ]; 30 | 31 | async getFriendRow(username: string): Promise { 32 | try { 33 | let details = await Manager.Leetcode.getUserDetails(username); 34 | 35 | if (details === null) { 36 | Manager.Friend.removeFriend(username); 37 | alert("User " + username + " does not exist. Removed from friends list."); 38 | return null; 39 | } 40 | 41 | const friendData: IFriendData = { 42 | ...details, 43 | displayName: details.name ? `${username} (${details.name})` : username 44 | }; 45 | 46 | friendData.rowElement = getFriendsTableRow(friendData); 47 | 48 | return friendData; 49 | } catch (e: any) { 50 | Manager.Logger.warn(FriendsPage.name, e); 51 | return null; 52 | } 53 | } 54 | 55 | private resetSortingIcons(): void { 56 | try { 57 | const tableParent = docFind(Selectors.lc.friend.table_parent); 58 | 59 | for (let i = 0; i < this.fx_headers.length; i++) { 60 | let fx_header = docFind(this.fx_headers[i], tableParent); 61 | const fx_header_svg = docFind("svg", fx_header.parentElement); 62 | if (!fx_header_svg) return; 63 | 64 | fx_header_svg.innerHTML = FRIEND_UPDOWN_ARROW; 65 | fx_header_svg.setAttribute('viewBox', '0 0 24 24'); 66 | } 67 | } catch (e: any) { 68 | Manager.Logger.warn(FriendsPage.name, e); 69 | } 70 | } 71 | 72 | private updateSortIndicator(headerElement: HTMLElement, direction: SortType): void { 73 | try { 74 | const fx_header_svg = docFind("svg", headerElement.parentElement); 75 | 76 | if (direction === SortType.ASC) { 77 | fx_header_svg.innerHTML = FRIEND_UP_ARROW; 78 | } else { 79 | fx_header_svg.innerHTML = FRIEND_DOWN_ARROW; 80 | } 81 | fx_header_svg.setAttribute('viewBox', '0 0 14 14'); 82 | } catch (e: any) { 83 | Manager.Logger.warn(FriendsPage.name, e); 84 | } 85 | } 86 | 87 | private sortFriends(field: SortField, direction?: SortType): void { 88 | 89 | if (direction) { 90 | this.currentSortDirection = direction; 91 | } 92 | 93 | // Toggle direction if sorting by the same field again 94 | else if (this.currentSortField === field) { 95 | this.currentSortDirection = this.currentSortDirection === SortType.ASC ? SortType.DESC : SortType.ASC; 96 | } else { 97 | this.currentSortField = field; 98 | this.currentSortDirection = SortType.ASC; 99 | } 100 | 101 | this.resetSortingIcons(); 102 | 103 | // Sort the data array 104 | this.friendsData.sort((a, b) => { 105 | let aValue: any = a[field]; 106 | let bValue: any = b[field]; 107 | 108 | // Convert to lowercase for string comparison 109 | if (typeof aValue === 'string' && typeof bValue === 'string') { 110 | aValue = a.displayName.toLowerCase(); 111 | bValue = b.displayName.toLowerCase(); 112 | } 113 | 114 | // Handle special cases for values like "-" or percentages 115 | if (aValue === '-') aValue = 0; 116 | if (bValue === '-') bValue = 0; 117 | 118 | // Handle percentage values 119 | if (typeof aValue === 'string' && aValue.includes('%')) { 120 | aValue = parseFloat(aValue.replace('%', '')) / 100; 121 | } 122 | if (typeof bValue === 'string' && bValue.includes('%')) { 123 | bValue = parseFloat(bValue.replace('%', '')) / 100; 124 | } 125 | 126 | // Convert to numbers if applicable 127 | if (typeof aValue === 'string' && !isNaN(parseFloat(aValue))) { 128 | aValue = parseFloat(aValue); 129 | } 130 | if (typeof bValue === 'string' && !isNaN(parseFloat(bValue))) { 131 | bValue = parseFloat(bValue); 132 | } 133 | 134 | // Compare based on direction 135 | const modifier = this.currentSortDirection === 'asc' ? 1 : -1; 136 | if (aValue < bValue) return -1 * modifier; 137 | if (aValue > bValue) return 1 * modifier; 138 | return 0; 139 | }); 140 | 141 | // Update the DOM after sorting 142 | this.renderFriends(); 143 | } 144 | 145 | private renderFriends(): void { 146 | const rowGroup = docFind(Selectors.lc.friend.table.row_group); 147 | if (!rowGroup) return; 148 | 149 | rowGroup.innerHTML = ''; 150 | 151 | for (const friend of this.friendsData) { 152 | if (friend.rowElement) { 153 | rowGroup.appendChild(friend.rowElement); 154 | } 155 | } 156 | } 157 | 158 | async makeFriendsPage() { 159 | try { 160 | const tableParent = docFind(Selectors.lc.friend.table_parent); 161 | clearAllChildren(tableParent); 162 | 163 | const friendsTable = parseHTML(FRIEND_TABLE_HTML); 164 | tableParent.appendChild(friendsTable); 165 | 166 | this.resetSortingIcons(); 167 | 168 | const friendList = await Manager.Friend.getFriends(); 169 | 170 | const rowGroupSelector = Selectors.lc.friend.table.row_group; 171 | const huser = docFind(rowGroupSelector.head_row.name); 172 | const hrating = docFind(rowGroupSelector.head_row.rating); 173 | const hprobsolved = docFind(rowGroupSelector.head_row.problems_solved); 174 | const htop = docFind(rowGroupSelector.head_row.top); 175 | 176 | huser.textContent = `${huser.textContent} (${friendList.length}/${Config.App.MAX_FRIENDS})`; 177 | 178 | let rowGroup = docFind(rowGroupSelector); 179 | 180 | if (friendList.length == 0) { 181 | rowGroup.innerHTML = '
No Friends Added
'; 182 | return; 183 | } 184 | 185 | // Reset friends data array 186 | this.friendsData = []; 187 | 188 | // Get friend data and create rows 189 | let promises = friendList.map(async (username) => { 190 | const friendData = await this.getFriendRow(username); 191 | if (friendData) { 192 | this.friendsData.push(friendData); 193 | this.sortFriends('username', SortType.ASC); 194 | this.renderFriends(); 195 | } 196 | }); 197 | await Promise.all(promises); 198 | 199 | // Set up sort event handlers 200 | huser.parentElement?.addEventListener("click", () => { 201 | this.sortFriends('username'); 202 | this.updateSortIndicator(huser, this.currentSortDirection); 203 | }); 204 | 205 | hrating.parentElement?.addEventListener("click", () => { 206 | this.sortFriends('rating'); 207 | this.updateSortIndicator(hrating, this.currentSortDirection); 208 | }); 209 | 210 | hprobsolved.parentElement?.addEventListener("click", () => { 211 | this.sortFriends('problems_solved'); 212 | this.updateSortIndicator(hprobsolved, this.currentSortDirection); 213 | }); 214 | 215 | htop.parentElement?.addEventListener("click", () => { 216 | this.sortFriends('top'); 217 | this.updateSortIndicator(htop, this.currentSortDirection); 218 | }); 219 | } catch (e: any) { 220 | Manager.Logger.warn(FriendsPage.name, e); 221 | } 222 | } 223 | 224 | async action(_?: MutationRecord[], observer?: MutationObserver): Promise { 225 | try { 226 | this.makeFriendsPage(); 227 | observer?.disconnect(); 228 | 229 | Manager.Logger.log("Completed", FriendsPage.name); 230 | } catch (e: any) { 231 | Manager.Logger.warn(FriendsPage.name, e); 232 | } 233 | } 234 | 235 | apply(): void { 236 | let jsTimer = setInterval(function () { document.title = Config.Strings.FRIENDS_PAGE_TITLE }, 80); 237 | setTimeout(function () { clearInterval(jsTimer) }, Config.App.DEFAULT_INTERVAL_TIMEOUT); 238 | 239 | this.action(); 240 | } 241 | 242 | pages = [PageType.FRIENDS]; 243 | } -------------------------------------------------------------------------------- /src/core/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./contestRank" 2 | export * from "./friendsPage" 3 | export * from "./navbarFriendsButton" 4 | export * from "./problemCompanyTagsPremium" 5 | export * from "./problemsetCompaniesPremium" 6 | export * from "./profileAddFriendButton" -------------------------------------------------------------------------------- /src/core/modules/navbarFriendsButton.ts: -------------------------------------------------------------------------------- 1 | import { IModule } from "@/core/interfaces/module"; 2 | import { PageType } from "@/core/defines/pageType"; 3 | import { mutObserve, docFind, checkDone, makeRequest, getUrl } from "@/core/utils/helpers"; 4 | import { getNavbarFriendsIcon } from "@/components/navbarFriendsIcon"; 5 | import Selectors from "@/values/selectors"; 6 | import Manager from "../manager"; 7 | import { ResponseType } from "../defines/responseType"; 8 | 9 | export class NavbarFriendsButton implements IModule { 10 | 11 | async action(_?: MutationRecord[], observer?: MutationObserver): Promise { 12 | try { 13 | const iconContainer = docFind(Selectors.lc.navbar.root.icon_container, undefined, true); 14 | if (!iconContainer) return; 15 | if (checkDone(iconContainer)) return; 16 | 17 | const navbarFriendIcon = getNavbarFriendsIcon(PageType.Friends); 18 | 19 | const profileIconContainer = docFind(Selectors.lc.navbar.root.icon_container.profile_icon_container); 20 | 21 | iconContainer?.insertBefore(navbarFriendIcon, profileIconContainer); 22 | 23 | // observer?.disconnect(); 24 | 25 | Manager.Logger.log("Completed", NavbarFriendsButton.name); 26 | } catch (e: any) { 27 | Manager.Logger.warn(NavbarFriendsButton.name, e); 28 | } 29 | } 30 | 31 | apply(): void { 32 | if (docFind(Selectors.lc.static_dom.navbar, undefined, true)) { 33 | mutObserve(Selectors.lc.static_dom.navbar.self, this.action); 34 | } else { 35 | mutObserve(Selectors.lc.static_dom.next, this.action); 36 | } 37 | } 38 | 39 | pages = [PageType.ALL]; 40 | 41 | } -------------------------------------------------------------------------------- /src/core/modules/problemCompanyTagsPremium.ts: -------------------------------------------------------------------------------- 1 | import COMPANY_TAG_HTML from "@/values/html/company_tag.html?raw" 2 | 3 | import { IModule } from "@/core/interfaces/module"; 4 | import { PageType } from "@/core/defines/pageType"; 5 | import { mutObserve, docFind, checkDone, makeRequest, getUrl, clearAllChildren } from "@/core/utils/helpers"; 6 | import Selectors from "@/values/selectors"; 7 | import Config from "@/values/config"; 8 | import { getCompanyTagsModalContentDiv, getCompanyTagsModalATag } from "@/components/companyTagsPremium"; 9 | import Manager from "../manager"; 10 | 11 | interface IProblemInfo { 12 | "Rating": number, 13 | "ID": number, 14 | "Title": string, 15 | "TitleZH": string, 16 | "TitleSlug": string, 17 | "ContestSlug": string, 18 | "ProblemIndex": string, 19 | "ContestID_en": string, 20 | "ContestID_zh": string 21 | } 22 | 23 | interface IProblemCompanyRowInfo { 24 | "StartRow": number, 25 | "EndRow": number 26 | } 27 | 28 | interface ICompanyCount { 29 | companyName: string, 30 | count: number 31 | } 32 | 33 | interface ICompanyTags { 34 | "0 - 6 months": ICompanyCount[], 35 | "6 months - 1 year": ICompanyCount[], 36 | "1 year - 2 years": ICompanyCount[] 37 | } 38 | 39 | export class ProblemCompanyTagsPremium implements IModule { 40 | private problemContestInfo?: IProblemInfo; 41 | private problemCompanyRowInfo?: IProblemCompanyRowInfo; 42 | private problemCompanyTags?: ICompanyTags; 43 | 44 | async getProblemContestInfo(): Promise { 45 | if (this.problemContestInfo) return this.problemContestInfo; 46 | 47 | try { 48 | const url = Config.App.ZEROTRAC_RATING_URL; 49 | 50 | let data = await makeRequest(getUrl(url)) as IProblemInfo[]; 51 | 52 | /*************** SAMPLE DATA *************** 53 | [ 54 | { 55 | "Rating": 3018.4940165727, 56 | "ID": 1719, 57 | "Title": "Number Of Ways To Reconstruct A Tree", 58 | "TitleZH": "重构一棵树的方案数", 59 | "TitleSlug": "number-of-ways-to-reconstruct-a-tree", 60 | "ContestSlug": "biweekly-contest-43", 61 | "ProblemIndex": "Q4", 62 | "ContestID_en": "Biweekly Contest 43", 63 | "ContestID_zh": "第 43 场双周赛" 64 | } 65 | ] 66 | *********************************************/ 67 | 68 | const qslug = window.location.pathname.split("/")[2]; 69 | 70 | const problemContestInfo = data.find((problem: IProblemInfo) => { 71 | return problem.TitleSlug == qslug; 72 | }); 73 | 74 | if (!problemContestInfo) return null; 75 | 76 | this.problemContestInfo = problemContestInfo; 77 | 78 | return problemContestInfo; 79 | 80 | } catch (e: any) { 81 | Manager.Logger.warn(ProblemCompanyTagsPremium.name, e); 82 | return null; 83 | } 84 | } 85 | 86 | async getCompanyTagsRowInfo(problemSlug: string): Promise { 87 | if (this.problemCompanyRowInfo) return this.problemCompanyRowInfo; 88 | 89 | try { 90 | const url = Config.App.GSHEETS_COMPANY_TAGS_URL; 91 | let data = await makeRequest(getUrl(url)); 92 | if (!data.values || !data.values[0]) return null; 93 | 94 | /*************** SAMPLE DATA *************** 95 | { 96 | "range": "ProblemCompaniesTags_Map!A1:C2425", 97 | "majorDimension": "ROWS", 98 | "values": [ 99 | [ 100 | "Urls", 101 | "StartRow", 102 | "EndRow" 103 | ], 104 | [ 105 | "01-matrix", 106 | "2", 107 | "10" 108 | ], 109 | [ 110 | "132-pattern", 111 | "11", 112 | "16" 113 | ] 114 | } 115 | *********************************************/ 116 | 117 | data.values.shift(); 118 | 119 | const problemCompanyRowData = data.values.find((row: string[]) => { 120 | return row[0] == problemSlug; 121 | }) as string[]; 122 | 123 | if (!problemCompanyRowData) return null; 124 | 125 | this.problemCompanyRowInfo = { 126 | StartRow: parseInt(problemCompanyRowData[1]), 127 | EndRow: parseInt(problemCompanyRowData[2]) 128 | }; 129 | 130 | return this.problemCompanyRowInfo; 131 | } catch (e: any) { 132 | Manager.Logger.warn(ProblemCompanyTagsPremium.name, e); 133 | return null; 134 | } 135 | } 136 | 137 | async getCompanyTags(): Promise { 138 | if (this.problemCompanyTags) return this.problemCompanyTags; 139 | 140 | try { 141 | let problemSlug = window.location.pathname.split("/")[2]; 142 | let companyTagsRowInfo = await this.getCompanyTagsRowInfo(problemSlug); 143 | if (!companyTagsRowInfo) return null; 144 | 145 | const rowStart = companyTagsRowInfo.StartRow; 146 | const rowEnd = companyTagsRowInfo.EndRow; 147 | 148 | const url = `${Config.App.GSHEETS_COMPANY_DATA_URL}!${rowStart}:${rowEnd}?key=${Config.App.GSHEETS_COMPANY_DATA_KEY}`; 149 | const data = await makeRequest(getUrl(url)); 150 | if (!data.values || !data.values[0]) return null; 151 | 152 | /*************** SAMPLE DATA *************** 153 | { 154 | "range": "ProblemCompaniesTags!A6901:D6901", 155 | "majorDimension": "ROWS", 156 | "values": [ 157 | [ 158 | "number-of-flowers-in-full-bloom", 159 | "1 year - 2 years", 160 | "Google\n 4", 161 | "6901" 162 | ] 163 | ] 164 | } 165 | *********************************************/ 166 | 167 | let companyTags: ICompanyTags = { 168 | "0 - 6 months": [], 169 | "6 months - 1 year": [], 170 | "1 year - 2 years": [] 171 | }; 172 | 173 | data.values.forEach((row: string[]) => { 174 | const key = row[1] as keyof ICompanyTags; 175 | 176 | const count = parseInt(row[2].split('\n ')[1] || '0'); 177 | if (count === 0) return; 178 | 179 | companyTags[key].push({ 180 | companyName: row[2].split('\n ')[0], 181 | count 182 | }); 183 | }); 184 | 185 | this.problemCompanyTags = companyTags; 186 | 187 | return companyTags; 188 | } catch (e: any) { 189 | Manager.Logger.warn(ProblemCompanyTagsPremium.name, e); 190 | return null; 191 | } 192 | } 193 | 194 | async showCompanyTags() { 195 | try { 196 | const companyTagsModalBody = docFind(Selectors.lc.problem.companies_button.modal_body); 197 | 198 | if (checkDone(companyTagsModalBody)) return; 199 | 200 | clearAllChildren(companyTagsModalBody); 201 | companyTagsModalBody.setAttribute('style', 'min-height: 30vh'); 202 | 203 | const companyTags = await this.getCompanyTags(); 204 | 205 | const problemContestInfo = await this.getProblemContestInfo(); 206 | if (problemContestInfo && problemContestInfo.Rating) { 207 | const problemRating = Math.round(problemContestInfo.Rating); 208 | const problemContest = problemContestInfo.ContestID_en; 209 | const problemContestSlug = problemContestInfo.ContestSlug; 210 | const contentDiv = getCompanyTagsModalContentDiv(`Difficulty Rating: ${problemRating}`); 211 | contentDiv.appendChild(getCompanyTagsModalATag(`/contest/${problemContestSlug}`, problemContest)); 212 | companyTagsModalBody.appendChild(contentDiv); 213 | companyTagsModalBody.appendChild(document.createElement('br')); 214 | } else { 215 | companyTagsModalBody.appendChild(getCompanyTagsModalContentDiv("Unrated")); 216 | } 217 | 218 | if (!companyTags) { 219 | companyTagsModalBody.appendChild(getCompanyTagsModalContentDiv("No Company Tags")); 220 | return; 221 | } 222 | 223 | Object.keys(companyTags).forEach((key: string) => { 224 | const mkey = key as keyof ICompanyTags; 225 | 226 | const timeRange = key.split(" ").map((word) => { 227 | return word[0].toUpperCase() + word.slice(1); 228 | }).join(" "); 229 | 230 | companyTagsModalBody.innerHTML += `${timeRange}
`; 231 | 232 | if (companyTags[mkey].length == 0) { 233 | companyTagsModalBody.appendChild(getCompanyTagsModalContentDiv(Config.Strings.NA)); 234 | return; 235 | } 236 | 237 | companyTags[mkey].forEach((tag: ICompanyCount) => { 238 | 239 | let companyTagDiv = document.createElement('div'); 240 | companyTagDiv.innerHTML = COMPANY_TAG_HTML; 241 | companyTagDiv = companyTagDiv.firstChild as HTMLDivElement; 242 | 243 | docFind(Selectors.lc.problem.companies_button.modal_body.tag_num, companyTagDiv).style.background = "#ffa116"; 244 | docFind(Selectors.lc.problem.companies_button.modal_body.tag_name, companyTagDiv).innerHTML = tag.companyName; 245 | docFind(Selectors.lc.problem.companies_button.modal_body.tag_num, companyTagDiv).innerHTML = String(tag.count); 246 | 247 | companyTagsModalBody.appendChild(companyTagDiv); 248 | }); 249 | companyTagsModalBody.innerHTML += '

'; 250 | }); 251 | } catch (e: any) { 252 | Manager.Logger.warn(ProblemCompanyTagsPremium.name, e); 253 | } 254 | } 255 | 256 | async action(_?: MutationRecord[], observer?: MutationObserver): Promise { 257 | try { 258 | const btnCompanies = docFind(Selectors.lc.problem.companies_button); 259 | if (checkDone(btnCompanies)) return; 260 | 261 | const showCompanyTags = this.showCompanyTags.bind(this); 262 | 263 | btnCompanies.addEventListener('click', async function () { 264 | const interval = setInterval(showCompanyTags, 20); // Fix for dynamic layout 265 | setTimeout(() => clearInterval(interval), 100); 266 | }); 267 | 268 | await this.getProblemContestInfo(); 269 | await this.getCompanyTags(); 270 | 271 | // observer?.disconnect(); 272 | 273 | Manager.Logger.log("Completed", ProblemCompanyTagsPremium.name); 274 | 275 | } catch (e: any) { 276 | Manager.Logger.warn(ProblemCompanyTagsPremium.name, e); 277 | } 278 | } 279 | 280 | apply(): void { 281 | // this.action(); 282 | const action = this.action.bind(this); 283 | mutObserve(Selectors.lc.static_dom.next, action); 284 | } 285 | 286 | pages = [PageType.PROBLEM]; 287 | 288 | } -------------------------------------------------------------------------------- /src/core/modules/problemsetCompaniesPremium.ts: -------------------------------------------------------------------------------- 1 | // Note: This module is temporary and may be removed in the future; 2 | 3 | import PS_FREQUENCY_COLUMN_HTML from "@/values/html/ps_frequency_column.html?raw" 4 | import PS_PROBLEM_ROW_HTML from "@/values/html/ps_problem_row.html?raw" 5 | import PS_VIDEO_SOLUTION_SVG from "@/values/svg/ps_video_solution.svg?raw" 6 | import PS_NOTAC_SVG from "@/values/svg/ps_notac.svg?raw" 7 | 8 | import { IModule } from "@/core/interfaces/module"; 9 | import { PageType } from "@/core/defines/pageType"; 10 | import { mutObserve, docFind, checkDone, makeRequest } from "@/core/utils/helpers"; 11 | import Selectors from "@/values/selectors"; 12 | import Config from "@/values/config"; 13 | import Manager from "../manager"; 14 | 15 | interface ProblemData { 16 | problem_id: string; 17 | problem_name: string; 18 | problem_slug: string; 19 | problem_accepted: string; 20 | has_video_solution: boolean; 21 | problem_acceptance: string; 22 | problem_difficulty: string; 23 | problem_frequency: string; 24 | } 25 | 26 | interface LeetCodeProblemData { 27 | status: string; 28 | hasVideoSolution: boolean; 29 | acRate: string; 30 | } 31 | 32 | interface CompanyProblemsData { 33 | [duration: string]: ProblemData[]; 34 | } 35 | 36 | export class ProblemsetCompaniesPremium implements IModule { 37 | private companyProblemRanges: Map = new Map(); 38 | private companyProblems: Record = {}; 39 | private lcProblems: Record = {}; 40 | private currentCompany: string | null = null; 41 | private currentFrequency = 'All time'; 42 | private currentPage = 1; 43 | private originalTableBody: string | null = null; 44 | 45 | /** 46 | * Fetches company problem ranges from Google Sheets 47 | */ 48 | async fetchCompanyProblemRanges(): Promise { 49 | try { 50 | const data = await makeRequest(Config.App.GSHEETS_COMPANIES_PROBLEM_MAP_URL); 51 | if (!data.values || !data.values[0]) return; 52 | 53 | // First row contains headers, skip it 54 | data.values.shift(); 55 | 56 | this.companyProblemRanges = new Map(); 57 | 58 | data.values.forEach((row: string[]) => { 59 | this.companyProblemRanges.set(row[0], [row[1], row[2]]); 60 | }); 61 | } catch (error: any) { 62 | Manager.Logger.error("Error fetching company problem ranges:", error); 63 | } 64 | } 65 | 66 | /** 67 | * Fetches problems for a specific company 68 | */ 69 | async fetchCompanyProblems(companyName: string): Promise { 70 | if (!this.companyProblemRanges) return; 71 | 72 | const range = this.companyProblemRanges.get(companyName); 73 | if (!range) return; 74 | 75 | try { 76 | const data = await makeRequest(`${Config.App.GSHEETS_COMPANIES_PROBLEM_URL}!${range[0]}:${range[1]}?key=${Config.App.GSHEETS_COMPANY_DATA_KEY}`); 77 | if (!data.values || !data.values[0]) return; 78 | 79 | // Skip header row 80 | data.values.shift(); 81 | 82 | this.companyProblems[companyName] = data.values; 83 | } catch (error: any) { 84 | Manager.Logger.error(`Error fetching problems for ${companyName}:`, error); 85 | } 86 | } 87 | 88 | /** 89 | * Fetches problem data from LeetCode API 90 | */ 91 | async setLcProblemData(problemSlug: string): Promise { 92 | if (this.lcProblems[problemSlug]) return; 93 | 94 | const data = { 95 | query: `query questionData($titleSlug: String!) { 96 | question(titleSlug: $titleSlug) { 97 | status 98 | solution { 99 | hasVideoSolution 100 | } 101 | stats 102 | } 103 | }`, 104 | variables: { 105 | "titleSlug": problemSlug 106 | } 107 | }; 108 | 109 | try { 110 | const res = await makeRequest(Config.App.LEETCODE_API_URL, data); 111 | if (res.errors) return; 112 | 113 | const status = res.data.question.status; 114 | let hasVideoSolution = res.data.question.solution?.hasVideoSolution || false; 115 | const acRate = JSON.parse(res.data.question.stats).acRate; 116 | 117 | this.lcProblems[problemSlug] = { status, hasVideoSolution, acRate }; 118 | } catch (error: any) { 119 | Manager.Logger.error(`Error fetching LeetCode problem data for ${problemSlug}:`, error); 120 | } 121 | } 122 | 123 | /** 124 | * Gets problem data for a specific problem 125 | */ 126 | async getLcProblemData(problemSlug: string): Promise { 127 | if (!this.lcProblems[problemSlug]) { 128 | await this.setLcProblemData(problemSlug); 129 | } 130 | return this.lcProblems[problemSlug] || null; 131 | } 132 | 133 | /** 134 | * Processes and formats company problem data 135 | */ 136 | async setCompanyProblemsData(companyName: string): Promise { 137 | if (this.companyProblems[companyName] && typeof this.companyProblems[companyName] === 'object') return; 138 | 139 | if (!this.companyProblems[companyName]) { 140 | await this.fetchCompanyProblems(companyName); 141 | if (!this.companyProblems[companyName]) return; 142 | } 143 | 144 | const compProblems: CompanyProblemsData = { 145 | '6 months': [], 146 | '1 year': [], 147 | '2 years': [], 148 | 'All time': [], 149 | }; 150 | 151 | (this.companyProblems[companyName] as string[][]).forEach((row: string[]) => { 152 | const duration = row[3]; 153 | const problem: ProblemData = { 154 | problem_id: row[1], 155 | problem_name: row[4], 156 | problem_slug: row[6].split("/")[4], 157 | problem_accepted: "x", // "x" means not fetched 158 | has_video_solution: false, 159 | problem_acceptance: "x", 160 | problem_difficulty: row[7], 161 | problem_frequency: row[2] 162 | }; 163 | 164 | if (compProblems[duration]) { 165 | compProblems[duration].push(problem); 166 | } 167 | }); 168 | 169 | this.companyProblems[companyName] = compProblems; 170 | } 171 | 172 | /** 173 | * Returns the appropriate sort function based on the column 174 | */ 175 | getSortFunction(sortBy: string): (a: ProblemData, b: ProblemData) => number { 176 | switch (sortBy) { 177 | case 'problem_id': 178 | return (a, b) => parseInt(a[sortBy]) - parseInt(b[sortBy]); 179 | 180 | case 'problem_acceptance': 181 | return (a, b) => { 182 | const valA = parseFloat(a[sortBy].split("%")[0]); 183 | const valB = parseFloat(b[sortBy].split("%")[0]); 184 | return valA - valB; 185 | }; 186 | 187 | case 'problem_difficulty': 188 | return (a, b) => { 189 | const diff = ['Easy', 'Medium', 'Hard']; 190 | return diff.indexOf(a[sortBy]) - diff.indexOf(b[sortBy]); 191 | }; 192 | 193 | case 'problem_frequency': 194 | return (a, b) => parseFloat(a[sortBy]) - parseFloat(b[sortBy]); 195 | 196 | default: 197 | return (a, b) => a[sortBy as keyof ProblemData].toString().localeCompare(b[sortBy as keyof ProblemData].toString()); 198 | } 199 | } 200 | 201 | /** 202 | * Retrieves company problems with pagination and sorting 203 | */ 204 | async getCompanyProblems(companyName: string, duration: string, sortBy = 'problem_id'): Promise { 205 | if (!this.companyProblemRanges) { 206 | return null; 207 | } 208 | 209 | await this.setCompanyProblemsData(companyName); 210 | 211 | const companyData = this.companyProblems[companyName]; 212 | if (!companyData || !companyData[duration]) { 213 | return null; 214 | } 215 | 216 | const perPage = 50; 217 | const start = (this.currentPage - 1) * perPage; 218 | const end = Math.min(this.currentPage * perPage, companyData[duration].length); 219 | 220 | // Clone the array to avoid modifying the original 221 | let problems = [...companyData[duration]]; 222 | 223 | // Sort problems 224 | problems.sort(this.getSortFunction(sortBy)); 225 | 226 | // Apply pagination 227 | problems = problems.slice(start, end); 228 | 229 | // Fetch additional problem data 230 | const fetchPromises = problems.map(async (problem) => { 231 | if (problem.problem_accepted === "x") { 232 | const lcRes = await this.getLcProblemData(problem.problem_slug); 233 | if (lcRes) { 234 | problem.problem_accepted = lcRes.status; 235 | problem.has_video_solution = lcRes.hasVideoSolution; 236 | problem.problem_acceptance = lcRes.acRate; 237 | } 238 | } 239 | }); 240 | 241 | await Promise.all(fetchPromises); 242 | return problems; 243 | } 244 | 245 | /** 246 | * Creates and populates the problems table 247 | */ 248 | async createProblemsTable(companyName: string | null, duration: string, sortBy = 'problem_id', order = 0): Promise { 249 | if (!companyName) return; 250 | 251 | const tableBody = document.querySelector('[role="table"].border-spacing-0 [role="rowgroup"]'); 252 | if (!tableBody) return; 253 | 254 | // Normalize duration 255 | duration = duration.toLowerCase() === 'all time' ? 'All time' : duration; 256 | 257 | // Update URL 258 | history.replaceState(null, "", `?company=${this.currentCompany}&page=${this.currentPage}`); 259 | 260 | const problems = await this.getCompanyProblems(companyName, duration); 261 | if (!problems) return; 262 | 263 | tableBody.innerHTML = ""; 264 | 265 | // Apply sorting 266 | if (order !== 0) { 267 | problems.sort((a, b) => { 268 | const sortResult = this.getSortFunction(sortBy)(a, b); 269 | return order === -1 ? -sortResult : sortResult; 270 | }); 271 | } 272 | 273 | // Handle empty results 274 | if (problems.length === 0) { 275 | const navpage = document.querySelector('nav[role="navigation"]'); 276 | if (navpage) navpage.innerHTML = ""; 277 | return; 278 | } 279 | 280 | // Render problems 281 | problems.forEach((problem) => { 282 | const tempDiv = document.createElement('div'); 283 | tempDiv.innerHTML = PS_PROBLEM_ROW_HTML; 284 | const probRow = tempDiv.firstChild as HTMLElement; 285 | 286 | const problemLink = probRow.querySelector('.fx-prob-ques') as HTMLAnchorElement; 287 | problemLink.innerHTML = `${problem.problem_id}. ${problem.problem_name}`; 288 | problemLink.setAttribute('href', `/problems/${problem.problem_slug}`); 289 | 290 | const solutionLink = probRow.querySelector('.fx-prob-solution') as HTMLAnchorElement; 291 | solutionLink.setAttribute('href', `/problems/${problem.problem_slug}/solution`); 292 | 293 | if (problem.has_video_solution) { 294 | solutionLink.innerHTML = PS_VIDEO_SOLUTION_SVG; 295 | } 296 | 297 | const acceptanceEl = probRow.querySelector('.fx-prob-acceptance') as HTMLElement; 298 | acceptanceEl.innerHTML = problem.problem_acceptance; 299 | 300 | const difficultyEl = probRow.querySelector('.fx-prob-difficulty') as HTMLElement; 301 | difficultyEl.innerHTML = problem.problem_difficulty; 302 | 303 | // Set color based on difficulty 304 | if (problem.problem_difficulty === 'Easy') { 305 | difficultyEl.style.color = "#00b8a3"; 306 | } else if (problem.problem_difficulty === 'Medium') { 307 | difficultyEl.style.color = "#ffc01e"; 308 | } // Hard is default color 309 | 310 | const frequencyEl = probRow.querySelector('.fx-prob-frequency') as HTMLElement; 311 | frequencyEl.style.width = `${parseFloat(problem.problem_frequency) * 100}%`; 312 | 313 | const statusEl = probRow.querySelector('.fx-prob-solved-status') as HTMLElement; 314 | if (problem.problem_accepted === 'x' || problem.problem_accepted === null) { 315 | statusEl.innerHTML = ""; 316 | } else if (problem.problem_accepted === 'notac') { 317 | statusEl.innerHTML = PS_NOTAC_SVG; 318 | } // Solved icon is default 319 | 320 | tableBody.appendChild(probRow); 321 | }); 322 | 323 | // Update pagination 324 | this.managePagination(); 325 | } 326 | 327 | /** 328 | * Manages pagination for the problems table 329 | */ 330 | managePagination(): void { 331 | const paginationParent = document.querySelector('nav[role="navigation"]')?.parentElement; 332 | if (!paginationParent) return; 333 | 334 | const currentCompany = this.currentCompany; 335 | if (!currentCompany) return; 336 | 337 | const companyData = this.companyProblems[currentCompany]; 338 | if (!companyData || !companyData[this.currentFrequency]) return; 339 | 340 | if (companyData[this.currentFrequency].length === 0) return; 341 | 342 | const nav = document.createElement('nav'); 343 | nav.setAttribute('role', 'navigation'); 344 | nav.setAttribute('class', 'mb-6 md:mb-0 flex flex-wrap items-center space-x-2'); 345 | nav.style.maxWidth = "100%"; 346 | 347 | const selectedBtnClass = 'flex items-center justify-center px-3 h-8 rounded select-none focus:outline-none pointer-events-none bg-paper dark:bg-dark-gray-5 text-label-1 dark:text-dark-label-1 shadow-level1 dark:shadow-dark-level1'; 348 | const btnClass = 'flex items-center justify-center px-3 h-8 rounded select-none focus:outline-none bg-fill-3 dark:bg-dark-fill-3 text-label-2 dark:text-dark-label-2 hover:bg-fill-2 dark:hover:bg-dark-fill-2'; 349 | 350 | const changePage = (e: Event) => { 351 | const target = e.target as HTMLElement; 352 | document.querySelector(`#lx-pagenav-btn-${this.currentPage}`)?.setAttribute('class', btnClass); 353 | 354 | const totalPages = Math.ceil(companyData[this.currentFrequency].length / 50); 355 | 356 | if (target.innerHTML === '<') { 357 | this.currentPage = Math.max(this.currentPage - 1, 1); 358 | } else if (target.innerHTML === '>') { 359 | this.currentPage = Math.min(this.currentPage + 1, totalPages); 360 | } else { 361 | this.currentPage = parseInt(target.innerHTML, 10); 362 | } 363 | 364 | this.createProblemsTable(this.currentCompany, this.currentFrequency, 'problem_id', 0); 365 | document.querySelector(`#lx-pagenav-btn-${this.currentPage}`)?.setAttribute('class', selectedBtnClass); 366 | 367 | window.scrollTo({ 368 | top: 680, 369 | behavior: "smooth" 370 | }); 371 | }; 372 | 373 | const totalPages = Math.ceil(companyData[this.currentFrequency].length / 50); 374 | 375 | // Create pagination buttons 376 | for (let i = 0; i <= totalPages + 1; i++) { 377 | const btn = document.createElement('button'); 378 | btn.addEventListener('click', changePage); 379 | btn.id = `lx-pagenav-btn-${i}`; 380 | btn.setAttribute('class', btnClass); 381 | btn.innerHTML = i.toString(); 382 | nav.appendChild(btn); 383 | } 384 | 385 | paginationParent.innerHTML = ""; 386 | paginationParent.appendChild(nav); 387 | 388 | // Mark current page as selected 389 | document.querySelector(`#lx-pagenav-btn-${this.currentPage}`)?.setAttribute('class', selectedBtnClass); 390 | 391 | // Set previous and next buttons 392 | const prevBtn = document.querySelector('#lx-pagenav-btn-0'); 393 | const nextBtn = document.querySelector(`#lx-pagenav-btn-${totalPages + 1}`); 394 | 395 | if (prevBtn) prevBtn.innerHTML = '<'; 396 | if (nextBtn) nextBtn.innerHTML = '>'; 397 | } 398 | 399 | /** 400 | * Adds event listeners for sorting columns 401 | */ 402 | addSortingListeners(): void { 403 | const sortLi = document.querySelector("#fx-sort-li"); 404 | const titleLi = document.querySelector('div.mx-2[role="columnheader"]:nth-of-type(2)')?.firstChild; 405 | const acceptanceLi = document.querySelector('div.mx-2[role="columnheader"]:nth-of-type(4)')?.firstChild; 406 | const difficultyLi = document.querySelector('div.mx-2[role="columnheader"]:nth-of-type(5)')?.firstChild; 407 | 408 | if (sortLi) { 409 | sortLi.addEventListener("click", async () => { 410 | await this.createProblemsTable(this.currentCompany, this.currentFrequency, 'problem_frequency', -1); 411 | }); 412 | } 413 | 414 | if (titleLi) { 415 | titleLi.addEventListener("click", async () => { 416 | await this.createProblemsTable(this.currentCompany, this.currentFrequency, 'problem_name', 0); 417 | }); 418 | } 419 | 420 | if (acceptanceLi) { 421 | acceptanceLi.addEventListener("click", async () => { 422 | await this.createProblemsTable(this.currentCompany, this.currentFrequency, 'problem_acceptance', -1); 423 | }); 424 | } 425 | 426 | if (difficultyLi) { 427 | difficultyLi.addEventListener("click", async () => { 428 | await this.createProblemsTable(this.currentCompany, this.currentFrequency, 'problem_difficulty', 0); 429 | }); 430 | } 431 | } 432 | 433 | /** 434 | * Adds click handlers to company items in the sidebar 435 | */ 436 | setupSidebarCompanies(): void { 437 | const selectedColor = "#fcbf62"; 438 | const companyElements = document.querySelectorAll('div.swiper-slide.-mr-3.flex.flex-wrap.swiper-no-swiping-content.fx-sidebar-comp-done.swiper-slide-active a'); 439 | const tableBody = document.querySelector('div.mt-4.flex.flex-col.items-center.gap-4'); 440 | 441 | if (!tableBody) return; 442 | 443 | companyElements.forEach((element) => { 444 | const href = element.getAttribute('href'); 445 | if (!href) return; 446 | 447 | const companyName = href.split("/")[2]; 448 | element.setAttribute('company-name', companyName); 449 | 450 | element.addEventListener('click', () => { 451 | if (this.currentCompany) { 452 | const currentCompanyEl = document.querySelector(`a[company-name="${this.currentCompany}"] span`) as HTMLElement; 453 | if (currentCompanyEl) currentCompanyEl.style.background = ""; 454 | } else { 455 | this.originalTableBody = tableBody.innerHTML; 456 | } 457 | 458 | const freqElement = document.querySelector('.fx-freq-li[name="All time"]') as HTMLElement; 459 | if (freqElement) freqElement.style.background = ""; 460 | 461 | if (this.currentCompany === companyName) { 462 | // Deselect current company 463 | this.currentCompany = null; 464 | tableBody.innerHTML = this.originalTableBody || ""; 465 | 466 | const navElement = document.querySelector('nav[role="navigation"]'); 467 | if (navElement) navElement.innerHTML = ""; 468 | 469 | history.replaceState(null, "", window.location.pathname); 470 | } else { 471 | // Select new company 472 | this.currentCompany = companyName; 473 | const spanElement = element.querySelector('span'); 474 | if (spanElement) spanElement.style.background = selectedColor; 475 | 476 | this.currentPage = 1; 477 | this.createProblemsTable(companyName, 'All time'); 478 | } 479 | }); 480 | 481 | // Remove default link behavior 482 | element.removeAttribute('href'); 483 | }); 484 | } 485 | 486 | /** 487 | * Sets up the company tags premium functionality for the problemset page 488 | */ 489 | async setupProblemsetCompaniesPremium(): Promise { 490 | const sidebarComp = document.querySelector('.swiper-slide a.mb-4.mr-3'); 491 | if (!sidebarComp || document.querySelector("div.fx-sidebar-comp-done")) return; 492 | 493 | // Mark as done to prevent duplicate setup 494 | sidebarComp.parentElement?.classList.add("fx-sidebar-comp-done"); 495 | 496 | // Fetch company problem data 497 | await this.fetchCompanyProblemRanges(); 498 | 499 | // Set up frequency column 500 | const frequencyCol = document.querySelector('[role="columnheader"]:nth-of-type(6)'); 501 | if (frequencyCol) { 502 | frequencyCol.innerHTML = PS_FREQUENCY_COLUMN_HTML; 503 | 504 | const freqButton = document.querySelector("#fx-freq-button"); 505 | const freqMenu = document.querySelector("#fx-freq-menu"); 506 | const selectedColor = "#dedede"; 507 | 508 | if (freqButton && freqMenu) { 509 | const clickHandler = (e: MouseEvent) => { 510 | if (e.target !== freqButton) { 511 | freqMenu.classList.add("hidden"); 512 | 513 | // Find if a frequency item was clicked 514 | const path = e.composedPath(); 515 | const freqElement = path.find((element) => { 516 | if (element instanceof HTMLElement) { 517 | return element.classList && element.classList.contains("fx-freq-li"); 518 | } 519 | return false; 520 | }) as HTMLElement | undefined; 521 | 522 | if (freqElement) { 523 | // Update current frequency 524 | if (this.currentFrequency) { 525 | const currentFreqEl = document.querySelector(`li.fx-freq-li[name="${this.currentFrequency}"]`) as HTMLElement; 526 | if (currentFreqEl) currentFreqEl.style.background = ""; 527 | } 528 | 529 | this.currentFrequency = freqElement.getAttribute('name') || 'All time'; 530 | freqElement.style.background = selectedColor; 531 | 532 | this.currentPage = 1; 533 | this.createProblemsTable(this.currentCompany, this.currentFrequency); 534 | } 535 | 536 | document.removeEventListener("click", clickHandler); 537 | } 538 | }; 539 | 540 | freqButton.addEventListener("click", () => { 541 | freqMenu.classList.toggle("hidden"); 542 | document.addEventListener("click", clickHandler); 543 | }); 544 | } 545 | } 546 | 547 | this.setupSidebarCompanies(); 548 | 549 | this.addSortingListeners(); 550 | } 551 | 552 | async action(_?: MutationRecord[], observer?: MutationObserver): Promise { 553 | try { 554 | this.setupProblemsetCompaniesPremium(); 555 | 556 | } catch (e: any) { 557 | Manager.Logger.warn(ProblemsetCompaniesPremium.name, e); 558 | } 559 | } 560 | 561 | apply(): void { 562 | // this.action(); 563 | const action = this.action.bind(this); 564 | mutObserve(Selectors.lc.static_dom.next, action); 565 | } 566 | 567 | pages = [PageType.ALL]; 568 | 569 | } -------------------------------------------------------------------------------- /src/core/modules/profileAddFriendButton.ts: -------------------------------------------------------------------------------- 1 | import STAR_ICON_SVG from "@/values/svg/star_icon.svg?raw"; 2 | 3 | import { IModule } from "@/core/interfaces/module"; 4 | import { PageType } from "@/core/defines/pageType"; 5 | import { mutObserve, docFind, checkDone, docFindById } from "@/core/utils/helpers"; 6 | 7 | import Manager from "@/core/manager"; 8 | import Selectors from "@/values/selectors"; 9 | import Config from "@/values/config"; 10 | 11 | export class ProfileAddFriendButton implements IModule { 12 | 13 | async action(_?: MutationRecord[], observer?: MutationObserver): Promise { 14 | try { 15 | const name_box = docFind(Selectors.lc.profile.name_section.name_parent, undefined, true); 16 | if (checkDone(name_box)) return; 17 | 18 | const star = document.createElement('div'); 19 | star.style.cursor = "pointer"; 20 | 21 | let parser = new DOMParser(); 22 | let doc = parser.parseFromString(STAR_ICON_SVG, 'image/svg+xml'); 23 | let svg = doc.querySelector('svg'); 24 | svg?.setAttribute('fill', 'currentColor'); 25 | if (svg) star.appendChild(svg); 26 | 27 | name_box.appendChild(star); 28 | name_box.classList.add("flex", "items-center", "space-x-2"); 29 | 30 | observer?.disconnect(); 31 | 32 | star.addEventListener("click", () => ProfileAddFriendButton.toggleFriend()); 33 | await ProfileAddFriendButton.toggleFriend(true); 34 | 35 | Manager.Logger.log("Completed", ProfileAddFriendButton.name); 36 | 37 | } catch (e: any) { 38 | // Manager.Logger.warn(ProfileAddFriendButton.name, e); 39 | } 40 | } 41 | 42 | static async toggleFriend(setup = false): Promise { 43 | try { 44 | const star_path = docFind(Selectors.lc.profile.name_section.star_icon_path); 45 | 46 | const uname_box = docFind(Selectors.lc.profile.name_section.username); 47 | const username = uname_box.innerText; 48 | 49 | const isFriend = await Manager.Friend.isFriend(username); 50 | 51 | if (setup === true) { 52 | if (isFriend) { // User is Friend 53 | if (star_path) { 54 | star_path.style.fill = Config.Colors.FRIEND_STAR; 55 | } 56 | } else { // User is not Friend 57 | if (star_path) star_path.style.fill = Config.Colors.NOT_FRIEND_STAR; 58 | } 59 | return; 60 | } 61 | 62 | if (isFriend) { // User is Friend, Remove it 63 | if (star_path) star_path.style.fill = Config.Colors.NOT_FRIEND_STAR; 64 | Manager.Friend.removeFriend(username); 65 | } else { // User is not Friend, Add it 66 | try { 67 | Manager.Friend.addFriend(username); 68 | if (star_path) star_path.style.fill = Config.Colors.FRIEND_STAR; 69 | } catch (e: any) { 70 | Manager.Logger.warn(ProfileAddFriendButton.name, e); 71 | } 72 | } 73 | 74 | } catch (e: any) { 75 | Manager.Logger.warn(ProfileAddFriendButton.name, e); 76 | } 77 | } 78 | 79 | apply(): void { 80 | mutObserve(Selectors.lc.static_dom.next, this.action); 81 | } 82 | 83 | pages = [PageType.PROFILE]; 84 | 85 | blacklist_pages = [PageType.FRIENDS]; 86 | } -------------------------------------------------------------------------------- /src/core/utils/friendManager.ts: -------------------------------------------------------------------------------- 1 | import Manager from "../manager"; 2 | 3 | class FriendManager { 4 | 5 | static FRIENDS_LIMIT = 50; 6 | static FRIENDS_LOC: 'friends'; 7 | static FRIENDS_MESSAGE = `FRIEND_LIMIT_EXCEEDED: You can only add upto ${this.FRIENDS_LIMIT} friends, ensuring an inclusive and sustainable experience for everyone!`; 8 | 9 | static async getFriendList(): Promise { 10 | let friendList: string[] = await Manager.Storage.get(FriendManager.FRIENDS_LOC, []); 11 | return friendList; 12 | } 13 | 14 | static async isFriend(username: string): Promise { 15 | const friendList: string[] = await this.getFriendList(); 16 | return friendList.includes(username); 17 | } 18 | 19 | static async getFriends(): Promise { 20 | const friendList = await this.getFriendList(); 21 | return friendList; 22 | } 23 | 24 | static async getNumFriends(): Promise { 25 | const friendList = await this.getFriendList(); 26 | return friendList.length; 27 | } 28 | 29 | static async addFriend(username: string): Promise { 30 | let friendList: string[] = await this.getFriendList(); 31 | 32 | if (friendList.includes(username)) { 33 | return; 34 | } 35 | 36 | if (friendList.length >= this.FRIENDS_LIMIT) { 37 | throw new Error(this.FRIENDS_MESSAGE); 38 | } 39 | 40 | friendList.push(username); 41 | 42 | await Manager.Storage.set(FriendManager.FRIENDS_LOC, friendList); 43 | } 44 | 45 | static async removeFriend(username: string): Promise { 46 | const friendList: string[] = await this.getFriendList(); 47 | 48 | if (!friendList.includes(username)) { 49 | return; 50 | } 51 | 52 | const index = friendList.indexOf(username); 53 | friendList.splice(index, 1); 54 | 55 | await Manager.Storage.set(FriendManager.FRIENDS_LOC, friendList); 56 | } 57 | 58 | static async clearAllFriends() { 59 | await Manager.Storage.remove(FriendManager.FRIENDS_LOC); 60 | } 61 | } 62 | 63 | export { FriendManager }; -------------------------------------------------------------------------------- /src/core/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { BuildMode } from "../defines/buildModes"; 2 | import { RequestMethod } from "../defines/requestMethod"; 3 | import { Result } from "../defines/result"; 4 | import Config from "@/values/config"; 5 | import Manager from "../manager"; 6 | import { ResponseType } from "../defines/responseType"; 7 | 8 | function getBuildMode(): BuildMode { 9 | switch (import.meta.env.MODE) { 10 | case 'development': return BuildMode.DEV; 11 | case 'production': return BuildMode.PROD; 12 | case 'test': return BuildMode.TEST; 13 | case 'staging': return BuildMode.STAGE; 14 | default: return BuildMode.DEV; 15 | } 16 | } 17 | 18 | function mutObserve(element: Element | string | null, callback: MutationCallback): MutationObserver | null { 19 | const node = typeof element === 'string' ? document.querySelector(element) : element; 20 | if (!node) { 21 | // Manager.Logger.warn('Selector not found', element); 22 | return null; 23 | } 24 | const observer = new MutationObserver((mutations, observer) => { 25 | callback(mutations, observer); 26 | // observer.disconnect(); 27 | }); 28 | observer.observe(node, { childList: true, subtree: true }); 29 | return observer; 30 | } 31 | 32 | function getStringValue(val: number | string, defaultValue = Config.Strings.NA): string { 33 | return val === -1 ? defaultValue : val.toString(); 34 | } 35 | 36 | function docFind(selector: string | object, parentElement?: Element | Document | null, supressError = false): HTMLElement { 37 | if (parentElement === undefined) parentElement = document; 38 | if (parentElement === null) { 39 | throw new Error('Parent element is null'); 40 | } 41 | if (typeof selector === 'object') { 42 | //@ts-ignore 43 | if (selector.self) selector = selector.self; 44 | else { 45 | throw new Error('Selector must be a string'); 46 | } 47 | } 48 | const element = parentElement.querySelector(selector as string); 49 | if (!element && !supressError) { 50 | throw new Error(`Element not found: ${selector}`); 51 | } 52 | return element as HTMLElement; 53 | } 54 | 55 | function docFindById(selectorId: string | object, parentElement?: Element | Document): HTMLElement { 56 | const selector = '#' + selectorId; 57 | return docFind(selector, parentElement); 58 | } 59 | 60 | function clearAllChildren(element: Element) { 61 | element.innerHTML = ''; 62 | } 63 | 64 | function parseHTML(htmlText: string): HTMLElement { 65 | const parser = new DOMParser(); 66 | const doc = parser.parseFromString(htmlText, 'text/html'); 67 | return doc.body.firstChild as HTMLElement; 68 | } 69 | 70 | function checkDone(element: Element): boolean { 71 | const done = element.classList.contains('done'); 72 | if (!done) { 73 | element.classList.add('done'); 74 | } 75 | return done; 76 | } 77 | 78 | function getUrl(url: string) { 79 | return Config.App.PROXY_URL + url; 80 | } 81 | 82 | async function doFetch(url: string, config: RequestInit, timeout = Config.App.DEFAULT_NETWORK_TIMEOUT) { 83 | const controller = new AbortController(); 84 | const timeoutId = setTimeout(() => controller.abort(), timeout); 85 | 86 | try { 87 | const response = await fetch(url, { ...config, signal: controller.signal }); 88 | return response; 89 | } catch (error: any) { 90 | if (error.name === "AbortError") { 91 | throw Result.TIMEOUT; 92 | } 93 | throw error; 94 | } finally { 95 | clearTimeout(timeoutId); 96 | } 97 | } 98 | 99 | async function makeRequest(url: string, data?: Object, responseType?: ResponseType): Promise { 100 | const method = data ? RequestMethod.POST : RequestMethod.GET; 101 | data = data || {}; 102 | try { 103 | let config: RequestInit = { 104 | method: method, 105 | headers: { 106 | 'Content-Type': 'application/json' 107 | } 108 | } 109 | if (method === RequestMethod.POST) config.body = JSON.stringify(data); 110 | const response = await doFetch(url, config); 111 | if (!response.ok) { 112 | Manager.Logger.warn('makeRequest', response.statusText); 113 | throw Result.INVALID; 114 | } 115 | switch (responseType) { 116 | case ResponseType.BLOB: 117 | return await response.blob(); 118 | case ResponseType.TEXT: 119 | return await response.text(); 120 | case ResponseType.JSON: 121 | return await response.json(); 122 | default: 123 | return await response.json(); 124 | } 125 | } catch (error: any) { 126 | Manager.Logger.error('makeRequest', error); 127 | throw Result.ERROR; 128 | } 129 | } 130 | 131 | export { 132 | getBuildMode, 133 | mutObserve, 134 | getStringValue, 135 | docFind, 136 | docFindById, 137 | clearAllChildren, 138 | parseHTML, 139 | checkDone, 140 | getUrl, 141 | makeRequest 142 | }; -------------------------------------------------------------------------------- /src/core/utils/leetcodeManager.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest, getUrl } from './helpers'; 2 | import { Result } from '../defines/result'; 3 | import Config from '@/values/config' 4 | import Manager from '../manager'; 5 | 6 | export type IUserDetails = { 7 | username: string; 8 | avatar: string; 9 | name: string; 10 | rating: number; 11 | contests: string; 12 | problems_solved: number; 13 | easy: number; 14 | medium: number; 15 | hard: number; 16 | top: string; 17 | } 18 | 19 | export interface IFriendData extends IUserDetails { 20 | displayName: string; 21 | rowElement?: HTMLElement; 22 | } 23 | 24 | export type IUserContestDetails = { 25 | username?: string 26 | rank: number; 27 | score: number; 28 | old_rating: number; 29 | delta_rating: number; 30 | new_rating: number; 31 | }; 32 | 33 | class LeetcodeManager { 34 | 35 | static API_URL = "https://leetcode.com/graphql"; 36 | 37 | static async getUserDetails(username: string): Promise { 38 | const data = { 39 | query: `query userCombinedInfo($username: String!) { 40 | matchedUser(username: $username) { 41 | profile { 42 | userAvatar 43 | realName 44 | } 45 | } 46 | userContestRanking(username: $username) { 47 | attendedContestsCount 48 | rating 49 | topPercentage 50 | } 51 | matchedUser(username: $username) { 52 | submitStatsGlobal { 53 | acSubmissionNum { 54 | count 55 | } 56 | } 57 | } 58 | }`, 59 | variables: { 60 | username: username 61 | } 62 | }; 63 | 64 | try { 65 | const result = await makeRequest(Config.App.LEETCODE_API_URL, data); 66 | if (!!result?.data?.matchedUser === false) return null; 67 | 68 | const details: IUserDetails = { 69 | username: username, 70 | avatar: result.data.matchedUser.profile.userAvatar, 71 | name: result.data.matchedUser.profile.realName, 72 | rating: result.data.userContestRanking ? result.data.userContestRanking.rating : "-", 73 | contests: result.data.userContestRanking ? result.data.userContestRanking.attendedContestsCount : "-", 74 | problems_solved: result.data.matchedUser.submitStatsGlobal.acSubmissionNum[0].count, 75 | easy: result.data.matchedUser.submitStatsGlobal.acSubmissionNum[1].count, 76 | medium: result.data.matchedUser.submitStatsGlobal.acSubmissionNum[2].count, 77 | hard: result.data.matchedUser.submitStatsGlobal.acSubmissionNum[3].count, 78 | top: result.data.userContestRanking ? result.data.userContestRanking.topPercentage : "-" 79 | }; 80 | 81 | return details; 82 | } 83 | catch (e: any) { 84 | Manager.Logger.error(LeetcodeManager.name, e); 85 | throw e; 86 | } 87 | } 88 | 89 | static async isUserExist(username: string): Promise { 90 | const data = `{ 91 | "query": "query userPublicProfile($username: String!) { matchedUser(username: $username) { username } } ", 92 | "variables": { 93 | "username": "${username}" 94 | }, 95 | "operationName": "userPublicProfile" 96 | } `; 97 | 98 | try { 99 | const response = await fetch(Config.App.LEETCODE_API_URL, { 100 | method: "POST", 101 | headers: { 102 | "Content-Type": "application/json", 103 | }, 104 | body: data, 105 | }); 106 | 107 | const result = await response.json(); 108 | return !!result?.data?.matchedUser; 109 | } 110 | catch (e: any) { 111 | Manager.Logger.error(LeetcodeManager.name, e); 112 | throw e; 113 | } 114 | } 115 | 116 | static async getUserContestDetails(username: string): Promise { 117 | const contest_name = window.location.pathname.split("/")[2]; 118 | 119 | /* SAMPLE RESULT DATA 120 | [ 121 | { 122 | "_id": "64f406d063900e4acc4931a2", 123 | "contest_name": "weekly-contest-361", 124 | "contest_id": 899, 125 | "username": "usephysics", 126 | "user_slug": "usephysics", 127 | "country_code": "IN", 128 | "country_name": "India", 129 | "rank": 4342, 130 | "score": 18, 131 | "finish_time": "2023-09-03T03:03:19", 132 | "data_region": "US", 133 | "insert_time": "2023-09-03T04:08:37.929000", 134 | "attendedContestsCount": 48, 135 | "old_rating": 1578.3541615854353, 136 | "new_rating": 3309.879095204823, 137 | "delta_rating": 31.52493361938756, 138 | "predict_time": "2023-09-03T04:13:51.395000" 139 | } 140 | ]*/ 141 | 142 | try { 143 | const result = await makeRequest(getUrl(`${Config.App.LCCN_API_URL}?username=${username}&contest_name=${contest_name}`)); 144 | 145 | if (!result || !result.length) return null; 146 | if (result.detail && result.detail.startsWith('contest not found')) throw Result.NO_DATA; 147 | 148 | let user_contest_details = { 149 | rank: result[0] ? result[0].rank : -1, 150 | score: result[0] ? result[0].score : -1, 151 | old_rating: result[0] ? Math.round(result[0].old_rating) : -1, 152 | delta_rating: result[0] ? Math.round(result[0].delta_rating) : -1, 153 | new_rating: result[0] ? Math.round(result[0].new_rating) : -1, 154 | } 155 | return user_contest_details; 156 | 157 | } 158 | catch (e: any) { 159 | Manager.Logger.error(LeetcodeManager.name, e); 160 | 161 | if (e == Result.TIMEOUT){ 162 | let user_contest_details: IUserContestDetails = { 163 | rank: -1, 164 | score: -1, 165 | old_rating: -1, 166 | delta_rating: -1, 167 | new_rating: -1, 168 | } 169 | return user_contest_details; 170 | } 171 | 172 | throw Result.ERROR; 173 | } 174 | } 175 | 176 | static isDarkTheme(): boolean { 177 | return document.documentElement.style.colorScheme !== 'light'; 178 | } 179 | } 180 | 181 | export { LeetcodeManager }; -------------------------------------------------------------------------------- /src/core/utils/logManager.ts: -------------------------------------------------------------------------------- 1 | import { BuildMode } from "../defines/buildModes"; 2 | import { getBuildMode } from "./helpers"; 3 | 4 | type Message = string | object | any[]; 5 | 6 | class LogManager { 7 | 8 | /** 9 | * Following will be for development mode only 10 | * @param tag - The tag to be used for logging 11 | * @param message - The message to be logged 12 | */ 13 | 14 | static log(tag: string, message: Message) { 15 | if (getBuildMode() !== BuildMode.DEV) return; 16 | 17 | console.log(`[LX #${tag}] ${message}`); 18 | } 19 | 20 | static warn(tag: string, message: Message) { 21 | if (getBuildMode() !== BuildMode.DEV) return; 22 | 23 | console.warn(`[LX #${tag}] ${message}`); 24 | } 25 | 26 | /** 27 | * Following will be for development and production mode 28 | * @param tag - The tag to be used for logging 29 | * @param message - The message to be logged 30 | */ 31 | 32 | static info(tag: string, message: Message) { 33 | console.info(`[LX #${tag}] ${message}`); 34 | } 35 | 36 | static error(tag: string, message: Message) { 37 | console.error(`[LX #${tag}] ${message}`); 38 | } 39 | } 40 | 41 | export { LogManager }; -------------------------------------------------------------------------------- /src/core/utils/metaManager.ts: -------------------------------------------------------------------------------- 1 | import { BrowserType } from "../defines/browserType"; 2 | import Manager from "../manager"; 3 | 4 | class MetaManager { 5 | 6 | static getAppName(): string { 7 | const manifest = browser.runtime.getManifest(); 8 | return manifest.name; 9 | } 10 | 11 | static getAppVersionString(){ 12 | const manifest = browser.runtime.getManifest(); 13 | return manifest.version; 14 | } 15 | 16 | static getAppVersionNumber(): number[] { 17 | const manifest = browser.runtime.getManifest(); 18 | return manifest.version.split('.').map(Number); 19 | } 20 | 21 | static getAppDescription(): string { 22 | const manifest = browser.runtime.getManifest(); 23 | return manifest.description || ''; 24 | } 25 | 26 | static async getActivated(): Promise { 27 | const isActivated: boolean = await Manager.Storage.get('activated', true); 28 | return isActivated; 29 | } 30 | 31 | static async setActivated(activated: boolean): Promise { 32 | await Manager.Storage.set('activated', activated); 33 | } 34 | 35 | static async getBrowser(): Promise { 36 | const userAgent = navigator.userAgent; 37 | 38 | if (userAgent.includes(BrowserType.Firefox)) { 39 | return BrowserType.Firefox; 40 | } else if (userAgent.includes(BrowserType.Chrome)) { 41 | return BrowserType.Chrome; 42 | } 43 | return BrowserType.Other; 44 | } 45 | 46 | } 47 | 48 | export { MetaManager }; -------------------------------------------------------------------------------- /src/core/utils/storageManager.ts: -------------------------------------------------------------------------------- 1 | import { BrowserType } from "../defines/browserType"; 2 | 3 | class StorageManager { 4 | 5 | static async get(key: string, fallback: T): Promise { 6 | const value = await storage.getItem(`local:${key}`); 7 | return (value ?? fallback) as T; 8 | } 9 | 10 | static async set(key: string, value: any): Promise { 11 | await storage.setItem(`local:${key}`, value); 12 | } 13 | 14 | static async remove(key: string): Promise { 15 | await storage.removeItem(`local:${key}`); 16 | } 17 | 18 | } 19 | 20 | export { StorageManager }; -------------------------------------------------------------------------------- /src/entries/background/index.ts: -------------------------------------------------------------------------------- 1 | import { storage } from 'wxt/storage'; 2 | import { registerBackgroundService } from './service'; 3 | import Manager from '@/core/manager'; 4 | import { isMigrationNeeded, runMigration } from './migrate'; 5 | 6 | registerBackgroundService(); 7 | 8 | browser.runtime.onInstalled.addListener(async function (details) { 9 | if (details.reason === 'install') { 10 | Manager.Logger.info('BG', 'Extension Installed.'); 11 | Manager.Storage.set('activated', true); 12 | } 13 | 14 | else if (details.reason === 'update') { 15 | Manager.Logger.info('BG', `Extension Updated. v${details.previousVersion} -> v${Manager.Meta.getAppVersionString()}`); 16 | 17 | const previous_version = details.previousVersion || '0.0.0'; 18 | await Manager.Storage.set('previous_version', previous_version); 19 | 20 | if (await isMigrationNeeded(previous_version)) { 21 | const { migrationVersion } = await runMigration(); 22 | if (migrationVersion) { 23 | const currentVersion = Manager.Meta.getAppVersionString(); 24 | Manager.Logger.info('BG', `Migration Completed. (MIG_v${migrationVersion}) v${previous_version} -> v${currentVersion}`); 25 | } else { 26 | Manager.Logger.info('BG', 'No migration needed.'); 27 | } 28 | } 29 | } 30 | 31 | else { 32 | Manager.Logger.info('BG', 'Chrome Updated.'); 33 | } 34 | }); 35 | 36 | browser.runtime.onMessage.addListener(async function (message: any, sender, sendResponse) { 37 | if (message.action === 'consoleLog') { 38 | Manager.Logger.log('BG', message); 39 | sendResponse({ success: true, message: 'Logged successfully.' }); 40 | } else if (message.action === "isActivated") { 41 | const activated = Manager.Storage.get('activated', true); 42 | sendResponse({ activated }); 43 | } 44 | 45 | return true; 46 | }); 47 | 48 | async function addFriendsFromFileContent(content: string) { 49 | try { 50 | let decoded_content = atob(content); 51 | let regex = /^[a-zA-Z0-9;_]+$/; 52 | if (!regex.test(decoded_content)) { 53 | // openModal("Invalid users"); 54 | Manager.Logger.log('BG', "Invalid users"); 55 | return; 56 | } 57 | 58 | let friendList = decoded_content.split(";"); 59 | 60 | friendList = [...new Set(decoded_content.split(";"))]; 61 | if (friendList.length > Manager.Friend.FRIENDS_LIMIT) { 62 | // openModal(Manager.Friend.FRIENDS_MESSAGE); 63 | Manager.Logger.log('BG', Manager.Friend.FRIENDS_MESSAGE); 64 | return; 65 | } 66 | 67 | let invalid_users: Array = []; 68 | let valid_users: Array = []; 69 | 70 | let promises = friendList.map(async username => { 71 | const user_exists = await Manager.Leetcode.isUserExist(username); 72 | if (user_exists) { 73 | valid_users.push(username); 74 | } else { 75 | invalid_users.push(username); 76 | } 77 | }); 78 | 79 | const result = await Promise.all(promises).then(async () => { 80 | const friends = valid_users; 81 | if (friends.length == 0 && invalid_users.length > 0) { 82 | // openModal("No valid users to import."); 83 | Manager.Logger.log('BG', "No valid users to import."); 84 | return; 85 | } 86 | await Manager.Storage.set('friends', friends); 87 | if (invalid_users.length > 0) { 88 | // openModal(friends.length + ' Friend(s) imported successfully. Invalid users: ' + invalid_users.join(" ")); 89 | Manager.Logger.log('BG', friends.length + ' Friend(s) imported successfully. Invalid users: ' + invalid_users.join(" ")); 90 | } else { 91 | // openModal(friends.length + ' Friend(s) imported successfully.'); 92 | Manager.Logger.log('BG', friends.length + ' Friend(s) imported successfully.'); 93 | } 94 | return; 95 | }); 96 | 97 | return -1; 98 | 99 | } catch (e: any) { 100 | // openModal(`Something went wrong\n${e.message}`); 101 | Manager.Logger.log('BG', `Something went wrong\n${e.message}`); 102 | return; 103 | } 104 | } 105 | 106 | export default defineBackground(() => { 107 | Manager.Logger.log('Background Listening', { id: browser.runtime.id }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/entries/background/migrate.ts: -------------------------------------------------------------------------------- 1 | import Manager from "@/core/manager"; 2 | import { FriendManager } from "@/core/utils/friendManager"; 3 | 4 | function getVersionDiff(a: number[], b: number[]): number { 5 | const maxLength = Math.max(a.length, b.length); 6 | let totalDifference = 0; 7 | 8 | for (let i = 0; i < maxLength; i++) { 9 | const numA = a[i] ?? 0; 10 | const numB = b[i] ?? 0; 11 | const diff = numA - numB; 12 | 13 | if (diff !== 0) { 14 | totalDifference += diff * Math.pow(1000, maxLength - i - 1); 15 | } 16 | } 17 | 18 | return totalDifference; 19 | } 20 | 21 | async function isMigrationNeeded(previous_version: string): Promise { 22 | const currentVersion = Manager.Meta.getAppVersionNumber(); 23 | 24 | const previousVersion = getVersionNumber(previous_version); 25 | 26 | let shouldMigrate = getVersionDiff(currentVersion, previousVersion) > 0; 27 | if (!shouldMigrate) return false; 28 | 29 | shouldMigrate = getApplicableMigration(migrations, previousVersion) !== null; 30 | 31 | return shouldMigrate; 32 | } 33 | 34 | const migrations: Record Promise> = { 35 | "1.0.4": async () => { 36 | // Migration logic till version 1.0.4 37 | 38 | const OLD_FRIENDS_LOC = 'myfriends'; 39 | 40 | const currentFriends = await Manager.Storage.get(FriendManager.FRIENDS_LOC, []); 41 | 42 | const data = await browser.storage.local.get([OLD_FRIENDS_LOC]); 43 | const oldFriends: string[] = Array.isArray(data.myfriends) ? data.myfriends : []; 44 | 45 | const mergedFriends = [...new Set([...currentFriends, ...oldFriends])]; 46 | 47 | await Manager.Storage.set(FriendManager.FRIENDS_LOC, mergedFriends); 48 | await browser.storage.local.remove([OLD_FRIENDS_LOC]); 49 | 50 | await Manager.Storage.set('migration_done', true); 51 | } 52 | }; 53 | 54 | function getVersionNumber(version: string): number[] { 55 | return version.split('.').map(Number); 56 | } 57 | 58 | // Get the earliest applicable migration version 59 | function getApplicableMigration( 60 | migrations: Record Promise>, 61 | previousVersion: number[] 62 | ): string | null { 63 | const applicableVersions = Object.keys(migrations) 64 | .map(v => getVersionNumber(v)) 65 | .filter(v => getVersionDiff(v, previousVersion) >= 0) 66 | .sort(getVersionDiff); 67 | 68 | if (applicableVersions.length === 0) return null; 69 | const earliest = applicableVersions[0]; 70 | return earliest.join('.') as keyof typeof migrations; 71 | } 72 | 73 | async function runMigration(): Promise<{ migrationVersion: string | null }> { 74 | const previous_version = await Manager.Storage.get('previous_version', '0.0.0'); 75 | const previousVersion = getVersionNumber(previous_version); 76 | const migrationVersion = getApplicableMigration(migrations, previousVersion); 77 | 78 | Manager.Logger.info('BG', `MXMX PREV => ${previousVersion}`); 79 | 80 | if (!migrationVersion) return {migrationVersion: null}; 81 | const migration = migrations[migrationVersion]; 82 | 83 | if (migration) { 84 | await migration(); 85 | } 86 | 87 | return {migrationVersion}; 88 | } 89 | 90 | export { isMigrationNeeded, runMigration }; -------------------------------------------------------------------------------- /src/entries/background/service.ts: -------------------------------------------------------------------------------- 1 | import Manager from '@/core/manager'; 2 | import { defineProxyService } from '@webext-core/proxy-service'; 3 | 4 | class BackgroundService { 5 | 6 | console: Console = console; 7 | 8 | browser: typeof browser = browser; 9 | 10 | storage = storage; 11 | 12 | } 13 | 14 | export const [registerBackgroundService, getBackgroundService] = defineProxyService( 15 | 'BackgroundService', 16 | () => new BackgroundService(), 17 | ); 18 | -------------------------------------------------------------------------------- /src/entries/content.ts: -------------------------------------------------------------------------------- 1 | import App from '@/core/app'; 2 | 3 | export default defineContentScript({ 4 | matches: ["*://leetcode.com/*"], 5 | async main() { 6 | const app = new App(); 7 | await app.applyModules(); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/entries/import_friends/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Import Friends 6 | 57 | 58 | 59 |
60 |

Import Friends

61 |

Select a .lx file containing your friends list.

62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/entries/import_friends/main.ts: -------------------------------------------------------------------------------- 1 | import Manager from "@/core/manager"; 2 | import { docFind } from "@/core/utils/helpers"; 3 | 4 | document.addEventListener('DOMContentLoaded', function (): void { 5 | const fileInput = document.getElementById('file-input') as HTMLInputElement; 6 | const importButton = document.getElementById('import-button') as HTMLButtonElement; 7 | const resultDiv = document.getElementById('result') as HTMLDivElement; 8 | 9 | let selectedFile: File | null = null; 10 | 11 | // Enable import button when a file is selected 12 | fileInput.addEventListener('change', function (event: Event): void { 13 | const target = event.target as HTMLInputElement; 14 | selectedFile = target.files ? target.files[0] : null; 15 | 16 | if (selectedFile) { 17 | importButton.disabled = false; 18 | } else { 19 | importButton.disabled = false; 20 | } 21 | }); 22 | 23 | // Handle import button click 24 | importButton.addEventListener('click', function (): void { 25 | if (!selectedFile) { 26 | showResult('Please select a file first.', 'error'); 27 | return; 28 | } 29 | 30 | const reader = new FileReader(); 31 | 32 | reader.onload = async function (e: ProgressEvent): Promise { 33 | const content = reader.result as string; 34 | await addFriendsFromFileContent(content); 35 | } 36 | 37 | reader.readAsText(selectedFile); 38 | }); 39 | 40 | function showResult(message: string, type: 'success' | 'error'): void { 41 | resultDiv.textContent = message; 42 | resultDiv.className = type; 43 | resultDiv.style.display = 'block'; 44 | } 45 | 46 | async function addFriendsFromFileContent(content: string) { 47 | try { 48 | let decoded_content = atob(content); 49 | let regex = /^[a-zA-Z0-9;_]+$/; 50 | if (!regex.test(decoded_content)) { 51 | showResult("Invalid users", 'error'); 52 | return; 53 | } 54 | 55 | let friendList = decoded_content.split(";"); 56 | 57 | friendList = [...new Set(decoded_content.split(";"))]; 58 | if (friendList.length > Manager.Friend.FRIENDS_LIMIT) { 59 | showResult(Manager.Friend.FRIENDS_MESSAGE, 'error'); 60 | return; 61 | } 62 | 63 | let invalid_users: Array = []; 64 | let valid_users: Array = []; 65 | 66 | let promises = friendList.map(async username => { 67 | const user_exists = true; // await Manager.Leetcode.isUserExist(username); 68 | if (user_exists) { 69 | valid_users.push(username); 70 | } else { 71 | invalid_users.push(username); 72 | } 73 | }); 74 | 75 | const result = await Promise.all(promises).then(async () => { 76 | const friends = valid_users; 77 | if (friends.length == 0 && invalid_users.length > 0) { 78 | showResult("No valid users to import.", 'error'); 79 | return; 80 | } 81 | await Manager.Storage.set('friends', friends); 82 | if (invalid_users.length > 0) { 83 | showResult(friends.length + ' Friend(s) imported successfully. Invalid users: ' + invalid_users.join(" "), 'success'); 84 | } else { 85 | showResult(friends.length + ' Friend(s) imported successfully.', 'success'); 86 | } 87 | return; 88 | }); 89 | 90 | return -1; 91 | 92 | } catch (e: any) { 93 | showResult(`Something went wrong\n${e.message}`, 'error'); 94 | return; 95 | } 96 | } 97 | }); -------------------------------------------------------------------------------- /src/entries/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Title

13 |

Description

14 |
15 | 16 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/entries/popup/main.ts: -------------------------------------------------------------------------------- 1 | import Manager from '@/core/manager'; 2 | import { getBackgroundService } from '@/entries/background/service'; 3 | import { docFind } from '@/core/utils/helpers'; 4 | import { BrowserType } from '@/core/defines/browserType'; 5 | 6 | const bg = getBackgroundService(); 7 | 8 | document.addEventListener("DOMContentLoaded", () => { 9 | const ACTIVATED_STROKE_COLOR = '#007bff'; 10 | const DEACTIVATED_STROKE_COLOR = '#a4a4a4'; 11 | 12 | const appTitle = docFind("#lx-title"); 13 | const appDescription = docFind("#lx-desc"); 14 | const appEmojis = ['😵', '😍', '😘', '❤️️', '🥰️']; 15 | const iEmoji = Math.floor(Math.random() * appEmojis.length); 16 | 17 | const activateExtButton = docFind("#activateExt") as HTMLButtonElement; 18 | const exportFriendsButton = docFind("#exportFriends") as HTMLButtonElement; 19 | const importFriendsButton = docFind("#importFriends") as HTMLButtonElement; 20 | const usersFileInput = docFind("#usersFileInput") as HTMLInputElement; 21 | const clearFriendsButton = docFind("#clearFriends") as HTMLButtonElement; 22 | 23 | appTitle.textContent = Manager.Meta.getAppName(); 24 | appTitle.appendChild(document.createTextNode(` ${appEmojis[iEmoji]} v${Manager.Meta.getAppVersionString()}`)); 25 | appDescription.textContent = Manager.Meta.getAppDescription(); 26 | 27 | (async () => { 28 | if (await Manager.Meta.getActivated()) { 29 | activateExtButton.querySelector('path')?.setAttribute('stroke', ACTIVATED_STROKE_COLOR); 30 | } else { 31 | activateExtButton.querySelector('path')?.setAttribute('stroke', DEACTIVATED_STROKE_COLOR); 32 | } 33 | })(); 34 | 35 | activateExtButton.addEventListener("click", async function (e) { 36 | const isActivated = await Manager.Meta.getActivated(); 37 | 38 | if (isActivated) { 39 | activateExtButton.querySelector('path')?.setAttribute('stroke', DEACTIVATED_STROKE_COLOR); 40 | await Manager.Meta.setActivated(false); 41 | } else { 42 | activateExtButton.querySelector('path')?.setAttribute('stroke', ACTIVATED_STROKE_COLOR); 43 | await Manager.Meta.setActivated(true); 44 | } 45 | }); 46 | 47 | exportFriendsButton.addEventListener("click", async () => { 48 | const friendList = await Manager.Friend.getFriends(); 49 | 50 | if (friendList.length === 0) { 51 | openModal('No Friends to export'); 52 | return; 53 | } 54 | 55 | let myFriends = btoa(friendList.join(";")); 56 | let blob = new Blob([myFriends], { type: "application/octet-stream" }); 57 | let date = new Date(); 58 | let filename = "lc-friends-" + date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + ".lx"; 59 | 60 | await browser.downloads.download({ url: URL.createObjectURL(blob), filename: filename }); 61 | }); 62 | 63 | importFriendsButton.addEventListener("click", async () => { 64 | if (await Manager.Meta.getBrowser() === BrowserType.Firefox) { 65 | browser.tabs.create({ 66 | url: browser.runtime.getURL("/import_friends.html") 67 | }).catch((error: Error) => { 68 | Manager.Logger.error("Error opening import page:", error); 69 | }); 70 | 71 | // openModal("This feature is not yet supported in Firefox."); 72 | return; 73 | } 74 | 75 | usersFileInput.click(); 76 | }); 77 | 78 | usersFileInput.addEventListener("change", async () => { 79 | if (!usersFileInput.files) return; 80 | 81 | const file = usersFileInput.files[0]; 82 | const reader = new FileReader(); 83 | reader.onload = async () => { 84 | const content = reader.result as string; 85 | await addFriendsFromFileContent(content); 86 | }; 87 | 88 | reader.readAsText(file); 89 | }); 90 | 91 | clearFriendsButton.addEventListener("click", async () => { 92 | const numFriends = await Manager.Friend.getNumFriends(); 93 | await Manager.Friend.clearAllFriends(); 94 | 95 | openModal(`Cleared ${numFriends} Friend(s)`); 96 | }); 97 | }); 98 | 99 | function openModal(message: string) { 100 | var modal = docFind("#myModal") as HTMLElement; 101 | var modalMessage = docFind("#modalMessage") as HTMLElement; 102 | modalMessage.textContent = message; 103 | modal.style.display = "block"; 104 | 105 | modal.addEventListener("click", function (e) { 106 | closeModal(); 107 | }); 108 | } 109 | 110 | function closeModal() { 111 | var modal = docFind('#myModal') as HTMLElement; 112 | modal.style.display = "none"; 113 | } 114 | 115 | async function addFriendsFromFileContent(content: string) { 116 | try { 117 | let decoded_content = atob(content); 118 | let regex = /^[a-zA-Z0-9;_]+$/; 119 | if (!regex.test(decoded_content)) { 120 | openModal("Invalid users"); 121 | return; 122 | } 123 | 124 | let friendList = decoded_content.split(";"); 125 | 126 | friendList = [...new Set(decoded_content.split(";"))]; 127 | if (friendList.length > Manager.Friend.FRIENDS_LIMIT) { 128 | openModal(Manager.Friend.FRIENDS_MESSAGE); 129 | return; 130 | } 131 | 132 | let invalid_users: Array = []; 133 | let valid_users: Array = []; 134 | 135 | let promises = friendList.map(async username => { 136 | const user_exists = await Manager.Leetcode.isUserExist(username); 137 | if (user_exists) { 138 | valid_users.push(username); 139 | } else { 140 | invalid_users.push(username); 141 | } 142 | }); 143 | 144 | const result = await Promise.all(promises).then(async () => { 145 | const friends = valid_users; 146 | if (friends.length == 0 && invalid_users.length > 0) { 147 | openModal("No valid users to import."); 148 | return; 149 | } 150 | await Manager.Storage.set('friends', friends); 151 | if (invalid_users.length > 0) { 152 | openModal(friends.length + ' Friend(s) imported successfully. Invalid users: ' + invalid_users.join(" ")); 153 | } else { 154 | openModal(friends.length + ' Friend(s) imported successfully.'); 155 | } 156 | return; 157 | }); 158 | 159 | return -1; 160 | 161 | } catch (e: any) { 162 | openModal(`Something went wrong\n${e.message}`); 163 | return; 164 | } 165 | } -------------------------------------------------------------------------------- /src/entries/popup/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | background-color: #f2f2f2; 4 | text-align: center; 5 | margin: 0; 6 | padding: 0; 7 | min-width: 310px; 8 | max-width: 310px; 9 | } 10 | 11 | h3 { 12 | color: #333; 13 | } 14 | 15 | #lx-desc { 16 | color: #555; 17 | padding: 5px; 18 | font-size: 14px; 19 | } 20 | 21 | .lx-btn { 22 | background-color: #ed9411; 23 | min-width: 300px; 24 | color: #fff; 25 | border: none; 26 | padding: 5px; 27 | margin: 5px; 28 | margin-top: 0px; 29 | cursor: pointer; 30 | font-size: 16px; 31 | } 32 | 33 | .lx-btn:hover { 34 | background-color: #e37c07; 35 | transition: background-color 0.2s ease-in; 36 | } 37 | 38 | #activateExt { 39 | width: 60px; 40 | height: 60px; 41 | margin: 10px auto; 42 | cursor: pointer; 43 | } 44 | 45 | #activateExt path { 46 | stroke: "#a4a4a4"; 47 | } 48 | #activateExt path.active { 49 | stroke: "#007bff"; 50 | } 51 | 52 | 53 | /******* MODAL *******/ 54 | 55 | .modal { 56 | display: none; 57 | position: fixed; 58 | top: 0; 59 | left: 0; 60 | width: 100%; 61 | height: 100%; 62 | background-color: rgba(0, 0, 0, 0.7); 63 | z-index: 1; 64 | } 65 | 66 | .modal-content { 67 | background-color: #fff; 68 | margin: 15% auto; 69 | padding: 20px; 70 | width: 70%; 71 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 72 | text-align: center; 73 | } 74 | 75 | .close { 76 | color: #aaa; 77 | float: right; 78 | font-size: 28px; 79 | font-weight: bold; 80 | cursor: pointer; 81 | } -------------------------------------------------------------------------------- /src/public/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/src/public/icons/icon-128.png -------------------------------------------------------------------------------- /src/public/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/src/public/icons/icon-16.png -------------------------------------------------------------------------------- /src/public/icons/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/src/public/icons/icon-24.png -------------------------------------------------------------------------------- /src/public/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/src/public/icons/icon-48.png -------------------------------------------------------------------------------- /src/public/icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binbard/leet-xt/8e5b6c97cfaae3434452edeb5868cdcf744c168b/src/public/icons/icon-96.png -------------------------------------------------------------------------------- /src/values/config/app.ts: -------------------------------------------------------------------------------- 1 | export const LEETCODE_API_URL = "https://leetcode.com/graphql/"; 2 | 3 | export const PROXY_URL = "https://wandb-berm-1c80.public-antagonist-58.workers.dev/?"; 4 | 5 | export const LCCN_API_URL = "https://lccn.lbao.site/api/v1/contest-records/user"; 6 | 7 | export const LEETCARD_BASE_URL = "https://leetcard.jacoblin.cool" 8 | 9 | export const ZEROTRAC_RATING_URL = "https://zerotrac.github.io/leetcode_problem_rating/data.json"; 10 | 11 | 12 | const GSHEET_BASE_URL = "https://sheets.googleapis.com/v4/spreadsheets/1ilv8yYAIcggzTkehjuB_dsRI4LUxjkTPZz4hsBKJvwo/values"; 13 | 14 | export const GSHEETS_COMPANY_DATA_KEY = "AIzaSyDDAE3rf1fjLGKM0FUHQeTcsmS6fCQjtDs"; 15 | 16 | export const GSHEETS_COMPANY_TAGS_URL = `${GSHEET_BASE_URL}/ProblemCompaniesTags_Map!A:C?key=${GSHEETS_COMPANY_DATA_KEY}`; 17 | 18 | export const GSHEETS_COMPANY_DATA_URL = `${GSHEET_BASE_URL}/ProblemCompaniesTags`; 19 | 20 | export const GSHEETS_COMPANIES_PROBLEM_MAP_URL = `${GSHEET_BASE_URL}/CompaniesProblem_Map!A:C?key=${GSHEETS_COMPANY_DATA_KEY}`; 21 | 22 | export const GSHEETS_COMPANIES_PROBLEM_URL = `${GSHEET_BASE_URL}/CompaniesProblem`; 23 | 24 | 25 | export const DEFAULT_NETWORK_TIMEOUT = 25_000; 26 | 27 | export const DEFAULT_INTERVAL_TIMEOUT = 5_000; 28 | 29 | export const MAX_FRIENDS = 50; -------------------------------------------------------------------------------- /src/values/config/colors.ts: -------------------------------------------------------------------------------- 1 | export const FRIEND_STAR = 'rgb(255, 226, 96)'; // #ffe260 2 | export const NOT_FRIEND_STAR = 'rgb(204, 204, 215)'; // #ccccd7 -------------------------------------------------------------------------------- /src/values/config/index.ts: -------------------------------------------------------------------------------- 1 | import * as App from './app'; 2 | import * as Strings from './strings'; 3 | import * as Colors from './colors'; 4 | 5 | const Config = { 6 | App, 7 | Colors, 8 | Strings, 9 | }; 10 | 11 | export default Config; -------------------------------------------------------------------------------- /src/values/config/strings.ts: -------------------------------------------------------------------------------- 1 | export const UNKNOWN = "" 2 | export const NA = "N/A" 3 | export const CSS_PREFIX = "lx" 4 | export const FRIENDS_PAGE_TITLE = "Friends - Leetcode" -------------------------------------------------------------------------------- /src/values/html/company_tag.html: -------------------------------------------------------------------------------- 1 | Google1208'; -------------------------------------------------------------------------------- /src/values/html/contest_friend_table.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
9 |
11 | Rank
12 |
14 | Name
15 |
17 | Score
18 |
21 | Old Rating
22 |
25 | Δ
26 |
29 | New Rating
30 |
31 |
32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /src/values/html/contest_friend_table_column.html: -------------------------------------------------------------------------------- 1 |
3 | Custom Message
-------------------------------------------------------------------------------- /src/values/html/friend_row.html: -------------------------------------------------------------------------------- 1 |
4 |
neal_wu
9 |
11 |
12 |
13 |
14 | 18 |
19 |
20 |
21 |
22 |
3686 (51)
25 |
253 (60+141+52)
30 |
0.01%
32 |
-------------------------------------------------------------------------------- /src/values/html/friend_table.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
Users
22 | 24 | 25 | 26 |
27 |
28 |
31 |
32 |
Rating
36 | 38 | 39 | 40 |
41 |
42 |
45 |
46 |
Problems Solved
47 | 51 | 53 | 54 | 55 |
56 |
57 |
60 |
61 |
Top
65 | 67 | 68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
-------------------------------------------------------------------------------- /src/values/html/premium_company_tags_body.html: -------------------------------------------------------------------------------- 1 |
2 | This is a sample message 3 |
4 |
-------------------------------------------------------------------------------- /src/values/html/ps_frequency_column.html: -------------------------------------------------------------------------------- 1 |
2 | Frequency 5 | 8 | 9 | 44 |
F -------------------------------------------------------------------------------- /src/values/html/ps_problem_row.html: -------------------------------------------------------------------------------- 1 |
4 |
7 | 10 |
11 |
13 |
14 | 21 |
22 |
23 | 35 |
37.3%
37 |
Hard
40 |
42 |
45 |
47 |
48 |
49 |
-------------------------------------------------------------------------------- /src/values/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import lc from './lc'; 2 | import Config from '@/values/config'; 3 | 4 | export const css_selectors = { 5 | lc, 6 | } 7 | 8 | function createSelfProxy(target: any, currentPath = Config.Strings.CSS_PREFIX): typeof css_selectors { 9 | 10 | const stringObj = new String(target.self || currentPath); 11 | 12 | //@ts-ignore 13 | return new Proxy(stringObj, { 14 | get(_, prop: any) { 15 | // Handle native props 16 | if (prop in stringObj) { 17 | return stringObj[prop]; 18 | } 19 | 20 | // Forward property access to the target object 21 | const value = target[prop]; 22 | 23 | if (typeof value === 'string' && value === '') { 24 | return `${currentPath}-${prop}`; // # here 25 | } 26 | 27 | // Recursively proxy nested objects 28 | if (value && typeof value === 'object') { 29 | return createSelfProxy(value, `${currentPath}-${prop}`); 30 | } 31 | 32 | return value; 33 | } 34 | }); 35 | } 36 | 37 | 38 | const Selectors = createSelfProxy(css_selectors); 39 | 40 | export default Selectors; -------------------------------------------------------------------------------- /src/values/selectors/lc/contest.ts: -------------------------------------------------------------------------------- 1 | import friend from "./friend"; 2 | 3 | export default { 4 | ranking: { 5 | container: { 6 | self: ".mx-auto.w-full", 7 | table_container: { 8 | self: ".relative.flex.w-full.justify-center", 9 | original_table: ".relative.flex.w-full.flex-col", 10 | friend_table: { 11 | self: "#lx-contest-friend-table", 12 | body: { 13 | self: "#lx-contest-friend-table-body", 14 | row: { 15 | self: "#lx-contest-friend-table-row", 16 | COLUMN: "#lx-contest-friend-table-row-column", 17 | rank: "#lx-contest-friend-table-row-rank", 18 | name: "#lx-contest-friend-table-row-name", 19 | score: "#lx-contest-friend-table-row-score", 20 | old_rating: "#lx-contest-friend-table-row-old-rating", 21 | delta_rating: "#lx-contest-friend-table-row-delta-rating", 22 | new_rating: "#lx-contest-friend-table-row-new-rating", 23 | } 24 | } 25 | } 26 | }, 27 | pagination_nav: "nav[role='navigation']", 28 | people_icon: { 29 | mode: "#lx-people-mode", 30 | dark: "#lx-people-dark", 31 | light: "#lx-people-light", 32 | }, 33 | }, 34 | } 35 | } -------------------------------------------------------------------------------- /src/values/selectors/lc/friend.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | table: { 3 | row_group: { 4 | self: "#friends-rowgroup", 5 | head_row: { 6 | name: "#lx-huser", 7 | rating: "#lx-hrating", 8 | problems_solved: "#lx-hprobsolved", 9 | top: "#lx-htop", 10 | }, 11 | row: { 12 | self: "#lx-frow", 13 | avatar: "#lx-favatar", 14 | name: "#lx-fname", 15 | rating: "#lx-frating", 16 | numcontest: "#lx-fnumcontest", 17 | problems_solved: "#lx-ftotal", 18 | easy: "#lx-feasy", 19 | medium: "#lx-fmedium", 20 | hard: "#lx-fhard", 21 | top: "#lx-ftop", 22 | }, 23 | }, 24 | }, 25 | table_parent: "div.mx-auto.w-full.grow.p-4", 26 | } -------------------------------------------------------------------------------- /src/values/selectors/lc/index.ts: -------------------------------------------------------------------------------- 1 | import contest from './contest'; 2 | import friend from './friend'; 3 | import navbar from './navbar'; 4 | import problem from './problem'; 5 | import profile from './profile'; 6 | import static_dom from './static_dom'; 7 | 8 | export default { 9 | contest, 10 | friend, 11 | navbar, 12 | problem, 13 | profile, 14 | static_dom, 15 | } -------------------------------------------------------------------------------- /src/values/selectors/lc/navbar.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | self: '#leetcode-navbar', 4 | icon_container: { 5 | self: '#leetcode-navbar > div.display-none.m-auto.w-full.items-center.justify-center.px-6 > div > div', 6 | user_avatar: '#navbar_user_avatar', 7 | profile_icon_container: '#leetcode-navbar > div.display-none.m-auto.w-full.items-center.justify-center.px-6 > div > div > div:nth-child(3)', 8 | }, 9 | }, 10 | } -------------------------------------------------------------------------------- /src/values/selectors/lc/problem.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | companies_button: { 3 | self: "div.flex.w-full.flex-1.flex-col.gap-4.overflow-y-auto.px-4.py-5 > div.flex.gap-1 > div:nth-child(3)", 4 | modal_title: "div.my-8 div.flex.py-4", 5 | modal_body: { 6 | self: "div.px-6.pb-6.pt-4", 7 | tag_num: "#lx-tag-num", 8 | tag_name: "#lx-tagname", 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/values/selectors/lc/profile.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name_section: { 3 | name_parent: ".flex.items-center.gap-1", 4 | username: ".text-label-3.text-xs", 5 | star_icon_path: '#lx-star_icon_path', 6 | } 7 | } -------------------------------------------------------------------------------- /src/values/selectors/lc/static_dom.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | app: "#app", 3 | navbar: { 4 | self: '#navbar-root', 5 | user_avatar: '#navbar_user_avatar', 6 | }, 7 | next: "#__next", 8 | } -------------------------------------------------------------------------------- /src/values/svg/friend_down_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/values/svg/friend_up_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/values/svg/friend_updown_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/values/svg/people_dark.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /src/values/svg/people_icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/values/svg/people_light.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/values/svg/ps_notac.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | -------------------------------------------------------------------------------- /src/values/svg/ps_video_solution.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/values/svg/star_icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.wxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /wxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'wxt'; 2 | import { resolve } from 'node:path'; 3 | 4 | // See https://wxt.dev/api/config.html 5 | export default defineConfig({ 6 | // Relative to project root 7 | srcDir: "src", // default: "." 8 | outDir: "dist", // default: ".output" 9 | 10 | // Relative to srcDir 11 | entrypointsDir: "entries", // default: "entrypoints" 12 | modulesDir: "wxt-modules", // default: "modules" 13 | 14 | manifest: { 15 | short_name: "Leet Xt", 16 | name: "Leet Xt - Supercharge your Leetcode practice", 17 | description: "Supercharge your LeetCode practice - Add to Friends, Premium Features, per Contest Friends Rating, and more!", 18 | icons: { 19 | "16": "icons/icon-16.png", 20 | "48": "icons/icon-48.png", 21 | "128": "icons/icon-128.png", 22 | }, 23 | host_permissions: [ 24 | "https://sheets.googleapis.com/*", 25 | "https://wandb-berm-1c80.public-antagonist-58.workers.dev/*", 26 | ], 27 | permissions: ['storage', 'downloads', 'unlimitedStorage'], 28 | }, 29 | 30 | runner: { 31 | chromiumProfile: resolve('.wxt/chrome-data'), 32 | keepProfileChanges: true, 33 | }, 34 | }) --------------------------------------------------------------------------------