diff --git a/.vscode/launch.json b/.vscode/launch.json index 35677aff..79598635 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,27 +1,31 @@ // A launch configuration that compiles the extension and then opens it inside a new window { - "version": "0.1.0", - "configurations": [{ - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}"], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": ["${workspaceRoot}/out/**/*.js"], - "preLaunchTask": "npm" - }, - { - "name": "Launch Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": ["${workspaceRoot}/out/test/**/*.js"], - "preLaunchTask": "npm" - } - ] + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}"], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/out/**/*.js"], + "preLaunchTask": "npm" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}", + "--extensionTestsPath=${workspaceRoot}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/out/test/**/*.js"], + "preLaunchTask": "npm" + } + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d31b1591..29406b31 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,23 +8,23 @@ // A task runner that calls a custom npm script that compiles the extension. { - "version": "0.1.0", + "version": "0.1.0", - // we want to run npm - "command": "npm", + // we want to run npm + "command": "npm", - // the command is a shell script - "isShellCommand": true, + // the command is a shell script + "isShellCommand": true, - // show the output window only if unrecognized errors occur. - "showOutput": "silent", + // show the output window only if unrecognized errors occur. + "showOutput": "silent", - // we run the custom script "compile" as defined in package.json - "args": ["run", "compile", "--loglevel", "silent"], + // we run the custom script "compile" as defined in package.json + "args": ["run", "compile", "--loglevel", "silent"], - // The tsc compiler is started in watching mode - "isWatching": true, + // The tsc compiler is started in watching mode + "isWatching": true, - // use the standard tsc in watch mode problem matcher to find compile problems in the output. - "problemMatcher": "$tsc-watch" -} \ No newline at end of file + // use the standard tsc in watch mode problem matcher to find compile problems in the output. + "problemMatcher": "$tsc-watch" +} diff --git a/README.md b/README.md index ff074113..f345a33a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # Settings Sync [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Synchronize%20your%20%40VisualStudio%20%40code%20Settings%20Across%20Multiple%20Machines%20using%20%40github%20GIST%20by%20%40itsShanKhan&url=https://github.com/shanalikhan/code-settings-sync&via=code&hashtags=code,vscode,SettingsSync,developers) [![Follow](https://img.shields.io/twitter/follow/itsShanKhan.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=itsShanKhan) - - - **Previously known as Visual Studio Code Settings Sync** [![Version](https://vsmarketplacebadge.apphb.com/version/Shan.code-settings-sync.svg)](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) [![Travis](https://img.shields.io/travis/rust-lang/rust.svg)](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) @@ -11,7 +8,7 @@ ## Support -While being free and open source, if you find it useful, please consider supporting it by donating via PayPal or Open Collective. If you are using it in office as a team, please ask your company to support us via Open Collective from just 2$ per month! +While being free and open source, if you find it useful, please consider supporting it by donating via PayPal or Open Collective. If you are using it in office as a team, please ask your company to support us via Open Collective from just 2\$ per month! @@ -38,11 +35,11 @@ While being free and open source, if you find it useful, please consider support

-**Type Sync in command Palette in order to view all commands.** +**Type `Sync` in command Palette in order to view all commands.** ## Key Features -``` +``` 1. Use your GitHub account token and Gist. 2. Easy to Upload and Download on one click. 3. Show a summary page at the end with details about config and extensions effected. @@ -51,10 +48,11 @@ While being free and open source, if you find it useful, please consider support 6. Share the Gist with other users and let them download your settings. 7. Supports GitHub Enterprise 8. Support pragmas with @sync keywords: host, os and env are supported. +9. GUI for changing settings / logging in ``` - ## It Syncs + ``` All extensions and complete User Folder that Contains 1. Settings File @@ -66,90 +64,39 @@ All extensions and complete User Folder that Contains ``` ## Shortcuts + ``` 1. Upload Key : Shift + Alt + U 2. Download Key : Shift + Alt + D ``` -## Steps To Get a Personal Access Token from GitHub - -This extension requires a Personal Access Token from your GitHub account. You can create one by simply following the steps shown in the pictures below. Make sure you add **Gist** in scope. - -**Go to [Settings](https://github.com/settings) / [Developer settings](https://github.com/settings/tokens) / [Personal access tokens](https://github.com/settings/tokens) / Generate New Token** - - -![Goto Settings / Developer settings / Personal Access Tokens](https://shanalikhan.github.io/img/github1.PNG) - -**Select Gist From Scopes.** - -![Select Scopes](https://shanalikhan.github.io/img/github2.PNG) - -**Get an Access Token.** - -![Get Access Token](https://shanalikhan.github.io/img/github3.PNG) - - -> Save the Token somewhere for future use (i.e. to upload from other machines). - - ## Upload Your Settings For the first time -**Press Shift + Alt + U it will ask your GitHub account access token.** - > Type ">Sync" In Command Palette into order download / upload -This will automatically open your GitHub settings page, allowing you to generate a new token for the application, as explained in the previous section. This token will allow the extension to create gists. - -Enter the GitHub token in the window and click enter. - -![github account access token](https://shanalikhan.github.io/img/upload1.png) +When downloading or uploading for the first time, the welcome page will automatically open, where you can log in with GitHub. -**Upload your settings automatically and the extension gives you Gist ID in the system message.** -Gist ID is needed to access the data you have uploaded with your token. Copy this Gist ID in order to download the settings to other machines. +![Login with GitHub](https://user-images.githubusercontent.com/25834068/55663873-93c21280-57d9-11e9-8c24-7088a81ed47c.png) -![uploaded automatically](https://shanalikhan.github.io/img/upload2.png) - -You can always **verify created gist** on the following url: - -> https://gist.github.com/{your_userName}/{gist_id} - -Here is the gif of the complete process when you execute the Upload command (Might take some time to load) - -![Upload](https://media.giphy.com/media/xT9IglKxSqs2Wdwq2c/source.gif) +You can always **verify created gist** by going to `https://gist.github.com` and checking for a gist named `cloudSettings` ## Download your Settings -**Press Shift + Alt + D it will ask your GitHub Gist ID.** - > Type ">Sync" In Command Palette into order download / upload -**Enter Your GitHub Token.** - -Enter the GitHub token in the window and click enter. +**Login with GitHub** -![github account access token](https://shanalikhan.github.io/img/upload1.png) +**Choose your existing Gist** -**Enter Your Gist ID.** - -You need to enter your Gist ID in order to download the files you have uploaded with Shift + Alt + U. - -![Enter Your Gist ID](https://shanalikhan.github.io/img/download2.png) +![Existing Gist](https://user-images.githubusercontent.com/25834068/55663880-a9373c80-57d9-11e9-8afc-c0e79cc1b3b5.png) **Settings Downloaded.** -You are Done! All your files are downloaded - -![Enter Your Gist ID](https://shanalikhan.github.io/img/download3.png) - -Here is the gif of the complete process when you execute the Download command (Might take time to load) +You are Done! All your files are downloaded -![Download](https://media.giphy.com/media/xT9Iglsi3CS9noE8tW/source.gif) - - -## Reset Token / Gist Settings - -> Type ">Sync" In Command Palette and select Reset Token and Gist Settings +## Reset Extension Settings +> Select **"> Sync : Reset Extension Settings"** in the Command Palette to reset your settings ## Toggle Auto Download @@ -174,7 +121,6 @@ Please make sure you have valid github Token and Gist available to make it work Select Command **"Sync : Advanced Options > Toggle Auto-Upload on Settings Change"** command to Turn ON / OFF the auto-upload. - ## Toggle Summary Summary is **enabled by default** which shows all files and extensions that are added or deleted on a single page. @@ -191,9 +137,10 @@ Select Command **"Sync : Advanced Options > Share Settings with Public GIST"** Other users can give your Gist Id to download the Gist, but they can't upload their settings on your Gist. - ## Settings +Settings can be changed through the settings page, which can be accessed through **"> Sync : Advanced Options > Open Settings Page"** + There are two types of settings in Settings Sync. I will recommend you to read the configurations details [here](https://medium.com/@itsShanKhan/visual-studio-code-settings-sync-configurations-ed8dd6fd9753). @@ -204,7 +151,7 @@ You can customize the settings in gist settings like: ``` 1. Configure Gist Id (Environment) -2. Configure auto upload / download for Github Gist +2. Configure auto upload / download for Github Gist 3. Configure extension sync behaviour 4. Configure force download 5. Configure quiet sync @@ -243,33 +190,26 @@ You can customize the sync: ```json { - "ignoreUploadFiles": [ - "projects.json", - "projects_cache_vscode.json", - "projects_cache_git.json", - "projects_cache_svn.json", - "gpm_projects.json", - "gpm-recentItems.json" - ], - "ignoreUploadFolders": [ - "workspaceStorage" - ], - "ignoreExtensions": [ - "ignored_extension_name" - ], - "gistDescription": "Visual Studio Code Settings Sync Gist", - "version": 310, - "token": "YOUR_GITHUB_TOKEN_HERE", - "downloadPublicGist": false, - "supportedFileExtensions": [ - "json", - "code-snippets" - ], - "openTokenLink": true, - "lastUpload": null, - "lastDownload": null, - "githubEnterpriseUrl": null, - "hostName": null + "ignoreUploadFiles": [ + "projects.json", + "projects_cache_vscode.json", + "projects_cache_git.json", + "projects_cache_svn.json", + "gpm_projects.json", + "gpm-recentItems.json" + ], + "ignoreUploadFolders": ["workspaceStorage"], + "ignoreExtensions": ["ignored_extension_name"], + "gistDescription": "Visual Studio Code Settings Sync Gist", + "version": 310, + "token": "YOUR_GITHUB_TOKEN_HERE", + "downloadPublicGist": false, + "supportedFileExtensions": ["json", "code-snippets"], + "openTokenLink": true, + "lastUpload": null, + "lastDownload": null, + "githubEnterpriseUrl": null, + "hostName": null } ``` @@ -299,20 +239,18 @@ Thank you to all our backers! [[Become a backer](https://opencollective.com/code [](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=4W3EWHHBSYMM8&lc=IE&item_name=Code%20Settings%20Sync&item_number=visual%20studio%20code%20settings%20sync¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted) - ### Sponsors Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/code-settings-sync)) ## [Contributors](https://github.com/shanalikhan/code-settings-sync/graphs/contributors) + # [Release Notes](https://shanalikhan.github.io/2016/05/14/Visual-studio-code-sync-settings-release-notes.html) # License - [![Version](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/shanalikhan/code-settings-sync/blob/master/LICENSE) - [![Version](https://vsmarketplacebadge.apphb.com/version/Shan.code-settings-sync.svg)](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) [![Installs](https://vsmarketplacebadge.apphb.com/installs/Shan.code-settings-sync.svg)](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) diff --git a/package.json b/package.json index b9f55bf7..6fcafd16 100644 --- a/package.json +++ b/package.json @@ -123,11 +123,6 @@ "default": false, "description": "%ext.config.quietSync%" }, - "sync.askGistName": { - "type": "boolean", - "default": false, - "description": "%ext.config.askGistName%" - }, "sync.removeExtensions": { "type": "boolean", "default": true, @@ -154,9 +149,12 @@ "devDependencies": { "@types/chai": "4.1.7", "@types/chokidar": "^2.1.3", + "@types/express": "^4.16.1", "@types/fs-extra": "^5.0.5", + "@types/lodash": "^4.14.123", "@types/mocha": "^5.2.6", "@types/node": "^11.13.0", + "@types/node-fetch": "^2.1.7", "chai": "^4.2.0", "clean-webpack-plugin": "^2.0.1", "mocha": "^6.0.2", @@ -171,10 +169,16 @@ }, "dependencies": { "@octokit/rest": "^16.16.4", + "adm-zip": "^0.4.13", "chokidar": "^2.1.5", + "const": "^1.0.0", + "express": "^4.16.4", "fs-extra": "^7.0.1", "https-proxy-agent": "^2.2.1", "lockfile": "^1.0.4", - "temp": "^0.9.0" + "lodash": "^4.17.11", + "node-fetch": "^2.3.0", + "temp": "^0.9.0", + "webpack-node-externals": "^1.7.2" } } diff --git a/package.nls.json b/package.nls.json index 4a88d4db..32d101d4 100644 --- a/package.nls.json +++ b/package.nls.json @@ -36,6 +36,7 @@ "cmd.resetSettings.info.resetting": "Sync : Resetting Your Settings.", "cmd.resetSettings.info.settingClear": "Sync : Settings Cleared.", "cmd.otherOptions.title": "Sync : Advanced Options", + "cmd.otherOptions.openSettings": "Sync : Open Settings", "cmd.otherOptions.editLocalSetting": "Sync : Edit Extension Local Settings", "cmd.otherOptions.shareSetting": "Sync : Share Settings with Public GIST", "cmd.otherOptions.shareSetting.beforeConfirm": "Sync : This will remove current GIST and upload settings on new public GIST. Do you want to continue?", @@ -69,7 +70,7 @@ "cmd.otherOptions.quietSync.off": "Sync : Summary will be shown upon download / upload.", "cmd.otherOptions.warning.tokenNotRequire": "Sync : Settings Sync will not ask for GitHub Token from now on.", "cmd.otherOptions.error.toggleFail": "Sync : Unable to Toggle.", - "cmd.otherOptions.triggerReset" : "Sync : Do you want to reset the settings ?", + "cmd.otherOptions.triggerReset": "Sync : Do you want to reset the settings ?", "common.info.installed": "Sync : Settings created, thank you for installing!", "common.info.needHelp": "Sync : Need Help configuring this extension?", "common.info.excludeFile": "Sync : You can exclude any file / folder for upload and settings for download.", @@ -101,6 +102,5 @@ "common.prompt.enterGistId": "Enter Gist Id from previously uploaded settings. You can also set manually in code settings (sync.gist). Press [Enter] or [Esc] to cancel.", "common.prompt.enterGithubAccessToken": "You also manually add a token (User Folder / syncLocalSettings.json). Press [Enter] or [Esc] to cancel.", "common.prompt.restartCode": "Do you want to reload to apply extensions and configurations?", - "common.button.yes" :"Yes" - + "common.button.yes": "Yes" } diff --git a/release-notes.json b/release-notes.json new file mode 100644 index 00000000..a632a1bc --- /dev/null +++ b/release-notes.json @@ -0,0 +1,30 @@ +{ + "changes": [ + { + "details": "Auto upload doesn't work when make change on settings (Thanks to @arnohovhannisyan for PR #807)", + "type": "FIX", + "color": "danger" + }, + { + "details": "Disable change detection for workspace storage folder (Thanks to @knyhle for PR #811)", + "type": "FIX", + "color": "danger" + }, + { + "details": "Pretiffy Custom Settings JSON (Thanks to @knyhle for PR #812)", + "type": "NEW", + "color": "success" + }, + { + "details": "Remove manual vsix package installation in favour of extension download by CLI (#820)", + "type": "NEW", + "color": "success" + }, + { + "details": "Remove replaceCodeSettings from Settings Sync configurations (#805)", + "type": "NEW", + "color": "success" + } + ], + "currentVersion": "v3.2.8" +} diff --git a/src/commons.ts b/src/commons.ts index cc3cba77..ec70f4dd 100644 --- a/src/commons.ts +++ b/src/commons.ts @@ -1,547 +1,727 @@ -"use strict"; -import * as vscode from "vscode"; -import { Environment } from "./environmentPath"; -import localize from "./localize"; -import { AutoUploadService } from "./service/autoUploadService"; -import { File, FileService } from "./service/fileService"; -import { ExtensionInformation } from "./service/pluginService"; -import { CustomSettings, ExtensionConfig, LocalConfig } from "./setting"; - -export default class Commons { - public static outputChannel: vscode.OutputChannel = null; - public static LogException( - error: any, - message: string, - msgBox: boolean, - callback?: () => void - ): void { - if (error) { - console.error(error); - if (error.status === 500) { - message = localize("common.error.connection"); - msgBox = false; - } else if (error.status === 401) { - msgBox = true; - message = localize("common.error.invalidToken"); - } else if (error.status === 4) { - message = localize("common.error.canNotSave"); - } else if (error.message) { - try { - message = JSON.parse(error.message).message; - if (message.toLowerCase() === "not found") { - msgBox = true; - message = localize("common.error.invalidGistId"); - } - } catch (error) { - // message = error.message; - } - } - } - - if (msgBox === true) { - vscode.window.showErrorMessage(message); - vscode.window.setStatusBarMessage("").dispose(); - } else { - vscode.window.setStatusBarMessage(message, 5000); - } - - if (callback) { - callback.apply(this); - } - } - - public static GetInputBox(token: boolean) { - if (token) { - const options: vscode.InputBoxOptions = { - placeHolder: localize("common.placeholder.enterGithubAccessToken"), - password: false, - prompt: localize("common.prompt.enterGithubAccessToken"), - ignoreFocusOut: true - }; - return options; - } else { - const options: vscode.InputBoxOptions = { - placeHolder: localize("common.placeholder.enterGistId"), - password: false, - prompt: localize("common.prompt.enterGistId"), - ignoreFocusOut: true - }; - return options; - } - } - - public autoUploadService: AutoUploadService; - - public ERROR_MESSAGE: string = localize("common.error.message"); - - constructor( - private en: Environment, - private context: vscode.ExtensionContext - ) { - this.InitializeAutoUpload(); - } - - public async InitializeAutoUpload() { - const ignored = await AutoUploadService.GetIgnoredItems( - await this.GetCustomSettings() - ); - this.autoUploadService = new AutoUploadService({ - en: this.en, - commons: this, - ignored - }); - } - - public async InitalizeSettings( - askToken: boolean, - askGist: boolean - ): Promise { - const settings: LocalConfig = new LocalConfig(); - const extSettings: ExtensionConfig = this.GetSettings(); - const cusSettings: CustomSettings = await this.GetCustomSettings(); - - if (cusSettings.token === "") { - if (askToken === true) { - askToken = !cusSettings.downloadPublicGist; - } - - if (askToken) { - if (cusSettings.openTokenLink) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse("https://github.com/settings/tokens") - ); - } - const tokTemp: string = await this.GetTokenAndSave(cusSettings); - if (!tokTemp) { - const msg = localize("common.error.tokenNotSave"); - vscode.window.showErrorMessage(msg); - throw new Error(msg); - } - cusSettings.token = tokTemp; - } - } - - if (extSettings.gist === "") { - if (askGist) { - const gistTemp: string = await this.GetGistAndSave(extSettings); - if (!gistTemp) { - const msg = localize("common.error.gistNotSave"); - vscode.window.showErrorMessage(msg); - throw new Error(msg); - } - extSettings.gist = gistTemp; - } - } - settings.customConfig = cusSettings; - settings.extConfig = extSettings; - return settings; - } - - public async GetCustomSettings(): Promise { - let customSettings: CustomSettings = new CustomSettings(); - try { - const customExist: boolean = await FileService.FileExists( - this.en.FILE_CUSTOMIZEDSETTINGS - ); - if (customExist) { - const customSettingStr: string = await FileService.ReadFile( - this.en.FILE_CUSTOMIZEDSETTINGS - ); - const tempObj = JSON.parse(customSettingStr); - - Object.assign(customSettings, tempObj); - customSettings.token = customSettings.token.trim(); - return customSettings; - } - } catch (e) { - Commons.LogException( - e, - "Sync : Unable to read " + - this.en.FILE_CUSTOMIZEDSETTINGS_NAME + - ". Make sure its Valid JSON.", - true - ); - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "http://shanalikhan.github.io/2017/02/19/Option-to-ignore-settings-folders-code-settings-sync.html" - ) - ); - customSettings = null; - return customSettings; - } - } - - public async SetCustomSettings(setting: CustomSettings): Promise { - try { - await FileService.WriteFile( - this.en.FILE_CUSTOMIZEDSETTINGS, - JSON.stringify(setting, null, 4) - ); - return true; - } catch (e) { - Commons.LogException( - e, - "Sync : Unable to write " + this.en.FILE_CUSTOMIZEDSETTINGS_NAME, - true - ); - return false; - } - } - - public async StartMigrationProcess(): Promise { - const fileExist: boolean = await FileService.FileExists( - this.en.FILE_CUSTOMIZEDSETTINGS - ); - let customSettings: CustomSettings = null; - const firstTime: boolean = !fileExist; - let fileChanged: boolean = firstTime; - - if (fileExist) { - customSettings = await this.GetCustomSettings(); - } else { - customSettings = new CustomSettings(); - } - // vscode.workspace.getConfiguration().update("sync.version", undefined, true); - - if (firstTime) { - const openExtensionPage = localize("common.action.openExtPage"); - vscode.window.showInformationMessage(localize("common.info.installed")); - vscode.window - .showInformationMessage( - localize("common.info.needHelp"), - openExtensionPage - ) - .then((val: string) => { - if (val === openExtensionPage) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync" - ) - ); - } - }); - } else if (customSettings.version < Environment.CURRENT_VERSION) { - fileChanged = true; - // #TODO : Remove this in new update - const newIgnoredList = new CustomSettings().ignoreUploadFiles; - newIgnoredList.forEach(m => { - if (customSettings.ignoreUploadFiles.indexOf(m) === -1) { - customSettings.ignoreUploadFiles.push(m); - } - }); - - if (this.context.globalState.get("synctoken")) { - const token = this.context.globalState.get("synctoken"); - if (token !== "") { - customSettings.token = String(token); - this.context.globalState.update("synctoken", ""); - vscode.window.showInformationMessage( - localize("common.info.setToken") - ); - } - } - - const releaseNotes = localize("common.action.releaseNotes"); - const writeReview = localize("common.action.writeReview"); - const support = localize("common.action.support"); - const joinCommunity = localize("common.action.joinCommunity"); - if (!customSettings.disableUpdateMessage) { - vscode.window - .showInformationMessage( - localize("common.info.updateTo", Environment.getVersion()), - releaseNotes, - writeReview, - support, - joinCommunity - ) - .then((val: string) => { - if (val === releaseNotes) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "http://shanalikhan.github.io/2016/05/14/Visual-studio-code-sync-settings-release-notes.html" - ) - ); - } - if (val === writeReview) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync#review-details" - ) - ); - } - if (val === support) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=4W3EWHHBSYMM8&lc=IE&item_name=Code%20Settings%20Sync&item_number=visual%20studio%20code%20settings%20sync¤cy_code=USD&bn=PP-DonationsBF:btn_donate_SM.gif:NonHosted" - ) - ); - } - if (val === joinCommunity) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "https://join.slack.com/t/codesettingssync/shared_invite/enQtMzE3MjY5NTczNDMwLTYwMTIwNGExOGE2MTJkZWU0OTU5MmI3ZTc4N2JkZjhjMzY1OTk5OGExZjkwMDMzMDU4ZTBlYjk5MGQwZmMyNzk" - ) - ); - } - }); - } - } - - if (fileChanged) { - customSettings.version = Environment.CURRENT_VERSION; - await this.SetCustomSettings(customSettings); - } - return true; - } - - public async SaveSettings(setting: ExtensionConfig): Promise { - const config = vscode.workspace.getConfiguration("sync"); - const allKeysUpdated = new Array>(); - - const keys = Object.keys(setting); - keys.forEach(async keyName => { - if (setting[keyName] == null) { - setting[keyName] = ""; - } - if (keyName.toLowerCase() !== "token") { - if (config.get(keyName) !== setting[keyName]) { - allKeysUpdated.push(config.update(keyName, setting[keyName], true)); - } - } - }); - - try { - await Promise.all(allKeysUpdated); - if (this.context.globalState.get("syncCounter")) { - const counter = this.context.globalState.get("syncCounter"); - let count: number = parseInt(counter + "", 10); - if (count % 450 === 0) { - this.DonateMessage(); - } - count = count + 1; - this.context.globalState.update("syncCounter", count); - } else { - this.context.globalState.update("syncCounter", 1); - } - return true; - } catch (err) { - Commons.LogException(err, this.ERROR_MESSAGE, true); - return false; - } - } - - public async DonateMessage(): Promise { - const donateNow = localize("common.action.donate"); - const writeReview = localize("common.action.writeReview"); - const res = await vscode.window.showInformationMessage( - localize("common.info.donate"), - donateNow, - writeReview - ); - - if (res === donateNow) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=4W3EWHHBSYMM8&lc=IE&item_name=Code%20Settings%20Sync&item_number=visual%20studio%20code%20settings%20sync¤cy_code=USD&bn=PP-DonationsBF:btn_donate_SM.gif:NonHosted" - ) - ); - } else if (res === writeReview) { - vscode.commands.executeCommand( - "vscode.open", - vscode.Uri.parse( - "https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync#review-details" - ) - ); - } - } - - public GetSettings(): ExtensionConfig { - const settings = new ExtensionConfig(); - - for (const key of Object.keys(settings)) { - if (key !== "token") { - settings[key] = vscode.workspace.getConfiguration("sync").get(key); - } - } - - settings.gist = settings.gist.trim(); - return settings; - } - - public async GetTokenAndSave(sett: CustomSettings): Promise { - const opt = Commons.GetInputBox(true); - - const token = ((await vscode.window.showInputBox(opt)) || "").trim(); - - if (token && token !== "esc") { - sett.token = token; - const saved = await this.SetCustomSettings(sett); - if (saved) { - vscode.window.setStatusBarMessage( - localize("common.info.tokenSaved"), - 1000 - ); - } - } - - return token; - } - public async GetGistAndSave(sett: ExtensionConfig): Promise { - const opt = Commons.GetInputBox(false); - - const gist = ((await vscode.window.showInputBox(opt)) || "").trim(); - - if (gist && gist !== "esc") { - sett.gist = gist; - const saved = await this.SaveSettings(sett); - if (saved) { - vscode.window.setStatusBarMessage( - localize("common.info.gistSaved"), - 1000 - ); - } - return gist; - } - } - - /** - * IgnoreSettings - */ - public async GetIgnoredSettings(settings: string[]): Promise { - const ignoreSettings: object = {}; - const config = vscode.workspace.getConfiguration(); - const keysUpdated: Array> = []; - - for (const key of settings) { - let keyValue: object = null; - keyValue = config.get(key, null); - if (keyValue !== null) { - ignoreSettings[key] = keyValue; - keysUpdated.push(config.update(key, undefined, true)); - } - } - - await Promise.all(keysUpdated); - - return ignoreSettings; - } - - /** - * RestoreIgnoredSettings - */ - public SetIgnoredSettings(ignoredSettings: object): void { - const config = vscode.workspace.getConfiguration(); - const keysUpdated: Array> = []; - for (const key of Object.keys(ignoredSettings)) { - keysUpdated.push(config.update(key, ignoredSettings[key], true)); - } - } - - /** - * AskGistName - */ - public async AskGistName(): Promise { - return vscode.window.showInputBox({ - prompt: localize("common.prompt.multipleGist"), - ignoreFocusOut: true, - placeHolder: localize("common.placeholder.multipleGist") - }); - } - - public ShowSummaryOutput( - upload: boolean, - files: File[], - removedExtensions: ExtensionInformation[], - addedExtensions: ExtensionInformation[], - ignoredExtensions: ExtensionInformation[], - syncSettings: LocalConfig - ) { - if (Commons.outputChannel === null) { - Commons.outputChannel = vscode.window.createOutputChannel( - "Code Settings Sync" - ); - } - - const outputChannel = Commons.outputChannel; - outputChannel.appendLine( - `CODE SETTINGS SYNC ${upload ? "UPLOAD" : "DOWNLOAD"} SUMMARY` - ); - outputChannel.appendLine(`Version: ${Environment.getVersion()}`); - outputChannel.appendLine(`--------------------`); - outputChannel.appendLine( - `GitHub Token: ${syncSettings.customConfig.token || "Anonymous"}` - ); - outputChannel.appendLine(`GitHub Gist: ${syncSettings.extConfig.gist}`); - outputChannel.appendLine( - `GitHub Gist Type: ${syncSettings.publicGist ? "Public" : "Secret"}` - ); - outputChannel.appendLine(``); - if (!syncSettings.customConfig.token) { - outputChannel.appendLine( - `Anonymous Gist cannot be edited, the extension will always create a new one during upload.` - ); - } - outputChannel.appendLine( - `Restarting Visual Studio Code may be required to apply color and file icon theme.` - ); - outputChannel.appendLine(`--------------------`); - - outputChannel.appendLine(`Files ${upload ? "Upload" : "Download"}ed:`); - files - .filter(item => item.fileName.indexOf(".") > 0) - .forEach(item => { - outputChannel.appendLine(` ${item.fileName} > ${item.gistName}`); - }); - - outputChannel.appendLine(``); - outputChannel.appendLine(`Extensions Ignored:`); - - if (!ignoredExtensions || ignoredExtensions.length === 0) { - outputChannel.appendLine(` No extensions ignored.`); - } else { - ignoredExtensions.forEach(extn => { - outputChannel.appendLine(` ${extn.name} v${extn.version}`); - }); - } - - outputChannel.appendLine(``); - outputChannel.appendLine(`Extensions Removed:`); - - if (!syncSettings.extConfig.removeExtensions) { - outputChannel.appendLine(` Feature Disabled.`); - } else { - if (!removedExtensions || removedExtensions.length === 0) { - outputChannel.appendLine(` No extensions removed.`); - } else { - removedExtensions.forEach(extn => { - outputChannel.appendLine(` ${extn.name} v${extn.version}`); - }); - } - } - - if (addedExtensions) { - outputChannel.appendLine(``); - outputChannel.appendLine(`Extensions Added:`); - - if (addedExtensions.length === 0) { - outputChannel.appendLine(` No extensions installed.`); - } - - addedExtensions.forEach(extn => { - outputChannel.appendLine(` ${extn.name} v${extn.version}`); - }); - } - - outputChannel.appendLine(`--------------------`); - outputChannel.append(`Done.`); - outputChannel.show(true); - } -} +"use strict"; +import { readFileSync } from "fs"; +import { has, set } from "lodash"; +import * as vscode from "vscode"; +import { Environment } from "./environmentPath"; +import localize from "./localize"; +import { AutoUploadService } from "./service/autoUploadService"; +import { File, FileService } from "./service/fileService"; +import { GitHubOAuthService } from "./service/oauthService"; +import { ExtensionInformation } from "./service/pluginService"; +import { CustomSettings, ExtensionConfig, LocalConfig } from "./setting"; + +enum SettingType { + TextInput, + Checkbox, + TextArea +} + +export default class Commons { + public static outputChannel: vscode.OutputChannel = null; + public static LogException( + error: any, + message: string, + msgBox: boolean, + callback?: () => void + ): void { + if (error) { + console.error(error); + if (error.status === 500) { + message = localize("common.error.connection"); + msgBox = false; + } else if (error.status === 401) { + msgBox = true; + message = localize("common.error.invalidToken"); + } else if (error.status === 4) { + message = localize("common.error.canNotSave"); + } else if (error.message) { + try { + message = JSON.parse(error.message).message; + if (message.toLowerCase() === "not found") { + msgBox = true; + message = localize("common.error.invalidGistId"); + } + } catch (error) { + // message = error.message; + } + } + } + + if (msgBox === true) { + vscode.window.showErrorMessage(message); + vscode.window.setStatusBarMessage("").dispose(); + } else { + vscode.window.setStatusBarMessage(message, 5000); + } + + if (callback) { + callback.apply(this); + } + } + + public static GetInputBox(token: boolean) { + if (token) { + const options: vscode.InputBoxOptions = { + placeHolder: localize("common.placeholder.enterGithubAccessToken"), + password: false, + prompt: localize("common.prompt.enterGithubAccessToken"), + ignoreFocusOut: true + }; + return options; + } else { + const options: vscode.InputBoxOptions = { + placeHolder: localize("common.placeholder.enterGistId"), + password: false, + prompt: localize("common.prompt.enterGistId"), + ignoreFocusOut: true + }; + return options; + } + } + + public SettingsView: string; + public LandingPageView: string; + + public autoUploadService: AutoUploadService; + + public ERROR_MESSAGE: string = localize("common.error.message"); + + private customizableSettings = [ + { + name: "Hostname (optional)", + placeholder: "Enter Hostname", + type: SettingType.TextInput, + correspondingSetting: "hostName" + }, + { + name: "Ignored Files", + placeholder: "Enter one file per line", + type: SettingType.TextArea, + correspondingSetting: "ignoreUploadFiles" + }, + { + name: "Ignored Folders", + placeholder: "Enter one folder per line", + type: SettingType.TextArea, + correspondingSetting: "ignoreUploadFolders" + }, + { + name: "Ignored Extensions", + placeholder: "Enter one extension per line (full name)", + type: SettingType.TextArea, + correspondingSetting: "ignoreExtensions" + }, + { + name: "Supported File Extensions", + placeholder: "Enter one file extension per line", + type: SettingType.TextArea, + correspondingSetting: "supportedFileExtensions" + }, + { + name: "Access Token", + placeholder: "Enter Token", + type: SettingType.TextInput, + correspondingSetting: "token" + }, + { + name: "Gist Description", + placeholder: "Enter Gist Description", + type: SettingType.TextInput, + correspondingSetting: "gistDescription" + }, + { + name: "GitHub Enterprise URL (optional)", + placeholder: "Enter GitHub Enterprise URL", + type: SettingType.TextInput, + correspondingSetting: "githubEnterpriseUrl" + }, + { + name: "Ask Gist Name", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "askGistName" + }, + { + name: "Download Public Gist", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "downloadPublicGist" + }, + { + name: "Open Token Link", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "openTokenLink" + } + ]; + + private extensionSettings = [ + { + name: "Gist ID", + placeholder: "Enter Gist ID", + type: SettingType.TextInput, + correspondingSetting: "gist", + tooltip: localize("ext.config.gist") + }, + { + name: "Auto Download", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "autoDownload", + tooltip: localize("ext.config.autoDownload") + }, + { + name: "Auto Upload", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "autoUpload", + tooltip: localize("ext.config.autoUpload") + }, + { + name: "Force Download", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "forceDownload", + tooltip: localize("ext.config.forceDownload") + }, + { + name: "Quiet Sync", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "quietSync", + tooltip: localize("ext.config.quietSync") + }, + { + name: "Remove Extensions", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "removeExtensions", + tooltip: localize("ext.config.removeExtensions") + }, + { + name: "Sync Extensions", + placeholder: "", + type: SettingType.Checkbox, + correspondingSetting: "syncExtensions", + tooltip: localize("ext.config.syncExtensions") + } + ]; + + constructor( + private en: Environment, + private context: vscode.ExtensionContext + ) { + this.InitializeAutoUpload(); + this.SettingsView = readFileSync( + `${this.context.extensionPath}/ui/settings/settings.html`, + { + encoding: "utf8" + } + ); + this.LandingPageView = readFileSync( + `${this.context.extensionPath}/ui/landing-page/landing-page.html`, + { + encoding: "utf8" + } + ); + } + + public async OpenSettingsPage() { + const customSettings = await this.GetCustomSettings(); + const extSettings = await this.GetSettings(); + const content: string = this.SettingsView.replace( + new RegExp("@GLOBAL_DATA", "g"), + JSON.stringify(customSettings) + ) + .replace(new RegExp("@ENV_DATA", "g"), JSON.stringify(extSettings)) + .replace( + new RegExp("@GLOBAL_MAP", "g"), + JSON.stringify(this.customizableSettings) + ) + .replace( + new RegExp("@ENV_MAP", "g"), + JSON.stringify(this.extensionSettings) + ) + .replace( + new RegExp("@PWD", "g"), + vscode.Uri.file(this.context.extensionPath) + .with({ + scheme: "vscode-resource" + }) + .toString() + ); + const settingsPanel = vscode.window.createWebviewPanel( + "syncSettings", + "Sync Settings", + vscode.ViewColumn.One, + { + retainContextWhenHidden: true, + enableScripts: true + } + ); + settingsPanel.webview.html = content; + settingsPanel.webview.onDidReceiveMessage(message => + this.ReceiveSettingChange(message) + ); + } + + public async ReceiveSettingChange(message: { + command: string; + text: string; + type: string; + }) { + let value: any = message.text; + if (message.text === "true" || message.text === "false") { + value = message.text === "true"; + } + if (message.type === "global") { + const customSettings = await this.GetCustomSettings(); + if (has(customSettings, message.command)) { + set(customSettings, message.command, value); + this.SetCustomSettings(customSettings); + } + } else { + const extSettings = await this.GetSettings(); + extSettings[message.command] = value; + this.SaveSettings(extSettings); + } + } + + public async OpenLandingPage() { + const releaseNotes = require("../release-notes.json"); + const content: string = this.LandingPageView.replace( + new RegExp("@PWD", "g"), + vscode.Uri.file(this.context.extensionPath) + .with({ + scheme: "vscode-resource" + }) + .toString() + ).replace("@RELEASE_NOTES", JSON.stringify(releaseNotes)); + const landingPanel = vscode.window.createWebviewPanel( + "landingPage", + "Welcome to Settings Sync", + vscode.ViewColumn.One, + { + retainContextWhenHidden: true, + enableScripts: true + } + ); + landingPanel.webview.html = content; + landingPanel.webview.onDidReceiveMessage(async message => { + switch (message.command) { + case "loginWithGitHub": + new GitHubOAuthService( + 54321, + this, + this.context.extensionPath + ).StartProcess(); + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://github.com/login/oauth/authorize?scope=gist%20read:user&client_id=cfd96460d8b110e2351b&redirect_uri=http://localhost:54321/callback" + ) + ); + break; + case "editConfiguration": + this.OpenSettingsPage(); + break; + } + }); + } + + public async InitializeAutoUpload() { + const ignored = await AutoUploadService.GetIgnoredItems( + await this.GetCustomSettings() + ); + this.autoUploadService = new AutoUploadService({ + en: this.en, + commons: this, + ignored + }); + } + + public async InitalizeSettings(): Promise { + const settings: LocalConfig = new LocalConfig(); + const extSettings: ExtensionConfig = this.GetSettings(); + const cusSettings: CustomSettings = await this.GetCustomSettings(); + + if (cusSettings.token === "" || extSettings.gist === "") { + this.OpenLandingPage(); + } + + settings.customConfig = cusSettings; + settings.extConfig = extSettings; + return settings; + } + + public async GetCustomSettings(): Promise { + let customSettings: CustomSettings = new CustomSettings(); + try { + const customExist: boolean = await FileService.FileExists( + this.en.FILE_CUSTOMIZEDSETTINGS + ); + if (customExist) { + const customSettingStr: string = await FileService.ReadFile( + this.en.FILE_CUSTOMIZEDSETTINGS + ); + const tempObj = JSON.parse(customSettingStr); + + Object.assign(customSettings, tempObj); + customSettings.token = customSettings.token.trim(); + return customSettings; + } + } catch (e) { + Commons.LogException( + e, + "Sync : Unable to read " + + this.en.FILE_CUSTOMIZEDSETTINGS_NAME + + ". Make sure its Valid JSON.", + true + ); + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "http://shanalikhan.github.io/2017/02/19/Option-to-ignore-settings-folders-code-settings-sync.html" + ) + ); + customSettings = null; + return customSettings; + } + } + + public async SetCustomSettings(setting: CustomSettings): Promise { + try { + await FileService.WriteFile( + this.en.FILE_CUSTOMIZEDSETTINGS, + JSON.stringify(setting, null, 4) + ); + return true; + } catch (e) { + Commons.LogException( + e, + "Sync : Unable to write " + this.en.FILE_CUSTOMIZEDSETTINGS_NAME, + true + ); + return false; + } + } + + public async StartMigrationProcess(): Promise { + const fileExist: boolean = await FileService.FileExists( + this.en.FILE_CUSTOMIZEDSETTINGS + ); + let customSettings: CustomSettings = null; + const firstTime: boolean = !fileExist; + let fileChanged: boolean = firstTime; + + if (fileExist) { + customSettings = await this.GetCustomSettings(); + } else { + customSettings = new CustomSettings(); + } + // vscode.workspace.getConfiguration().update("sync.version", undefined, true); + + if (firstTime) { + const openExtensionPage = localize("common.action.openExtPage"); + vscode.window.showInformationMessage(localize("common.info.installed")); + vscode.window + .showInformationMessage( + localize("common.info.needHelp"), + openExtensionPage + ) + .then((val: string) => { + if (val === openExtensionPage) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync" + ) + ); + } + }); + } else if (customSettings.version < Environment.CURRENT_VERSION) { + fileChanged = true; + // #TODO : Remove this in new update + const newIgnoredList = new CustomSettings().ignoreUploadFiles; + newIgnoredList.forEach(m => { + if (customSettings.ignoreUploadFiles.indexOf(m) === -1) { + customSettings.ignoreUploadFiles.push(m); + } + }); + + if (this.context.globalState.get("synctoken")) { + const token = this.context.globalState.get("synctoken"); + if (token !== "") { + customSettings.token = String(token); + this.context.globalState.update("synctoken", ""); + vscode.window.showInformationMessage( + localize("common.info.setToken") + ); + } + } + + const releaseNotes = localize("common.action.releaseNotes"); + const writeReview = localize("common.action.writeReview"); + const support = localize("common.action.support"); + const joinCommunity = localize("common.action.joinCommunity"); + if (!customSettings.disableUpdateMessage) { + vscode.window + .showInformationMessage( + localize("common.info.updateTo", Environment.getVersion()), + releaseNotes, + writeReview, + support, + joinCommunity + ) + .then((val: string) => { + if (val === releaseNotes) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "http://shanalikhan.github.io/2016/05/14/Visual-studio-code-sync-settings-release-notes.html" + ) + ); + } + if (val === writeReview) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync#review-details" + ) + ); + } + if (val === support) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=4W3EWHHBSYMM8&lc=IE&item_name=Code%20Settings%20Sync&item_number=visual%20studio%20code%20settings%20sync¤cy_code=USD&bn=PP-DonationsBF:btn_donate_SM.gif:NonHosted" + ) + ); + } + if (val === joinCommunity) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://join.slack.com/t/codesettingssync/shared_invite/enQtMzE3MjY5NTczNDMwLTYwMTIwNGExOGE2MTJkZWU0OTU5MmI3ZTc4N2JkZjhjMzY1OTk5OGExZjkwMDMzMDU4ZTBlYjk5MGQwZmMyNzk" + ) + ); + } + }); + } + } + + if (fileChanged) { + customSettings.version = Environment.CURRENT_VERSION; + await this.SetCustomSettings(customSettings); + } + return true; + } + + public async SaveSettings(setting: ExtensionConfig): Promise { + const config = vscode.workspace.getConfiguration("sync"); + const allKeysUpdated = new Array>(); + + const keys = Object.keys(setting); + keys.forEach(async keyName => { + if (setting[keyName] == null) { + setting[keyName] = ""; + } + if (keyName.toLowerCase() !== "token") { + if (config.get(keyName) !== setting[keyName]) { + allKeysUpdated.push(config.update(keyName, setting[keyName], true)); + } + } + }); + + try { + await Promise.all(allKeysUpdated); + if (this.context.globalState.get("syncCounter")) { + const counter = this.context.globalState.get("syncCounter"); + let count: number = parseInt(counter + "", 10); + if (count % 450 === 0) { + this.DonateMessage(); + } + count = count + 1; + this.context.globalState.update("syncCounter", count); + } else { + this.context.globalState.update("syncCounter", 1); + } + return true; + } catch (err) { + Commons.LogException(err, this.ERROR_MESSAGE, true); + return false; + } + } + + public async DonateMessage(): Promise { + const donateNow = localize("common.action.donate"); + const writeReview = localize("common.action.writeReview"); + const res = await vscode.window.showInformationMessage( + localize("common.info.donate"), + donateNow, + writeReview + ); + + if (res === donateNow) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=4W3EWHHBSYMM8&lc=IE&item_name=Code%20Settings%20Sync&item_number=visual%20studio%20code%20settings%20sync¤cy_code=USD&bn=PP-DonationsBF:btn_donate_SM.gif:NonHosted" + ) + ); + } else if (res === writeReview) { + vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse( + "https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync#review-details" + ) + ); + } + } + + public GetSettings(): ExtensionConfig { + const settings = new ExtensionConfig(); + + for (const key of Object.keys(settings)) { + if (key !== "token") { + settings[key] = vscode.workspace.getConfiguration("sync").get(key); + } + } + + settings.gist = settings.gist.trim(); + return settings; + } + + /** + * IgnoreSettings + */ + public async GetIgnoredSettings(settings: string[]): Promise { + const ignoreSettings: object = {}; + const config = vscode.workspace.getConfiguration(); + const keysUpdated: Array> = []; + + for (const key of settings) { + let keyValue: object = null; + keyValue = config.get(key, null); + if (keyValue !== null) { + ignoreSettings[key] = keyValue; + keysUpdated.push(config.update(key, undefined, true)); + } + } + + await Promise.all(keysUpdated); + + return ignoreSettings; + } + + /** + * RestoreIgnoredSettings + */ + public SetIgnoredSettings(ignoredSettings: object): void { + const config = vscode.workspace.getConfiguration(); + const keysUpdated: Array> = []; + for (const key of Object.keys(ignoredSettings)) { + keysUpdated.push(config.update(key, ignoredSettings[key], true)); + } + } + + /** + * AskGistName + */ + public async AskGistName(): Promise { + return vscode.window.showInputBox({ + prompt: localize("common.prompt.multipleGist"), + ignoreFocusOut: true, + placeHolder: localize("common.placeholder.multipleGist") + }); + } + + public ShowSummaryOutput( + upload: boolean, + files: File[], + removedExtensions: ExtensionInformation[], + addedExtensions: ExtensionInformation[], + ignoredExtensions: ExtensionInformation[], + syncSettings: LocalConfig + ) { + if (Commons.outputChannel === null) { + Commons.outputChannel = vscode.window.createOutputChannel( + "Code Settings Sync" + ); + } + + const outputChannel = Commons.outputChannel; + outputChannel.appendLine( + `CODE SETTINGS SYNC ${upload ? "UPLOAD" : "DOWNLOAD"} SUMMARY` + ); + outputChannel.appendLine(`Version: ${Environment.getVersion()}`); + outputChannel.appendLine(`--------------------`); + outputChannel.appendLine( + `GitHub Token: ${syncSettings.customConfig.token || "Anonymous"}` + ); + outputChannel.appendLine(`GitHub Gist: ${syncSettings.extConfig.gist}`); + outputChannel.appendLine( + `GitHub Gist Type: ${syncSettings.publicGist ? "Public" : "Secret"}` + ); + outputChannel.appendLine(``); + if (!syncSettings.customConfig.token) { + outputChannel.appendLine( + `Anonymous Gist cannot be edited, the extension will always create a new one during upload.` + ); + } + outputChannel.appendLine( + `Restarting Visual Studio Code may be required to apply color and file icon theme.` + ); + outputChannel.appendLine(`--------------------`); + + outputChannel.appendLine(`Files ${upload ? "Upload" : "Download"}ed:`); + files + .filter(item => item.fileName.indexOf(".") > 0) + .forEach(item => { + outputChannel.appendLine(` ${item.fileName} > ${item.gistName}`); + }); + + outputChannel.appendLine(``); + outputChannel.appendLine(`Extensions Ignored:`); + + if (!ignoredExtensions || ignoredExtensions.length === 0) { + outputChannel.appendLine(` No extensions ignored.`); + } else { + ignoredExtensions.forEach(extn => { + outputChannel.appendLine(` ${extn.name} v${extn.version}`); + }); + } + + outputChannel.appendLine(``); + outputChannel.appendLine(`Extensions Removed:`); + + if (!syncSettings.extConfig.removeExtensions) { + outputChannel.appendLine(` Feature Disabled.`); + } else { + if (!removedExtensions || removedExtensions.length === 0) { + outputChannel.appendLine(` No extensions removed.`); + } else { + removedExtensions.forEach(extn => { + outputChannel.appendLine(` ${extn.name} v${extn.version}`); + }); + } + } + + if (addedExtensions) { + outputChannel.appendLine(``); + outputChannel.appendLine(`Extensions Added:`); + + if (addedExtensions.length === 0) { + outputChannel.appendLine(` No extensions installed.`); + } + + addedExtensions.forEach(extn => { + outputChannel.appendLine(` ${extn.name} v${extn.version}`); + }); + } + + outputChannel.appendLine(`--------------------`); + outputChannel.append(`Done.`); + outputChannel.show(true); + } +} diff --git a/src/service/oauthService.ts b/src/service/oauthService.ts new file mode 100644 index 00000000..2accc37d --- /dev/null +++ b/src/service/oauthService.ts @@ -0,0 +1,114 @@ +import * as express from "express"; +import { readFileSync } from "fs"; +import { Server } from "http"; +import fetch from "node-fetch"; +import { URLSearchParams } from "url"; +import * as vscode from "vscode"; +import Commons from "../commons"; + +export class GitHubOAuthService { + public app: express.Express; + public server: Server; + public GistSelectionView: string; + + constructor( + public port: number, + public commons: Commons, + public extensionPath: string + ) { + this.app = express(); + this.app.use(express.json(), express.urlencoded({ extended: false })); + this.GistSelectionView = readFileSync( + `${this.extensionPath}/ui/gist-selection/gist-selection.html`, + { encoding: "utf8" } + ); + } + + public async StartProcess() { + this.server = this.app.listen(this.port); + this.app.get("/callback", async (req, res) => { + const params = new URLSearchParams( + await (await this.getToken(req.param("code"))).text() + ); + + res.send(""); + this.server.close(); + + const token = params.get("access_token"); + this.saveToken(token); + + const user = await this.getUser(token); + + const gists = await this.getGists(token, user); + this.saveGistId(gists); + }); + } + + public async getToken(code: string) { + const params = new URLSearchParams(); + params.append("client_id", "cfd96460d8b110e2351b"); + params.append("client_secret", "ed46bd3a0f736e0da57308e86ca5fa3cf8688582"); + params.append("code", code); + + return await fetch("https://github.com/login/oauth/access_token", { + method: "POST", + body: params + }); + } + + public async getGists(token: string, user: string) { + const res = await fetch(`https://api.github.com/users/${user}/gists`, { + method: "GET", + headers: { Authorization: `token ${token}` } + }); + const gists = await res.json(); + return gists; + } + + public async saveToken(token: string) { + const currentSettings = await this.commons.GetCustomSettings(); + currentSettings.token = token; + this.commons.SetCustomSettings(currentSettings); + } + + public async saveGistId(gists: any) { + const content: string = this.GistSelectionView.replace( + new RegExp("@GISTS", "g"), + JSON.stringify(gists) + ).replace( + new RegExp("@PWD", "g"), + vscode.Uri.file(this.extensionPath) + .with({ + scheme: "vscode-resource" + }) + .toString() + ); + const gistSelectionPanel = vscode.window.createWebviewPanel( + "selectGist", + "Select Your Existing Gist", + vscode.ViewColumn.One, + { + retainContextWhenHidden: true, + enableScripts: true + } + ); + gistSelectionPanel.webview.html = content; + gistSelectionPanel.webview.onDidReceiveMessage(async message => { + if (!message.close) { + const extSettings = await this.commons.GetSettings(); + extSettings.gist = message.id; + this.commons.SaveSettings(extSettings); + } + return gistSelectionPanel.dispose(); + }); + } + + public async getUser(token: string) { + const res = await fetch("https://api.github.com/user", { + method: "GET", + headers: { Authorization: `token ${token}` } + }); + const json = await res.json(); + return json.login; + } +} diff --git a/src/sync.ts b/src/sync.ts index ce212f3e..40616e30 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -49,6 +49,11 @@ export class Sync { const gistAvailable: boolean = startUpSetting.gist != null && startUpSetting.gist !== ""; + if (!tokenAvailable) { + globalCommonService.OpenLandingPage(); + return; + } + if (gistAvailable) { if (startUpSetting.autoDownload) { vscode.commands @@ -86,7 +91,7 @@ export class Sync { globalCommonService.autoUploadService.StopWatching(); try { - localConfig = await globalCommonService.InitalizeSettings(true, false); + localConfig = await globalCommonService.InitalizeSettings(); localConfig.publicGist = false; if (args.length > 0) { if (args[0] === "publicGIST") { @@ -383,7 +388,7 @@ export class Sync { globalCommonService.autoUploadService.StopWatching(); try { - localSettings = await globalCommonService.InitalizeSettings(true, true); + localSettings = await globalCommonService.InitalizeSettings(); await StartDownload(localSettings.extConfig, localSettings.customConfig); } catch (err) { Commons.LogException(err, globalCommonService.ERROR_MESSAGE, true); @@ -754,6 +759,7 @@ export class Sync { const gistAvailable: boolean = setting.gist != null && setting.gist !== ""; const items: string[] = [ + "cmd.otherOptions.openSettings", "cmd.otherOptions.editLocalSetting", "cmd.otherOptions.shareSetting", "cmd.otherOptions.downloadSetting", @@ -782,6 +788,9 @@ export class Sync { const handlerMap = { 0: async () => { + common.OpenSettingsPage(); + }, + 1: async () => { const file: vscode.Uri = vscode.Uri.file(env.FILE_CUSTOMIZEDSETTINGS); fs.openSync(file.fsPath, "r"); const document = await vscode.workspace.openTextDocument(file); @@ -791,7 +800,7 @@ export class Sync { true ); }, - 1: async () => { + 2: async () => { // share public gist const answer = await vscode.window.showInformationMessage( localize("cmd.otherOptions.shareSetting.beforeConfirm"), @@ -807,26 +816,26 @@ export class Sync { await common.SetCustomSettings(customSettings); } }, - 2: async () => { + 3: async () => { // Download Settings from Public GIST selectedItem = 2; customSettings.downloadPublicGist = true; settingChanged = true; await common.SetCustomSettings(customSettings); }, - 3: async () => { + 4: async () => { // toggle force download selectedItem = 3; settingChanged = true; setting.forceDownload = !setting.forceDownload; }, - 4: async () => { + 5: async () => { // toggle auto upload selectedItem = 4; settingChanged = true; setting.autoUpload = !setting.autoUpload; }, - 5: async () => { + 6: async () => { // auto download on startup selectedItem = 5; settingChanged = true; @@ -841,7 +850,7 @@ export class Sync { setting.autoDownload = !setting.autoDownload; }, - 6: async () => { + 7: async () => { // page summary toggle selectedItem = 6; settingChanged = true; @@ -852,7 +861,7 @@ export class Sync { } setting.quietSync = !setting.quietSync; }, - 7: async () => { + 8: async () => { // add customized sync file const options: vscode.InputBoxOptions = { ignoreFocusOut: true, @@ -875,7 +884,7 @@ export class Sync { } } }, - 8: async () => { + 9: async () => { // Import customized sync file to workspace const customFiles = await this.getCustomFilesFromGist( customSettings, @@ -919,7 +928,7 @@ export class Sync { } } }, - 9: async () => { + 10: async () => { vscode.commands.executeCommand( "vscode.open", vscode.Uri.parse( @@ -927,7 +936,7 @@ export class Sync { ) ); }, - 10: async () => { + 11: async () => { vscode.commands.executeCommand( "vscode.open", vscode.Uri.parse( @@ -935,7 +944,7 @@ export class Sync { ) ); }, - 11: async () => { + 12: async () => { vscode.commands.executeCommand( "vscode.open", vscode.Uri.parse( diff --git a/ui/gist-selection/gist-selection.html b/ui/gist-selection/gist-selection.html new file mode 100644 index 00000000..c60e154e --- /dev/null +++ b/ui/gist-selection/gist-selection.html @@ -0,0 +1,69 @@ + + + + + + + + + + + +
+
+
+ +

+ Select Your Existing Gist +

+
+ Create a new Gist +
+
+
+ + + + + + + + diff --git a/ui/gist-selection/gist-selection.js b/ui/gist-selection/gist-selection.js new file mode 100644 index 00000000..31119f55 --- /dev/null +++ b/ui/gist-selection/gist-selection.js @@ -0,0 +1,60 @@ +// @ts-nocheck + +const vscode = acquireVsCodeApi(); + +/* https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site */ +function timeSince(date) { + var seconds = Math.floor((new Date() - date) / 1000); + + var interval = Math.floor(seconds / 31536000); + + if (interval > 1) { + return interval + " years"; + } + interval = Math.floor(seconds / 2592000); + if (interval > 1) { + return interval + " months"; + } + interval = Math.floor(seconds / 86400); + if (interval > 1) { + return interval + " days"; + } + interval = Math.floor(seconds / 3600); + if (interval > 1) { + return interval + " hours"; + } + interval = Math.floor(seconds / 60); + if (interval > 1) { + return interval + " minutes"; + } + return Math.floor(seconds) + " seconds"; +} + +function appendHTML(parent, html) { + var div = document.createElement("div"); + div.innerHTML = html; + while (div.children.length > 0) { + parent.appendChild(div.children[0]); + } + div.remove(); +} + +function saveGistId(id) { + vscode.postMessage({ id }); +} + +const selectionContainer = document.getElementById("selectionContainer"); + +const selectionTemplate = ` +`; + +gists.forEach(gist => { + const html = selectionTemplate + .replace(new RegExp("@description", "g"), gist.description) + .replace(new RegExp("@id", "g"), gist.id) + .replace( + new RegExp("@timestamp", "g"), + timeSince(new Date(gist.updated_at)) + ); + appendHTML(selectionContainer, html); +}); diff --git a/ui/landing-page/landing-page.html b/ui/landing-page/landing-page.html new file mode 100644 index 00000000..5e06883d --- /dev/null +++ b/ui/landing-page/landing-page.html @@ -0,0 +1,160 @@ + + + + + + + + + + + +
+
+
+ +
+
+
+

+ What's New in + vX.X.X +

+
+
+
+
+

+ Configuration +

+

+ Login via GitHub to setup Settings Sync, or configure the + settings manually. +

+ Login with GitHub + Edit Configuration +
+
+

+ Show Your Support +

+

+ While being free and open source, if you find Settings Sync + useful, please consider supporting it by donating via PayPal + or Open Collective. +

+ Become a Sponsor + Donate via PayPal + +
+
+
+
+
+
+

+ Need Help? +

+ Homepage
+ Questions & Issues
+
+
+
+
+

+ Sponsors +

+

+ Settings Sync is looking for sponsors to display here. Contact + me on Twitter or + GitHub +

+
+
+
+
+
+
+ + + + + + + + diff --git a/ui/landing-page/landing-page.js b/ui/landing-page/landing-page.js new file mode 100644 index 00000000..da19a15d --- /dev/null +++ b/ui/landing-page/landing-page.js @@ -0,0 +1,31 @@ +// @ts-nocheck +const vscode = acquireVsCodeApi(); + +function sendCommand(args) { + vscode.postMessage({ + command: args + }); +} + +function appendHTML(parent, html) { + var div = document.createElement("div"); + div.innerHTML = html; + while (div.children.length > 0) { + parent.appendChild(div.children[0]); + } + div.remove(); +} + +const releaseNoteTemplate = `
@TYPE@NOTE
`; + +const notesElement = document.getElementById("notes"); +releaseNotes.changes.forEach(change => { + const html = releaseNoteTemplate + .replace(new RegExp("@NOTE", "g"), change.details) + .replace(new RegExp("@TYPE", "g"), change.type) + .replace(new RegExp("@COLOR", "g"), change.color); + appendHTML(notesElement, html); +}); + +const currentVersionElement = document.getElementById("current-version"); +currentVersionElement.innerHTML = releaseNotes.currentVersion; diff --git a/ui/settings/settings.html b/ui/settings/settings.html new file mode 100644 index 00000000..8fb54141 --- /dev/null +++ b/ui/settings/settings.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + +
+
+
+ +
+
+

+ Environment Settings +

+
+

+
+
+

+ Global Settings +

+
+
+
+
+
+
+ + + + + + + + + diff --git a/ui/settings/settings.js b/ui/settings/settings.js new file mode 100644 index 00000000..f08e5c37 --- /dev/null +++ b/ui/settings/settings.js @@ -0,0 +1,187 @@ +// @ts-nocheck + +function appendHTML(parent, html) { + var div = document.createElement("div"); + div.innerHTML = html; + while (div.children.length > 0) { + parent.appendChild(div.children[0]); + } + div.remove(); +} + +const vscode = acquireVsCodeApi(); + +const textInputTemplate = `
+ + @tooltip + +
`; + +const checkboxTemplate = `
+ + + @tooltip +
`; + +const textareaTemplate = `
+ + @tooltip + +
`; + +const globalParent = document.getElementById("globalSettings"); +const envParent = document.getElementById("environmentSettings"); +const saveStatus = document.getElementById("saveStatus"); + +globalMap.forEach(settingMap => { + let template; + switch (settingMap.type) { + case 0: + template = textInputTemplate; + break; + case 1: + template = checkboxTemplate; + break; + case 2: + template = textareaTemplate; + break; + } + const html = template + .replace(new RegExp("@name", "g"), settingMap.name) + .replace(new RegExp("@placeholder", "g"), settingMap.placeholder) + .replace( + new RegExp("@correspondingSetting", "g"), + settingMap.correspondingSetting + ) + .replace(new RegExp("@tooltip"), "") + .replace(new RegExp("@settingType", "g"), "global"); + appendHTML(globalParent, html); +}); + +envMap.forEach(envMap => { + let template; + switch (envMap.type) { + case 0: + template = textInputTemplate; + break; + case 1: + template = checkboxTemplate; + break; + } + const html = template + .replace(new RegExp("@name", "g"), envMap.name) + .replace(new RegExp("@placeholder", "g"), envMap.placeholder) + .replace( + new RegExp("@correspondingSetting", "g"), + envMap.correspondingSetting + ) + .replace( + new RegExp("@tooltip"), + `` + ) + .replace(new RegExp("@settingType", "g"), "env"); + appendHTML(envParent, html); +}); + +$(document).ready(function() { + save(); + $('[data-toggle="tooltip"]').tooltip(); + $(".text") + .each((i, el) => { + if ($(el).attr("settingType") === "global") { + $(el).val(_.get(globalData, $(el).attr("setting"))); + } else { + $(el).val(envData[$(el).attr("setting")]); + } + }) + .change(function() { + save(); + let val = $(this).val(); + vscode.postMessage({ + command: $(this).attr("setting"), + text: val, + type: $(this).attr("settingType") + }); + }); + $(".checkbox") + .each((i, el) => { + if ($(el).attr("settingType") === "global") { + $(el).prop("checked", _.get(globalData, $(el).attr("setting"))); + } else { + $(el).prop("checked", envData[$(el).attr("setting")]); + } + }) + .change(function() { + save(); + let val = $(this).is(":checked"); + vscode.postMessage({ + command: $(this).attr("setting"), + text: val, + type: $(this).attr("settingType") + }); + }); + $(".textarea") + .each((i, el) => { + let str = ""; + const items = _.get(globalData, $(el).attr("setting")); + items.forEach(item => (str += item + "\n")); + $(el).val(str.slice(0, -1)); + $(el).prop("rows", items.length); + }) + .change(function() { + save(); + let val = []; + $(this) + .val() + .split("\n") + .forEach(item => { + if (item !== "") { + val.push(item); + } + }); + vscode.postMessage({ + command: $(this).attr("setting"), + text: val, + type: "global" + }); + }); +}); + +function save() { + saveStatus.innerHTML = ``; + setTimeout( + () => + (saveStatus.innerHTML = ``), + 1000 + ); +} diff --git a/ui/shared/page-header.js b/ui/shared/page-header.js new file mode 100644 index 00000000..450189d3 --- /dev/null +++ b/ui/shared/page-header.js @@ -0,0 +1,19 @@ +document.querySelector( + "page-header" +).innerHTML = `

+ +Settings Sync +

+

+Synchronize Settings, Snippets, Themes, File Icons, Launch, +Keybindings, Workspaces and Extensions Across Multiple Machines +Using GitHub Gist +

+

+by @shanalikhan +

`; diff --git a/ui/shared/styles.css b/ui/shared/styles.css new file mode 100644 index 00000000..013c6495 --- /dev/null +++ b/ui/shared/styles.css @@ -0,0 +1,129 @@ +:root { + --btn: #0e629c; + --btn-hover: #15496e; + --btn-click: #0e3450; +} +#selectionContainer { + width: 60%; +} +.dock-bottom-left { + position: fixed; + bottom: 4rem; + left: 4rem; +} +html, +body { + background-color: rgb(10, 1, 15); + font-family: "Roboto", sans-serif !important; + color: white; + letter-spacing: 0.0625em; +} +.logo { + vertical-align: -4px; +} +.change { + line-height: 150%; +} +.badge-success { + color: #fff; + background-color: #73c991; +} +.badge-danger { + color: #fff; + background-color: #ce4f59; +} +.badge { + vertical-align: 2px; + width: 6rem; +} +.opacity-50 { + filter: opacity(0.5); + -webkit-filter: opacity(0.5); +} +.scrollable { + overflow-y: auto; + height: 35rem; +} +.masthead { + position: relative; + width: 100%; + height: auto; + min-height: 35rem; + padding: 15rem 0; + background-position: center; + background-repeat: no-repeat; + background-attachment: scroll; + background-size: cover; + margin-top: 40px; + margin-bottom: 40px; +} +.masthead h1 { + font-size: 2.5rem; + line-height: 2.5rem; + font-weight: bold; + font-family: "Open Sans"; +} +.masthead h2 { + /* max-width: 20rem; */ + font-size: 1rem; +} +@media (min-width: 768px) { + .masthead h1 { + font-size: 4rem; + line-height: 4rem; + } +} +@media (min-width: 992px) { + .masthead { + /* height: 100vh; */ + padding: 0; + } + .masthead h1 { + font-size: 6.5rem; + line-height: 6.5rem; + } + .masthead h2 { + /* max-width: 30rem; */ + font-size: 1.25rem; + } +} +.btn { + -webkit-box-shadow: 0 0.1875rem 0.1875rem 0 rgba(0, 0, 0, 0.1) !important; + box-shadow: 0 0.1875rem 0.1875rem 0 rgba(0, 0, 0, 0.1) !important; + padding: 1.25rem 2rem; + font-size: 80%; + text-transform: uppercase; + letter-spacing: 0.15rem; + border: 0; +} +.btn-primary { + background-color: var(--btn); +} +.btn-primary:hover { + background-color: var(--btn-hover); +} +.btn-primary:focus { + background-color: var(--btn-hover); + color: #fff; +} +.btn-primary:active { + background-color: var(--btn-click) !important; +} +.text, +.textarea { + -webkit-box-shadow: 0 0.1875rem 0.1875rem 0 rgba(0, 0, 0, 0.1) !important; + box-shadow: 0 0.1875rem 0.1875rem 0 rgba(0, 0, 0, 0.1) !important; + padding: 1.25rem 2rem; + height: auto; + border: 0; +} +a { + /* color: #64a19d; */ + color: var(--btn); +} +a:focus, +a:hover { + text-decoration: none; + /* color: #3c6360; */ + color: var(--btn-hover); +} diff --git a/webpack.config.js b/webpack.config.js index d0f199be..79ee272b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,7 @@ const path = require("path"); const CleanWebpackPlugin = require("clean-webpack-plugin").default; +const nodeExternals = require("webpack-node-externals"); const webpack = require("webpack"); /** @type WebpackOptions */ @@ -30,11 +31,14 @@ const config = { } ] }, - externals: { - vscode: "commonjs vscode", - fsevents: "commonjs fsevents", - "original-fs": "commonjs original-fs" - }, + externals: [ + { + vscode: "commonjs vscode", + fsevents: "commonjs fsevents", + "original-fs": "commonjs original-fs" + }, + nodeExternals() + ], plugins: [new CleanWebpackPlugin()], optimization: { minimize: true