├── .yarnrc ├── .gitignore ├── tests └── .vscode │ └── settings.json ├── images ├── icon.png └── start-extension.gif ├── src ├── ssl │ ├── cert.p12 │ ├── cert.pem │ └── key.pem ├── types │ ├── replace-object.ts │ ├── webview-message.ts │ ├── webview-context.ts │ └── data.ts ├── constants │ ├── webview │ │ ├── index.ts │ │ ├── post-message.ts │ │ └── config.ts │ └── configs.ts ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── helpers │ ├── common.ts │ ├── server.ts │ ├── extension.ts │ └── webview.ts ├── classes │ ├── webview-panel-serializer.ts │ └── webview-view-provider.ts ├── extension.ts └── webviews │ ├── browser.ts │ └── changes.ts ├── .vscodeignore ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── .github └── FUNDING.yml ├── .eslintrc.json ├── tsconfig.json ├── example └── .vscode │ └── settings.json ├── DEVELOP.md ├── LICENSE.md ├── README.md ├── vsc-extension-quickstart.md ├── CHANGELOG.md ├── assets ├── js │ ├── proxy.js │ └── jquery-3.7.1.slim.min.js ├── css │ └── browser.css └── browser.html └── package.json /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /tests/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vs-browser.showStatusBarItem": true 3 | } 4 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phu1237/vscode-vs-browser/HEAD/images/icon.png -------------------------------------------------------------------------------- /src/ssl/cert.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phu1237/vscode-vs-browser/HEAD/src/ssl/cert.p12 -------------------------------------------------------------------------------- /src/types/replace-object.ts: -------------------------------------------------------------------------------- 1 | interface ReplaceObject { 2 | [key: string]: any; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/webview-message.ts: -------------------------------------------------------------------------------- 1 | interface WebviewMessage { 2 | type: string; 3 | value?: any; 4 | } 5 | -------------------------------------------------------------------------------- /images/start-extension.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phu1237/vscode-vs-browser/HEAD/images/start-extension.gif -------------------------------------------------------------------------------- /src/types/webview-context.ts: -------------------------------------------------------------------------------- 1 | interface WebviewContext { 2 | extensionPath: string; 3 | webviewUri: string; 4 | } 5 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | 12 | !assets/ 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Phu1237] 4 | patreon: Phu1237 5 | buy_me_a_coffee: Phu1237 6 | thanks_dev: u/gh/phu1237 7 | custom: ['https://www.paypal.me/Phu1237', 'https://me.momo.vn/Phu1237'] 8 | -------------------------------------------------------------------------------- /src/constants/webview/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import config from "./config"; 3 | import post_message from "./post-message"; 4 | 5 | export default { 6 | CONFIG: config, 7 | POST_MESSAGE: post_message, 8 | }; 9 | -------------------------------------------------------------------------------- /src/constants/webview/post-message.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | const TYPE = { 3 | FAVOURITE_ADD: "favorite-add", 4 | FAVOURITE_REMOVE: "favorite-remove", 5 | GO_TO_SETTINGS: "go-to-settings", 6 | OPEN_INSPECTOR: "open-inspector", 7 | REFRESH_FAVOURITES: "refresh-favorites", 8 | RELOAD: "reload", 9 | SHOW_MESSAGE_BOX: "show-message-box", 10 | }; 11 | 12 | export default { 13 | TYPE, 14 | }; 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /src/constants/webview/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | const BASE = { 3 | BROWSER: { 4 | viewType: "browser", 5 | title: "VS Browser", 6 | }, 7 | PROXY: { 8 | viewType: "proxy", 9 | title: "VS Browser - Proxy", 10 | proxyMode: true, 11 | }, 12 | WITHOUT_PROXY: { 13 | viewType: "withoutproxy", 14 | title: "VS Browser - Without proxy", 15 | proxyMode: false, 16 | }, 17 | }; 18 | 19 | export default { 20 | BASE, 21 | }; 22 | -------------------------------------------------------------------------------- /src/constants/configs.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarAlignment } from "vscode"; 2 | 3 | /* eslint-disable @typescript-eslint/naming-convention */ 4 | const FAVOURITES_SAVING_PROFILE = { 5 | NAME: { 6 | AUTO: "auto", 7 | GLOBAL: "global", 8 | WORKSPACE: "workspace", 9 | }, 10 | DEFAULT: "global", 11 | }; 12 | 13 | const STATUS_BAR_ITEM = { 14 | ALIGNMENT: StatusBarAlignment.Right, 15 | PRIORITY: 100, 16 | }; 17 | 18 | export default { 19 | FAVOURITES_SAVING_PROFILE, 20 | STATUS_BAR_ITEM, 21 | }; 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | after(() => { 12 | vscode.window.showInformationMessage("All tests done!"); 13 | }); 14 | 15 | test("Sample test", () => { 16 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 17 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/types/data.ts: -------------------------------------------------------------------------------- 1 | export interface FavouriteData { 2 | [domain: string]: string; 3 | } 4 | 5 | type Data = { 6 | viewType: string; 7 | title: string; 8 | proxyMode?: boolean; 9 | url?: string; 10 | favourites?: FavouriteData; 11 | autoCompleteUrl?: string; 12 | localProxyServerEnabled?: boolean; 13 | localProxyServerPort?: number; 14 | localProxyServerCookieDomainRewrite?: boolean; 15 | localProxyServerForceLocation?: boolean; 16 | reloadOnSave?: boolean; 17 | autoReloadDurationEnabled?: boolean; 18 | autoReloadDurationTime?: number; 19 | columnToShowIn?: string; 20 | showMessageDialog?: boolean; 21 | showStatusBarItem?: boolean; 22 | }; 23 | 24 | export default Data; 25 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests"); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /example/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vs-browser.proxyMode": false, 3 | "vs-browser.url": "http://localhost", 4 | "vs-browser.favourites.list": {}, 5 | "vs-browser.favourites.savingProfile": "global", 6 | "vs-browser.autoCompleteUrl": "http://", 7 | "vs-browser.localProxyServer.enabled": false, 8 | "vs-browser.localProxyServer.port": 9999, 9 | "vs-browser.localProxyServer.cookieDomainRewrite": false, 10 | "vs-browser.localProxyServer.forceLocation": false, 11 | "vs-browser.reload.onSave": false, 12 | "vs-browser.reload.autoReloadDurationEnabled": false, 13 | "vs-browser.reload.autoReloadDurationTime": 15000, 14 | "vs-browser.columnToShowIn": "Two", 15 | "vs-browser.showMessageDialog": false, 16 | "vs-browser.showViewContainer": true, 17 | "vs-browser.showStatusBarItem": true, 18 | "vs-browser.showUpdateChanges": true 19 | } 20 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # VS Browser 2 | 3 | ## Install dependencies 4 | 5 | ```bash 6 | yarn install 7 | ``` 8 | 9 | ## Debug/ Development 10 | 11 | Simply press F5 key to start debugging session 12 | 13 | - [Debugging in Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging) 14 | - VS Code Icons: [VS Code Icons](https://) 15 | - Icons: [Icons](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing) 16 | - Usage: 17 | - In label: [https://code.visualstudio.com/api/references/icons-in-labels#icons-in-labels](https://code.visualstudio.com/api/references/icons-in-labels#icons-in-labels) 18 | - In webview: [https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts](https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts) 19 | 20 | ## Useful 21 | 22 | Nothing here yet 23 | -------------------------------------------------------------------------------- /src/helpers/common.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * Show message box 5 | * @param type Type of message 6 | * @param message Context of message 7 | * @param options https://code.visualstudio.com/api/references/vscode-api#MessageOptions 8 | */ 9 | export function showMessage( 10 | type: string, 11 | message: string, 12 | options: Object = {} 13 | ) { 14 | const configs = vscode.workspace.getConfiguration("vs-browser"); 15 | let showMessageDialog = configs.get("showMessageDialog") || false; 16 | if (showMessageDialog) { 17 | switch (type) { 18 | case "error": 19 | vscode.window.showErrorMessage(message, options); 20 | break; 21 | case "warning": 22 | vscode.window.showWarningMessage(message, options); 23 | break; 24 | case "info": 25 | vscode.window.showInformationMessage(message, options); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import * as glob from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise((c, e) => { 15 | glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}", 14 | "--disable-extensions" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 28 | ], 29 | "outFiles": [ 30 | "${workspaceFolder}/out/test/**/*.js" 31 | ], 32 | "preLaunchTask": "${defaultBuildTask}" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Phu1237 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. -------------------------------------------------------------------------------- /src/classes/webview-panel-serializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtensionContext, 3 | WebviewPanel, 4 | WebviewPanelSerializer as VscodeWebviewPanelSerializer, 5 | } from "vscode"; 6 | 7 | import * as webviewHelper from "../helpers/webview"; 8 | 9 | import browserWebview from "../webviews/browser"; 10 | 11 | class WebviewPanelSerializer implements VscodeWebviewPanelSerializer { 12 | private context: ExtensionContext; 13 | 14 | constructor(context: ExtensionContext) { 15 | this.context = context; 16 | } 17 | async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any) { 18 | // `state` is the state persisted using `setState` inside the webview 19 | console.log("Got state: " + JSON.stringify(state)); 20 | 21 | // Restore the content of our webview. 22 | // 23 | // Make sure we hold on to the `webviewPanel` passed in here and 24 | // also restore any event listeners we need on it. 25 | webviewPanel = webviewHelper.createWebviewPanel( 26 | browserWebview, 27 | this.context, 28 | state, 29 | webviewPanel 30 | ); 31 | } 32 | } 33 | 34 | export default WebviewPanelSerializer; 35 | -------------------------------------------------------------------------------- /src/classes/webview-view-provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | ExtensionContext, 4 | WebviewView, 5 | WebviewViewResolveContext, 6 | } from "vscode"; 7 | 8 | import Data from "../types/data"; 9 | import { bindWebviewEvents } from "../helpers/webview"; 10 | 11 | class WebviewViewProvider { 12 | constructor( 13 | private readonly template: Function, 14 | private readonly context: ExtensionContext, 15 | private readonly data: Data 16 | ) {} 17 | 18 | // Resolves and sets up the Webview 19 | resolveWebviewView( 20 | webviewView: WebviewView, 21 | context: WebviewViewResolveContext, 22 | _token: CancellationToken 23 | ): void { 24 | // Configure Webview options 25 | webviewView.webview.options = { 26 | enableScripts: true, 27 | }; 28 | const { title, viewType, url } = context.state as Data; 29 | const state = { 30 | ...this.data, 31 | title, 32 | viewType, 33 | url, 34 | }; 35 | // Set the Webview content 36 | bindWebviewEvents(webviewView, this.template, this.context, state); 37 | } 38 | } 39 | 40 | export default WebviewViewProvider; 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VS Browser 2 | 3 | Built-in browser for Visual Studio Code 4 | 5 | ![Start extension](https://github.com/Phu1237/vscode-vs-browser/raw/master/images/start-extension.gif) 6 | 7 | ## Features 8 | 9 | - Browser while using Visual Studio Code 10 | - Auto reload your web page after a limited time 11 | - Using a proxy to load the web page 12 | 13 | ## Usage 14 | 15 | - Open the command palette (`Ctrl+Shift+P`) & enter "`VS Browser: Start Browser`" 16 | - You should update extension settings for a better experience 17 | 18 | ## Extension Settings 19 | 20 | See the [Features Contributions] tab or in `Settings > Extensions > VS Browser` for more information 21 | 22 | ## Donation 23 | 24 | Thank you for using my product 🎉 25 | 26 | This product is free but if you like my work and you want to support me, buy me a coffee ☕ 27 | 28 | - [Paypal](https://www.paypal.me/Phu1237) 29 | - [Buymeacoffee](https://www.buymeacoffee.com/Phu1237) 30 | - [Momo](https://me.momo.vn/Phu1237) 31 | 32 | ## Changelog 33 | 34 | ### See the list of changes [here](CHANGELOG.md) 35 | 36 | ## Known Issues 37 | 38 | - Sometimes, the dialog still displays even if nothing goes wrong 39 | - Local proxy server won't stop even if all browsers are closed 40 | - Local Proxy server does not support form submit (Pure/HTML form submit) yet 41 | 42 | **Enjoy!** 43 | -------------------------------------------------------------------------------- /src/ssl/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDozCCAougAwIBAgIJAJH8gVzu4xkEMA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV 3 | BAYTAlZOMRMwEQYDVQQIDApTb21lLVN0YXRlMRAwDgYDVQQKDAdQaHUxMjM3MRAw 4 | DgYDVQQDDAdQaHUxMjM3MSAwHgYJKoZIhvcNAQkBFhFQaHUxMjM3QGdtYWlsLmNv 5 | bTAeFw0yMjA1MDkxNTE4MzVaFw0zMjA1MDYxNTE4MzVaMGgxCzAJBgNVBAYTAlZO 6 | MRMwEQYDVQQIDApTb21lLVN0YXRlMRAwDgYDVQQKDAdQaHUxMjM3MRAwDgYDVQQD 7 | DAdQaHUxMjM3MSAwHgYJKoZIhvcNAQkBFhFQaHUxMjM3QGdtYWlsLmNvbTCCASIw 8 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJou+7tzmBmJbw8nD9Apdv9IffRD 9 | 4umFe6orL27LZmgr2l2iyCUVakSy773M32QLL/F8bwmUJxsC43ugaxwBLvdPBqic 10 | JLh+YD6Br6g6bPy2ffGfMP4d9qyyptL7W9R9G121dQa5FVe32t3WZsCDXTN3CTbS 11 | rcnqXPCHmZueY7GNskFLyqDDdvhaNIEkCPUhKJm7WlkEtaOrte0UWEYpqygHpCVe 12 | 6JjU5cbgErA5bmCkomrLuYJ6FyY6WMqtH7AhBjnkxFTQC153Lu4GDrIlfBrnv/pP 13 | 4wHBbt2MS6FN3V8aTFNiA2AX0z4IrYEiETFaWIkwKrvR47UdC9y7qsAYVUECAwEA 14 | AaNQME4wHQYDVR0OBBYEFKE5S0DywLWEZEZyMS1SubfxxAfCMB8GA1UdIwQYMBaA 15 | FKE5S0DywLWEZEZyMS1SubfxxAfCMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL 16 | BQADggEBADhxIkFsUPRRb/7iju+dodlooDw23tgST4iGjox2sdsQU7da679+1cfT 17 | upZPYEb8fJo0kyNKU4WOECBJUv2WTeGc2QZZzQkCWPBFYMRarzuKWH6mnZ0+HTDy 18 | 3176ZTNEpJgQxbx8DtL2VCqDp1N9QrT/ghPW2BjwWm9/F5ZcOt14ZzjzdQnzZbG+ 19 | JB+lu1KdwdV7G1NC1hDMMjhBiSi7r5j6gQCvU5CYFe+4SPNXANHgvZwqFI9OgBTs 20 | 2EIjwnA2mZuR+kbnyzXtJUr9ezktsWOw8YoLOtTfamot7nOOCNN8Gtl9IjBVBthl 21 | 23l3loVjb7oBAv8iEs8hDBRKL7uJM0Q= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from "vscode"; 4 | import * as extensionHelper from "./helpers/extension"; 5 | 6 | // Create output channel 7 | const outputConsole = vscode.window.createOutputChannel("VS Browser"); 8 | 9 | // this method is called when your extension is activated 10 | // your extension is activated the very first time the command is executed 11 | export function activate(context: vscode.ExtensionContext) { 12 | // Use the console to output diagnostic information (console.log) and errors (console.error) 13 | // This line of code will only be executed once when your extension is activated 14 | outputConsole.appendLine("Activated!"); 15 | 16 | // Check if the extension is updated 17 | extensionHelper.onVersionChanged(context, outputConsole); 18 | 19 | // Register Serializers for webviews type 20 | extensionHelper.registerWebviewPanelSerializers(context); 21 | 22 | // Register Status bar items 23 | extensionHelper.registerStatusBarItems(context); 24 | 25 | // Register Commands 26 | extensionHelper.registerCommands(context); 27 | 28 | // Register Views 29 | extensionHelper.registerViewContainer(context); 30 | 31 | // Watch configuration changes 32 | vscode.workspace.onDidChangeConfiguration( 33 | extensionHelper.handleConfigurationChange 34 | ); 35 | 36 | extensionHelper.updateContextKey(); 37 | } 38 | 39 | // this method is called when your extension is deactivated 40 | export function deactivate() {} 41 | -------------------------------------------------------------------------------- /src/ssl/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaLvu7c5gZiW8P 3 | Jw/QKXb/SH30Q+LphXuqKy9uy2ZoK9pdosglFWpEsu+9zN9kCy/xfG8JlCcbAuN7 4 | oGscAS73TwaonCS4fmA+ga+oOmz8tn3xnzD+HfassqbS+1vUfRtdtXUGuRVXt9rd 5 | 1mbAg10zdwk20q3J6lzwh5mbnmOxjbJBS8qgw3b4WjSBJAj1ISiZu1pZBLWjq7Xt 6 | FFhGKasoB6QlXuiY1OXG4BKwOW5gpKJqy7mCehcmOljKrR+wIQY55MRU0Atedy7u 7 | Bg6yJXwa57/6T+MBwW7djEuhTd1fGkxTYgNgF9M+CK2BIhExWliJMCq70eO1HQvc 8 | u6rAGFVBAgMBAAECggEBAJNFg2/mlwb1bl6T2hfkM2TeUmjqGykhNiExxMnYgQXJ 9 | tXexD+nUDcB3BmZN9acyxGWujMUifUPgu3cPoJ+MRdc15c9R7gVHuRibeTSAMjAl 10 | BCGyA/MGneMRdiWasHlD0srsk06LpWY3GFjC/TyvfWu2LNfv3s46DPI3I7MwVFt3 11 | 2ISLgiCI44HAFcwtN2m4uDmDlkknbttWIc1e5ckNPNSs/I1VB4F6rFtdJcBc4A/0 12 | xPBlhSA0P3HMcm0rM/Ev4Y+blHV0VHu38HryrRbk+q/WZ0lx6jewC7uHffm9PnYc 13 | pbpz5q2yyonvHTCBHN9TnQ/jQZdTqSo3b4ALndWxmNECgYEAyzfWp9y2i39AVzYY 14 | ru/0scp34Cj657GZ7ky8ccB7ITScQjG4SMt5NodrKKWJmeINUvteNSQjRJYX3//4 15 | PFrrDzdWH+XkIEkAXwLCzNChYRMJXysV+VNO+prkl2MWhdmJoBcnb8q2FbydcTx5 16 | VPQPfrYNr6a5Yr3jyCwCHJL/b3UCgYEAwjrKE5bk1pbezi5o0cDk/Nay+IOYuJEG 17 | b1TdQUwqqh6QjjHrblyjTha/Xhq5L2WnYe52fMJQbY04IhRELzZcM/RCeHmRRB/z 18 | tqu1/ioOQnE0ygzDuAUYRTET0RPKY1Sw2IECBqsckbEH0IXrIZm76pkQ9qH2XdQ8 19 | QUiiPMbsQR0CgYBhC9Rtq2Bdc7KFM2dMO3lOYlMm8EsJ1G2fOHQRmoryi45HqjjI 20 | QqsrGDZbXdo9a49TXolZtV1GRqs2JKYmJID2bkWpy/5KULJlrQrcBpHaqt2h3hCL 21 | VXZ1BP0/MmmyF/W6RvRlVZfo/37scaW7pSW1LDmS9Xq4pdgeYq3LeYOUdQKBgQCm 22 | 94bThDnARv3N3JN7DQlMWPyNKgNVVkZj2l5BTcq3Z4L42ez7eGNiM6faq7paBlb3 23 | ltkAssIVCvsgQK/ErkRg3S9vBYJdP+JwO/g9P8X2U/7/Wi29uZN9l4zjhNHMxfco 24 | hu5I8Tuj+r0vApB42AF00JSGkACL9HC/5kKhYJej+QKBgDIb1RCehnxN/K2k/EwO 25 | VtnfbpB7IaTp5QxVGdp1w1lfxwyCnNVDiOUDtMKz8mypcWbS5BUW6+Xd6J6Kowi8 26 | 4Sem95Uo8duyEOYZ9Z6wzRZeWMI/11POnRWiY/ulDMUjMvxsngIl9h/AgBDW0wVk 27 | /c3qcGNaWPK1IcNRSFtCF3BX 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # VS Browser 2 | 3 | ## CHANGELOG 4 | 5 | ### [2.2.0] 6 | 7 | - Add: Favorite pages function 8 | - Add: Use by View (If you want to use in sidebar you need to move it to the place where you want to) 9 | - Add: Example settings for vs code ([example/.vscode](example/.vscode)) 10 | - Add: Develop documentation ([DEVELOP.md](DEVELOP.md)) 11 | - Add: Changelog documentation ([CHANGELOG.md](CHANGELOG.md)) 12 | - Refactor: Replace Bootstrap icons with Codicons 13 | - Refactor: Code and structure 14 | - Fix: Error dialog is not showing 15 | 16 | ### [2.1.0] BREAKING CHANGES VERSION 17 | 18 | - `Add`: Add show update change setting to Show "New version changes" after the updated 19 | - `Refactor`: Refactor source code. Change some setting names so it may not get your old setting 20 | - `Fix`: Not start with old URL after reopening VS Code 21 | - `Fix`: Auto reload after duration time not working correctly 22 | 23 | ### [2.0.11] 24 | 25 | - Local Proxy Server: It's working more correctly now. Please give it a try 26 | - Fix: Localhost website now works even if Proxy Mode is enabled 27 | - Fix: Url on the Address bar now correct with the current page 28 | - Fix: The state of your old session will be restored after reopening VS Code 29 | 30 | ### [2.0.8] 31 | 32 | - `Local Proxy server`: Add `forceLocation`(Some websites redirect the user with some weird status code (eg. 400), so you can bypass the status code and keep redirecting the user to the correct URL) 33 | 34 | ### [2.0.6] 35 | 36 | - Fix the `reload.autoReloadEnabled` name (`reload.autoReloadenabled` => `reload.autoReloadEnabled`) 37 | - Fix `location` undefined 38 | 39 | ### [2.0.3] 40 | 41 | - `Local Proxy server`: Update `location` depends on header location not just status code 42 | - `Local Proxy server`: Add `Cookie domain rewrite` 43 | 44 | ### [2.0.1] 45 | 46 | - Fix `proxy mode` not working 47 | 48 | ### [2.0.0] 49 | 50 | - Refactor code for better performance 51 | - Add `Local Proxy server` (Beta) (many bugs) 52 | - Add `Local Proxy server settings` 53 | - Add the `Auto-complete URL` setting 54 | - Change settings name 55 | - `proxy` => `proxyMode` 56 | - `reload.enableAutoReload` => `reload.autoReloadEnabled` 57 | - `reload.time` => `reload.reloadAutoReloadDurationTime` 58 | - Fix the `Start without Proxy` command not working properly 59 | - Fix `Auto reload` not reload the current page 60 | - Fix `Address bar value` not showing the current page URL 61 | 62 | ### [1.2.1] 63 | 64 | - Fix the `Updated changes window` that always shows after opening another workspace 65 | 66 | ### [1.2.0] 67 | 68 | - Fix `proxy` not working 69 | - Add the `Start with Proxy` command 70 | - Add the `Start without Proxy` command 71 | - Add `Updated changes window` when the extension is updated 72 | 73 | ### [1.1.1] 74 | 75 | - Add an option to disable the status bar item 76 | 77 | ### [1.1.0] 78 | 79 | - Add `reload on save` config 80 | - Add status bar item 81 | 82 | ### [1.0.6] 83 | 84 | - Improve UX 85 | 86 | ### [1.0.5] 87 | 88 | - Fix the missing page bottom 89 | 90 | ### [1.0.4] 91 | 92 | - Enable `find` in browser 93 | - Add `inspect` 94 | - Refactor source code 95 | 96 | ### [1.0.3] 97 | 98 | - Add instructions 99 | - Add column settings to show in 100 | - Restore to the previous page when re-open VS Code 101 | - Add the title for the button for easier use 102 | - Disable all settings by default 103 | - Fix button hover color 104 | - Fix the error when the URL don't have HTTP or HTTPS 105 | 106 | ### [1.0.0] 107 | 108 | - First release of VS Browser 109 | -------------------------------------------------------------------------------- /assets/js/proxy.js: -------------------------------------------------------------------------------- 1 | customElements.define( 2 | "using-proxy", 3 | class extends HTMLIFrameElement { 4 | static get observedAttributes() { 5 | return ["src"]; 6 | } 7 | constructor() { 8 | super(); 9 | } 10 | attributeChangedCallback() { 11 | this.load(this.src); 12 | } 13 | connectedCallback() { 14 | this.sandbox = 15 | "" + this.sandbox || 16 | "allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation"; // all except allow-top-navigation 17 | } 18 | load(url, options) { 19 | if (!url || !url.startsWith("http")) { 20 | throw new Error(`Proxy src ${url} does not start with http(s)://`); 21 | } 22 | console.log("Proxy loading:", url); 23 | this.srcdoc = ` 24 | 25 | 46 | 47 | 48 |
49 | 50 | `; 51 | this.fetchProxy(url, options, 0) 52 | .then((res) => res.text()) 53 | .then((data) => { 54 | if (data) { 55 | this.srcdoc = data.replace( 56 | /]*)>/i, 57 | ` 58 | 59 | 81 | ` 82 | ); 83 | this.setAttribute("srcurl", url); 84 | } 85 | }) 86 | .catch((e) => console.error("Cannot load Proxy:", e)); 87 | } 88 | fetchProxy(url, options, i) { 89 | let proxies = (options || {}).proxies || [ 90 | // 'https://morning-sea-28950.herokuapp.com/', 91 | // 'https://yacdn.org/proxy/', 92 | window.localProxy, 93 | "https://api.codetabs.com/v1/proxy/?quest=", 94 | ]; 95 | // clean up the array with undefined values 96 | proxies = proxies.filter((p) => !!p); 97 | return fetch(proxies[i] + url, options) 98 | .then((res) => { 99 | if (!window.forceLocation) { 100 | if (!res.ok) { 101 | throw new Error(`${res.status} ${res.statusText}`); 102 | } 103 | } 104 | return res; 105 | }) 106 | .catch((error) => { 107 | if (i === proxies.length - 1) { 108 | throw error; 109 | } 110 | return this.fetchProxy(url, options, i + 1); 111 | }); 112 | } 113 | }, 114 | { extends: "iframe" } 115 | ); 116 | -------------------------------------------------------------------------------- /assets/css/browser.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | iframe { 4 | margin: 0; 5 | padding: 0; 6 | border: 0; 7 | min-width: 100%; 8 | min-height: 100%; 9 | overflow: hidden; 10 | } 11 | 12 | body { 13 | display: flex; 14 | flex-direction: column; 15 | height: 100vh; 16 | } 17 | 18 | body.vscode-light { 19 | color: black; 20 | color-scheme: light; 21 | background-color: white; 22 | } 23 | 24 | body.vscode-dark { 25 | color: white; 26 | color-scheme: dark; 27 | background-color: black; 28 | } 29 | 30 | body.vscode-light #navbar, 31 | body.vscode-light #favbar, 32 | body.vscode-light #favbar .favourite-item-all, 33 | body.vscode-light #favbar .favourite-item-all--dropdown { 34 | background-color: #f7f7f7; 35 | border-top: 1px solid #ebebeb; 36 | } 37 | 38 | body.vscode-dark #navbar, 39 | body.vscode-dark #favbar, 40 | body.vscode-dark #favbar .favourite-item-all, 41 | body.vscode-dark #favbar .favourite-item-all--dropdown { 42 | background-color: #3b3b3b; 43 | } 44 | 45 | body.vscode-light #navbar #addressbar, 46 | body.vscode-light #navbar #btn-fav { 47 | background-color: #fff; 48 | color: black; 49 | } 50 | 51 | body.vscode-dark #navbar #addressbar, 52 | body.vscode-dark #navbar #btn-fav { 53 | background-color: #2b2b2b; 54 | color: white; 55 | } 56 | 57 | #navbar { 58 | display: flex; 59 | flex: 0 1; 60 | padding: 0.25rem; 61 | border-bottom: none; 62 | z-index: 100; 63 | } 64 | 65 | #navbar .addressbar { 66 | position: relative; 67 | display: flex; 68 | width: -webkit-fill-available; 69 | align-items: center; 70 | } 71 | 72 | #navbar .addressbar #addressbar { 73 | height: -webkit-fill-available; 74 | padding: 0.25rem 1rem; 75 | width: 100%; 76 | border: none; 77 | border-radius: 2px; 78 | font-size: 15px; 79 | } 80 | 81 | #navbar .addressbar #addressbar:focus { 82 | outline: 2px solid #466c88; 83 | } 84 | 85 | #navbar .addressbar #btn-fav { 86 | position: absolute; 87 | right: 2px; 88 | padding: 0.4rem; 89 | } 90 | 91 | #navbar button { 92 | all: unset; 93 | position: relative; 94 | display: flex; 95 | padding: 0.5rem; 96 | background-color: transparent; 97 | text-align: center; 98 | } 99 | 100 | #navbar button { 101 | cursor: pointer; 102 | } 103 | 104 | body.vscode-light #navbar button:hover, 105 | body.vscode-light #favbar .favourite-item:hover { 106 | background-color: #dcdcdc !important; 107 | } 108 | 109 | body.vscode-dark #navbar button:hover, 110 | body.vscode-dark #favbar .favourite-item:hover { 111 | background-color: #515151 !important; 112 | } 113 | 114 | #navbar button.active { 115 | color: green; 116 | } 117 | 118 | #navbar button i { 119 | margin: auto; 120 | font-size: 25px; 121 | } 122 | 123 | #navbar button#btn-reload.loading i { 124 | animation: animation-spin 3s infinite linear; 125 | } 126 | 127 | #navbar button#btn-reload { 128 | margin-right: 0.25rem; 129 | } 130 | 131 | #navbar button#btn-go { 132 | margin-left: 0.25rem; 133 | } 134 | 135 | #navbar button#btn-go-to-settings { 136 | margin-left: 0.25rem; 137 | } 138 | 139 | #favbar { 140 | position: relative; 141 | display: flex; 142 | font-size: medium; 143 | } 144 | 145 | #favbar .favourite-items { 146 | display: flex; 147 | padding: 5px; 148 | max-width: 80%; 149 | } 150 | 151 | #favbar .favourite-item { 152 | display: flex; 153 | padding: 2px 3px; 154 | cursor: pointer; 155 | user-select: none; 156 | } 157 | 158 | #favbar .favourite-item .favourite-item--icon { 159 | display: flex; 160 | margin-right: 3px; 161 | justify-content: center; 162 | align-items: center; 163 | } 164 | 165 | #favbar .favourite-item-all { 166 | position: relative; 167 | display: flex; 168 | margin-left: auto; 169 | padding: 5px; 170 | } 171 | 172 | #favbar .favourite-item-all--dropdown { 173 | position: absolute; 174 | display: none; 175 | top: 35px; 176 | right: 0px; 177 | width: fit-content; 178 | } 179 | 180 | .webview-container { 181 | box-sizing: border-box; 182 | flex: 1 0; 183 | padding: 0; 184 | } 185 | 186 | .webview-container #webview { 187 | transition: all 0.25s; 188 | margin: 0; 189 | padding: 0; 190 | visibility: visible; 191 | } 192 | 193 | .webview-container #webview-failed-load { 194 | display: none; 195 | position: absolute; 196 | padding: 0 2rem; 197 | width: 100%; 198 | max-width: 400px; 199 | top: calc(58%); 200 | left: 50%; 201 | transform: translateX(-50%) translateY(-50%); 202 | } 203 | 204 | .webview-container #webview-failed-load svg { 205 | margin: 0 auto; 206 | display: block; 207 | text-align: center; 208 | } 209 | 210 | .webview-container #webview-failed-load svg:before { 211 | animation: in-down 0.2s ease-in-out; 212 | font-size: 48px; 213 | width: 44px; 214 | height: 60px; 215 | } 216 | 217 | .webview-container #webview-failed-load h4 { 218 | opacity: 0; 219 | animation: in-up 0.2s ease-in-out forwards; 220 | animation-delay: 0.1s; 221 | text-align: center; 222 | } 223 | 224 | .hidden { 225 | display: none !important; 226 | } 227 | 228 | @keyframes animation-spin { 229 | from { 230 | transform: rotate(0deg); 231 | } 232 | 233 | to { 234 | transform: rotate(360deg); 235 | } 236 | } 237 | 238 | @keyframes in-down { 239 | from { 240 | transform: translateY(-50px); 241 | opacity: 0; 242 | } 243 | 244 | to { 245 | transform: translateY(0px); 246 | opacity: 1; 247 | } 248 | } 249 | 250 | @keyframes in-up { 251 | from { 252 | transform: translateY(50px); 253 | opacity: 0; 254 | } 255 | 256 | to { 257 | transform: translateY(0px); 258 | opacity: 1; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/webviews/browser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Data, { FavouriteData } from "../types/data"; 3 | import { readFileSync } from "fs"; 4 | import CONST_WEBVIEW from "../constants/webview"; 5 | 6 | function convertVarToHTML(v: any): string { 7 | var type = typeof v; 8 | switch (type) { 9 | case "string": 10 | return v; 11 | } 12 | return String(v); 13 | } 14 | 15 | function getFavourites( 16 | workspaceFolder?: vscode.WorkspaceFolder 17 | ): FavouriteData { 18 | let favorites = {}; 19 | const configs = vscode.workspace.getConfiguration("vs-browser.favourites"); 20 | const configsFavouritesList = configs.get("list") ?? {}; 21 | favorites = { 22 | ...configsFavouritesList, 23 | }; 24 | 25 | // Not supported yet 26 | if (workspaceFolder) { 27 | const workspaceConfigs = vscode.workspace.getConfiguration( 28 | "vs-browser.favourites", 29 | workspaceFolder 30 | ); 31 | const workspaceConfigsFavouritesList = 32 | workspaceConfigs.get("list") ?? {}; 33 | favorites = { 34 | ...workspaceConfigsFavouritesList, 35 | }; 36 | } 37 | 38 | return favorites; 39 | } 40 | 41 | export default (webviewContext: WebviewContext, data: Data) => { 42 | // Render asset url 43 | function asset(path: string): string { 44 | return webviewContext.webviewUri + path; 45 | } 46 | // Data 47 | const viewType: string = data["viewType"]; 48 | const title: string = data["title"]; 49 | // Get current config 50 | const configs = vscode.workspace.getConfiguration("vs-browser"); 51 | const proxyMode: boolean = 52 | data["proxyMode"] !== undefined 53 | ? data["proxyMode"] 54 | : configs.get("proxyMode") || false; 55 | const url: string = 56 | data["url"] !== undefined 57 | ? data["url"] 58 | : configs.get("url") || "http://localhost"; 59 | const favourites: FavouriteData = 60 | data["favourites"] !== undefined ? data["favourites"] : getFavourites(); 61 | const autoCompleteUrl: string = 62 | data["autoCompleteUrl"] !== undefined 63 | ? data["autoCompleteUrl"] 64 | : configs.get("autoCompleteUrl") || "http://"; 65 | const localProxyServerEnabled: boolean = 66 | data["localProxyServerEnabled"] !== undefined 67 | ? data["localProxyServerEnabled"] 68 | : configs.get("localProxyServer.enabled") || false; 69 | const localProxyServerPort: number = 70 | data["localProxyServerPort"] !== undefined 71 | ? data["localProxyServerPort"] 72 | : configs.get("localProxyServer.port") || 9999; 73 | const localProxyServerForceLocation: boolean = 74 | data["localProxyServerForceLocation"] !== undefined 75 | ? data["localProxyServerForceLocation"] 76 | : configs.get("localProxyServer.forceLocation") || false; 77 | const autoReloadDurationEnabled: boolean = 78 | data["autoReloadDurationEnabled"] !== undefined 79 | ? data["autoReloadDurationEnabled"] 80 | : configs.get("reload.autoReloadDurationEnabled") || false; 81 | const autoReloadDurationTime: number = 82 | data["autoReloadDurationTime"] !== undefined 83 | ? data["autoReloadDurationTime"] 84 | : configs.get("reload.autoReloadDurationTime") || 15000; 85 | 86 | const replaceObject: ReplaceObject = { 87 | codiconsCss: asset("node_modules/@vscode/codicons/dist/codicon.css"), 88 | browserCss: asset("assets/css/browser.css"), 89 | jqueryJS: asset("assets/js/jquery-3.7.1.slim.min.js"), 90 | proxyJS: asset("assets/js/proxy.js"), 91 | localProxyServerScript: 92 | localProxyServerEnabled === true 93 | ? `` 94 | : "", 95 | localProxyServerForceLocationScript: ``, 96 | proxyMode: proxyMode, 97 | url: `'${url}'`, 98 | favourites: JSON.stringify(favourites), 99 | autoCompleteUrl: `'${autoCompleteUrl}'`, 100 | localProxyServerEnabled: localProxyServerEnabled, 101 | localProxyServerPort: localProxyServerPort, 102 | localProxyServerForceLocation: localProxyServerForceLocation, 103 | autoReloadDurationEnabled: autoReloadDurationEnabled, 104 | autoReloadDurationTime: autoReloadDurationTime, 105 | viewType: `'${viewType}'`, 106 | title: `'${title}'`, 107 | // eslint-disable-next-line @typescript-eslint/naming-convention 108 | CONST_WEBVIEW_POST_MESSAGE_TYPE_FAVOURITE_ADD: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.FAVOURITE_ADD}'`, 109 | // eslint-disable-next-line @typescript-eslint/naming-convention 110 | CONST_WEBVIEW_POST_MESSAGE_TYPE_FAVOURITE_REMOVE: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.FAVOURITE_REMOVE}'`, 111 | // eslint-disable-next-line @typescript-eslint/naming-convention 112 | CONST_WEBVIEW_POST_MESSAGE_TYPE_GO_TO_SETTINGS: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.GO_TO_SETTINGS}'`, 113 | // eslint-disable-next-line @typescript-eslint/naming-convention 114 | CONST_WEBVIEW_POST_MESSAGE_TYPE_OPEN_INSPECTOR: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.OPEN_INSPECTOR}'`, 115 | // eslint-disable-next-line @typescript-eslint/naming-convention 116 | CONST_WEBVIEW_POST_MESSAGE_TYPE_REFRESH_FAVOURITES: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.REFRESH_FAVOURITES}'`, 117 | // eslint-disable-next-line @typescript-eslint/naming-convention 118 | CONST_WEBVIEW_POST_MESSAGE_TYPE_RELOAD: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.RELOAD}'`, 119 | // eslint-disable-next-line @typescript-eslint/naming-convention 120 | CONST_WEBVIEW_POST_MESSAGE_TYPE_SHOW_MESSAGE_BOX: `'${CONST_WEBVIEW.POST_MESSAGE.TYPE.SHOW_MESSAGE_BOX}'`, 121 | }; 122 | 123 | let html = readFileSync( 124 | webviewContext.extensionPath + "assets/browser.html", 125 | "utf8" 126 | ); 127 | for (const key in replaceObject) { 128 | html = html.replace( 129 | new RegExp(`{ ${key} }`, "g"), 130 | convertVarToHTML(replaceObject[key]) 131 | ); 132 | } 133 | 134 | console.log(html); 135 | 136 | return html; 137 | }; 138 | -------------------------------------------------------------------------------- /src/webviews/changes.ts: -------------------------------------------------------------------------------- 1 | import Data from "../types/data"; 2 | const packageJSON = require("../../package.json"); 3 | 4 | export default (webviewContext: WebviewContext, data: Data) => { 5 | // Render asset url 6 | function asset(path: string) { 7 | return webviewContext.webviewUri + path; 8 | } 9 | let extensionVersion = packageJSON.version || "0.0.0"; 10 | 11 | let content = ` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 34 | 35 | 36 | 37 |
38 |

Changes (version ${extensionVersion})

39 |
    40 |
  • 41 | Add: Favorite pages function 42 |
      43 |
    • Add: New setting vs-browser.favourites.list
    • 44 |
    • Add: New setting vs-browser.favourites.savingProfile
    • 45 |
    46 |
  • 47 |
  • 48 | Add: Use by View (If you want to use in sidebar you need to move it to the place where you want to) 49 |
      50 |
    • Add: New command VS Browser: Reset View Locations
    • 51 |
    • Add: New setting vs-browser.showViewContainer
    • 52 |
    53 |
  • 54 |
  • Add: Develop documentation here
  • 55 |
  • Add: Example settings for vs code here
  • 56 |
  • Refactor: Replace Bootstrap icons with Codicons
  • 57 |
  • Refactor: Code and structure
  • 58 |
  • Fix: Error dialog is not showing
  • 59 |
60 | VERSION 2.1.0 IS A BREAKING CHANGES VERSION SO PLEASE TAKE A LOOK AT YOUR SETTINGS.
I have change some setting name so it may not get your old setting.
Thanks!
61 |
62 |
63 |

Usage

64 |
    65 |
  • 66 | Open command palette (Ctrl+Shift+P) & enter "VS Browser: Start Browser" 67 |
  • 68 |
  • 69 | All extension settings are disable by default, you should update extension settings for a better experience 70 |
  • 71 |
72 |
73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | `; 82 | for (let property in packageJSON.contributes.configuration.properties) { 83 | let value = packageJSON.contributes.configuration.properties[property]; 84 | let key = property.replace(/vs-browser\./g, ""); 85 | let description = String(value.description) || ""; 86 | let defaultValue = String(value.default) || ""; 87 | 88 | content += ` 89 | 90 | 91 | 92 | `; 93 | } 94 | content += ` 95 |
SettingDescriptionDefault
${key}${description}${defaultValue}
96 |
97 | 100 |
101 |
102 |
103 |
104 |

*Bugs:

105 |
    106 |
  • 107 | Local Proxy server have many bugs right now 108 |
  • 109 |
  • 110 | Local Proxy server will not be automatically closed when you closed all the panel 111 |
  • 112 |
  • 113 | Local Proxy server form submit may not working correctly
    114 | Notes: 115 |
      116 |
    • Form without redirecting page (JS) still working
    • 117 |
    118 |
  • 119 |
  • 120 | Sometimes, the dialog still displays even if nothing goes wrong
    121 | Solves: 122 |
      123 |
    • Turn off showMessageDialogg setting
    • 124 |
    • Using Local Proxy Server
    • 125 |
    126 |
  • 127 |
128 | I will fix these bugs as soon as possible. 129 |
130 |
131 |

Donation

132 | This product is free but if you like my work and you want to support me, buy me a coffee and also rate my extension ☕
133 | 138 |
139 | Thanks for using my extension 🎉 140 |
141 | 151 | 152 | 153 | 154 | `; 155 | return content; 156 | }; 157 | -------------------------------------------------------------------------------- /src/helpers/server.ts: -------------------------------------------------------------------------------- 1 | import { workspace as VscodeWorkspace } from "vscode"; 2 | 3 | const http = require("http"); 4 | const connect = require("connect"); 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | const morgan = require("morgan"); 8 | const zlib = require("zlib"); 9 | 10 | let app = connect(); 11 | var httpProxy = require("http-proxy"); 12 | 13 | // Configuration 14 | const configs = VscodeWorkspace.getConfiguration("vs-browser"); 15 | const HOST = "localhost"; 16 | const PORT = configs.get("localProxyServer.port") || 9999; 17 | const cookieDomainRewrite = 18 | configs.get("localProxyServer.cookieDomainRewrite") || false; 19 | export let status = 0; 20 | export let online = 0; 21 | export let runningPort = 9999; 22 | 23 | var proxy = httpProxy.createProxyServer(); 24 | 25 | proxy.on("proxyReq", (proxyReq: any, req: any) => { 26 | if (req.body) { 27 | const bodyData = JSON.stringify(req.body); 28 | // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json 29 | proxyReq.setHeader("Content-Type", "application/json"); 30 | proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData)); 31 | // stream the content 32 | proxyReq.write(bodyData); 33 | } 34 | }); 35 | 36 | proxy.on("proxyRes", function (proxyRes: any, req: any, res: any) { 37 | res.setHeader("Access-Control-Allow-Origin", "*"); 38 | res.setHeader("X-Frame-Options", false); 39 | if (proxyRes.headers["content-type"]) { 40 | res.setHeader("Content-Type", proxyRes.headers["content-type"]); 41 | } 42 | 43 | proxyRes = decompress(proxyRes, proxyRes.headers["content-encoding"]); 44 | 45 | var buffer = Buffer.from("", "utf8"); 46 | proxyRes.on( 47 | "data", 48 | (chunk: any) => (buffer = Buffer.concat([buffer, chunk])) 49 | ); 50 | 51 | let body: any = null; 52 | proxyRes.on("end", function () { 53 | body = buffer.toString("utf8"); 54 | 55 | if ( 56 | res.hasHeader("Content-Type") && 57 | res.getHeader("Content-Type").match(/([^;]+)*/g)[0] === "text/html" 58 | ) { 59 | console.log(req); 60 | // console.log(proxyRes.headers); 61 | let url = req.originalUrl; 62 | let regex = /(http(|s))\:\/\/([^\/]+)*/g; 63 | url = url.match(regex)[0]; 64 | 65 | // Capture html tag not a|img and add the rest of the string of the tag after src|href="(capture)> to the new string 66 | 67 | // body = body.replaceAll(/<((?!a|img).*)(.*?) (src|href)=('|"|)((http(s|)).*?)>/g, '<$1$2 $3=$4http://' + HOST + ':' + PORT + '/$5>'); 68 | // a|img tag will be same as before for proxy work and image get directly 69 | // body = body.replaceAll(/<(a|img) (.*?)(src|href)=('|"|)\/(.*?)('|"|)>/g, '<$1 $2$3=$4' + url + '/$5>'); 70 | // Replace tag with src|href with relative path 71 | // body = body.replaceAll(/<((?!a|img).*) (.*?)(src|href)=('|"|)\/(.*?)('|"|)/g, '<$1 $2$3=$4http://' + HOST + ':' + PORT + '/' + url + '/$5>'); 72 | // body = body.replaceAll(//g, ''); 73 | // body = body.replaceAll(/head/g, '1'); 74 | } else if ( 75 | res.hasHeader("Content-Type") && 76 | (res.getHeader("Content-Type").match(/([^;]+)*/g)[0] === "text/css" || 77 | res.getHeader("Content-Type").match(/([^;]+)*/g)[0] === 78 | "text/javascript") 79 | ) { 80 | let url = req.originalUrl; 81 | let regex = /(http(|s))\:\/\/([^\/]+)*/g; 82 | url = url.match(regex)[0]; 83 | 84 | body = body.replaceAll( 85 | /(http(s|):\/\/(?!'|"))/g, 86 | "http://" + HOST + ":" + PORT + "/$1" 87 | ); 88 | } 89 | body = Buffer.from(body, "utf8"); 90 | res.write(body); 91 | res.end(); 92 | }); 93 | if ( 94 | proxyRes.statusCode === 301 || 95 | proxyRes.statusCode === 302 || 96 | proxyRes.hasOwnProperty("location") 97 | ) { 98 | res.writeHead(301, { 99 | location: "http://" + HOST + ":" + PORT + "/" + proxyRes.headers.location, 100 | }); 101 | res.end(); 102 | } 103 | }); 104 | 105 | export function start(callback: Function = () => {}) { 106 | if (status === 0 || PORT !== runningPort) { 107 | console.log("Local proxy server starting on port " + PORT); 108 | 109 | // Create the express app 110 | app.use(morgan("dev")); 111 | 112 | app.use(function (req: any, res: any) { 113 | let options = { 114 | target: null, 115 | ssl: { 116 | key: fs.readFile( 117 | path.resolve(__dirname, "ssl/key.pem"), 118 | function read(err: any, data: any) {} 119 | ), 120 | cert: fs.readFile( 121 | path.resolve(__dirname, "ssl/cert.pem"), 122 | function read(err: any, data: any) {} 123 | ), 124 | }, 125 | xfwd: true, 126 | secure: false, 127 | changeOrigin: true, 128 | hostRewrite: true, 129 | autoRewrite: true, 130 | toProxy: true, 131 | cookieDomainRewrite: "", 132 | selfHandleResponse: true, 133 | }; 134 | // Set the default response headers 135 | res.setHeader("Access-Control-Allow-Origin", "*"); 136 | res.setHeader("X-Frame-Options", false); 137 | 138 | let url = req.url; 139 | let regex = /(http(|s))\:\/\/([^\/]+)*/g; 140 | if (regex.test(url)) { 141 | url = url.match(regex)[0]; 142 | options.target = url; 143 | if (cookieDomainRewrite) { 144 | options.cookieDomainRewrite = url; 145 | } 146 | req.url = req.url.split("/").splice(4).join("/"); 147 | 148 | proxy.web(req, res, options); 149 | } else { 150 | res.writeHead(400, { contentType: "text/html" }); 151 | res.write("Invalid request"); 152 | res.end(); 153 | } 154 | }); 155 | 156 | http.createServer(app).listen(PORT, function () { 157 | status = 1; 158 | online++; 159 | runningPort = PORT; 160 | console.log("Local proxy server started on port " + PORT); 161 | callback(); 162 | }); 163 | } else { 164 | console.log("Local proxy server already started on port " + PORT); 165 | online++; 166 | callback(); 167 | } 168 | } 169 | 170 | export function stop(callback: Function = () => {}) { 171 | online--; 172 | if (status === 1 && online === 0) { 173 | console.log("Stopping local proxy server"); 174 | 175 | proxy.close(function () { 176 | console.log("Proxy server stopped"); 177 | 178 | app.close(function () { 179 | status = 0; 180 | console.log("Local proxy server stopped"); 181 | callback(); 182 | }); 183 | }); 184 | } else { 185 | console.log( 186 | "There are still " + 187 | online + 188 | " connects so the proxy server will not be closed" 189 | ); 190 | } 191 | } 192 | 193 | function decompress(proxyRes: any, contentEncoding: string) { 194 | let _proxyRes = proxyRes; 195 | let decompress; 196 | 197 | switch (contentEncoding) { 198 | case "gzip": 199 | decompress = zlib.createGunzip(); 200 | break; 201 | case "br": 202 | decompress = zlib.createBrotliDecompress(); 203 | break; 204 | case "deflate": 205 | decompress = zlib.createInflate(); 206 | break; 207 | default: 208 | break; 209 | } 210 | 211 | if (decompress) { 212 | _proxyRes.pipe(decompress); 213 | _proxyRes = decompress; 214 | } 215 | 216 | return _proxyRes; 217 | } 218 | -------------------------------------------------------------------------------- /src/helpers/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as webviewHelper from "./webview"; 3 | 4 | import WebviewPanelSerializer from "../classes/webview-panel-serializer"; 5 | import WebviewViewProvider from "../classes/webview-view-provider"; 6 | 7 | import CONST_WEBVIEW from "../constants/webview"; 8 | 9 | import browserWebview from "../webviews/browser"; 10 | import changesWebview from "../webviews/changes"; 11 | 12 | export const startStatusBarItem: vscode.StatusBarItem = 13 | vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); 14 | 15 | /** 16 | * Watch the extension version is changed 17 | * 18 | * @param context VS Code context 19 | * @param outputConsole output console 20 | */ 21 | export function onVersionChanged( 22 | context: vscode.ExtensionContext, 23 | outputConsole: vscode.OutputChannel 24 | ) { 25 | const configs = vscode.workspace.getConfiguration("vs-browser"); 26 | let oldVersion = context.globalState.get("version"); 27 | let extensionVersion = context.extension.packageJSON.version; 28 | let forceShowChanges = false; 29 | let showUpdateChanges = configs.get("showUpdateChanges"); 30 | if ( 31 | (oldVersion !== extensionVersion && showUpdateChanges) || 32 | forceShowChanges 33 | ) { 34 | context.globalState.update("version", extensionVersion); 35 | outputConsole.appendLine("> Extension is updated to " + extensionVersion); 36 | webviewHelper.createWebviewPanel(changesWebview, context, { 37 | viewType: "changes", 38 | title: "VS Browser - New version changes", 39 | localProxyServerEnabled: false, 40 | columnToShowIn: "Active", 41 | }); 42 | } 43 | } 44 | 45 | /** 46 | * Register Serializers for webviews type 47 | * 48 | * @param context VS Code context 49 | */ 50 | export function registerWebviewPanelSerializers( 51 | context: vscode.ExtensionContext 52 | ) { 53 | vscode.window.registerWebviewPanelSerializer( 54 | "vs-browser.browser", 55 | new WebviewPanelSerializer(context) 56 | ); 57 | vscode.window.registerWebviewPanelSerializer( 58 | "vs-browser.proxy", 59 | new WebviewPanelSerializer(context) 60 | ); 61 | vscode.window.registerWebviewPanelSerializer( 62 | "vs-browser.withoutproxy", 63 | new WebviewPanelSerializer(context) 64 | ); 65 | } 66 | 67 | /** 68 | * Register Commands 69 | * 70 | * @param context VS Code context 71 | */ 72 | export function registerCommands(context: vscode.ExtensionContext) { 73 | let start = vscode.commands.registerCommand("vs-browser.start", () => { 74 | // Create and show a new webview 75 | webviewHelper.createWebviewPanel( 76 | browserWebview, 77 | context, 78 | CONST_WEBVIEW.CONFIG.BASE.BROWSER 79 | ); 80 | }); 81 | context.subscriptions.push(start); 82 | 83 | // vs-browser.startWithProxy 84 | let startWithProxy = vscode.commands.registerCommand( 85 | "vs-browser.startWithProxy", 86 | () => { 87 | // Create and show a new webview 88 | webviewHelper.createWebviewPanel( 89 | browserWebview, 90 | context, 91 | CONST_WEBVIEW.CONFIG.BASE.PROXY 92 | ); 93 | } 94 | ); 95 | context.subscriptions.push(startWithProxy); 96 | 97 | // vs-browser.startWithoutProxy 98 | let startWithoutProxy = vscode.commands.registerCommand( 99 | "vs-browser.startWithoutProxy", 100 | () => { 101 | // Create and show a new webview 102 | webviewHelper.createWebviewPanel( 103 | browserWebview, 104 | context, 105 | CONST_WEBVIEW.CONFIG.BASE.WITHOUT_PROXY 106 | ); 107 | } 108 | ); 109 | context.subscriptions.push(startWithoutProxy); 110 | // vs-browser.resetViewLocations 111 | let resetViewLocation = vscode.commands.registerCommand( 112 | "vs-browser.resetViewLocations", 113 | () => { 114 | vscode.commands.executeCommand("vs-browser-browser.resetViewLocation"); 115 | vscode.commands.executeCommand("vs-browser-proxy.resetViewLocation"); 116 | vscode.commands.executeCommand( 117 | "vs-browser-without-proxy.resetViewLocation" 118 | ); 119 | } 120 | ); 121 | context.subscriptions.push(resetViewLocation); 122 | } 123 | 124 | /** 125 | * Register Status bar items 126 | * 127 | * @param context VS Code context 128 | */ 129 | export function registerStatusBarItems(context: vscode.ExtensionContext) { 130 | // register a new status bar item that we can now manage 131 | const configs = vscode.workspace.getConfiguration("vs-browser"); 132 | let showStatusBarItem = configs.get("showStatusBarItem") || false; 133 | startStatusBarItem.command = "vs-browser.start"; 134 | startStatusBarItem.text = "$(globe) VS Browser"; 135 | startStatusBarItem.tooltip = "Start VS Browser"; 136 | context.subscriptions.push(startStatusBarItem); 137 | if (showStatusBarItem) { 138 | startStatusBarItem.show(); 139 | } 140 | // show/hide status bar item when config changed 141 | vscode.workspace.onDidChangeConfiguration(() => { 142 | const configs = vscode.workspace.getConfiguration("vs-browser"); 143 | showStatusBarItem = configs.get("showStatusBarItem") || false; 144 | if (!showStatusBarItem) { 145 | startStatusBarItem.hide(); 146 | } else { 147 | startStatusBarItem.show(); 148 | } 149 | }); 150 | } 151 | 152 | /** 153 | * Register View Container 154 | * 155 | * @param context VS Code context 156 | */ 157 | export function registerViewContainer(context: vscode.ExtensionContext) { 158 | context.subscriptions.push( 159 | vscode.window.registerWebviewViewProvider( 160 | "vs-browser-browser", 161 | new WebviewViewProvider( 162 | browserWebview, 163 | context, 164 | CONST_WEBVIEW.CONFIG.BASE.BROWSER 165 | ) 166 | ) 167 | ); 168 | context.subscriptions.push( 169 | vscode.window.registerWebviewViewProvider( 170 | "vs-browser-proxy", 171 | new WebviewViewProvider( 172 | browserWebview, 173 | context, 174 | CONST_WEBVIEW.CONFIG.BASE.PROXY 175 | ) 176 | ) 177 | ); 178 | context.subscriptions.push( 179 | vscode.window.registerWebviewViewProvider( 180 | "vs-browser-without-proxy", 181 | new WebviewViewProvider( 182 | browserWebview, 183 | context, 184 | CONST_WEBVIEW.CONFIG.BASE.WITHOUT_PROXY 185 | ) 186 | ) 187 | ); 188 | } 189 | 190 | /** 191 | * Handle when the configuration change 192 | * 193 | * @param event An event describing the change in Configuration 194 | */ 195 | export function handleConfigurationChange( 196 | event: vscode.ConfigurationChangeEvent 197 | ) { 198 | const configs = vscode.workspace.getConfiguration("vs-browser"); 199 | if (event.affectsConfiguration("vs-browser.showViewContainer")) { 200 | updateContextKey(); 201 | } else if (event.affectsConfiguration("vs-browser.showStatusBarItem")) { 202 | const showStatusBarItem = configs.get("showStatusBarItem"); 203 | if (!showStatusBarItem) { 204 | startStatusBarItem.hide(); 205 | } else { 206 | startStatusBarItem.show(); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * Update VS Code context key to use when in package.json 213 | */ 214 | export function updateContextKey() { 215 | const configs = vscode.workspace.getConfiguration("vs-browser"); 216 | const showViewContainer = configs.get("showViewContainer"); 217 | 218 | vscode.commands.executeCommand( 219 | "setContext", 220 | "config.vs-browser.showViewContainer", 221 | showViewContainer 222 | ); 223 | } 224 | -------------------------------------------------------------------------------- /src/helpers/webview.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | // types 3 | import Data, { FavouriteData } from "../types/data"; 4 | // helpers 5 | import { showMessage } from "./common"; 6 | import * as server from "./server"; 7 | import CONST_CONFIGS from "../constants/configs"; 8 | import CONST_WEBVIEW from "../constants/webview"; 9 | import { startStatusBarItem } from "../helpers/extension"; 10 | 11 | let activePanels: Array = []; 12 | 13 | /** 14 | * Inject event and context to panel 15 | * 16 | * @param template Template of the webview 17 | * @param context Extension context 18 | * @param data Data to inject 19 | * @param webviewPanel Panel to show (ex: From restored state) 20 | * @returns 21 | */ 22 | export function createWebviewPanel( 23 | template: Function, 24 | context: vscode.ExtensionContext, 25 | data: Data, 26 | webviewPanel?: vscode.WebviewPanel 27 | ) { 28 | // Start proxy server 29 | let configs = vscode.workspace.getConfiguration("vs-browser"); 30 | let proxyMode = 31 | data["proxyMode"] !== undefined 32 | ? data["proxyMode"] 33 | : configs.get("proxyMode") || false; 34 | let localProxyServerEnabled = 35 | data["localProxyServerEnabled"] !== undefined 36 | ? data["localProxyServerEnabled"] 37 | : configs.get("localProxyServer.enabled") || false; 38 | if (proxyMode && localProxyServerEnabled) { 39 | server.start(function () { 40 | const configs = vscode.workspace.getConfiguration("vs-browser"); 41 | const port = configs.get("localProxyServer.port") || 9999; 42 | startStatusBarItem.text = "$(cloud) VS Browser: " + port; 43 | }); 44 | } 45 | 46 | let panel = webviewPanel; 47 | if (!panel) { 48 | // Create new column 49 | const column = 50 | data["columnToShowIn"] !== undefined 51 | ? data["columnToShowIn"] 52 | : configs.get("columnToShowIn") || "Two"; 53 | let columnToShowIn = vscode.ViewColumn.Two; 54 | switch (column) { 55 | case "One": 56 | columnToShowIn = vscode.ViewColumn.One; 57 | break; 58 | case "Two": 59 | columnToShowIn = vscode.ViewColumn.Two; 60 | break; 61 | case "Three": 62 | columnToShowIn = vscode.ViewColumn.Three; 63 | break; 64 | case "Active": 65 | columnToShowIn = vscode.ViewColumn.Active; 66 | break; 67 | case "Beside": 68 | columnToShowIn = vscode.ViewColumn.Beside; 69 | break; 70 | default: 71 | } 72 | panel = vscode.window.createWebviewPanel( 73 | "vs-browser." + data["viewType"], // Identifies the type of the webview. Used internally 74 | data["title"], // Title of the panel displayed to the user 75 | columnToShowIn, // Editor column to show the new webview panel in. 76 | { 77 | enableScripts: true, 78 | // freeze when panel not focused 79 | retainContextWhenHidden: true, 80 | // enable find widget 81 | enableFindWidget: true, 82 | } 83 | ); 84 | } 85 | 86 | bindWebviewEvents(panel, template, context, data); 87 | 88 | activePanels.push(panel); 89 | return panel; 90 | } 91 | 92 | /** 93 | * Get webview context 94 | * 95 | * @param webview 96 | * @param extensionUri 97 | * @param extensionPath 98 | * @param data 99 | * @returns 100 | */ 101 | export function getWebViewContent( 102 | template: Function, 103 | webview: vscode.Webview, 104 | extensionUri: vscode.Uri, 105 | extensionPath: string, 106 | data: Data 107 | ) { 108 | // Create uri for webview 109 | const webviewUri = webview.asWebviewUri( 110 | vscode.Uri.joinPath(extensionUri, "/") 111 | ) as unknown as string; 112 | 113 | return template( 114 | { 115 | webviewUri: webviewUri, 116 | extensionPath: extensionPath + "/", 117 | } as WebviewContext, 118 | data 119 | ); 120 | } 121 | 122 | export function sendMessageToActivePanels(message: WebviewMessage) { 123 | console.log("Sending message to active panels: ", activePanels, message); 124 | activePanels.forEach((activePanel) => { 125 | sendMessageToWebview(activePanel, message); 126 | }); 127 | } 128 | 129 | export function sendMessageToWebview( 130 | panel: vscode.WebviewPanel, 131 | message: WebviewMessage 132 | ) { 133 | panel.webview.postMessage(message); 134 | } 135 | 136 | export function bindWebviewEvents( 137 | panel: any, 138 | template: Function, 139 | context: vscode.ExtensionContext, 140 | data: Data 141 | ): void { 142 | let configs = vscode.workspace.getConfiguration("vs-browser"); 143 | panel.webview.html = getWebViewContent( 144 | template, 145 | panel.webview, 146 | context.extensionUri, 147 | context.extensionPath, 148 | data 149 | ); 150 | // Handle messages from the webview 151 | panel.webview.onDidReceiveMessage( 152 | (message: WebviewMessage) => { 153 | switch (message.type) { 154 | case CONST_WEBVIEW.POST_MESSAGE.TYPE.FAVOURITE_ADD: 155 | case CONST_WEBVIEW.POST_MESSAGE.TYPE.FAVOURITE_REMOVE: { 156 | const configs = vscode.workspace.getConfiguration( 157 | "vs-browser.favourites" 158 | ); 159 | const configsFavouritesSavingProfile = 160 | configs.get("savingProfile") || 161 | CONST_CONFIGS.FAVOURITES_SAVING_PROFILE.DEFAULT; 162 | 163 | let favouritesSavingProfile; 164 | if ( 165 | configsFavouritesSavingProfile === 166 | CONST_CONFIGS.FAVOURITES_SAVING_PROFILE.NAME.GLOBAL 167 | ) { 168 | favouritesSavingProfile = vscode.ConfigurationTarget.Global; 169 | } else if ( 170 | configsFavouritesSavingProfile === 171 | CONST_CONFIGS.FAVOURITES_SAVING_PROFILE.NAME.WORKSPACE 172 | ) { 173 | favouritesSavingProfile = vscode.ConfigurationTarget.Workspace; 174 | } 175 | 176 | let favourites = configs.get("list") || {}; 177 | favourites = { 178 | ...favourites, 179 | }; 180 | if (message.type === CONST_WEBVIEW.POST_MESSAGE.TYPE.FAVOURITE_ADD) { 181 | console.log("Click on Add to Favourites button"); 182 | favourites[message.value] = message.value; 183 | } else { 184 | console.log("Click on Remove from Favourites button"); 185 | delete favourites[message.value]; 186 | } 187 | configs.update("list", favourites, favouritesSavingProfile); 188 | console.log("Saved favorites: ", favourites); 189 | 190 | sendMessageToActivePanels({ 191 | type: CONST_WEBVIEW.POST_MESSAGE.TYPE.REFRESH_FAVOURITES, 192 | value: favourites, 193 | }); 194 | return; 195 | } 196 | case CONST_WEBVIEW.POST_MESSAGE.TYPE.OPEN_INSPECTOR: 197 | console.log("Click on Open Inspector button"); 198 | vscode.commands.executeCommand( 199 | "workbench.action.webview.openDeveloperTools" 200 | ); 201 | return; 202 | case CONST_WEBVIEW.POST_MESSAGE.TYPE.GO_TO_SETTINGS: 203 | vscode.commands.executeCommand( 204 | "workbench.action.openSettings", 205 | "vs-browser" 206 | ); 207 | return; 208 | case CONST_WEBVIEW.POST_MESSAGE.TYPE.SHOW_MESSAGE_BOX: 209 | let type = message.value.type; 210 | let text = message.value.text; 211 | let detail = message.value.detail; 212 | console.log(message.value.detail); 213 | showMessage(type, text, { 214 | detail: detail, 215 | }); 216 | return; 217 | } 218 | }, 219 | undefined, 220 | context.subscriptions 221 | ); 222 | // Handle panel state change event 223 | // panel.onDidChangeViewState( 224 | // (e: any) => { 225 | // let panel = e.webviewPanel; 226 | 227 | // switch (panel.viewColumn) { 228 | // case vscode.ViewColumn.One: 229 | // console.log("ViewColumn.One"); 230 | // return; 231 | 232 | // case vscode.ViewColumn.Two: 233 | // console.log("ViewColumn.Two"); 234 | // return; 235 | 236 | // case vscode.ViewColumn.Three: 237 | // console.log("ViewColumn.Three"); 238 | // return; 239 | // } 240 | // }, 241 | // null, 242 | // context.subscriptions 243 | // ); 244 | // Handle when panel is closed 245 | panel.onDidDispose( 246 | () => { 247 | // When the panel is closed, cancel any future updates to the webview content 248 | const configs = vscode.workspace.getConfiguration("vs-browser"); 249 | let localProxyServerEnabled = configs.get( 250 | "localProxyServer.enabled" 251 | ); 252 | if (localProxyServerEnabled) { 253 | server.stop(function () { 254 | startStatusBarItem.text = "$(globe) VS Browser"; 255 | }); 256 | } 257 | activePanels = activePanels.filter((p) => p !== panel); 258 | }, 259 | null, 260 | context.subscriptions 261 | ); 262 | 263 | // Handle when save file 264 | let reloadOnSave = 265 | data["reloadOnSave"] !== undefined 266 | ? data["reloadOnSave"] 267 | : configs.get("reload.onSave") || false; 268 | if (reloadOnSave) { 269 | vscode.workspace.onDidSaveTextDocument((document) => { 270 | if (document.fileName.endsWith("settings.json")) { 271 | console.log( 272 | "Edited settings file. Skip this reload.", 273 | document.fileName 274 | ); 275 | return; 276 | } 277 | panel.webview.postMessage({ 278 | type: CONST_WEBVIEW.POST_MESSAGE.TYPE.RELOAD, 279 | }); 280 | }); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vs-browser", 3 | "displayName": "VS Browser", 4 | "description": "Built-in browser for Visual Studio Code", 5 | "publisher": "Phu1237", 6 | "author": { 7 | "name": "Phu1237", 8 | "url": "https://github.com/Phu1237" 9 | }, 10 | "version": "2.2.0", 11 | "license": "SEE LICENSE IN LICENSE.md", 12 | "bugs": { 13 | "url": "https://github.com/Phu1237/vscode-vs-browser/issues", 14 | "email": "phu1237@gmail.com" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Phu1237/vscode-vs-browser" 19 | }, 20 | "engines": { 21 | "vscode": "^1.63.0" 22 | }, 23 | "categories": [ 24 | "Other" 25 | ], 26 | "icon": "images/icon.png", 27 | "keywords": [ 28 | "vs", 29 | "browser", 30 | "visual", 31 | "studio", 32 | "code", 33 | "vscode" 34 | ], 35 | "activationEvents": [ 36 | "onStartupFinished" 37 | ], 38 | "main": "./out/extension.js", 39 | "contributes": { 40 | "commands": [ 41 | { 42 | "command": "vs-browser.start", 43 | "title": "VS Browser: Start Browser" 44 | }, 45 | { 46 | "command": "vs-browser.startWithProxy", 47 | "title": "VS Browser: Start Browser with Proxy" 48 | }, 49 | { 50 | "command": "vs-browser.startWithoutProxy", 51 | "title": "VS Browser: Start Browser without Proxy" 52 | }, 53 | { 54 | "command": "vs-browser.resetViewLocations", 55 | "title": "VS Browser: Reset View Locations" 56 | } 57 | ], 58 | "viewsContainers": { 59 | "activitybar": [ 60 | { 61 | "id": "vs-browser", 62 | "title": "VS Browser", 63 | "icon": "$(globe)" 64 | } 65 | ] 66 | }, 67 | "views": { 68 | "vs-browser": [ 69 | { 70 | "type": "webview", 71 | "id": "vs-browser-browser", 72 | "icon": "$(globe)", 73 | "name": "VS Browser", 74 | "contextualTitle": "VS Browser", 75 | "when": "config.vs-browser.showViewContainer == true" 76 | }, 77 | { 78 | "type": "webview", 79 | "id": "vs-browser-proxy", 80 | "icon": "$(globe)", 81 | "name": "VS Browser with Proxy", 82 | "contextualTitle": "VS Browser with Proxy", 83 | "visibility": "collapsed", 84 | "when": "config.vs-browser.showViewContainer == true" 85 | }, 86 | { 87 | "type": "webview", 88 | "id": "vs-browser-without-proxy", 89 | "icon": "$(globe)", 90 | "name": "VS Browser without Proxy", 91 | "contextualTitle": "VS Browser without Proxy", 92 | "visibility": "collapsed", 93 | "when": "config.vs-browser.showViewContainer == true" 94 | } 95 | ] 96 | }, 97 | "configuration": { 98 | "title": "VS Browser", 99 | "properties": { 100 | "vs-browser.proxyMode": { 101 | "type": "boolean", 102 | "default": false, 103 | "description": "Use proxy to prevent some errors (Longer loading time and can not use with localhost except localProxyServer)" 104 | }, 105 | "vs-browser.url": { 106 | "type": "string", 107 | "default": "http://localhost", 108 | "description": "Default URL open when starting the browser" 109 | }, 110 | "vs-browser.favourites.list": { 111 | "type": "object", 112 | "default": {}, 113 | "description": "Favourite websites", 114 | "additionalProperties": { 115 | "type": "string", 116 | "default": "" 117 | } 118 | }, 119 | "vs-browser.favourites.savingProfile": { 120 | "type": "string", 121 | "default": "global", 122 | "enum": [ 123 | "auto", 124 | "global", 125 | "workspace" 126 | ], 127 | "enumDescriptions": [ 128 | "Let VS Code decide", 129 | "Use User profile for saving profile", 130 | "Use Workspace profile for saving profile" 131 | ], 132 | "description": "Favourite websites saving profile" 133 | }, 134 | "vs-browser.autoCompleteUrl": { 135 | "type": "string", 136 | "default": "http://", 137 | "enum": [ 138 | "http://", 139 | "https://", 140 | "https://www.google.com/search?q=" 141 | ], 142 | "enumDescriptions": [ 143 | "http://your-url", 144 | "https://your-url", 145 | "https://www.google.com/search?q=your-url" 146 | ], 147 | "description": "Auto-complete URL when your URL is not an absolute URL" 148 | }, 149 | "vs-browser.localProxyServer.enabled": { 150 | "type": "boolean", 151 | "default": false, 152 | "description": "Enable local proxy server (Beta)" 153 | }, 154 | "vs-browser.localProxyServer.port": { 155 | "type": "number", 156 | "default": 9999, 157 | "description": "Local proxy server port" 158 | }, 159 | "vs-browser.localProxyServer.cookieDomainRewrite": { 160 | "type": "boolean", 161 | "default": false, 162 | "description": "Enable cookie domain rewrite" 163 | }, 164 | "vs-browser.localProxyServer.forceLocation": { 165 | "type": "boolean", 166 | "default": false, 167 | "description": "Some websites redirect the user with some weird status code (eg. 400), so you can bypass the status code and keep redirecting the user to the correct URL" 168 | }, 169 | "vs-browser.reload.onSave": { 170 | "type": "boolean", 171 | "default": false, 172 | "description": "Auto reload the browser when a file is saved" 173 | }, 174 | "vs-browser.reload.autoReloadDurationEnabled": { 175 | "type": "boolean", 176 | "default": false, 177 | "description": "Auto reload the browser after a set time" 178 | }, 179 | "vs-browser.reload.autoReloadDurationTime": { 180 | "type": "number", 181 | "default": 15000, 182 | "description": "The duration time to reload in milliseconds" 183 | }, 184 | "vs-browser.columnToShowIn": { 185 | "type": "string", 186 | "default": "Two", 187 | "enum": [ 188 | "One", 189 | "Two", 190 | "Three", 191 | "Active", 192 | "Beside" 193 | ], 194 | "enumDescriptions": [ 195 | "Show in First column", 196 | "Show in Second Column", 197 | "Show in Third column", 198 | "Show in Active Column", 199 | "Show in Beside column of Active column" 200 | ], 201 | "description": "Default column to show in" 202 | }, 203 | "vs-browser.showMessageDialog": { 204 | "type": "boolean", 205 | "default": false, 206 | "description": "Show message dialog (sometimes the dialog still displays even if nothing goes wrong)" 207 | }, 208 | "vs-browser.showViewContainer": { 209 | "type": "boolean", 210 | "default": true, 211 | "description": "Show view container that has items you can move to the sidebar to use" 212 | }, 213 | "vs-browser.showStatusBarItem": { 214 | "type": "boolean", 215 | "default": true, 216 | "description": "Show status bar item" 217 | }, 218 | "vs-browser.showUpdateChanges": { 219 | "type": "boolean", 220 | "default": true, 221 | "description": "Show \"New version changes\" after the updated" 222 | } 223 | } 224 | } 225 | }, 226 | "scripts": { 227 | "vscode:prepublish": "yarn run compile", 228 | "compile": "tsc -p ./", 229 | "watch": "tsc -watch -p ./", 230 | "pretest": "yarn run compile && yarn run lint", 231 | "lint": "eslint src --ext ts", 232 | "test": "node ./out/test/runTest.js" 233 | }, 234 | "devDependencies": { 235 | "@types/glob": "^7.1.4", 236 | "@types/http-proxy": "^1.17.8", 237 | "@types/mocha": "^9.0.0", 238 | "@types/node": "14.x", 239 | "@types/vscode": "^1.63.0", 240 | "@typescript-eslint/eslint-plugin": "^5.1.0", 241 | "@typescript-eslint/parser": "^5.1.0", 242 | "@vscode/test-electron": "^1.6.2", 243 | "eslint": "^8.1.0", 244 | "glob": "^7.1.7", 245 | "mocha": "^9.1.3", 246 | "typescript": "^4.4.4" 247 | }, 248 | "dependencies": { 249 | "@vscode/codicons": "^0.0.36", 250 | "connect": "^3.7.0", 251 | "http-proxy": "^1.18.1", 252 | "morgan": "^1.10.0", 253 | "zlib": "^1.0.5" 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /assets/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | { localProxyServerScript } { localProxyServerForceLocationScript } 11 | 12 | 13 | 14 | 36 |
37 |
38 |
39 | |  40 |
41 |
42 | 43 |
44 |
All Favourites
45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 | 331 | 332 | 333 | 334 | -------------------------------------------------------------------------------- /assets/js/jquery-3.7.1.slim.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween | (c) OpenJS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},m=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||m).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),b=new RegExp(ge+"|>"),A=new RegExp(g),D=new RegExp("^"+t+"$"),N={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+d),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},L=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,O=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,P=/[+~]/,H=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),q=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=K(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){E={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(V(e),e=e||C,T)){if(11!==d&&(u=O.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return E.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return E.call(n,a),n}else{if(u[2])return E.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return E.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||p&&p.test(t))){if(c=t,f=e,1===d&&(b.test(t)||m.test(t))){(f=P.test(t)&&X(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=k)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+G(l[o]);c=l.join(",")}try{return E.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function B(e){return e[k]=!0,e}function F(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function $(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return B(function(o){return o=+o,B(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function X(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=C&&9===n.nodeType&&n.documentElement&&(r=(C=n).documentElement,T=!ce.isXMLDoc(C),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=C&&(t=C.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=F(function(e){return r.appendChild(e).id=ce.expando,!C.getElementsByName||!C.getElementsByName(ce.expando).length}),le.disconnectedMatch=F(function(e){return i.call(e,"*")}),le.scope=F(function(){return C.querySelectorAll(":scope")}),le.cssHas=F(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(x.filter.ID=function(e){var t=e.replace(H,q);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(H,q);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},x.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&T)return t.getElementsByClassName(e)},p=[],F(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+k+"-]").length||p.push("~="),e.querySelectorAll("a#"+k+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=C.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===C||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),C}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),T&&!h[t+" "]&&(!p||!p.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(H,q),e[3]=(e[3]||e[4]||e[5]||"").replace(H,q),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return N.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&A.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(H,q).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||E,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:k.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),C.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=m.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,E=ce(m);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;re=m.createDocumentFragment().appendChild(m.createElement("div")),(be=m.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),re.appendChild(be),le.checkClone=re.cloneNode(!0).cloneNode(!0).lastChild.checked,re.innerHTML="",le.noCloneChecked=!!re.cloneNode(!0).lastChild.defaultValue,re.innerHTML="",le.option=!!re.lastChild;var Te={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Ee(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Se=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ie(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function We(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===yt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(r)):t=m),o=!n&&[],(i=C.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||K})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Qe(le.pixelPosition,function(e,t){if(t)return t=Ve(e,n),$e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0