diff --git a/README.md b/README.md index 97ea0bd..658ccd5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ No, I'm building a tool that does the bare minimum what I require and what I don find in other open source tools. That doesn't mean of course that contributions related to Photoshop-esque features aren't welcomed. +## Dropbox integration + +Requires you to [register a client id or access token](https://www.dropbox.com/developers/apps). + ## Project setup ``` npm install @@ -52,4 +56,3 @@ npm run lint * Implement clone brush * Implement document crop * Implement change history -* Dropbox integration? diff --git a/package-lock.json b/package-lock.json index 8c529a8..aa0347a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "dependencies": { "canvas": "^2.6.1", "core-js": "^3.6.5", + "dropbox": "^8.2.0", "register-service-worker": "^1.7.1", "semantic-ui-css": "^2.4.1", "vue": "^2.6.11", + "vue-slider-component": "^3.2.11", "vuex": "^3.4.0" }, "devDependencies": { @@ -6437,6 +6439,17 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "node_modules/dropbox": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-8.2.0.tgz", + "integrity": "sha512-lA/Bb7ZWPaeDK+S4AIeAAlPc49nwbrTlgMSLO5Nghg6FfhqhAKdICtpENviwb0YT1Zgn1NmYk1LCJfiyrJ7lVQ==", + "dependencies": { + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=0.10.3" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -11809,6 +11822,14 @@ "node": ">=0.8" } }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -16942,6 +16963,14 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz", "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" }, + "node_modules/vue-class-component": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz", + "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==", + "peerDependencies": { + "vue": "^2.0.0" + } + }, "node_modules/vue-eslint-parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.2.0.tgz", @@ -17022,12 +17051,32 @@ "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", "dev": true }, + "node_modules/vue-property-decorator": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz", + "integrity": "sha512-O6OUN2OMsYTGPvgFtXeBU3jPnX5ffQ9V4I1WfxFQ6dqz6cOUbR3Usou7kgFpfiXDvV7dJQSFcJ5yUPgOtPPm1Q==", + "dependencies": { + "vue-class-component": "^7.1.0" + }, + "peerDependencies": { + "vue": "*" + } + }, "node_modules/vue-search-select": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/vue-search-select/-/vue-search-select-2.9.3.tgz", "integrity": "sha512-JQBTSKKX3nAoeeEaSqg2iQDtR1YBs0qZ08os/QDOqFocGPSvAffNb4VajSnlFWJlKQDJcev3iQuBLcWM2pxr0Q==", "dev": true }, + "node_modules/vue-slider-component": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.11.tgz", + "integrity": "sha512-2YyJW6TFnYk5FUvqQLvZcCJ+hthBXB819qNHtwnEUyDbOcTXV0n3Ou1ZphOi5FX9phlQIiC2NvjLuRAVmNq+Zw==", + "dependencies": { + "core-js": "^3.6.5", + "vue-property-decorator": "^8.0.0" + } + }, "node_modules/vue-style-loader": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", @@ -24056,6 +24105,14 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "dropbox": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-8.2.0.tgz", + "integrity": "sha512-lA/Bb7ZWPaeDK+S4AIeAAlPc49nwbrTlgMSLO5Nghg6FfhqhAKdICtpENviwb0YT1Zgn1NmYk1LCJfiyrJ7lVQ==", + "requires": { + "node-fetch": "^2.6.1" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -28510,6 +28567,11 @@ } } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -32872,7 +32934,8 @@ "vue-class-component": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz", - "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==" + "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==", + "requires": {} }, "vue-eslint-parser": { "version": "7.2.0", diff --git a/package.json b/package.json index b663ba0..e637cd6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "canvas": "^2.6.1", "core-js": "^3.6.5", + "dropbox": "^8.2.0", "register-service-worker": "^1.7.1", "semantic-ui-css": "^2.4.1", "vue": "^2.6.11", diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..9712c65 --- /dev/null +++ b/public/login.html @@ -0,0 +1,27 @@ + + +

oAuth result for PhotoMound

+

+ You should return to PhotoMound shortly. If not, an authentication + error has occurred with the external API. +

+ + + diff --git a/src/components/dialog-window/dialog-window.vue b/src/components/dialog-window/dialog-window.vue index 3d193d4..2c1c300 100644 --- a/src/components/dialog-window/dialog-window.vue +++ b/src/components/dialog-window/dialog-window.vue @@ -72,11 +72,11 @@ export default { 'closeDialog', ]), handleConfirm() { - this?.confirmHandler(); + this.confirmHandler?.(); this.close(); }, handleCancel() { - this?.cancelHandler(); + this.cancelHandler?.(); this.close(); }, close() { diff --git a/src/components/dropbox-file-browser/dropbox-file-browser.vue b/src/components/dropbox-file-browser/dropbox-file-browser.vue new file mode 100644 index 0000000..03d7466 --- /dev/null +++ b/src/components/dropbox-file-browser/dropbox-file-browser.vue @@ -0,0 +1,165 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 2020 - https://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + + + + diff --git a/src/components/dropbox-file-browser/messages.json b/src/components/dropbox-file-browser/messages.json new file mode 100644 index 0000000..7af9504 --- /dev/null +++ b/src/components/dropbox-file-browser/messages.json @@ -0,0 +1,7 @@ +{ + "en-US": { + "files": "Files", + "loading": "Loading...", + "couldNotRetrieveFilesForPath": "Could not retrieve files for \"{path}\"" + } +} diff --git a/src/components/options-panel/components/dropbox-file-selector/dropbox-file-selector.vue b/src/components/options-panel/components/dropbox-file-selector/dropbox-file-selector.vue new file mode 100644 index 0000000..f173032 --- /dev/null +++ b/src/components/options-panel/components/dropbox-file-selector/dropbox-file-selector.vue @@ -0,0 +1,98 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 2020 - https://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + + diff --git a/src/components/options-panel/components/dropbox-file-selector/messages.json b/src/components/options-panel/components/dropbox-file-selector/messages.json new file mode 100644 index 0000000..1f03633 --- /dev/null +++ b/src/components/options-panel/components/dropbox-file-selector/messages.json @@ -0,0 +1,9 @@ +{ + "en-US": { + "connectingToDropbox": "Connecting to Dropbox", + "loginToDropbox": "Log into Dropbox", + "connectedToDropbox": "Connected to Dropbox", + "login": "Login", + "selectFiles": "Select files" + } +} diff --git a/src/components/options-panel/components/file-selector/file-selector.vue b/src/components/options-panel/components/file-selector/file-selector.vue index 17a9e46..2b72979 100644 --- a/src/components/options-panel/components/file-selector/file-selector.vue +++ b/src/components/options-panel/components/file-selector/file-selector.vue @@ -42,9 +42,9 @@ diff --git a/src/components/options-panel/options-panel.vue b/src/components/options-panel/options-panel.vue index a5bae7e..b24b0b2 100644 --- a/src/components/options-panel/options-panel.vue +++ b/src/components/options-panel/options-panel.vue @@ -33,6 +33,11 @@ class="content" > + + @@ -48,6 +53,9 @@ export default { components: { FileSelector, }, + data: () => ({ + dropbox: false, + }), computed: { ...mapState([ "optionsPanelOpened", @@ -63,6 +71,14 @@ export default { this.setOptionsPanelOpened( !value ); } }, + importType() { + switch ( this.dropbox ) { + default: + return null; + case true: + return () => import( "./components/dropbox-file-selector/dropbox-file-selector" ); + } + }, activeToolOptions() { switch ( this.activeTool ) { default: diff --git a/src/definitions/modal-windows.js b/src/definitions/modal-windows.js index 03de793..9394990 100644 --- a/src/definitions/modal-windows.js +++ b/src/definitions/modal-windows.js @@ -20,4 +20,5 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export const RESIZE_DOCUMENT = 1; +export const RESIZE_DOCUMENT = 1; +export const DROPBOX_FILE_BROWSER = 2; diff --git a/src/mixins/image-to-document-manager.js b/src/mixins/image-to-document-manager.js new file mode 100644 index 0000000..3d4fe91 --- /dev/null +++ b/src/mixins/image-to-document-manager.js @@ -0,0 +1,70 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 2020 - https://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import{ mapGetters, mapMutations, mapActions } from "vuex"; + +export default { + computed: { + ...mapGetters([ + "activeDocument", + "layers", + ]) + }, + methods: { + ...mapMutations([ + "setActiveDocumentSize", + "addNewDocument", + "addLayer", + "addGraphicToLayer", + ]), + ...mapActions([ + "addImage", + ]), + async addLoadedFile( file, { image, size }) { + const { source } = await this.addImage({ file, image, size }); + + image.src = source; + + const currentDocumentIsEmpty = this.layers?.length === 1 && !this.layers[ 0 ].graphics.length; + + switch ( this.fileTarget) { + default: + case "layer": + // if this is the first content of an existing document, scale document to image size + if ( currentDocumentIsEmpty ) { + this.setActiveDocumentSize( size ); + } else if ( !this.activeDocument ) { + this.addNewDocument( file.name ); + } + this.addLayer(); + break; + case "document": + if ( !currentDocumentIsEmpty ) { + this.addNewDocument( file.name ); + } + this.setActiveDocumentSize( size ); + break; + } + this.addGraphicToLayer({ index: this.layers.length - 1, bitmap: image, size }); + }, + } +}; diff --git a/src/photomound.vue b/src/photomound.vue index b34259e..0800ad9 100644 --- a/src/photomound.vue +++ b/src/photomound.vue @@ -68,10 +68,12 @@ import DocumentCanvas from "@/components/document-canvas/document-canvas"; import OptionsPanel from "@/components/options-panel/options-panel"; import Toolbox from "@/components/toolbox/toolbox"; import DialogWindow from "@/components/dialog-window/dialog-window"; -import { RESIZE_DOCUMENT } from "@/definitions/modal-windows"; import { isMobile } from "@/utils/environment-util"; import store from "./store"; import messages from "./messages.json"; +import { + RESIZE_DOCUMENT, DROPBOX_FILE_BROWSER +} from "@/definitions/modal-windows"; Vue.use( Vuex ); Vue.use( VueI18n ); @@ -109,6 +111,8 @@ export default { return null; case RESIZE_DOCUMENT: return () => import( "@/components/edit-menu/resize-document/resize-document" ); + case DROPBOX_FILE_BROWSER: + return () => import( "@/components/dropbox-file-browser/dropbox-file-browser" ); } }, }, diff --git a/src/services/dropbox-service.js b/src/services/dropbox-service.js new file mode 100644 index 0000000..29a3fc7 --- /dev/null +++ b/src/services/dropbox-service.js @@ -0,0 +1,69 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 2020 - https://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { Dropbox } from "dropbox"; + +let accessToken; +let dbx; + +/** + * Authentication step 1: for interacting with Dropbox : request access token + * by opening an authentication page + */ +export const requestLogin = ( clientId, loginUrl ) => { + dbx = new Dropbox({ clientId }); + return dbx.auth.getAuthenticationUrl( loginUrl ); +} + +/** + * Authentication step 2: user has received access token, register it in the + * service and in Session storage so we can instantly authenticate on reload + */ +export const registerAccessToken = token => { + accessToken = token; + sessionStorage?.setItem( "dropboxToken", token ); + dbx = new Dropbox({ accessToken }); +}; + +export const isAuthenticated = async () => { + dbx = new Dropbox({ accessToken: accessToken ?? sessionStorage?.getItem( "dropboxToken" ) }); + try { + // this is a bit daft but does the trick, can we use a different method though? + await listFolder(); + return true; + } catch ( error ) { + return false; + } +} + +export const listFolder = ( path = "" ) => { + return dbx.filesListFolder({ path }); +} + +export const downloadFileAsBlob = async path => { + try { + const { result } = await dbx.filesDownload({ path }); + return URL.createObjectURL( result.fileBlob ); + } catch { + return null; + } +};