From caf3eccf71842dbad641fe44f998a4ddfe2a645a Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:53:49 +0100 Subject: [PATCH] 2946 feature request integrate file selector with google drive and onedrive (#3253) # Description of Changes Please provide a summary of the changes, including: - Why the change was made - Any challenges encountered - Added google drive integration config to premium settings in setting.yml - Added google drive button to file picker when enabled - Picker appears and allows users to load pdfs and other files into the tools Closes #(2946) --- ### Documentation [Docs Update PR](https://github.com/Stirling-Tools/Stirling-Tools.github.io/pull/67) --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- .../software/SPDF/EE/EEAppConfig.java | 12 ++ .../SPDF/model/ApplicationProperties.java | 21 +++ src/main/resources/settings.yml.template | 5 + src/main/resources/static/css/fileSelect.css | 25 +++ .../resources/static/images/google-drive.svg | 1 + src/main/resources/static/js/fileInput.js | 17 ++ .../resources/static/js/googleFilePicker.js | 158 ++++++++++++++++++ .../resources/templates/fragments/common.html | 29 +++- 8 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/static/images/google-drive.svg create mode 100644 src/main/resources/static/js/googleFilePicker.js diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index f1df7d340..a83b17090 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -11,6 +11,7 @@ import stirling.software.SPDF.EE.KeygenLicenseVerifier.License; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties.EnterpriseEdition; import stirling.software.SPDF.model.ApplicationProperties.Premium; +import stirling.software.SPDF.model.ApplicationProperties.Premium.ProFeatures.GoogleDrive; @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) @@ -43,6 +44,17 @@ public class EEAppConfig { return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin(); } + @Bean(name = "GoogleDriveEnabled") + public boolean googleDriveEnabled() { + return runningProOrHigher() + && applicationProperties.getPremium().getProFeatures().getGoogleDrive().isEnabled(); + } + + @Bean(name = "GoogleDriveConfig") + public GoogleDrive googleDriveConfig() { + return applicationProperties.getPremium().getProFeatures().getGoogleDrive(); + } + // TODO: Remove post migration public void migrateEnterpriseSettingsToPremium(ApplicationProperties applicationProperties) { EnterpriseEdition enterpriseEdition = applicationProperties.getEnterpriseEdition(); diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 6956a28fc..559c3b0c0 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -430,6 +430,7 @@ public class ApplicationProperties { public static class ProFeatures { private boolean ssoAutoLogin; private CustomMetadata customMetadata = new CustomMetadata(); + private GoogleDrive googleDrive = new GoogleDrive(); @Data public static class CustomMetadata { @@ -448,6 +449,26 @@ public class ApplicationProperties { : producer; } } + + @Data + public static class GoogleDrive { + private boolean enabled; + private String clientId; + private String apiKey; + private String appId; + + public String getClientId() { + return clientId == null || clientId.trim().isEmpty() ? "" : clientId; + } + + public String getApiKey() { + return apiKey == null || apiKey.trim().isEmpty() ? "" : apiKey; + } + + public String getAppId() { + return appId == null || appId.trim().isEmpty() ? "" : appId; + } + } } @Data diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index b7306861c..9066b29b2 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -72,6 +72,11 @@ premium: author: username creator: Stirling-PDF producer: Stirling-PDF + googleDrive: + enabled: false + clientId: '' + apiKey: '' + appId: '' legal: termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder diff --git a/src/main/resources/static/css/fileSelect.css b/src/main/resources/static/css/fileSelect.css index afb0b075f..88bef91d5 100644 --- a/src/main/resources/static/css/fileSelect.css +++ b/src/main/resources/static/css/fileSelect.css @@ -271,3 +271,28 @@ align-items: center; z-index: 9999; } + +.google-drive-button { + width: 2.5rem; + pointer-events: auto; + cursor: pointer; + transition-duration: 0.4s; + border-radius: 0.5rem; + box-shadow: 0 0 5px var(--md-sys-color-on-surface-variant); + background-color: var(--md-sys-color-on-surface-container-high) +} + +.horizontal-divider { + width: 85%; + border-top: 1px dashed; + padding: 0px; + margin: 10px; +} + +.google-drive-button img { + width:100% +} + +.google-drive-button:hover { + background-color: var(--md-sys-color-on-surface-variant) +} diff --git a/src/main/resources/static/images/google-drive.svg b/src/main/resources/static/images/google-drive.svg new file mode 100644 index 000000000..03b2f2129 --- /dev/null +++ b/src/main/resources/static/images/google-drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js index 28331ef01..32922390b 100644 --- a/src/main/resources/static/js/fileInput.js +++ b/src/main/resources/static/js/fileInput.js @@ -35,6 +35,7 @@ function setupFileInput(chooser) { const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt'); const inputContainerId = chooser.getAttribute('data-bs-element-container-id'); const showUploads = chooser.getAttribute('data-bs-show-uploads') === "true"; + const name = chooser.getAttribute('data-bs-unique-id') const noFileSelectedPrompt = chooser.getAttribute('data-bs-no-file-selected'); let inputContainer = document.getElementById(inputContainerId); @@ -87,6 +88,21 @@ function setupFileInput(chooser) { overlay = false; } + const googleDriveFileListener = function (e) { + const googleDriveFiles = e.detail; + + const fileInput = document.getElementById(elementId); + if (fileInput?.hasAttribute('multiple')) { + pushFileListTo(googleDriveFiles, allFiles); + } else if (fileInput) { + allFiles = [googleDriveFiles[0]]; + } + + const dataTransfer = new DataTransfer(); + allFiles.forEach((file) => dataTransfer.items.add(file)); + fileInput.files = dataTransfer.files; + fileInput.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { source: 'drag-drop' } })); + } const dropListener = function (e) { e.preventDefault(); @@ -137,6 +153,7 @@ function setupFileInput(chooser) { document.body.addEventListener('dragenter', dragenterListener); document.body.addEventListener('dragleave', dragleaveListener); document.body.addEventListener('drop', dropListener); + document.body.addEventListener(name + 'GoogleDriveDrivePicked', googleDriveFileListener); $('#' + elementId).on('change', async function (e) { let element = e.target; diff --git a/src/main/resources/static/js/googleFilePicker.js b/src/main/resources/static/js/googleFilePicker.js new file mode 100644 index 000000000..205167885 --- /dev/null +++ b/src/main/resources/static/js/googleFilePicker.js @@ -0,0 +1,158 @@ +const SCOPES = "https://www.googleapis.com/auth/drive.readonly"; +const SESSION_STORAGE_ID = "googleDrivePickerAccessToken"; + +let tokenClient; +let accessToken = sessionStorage.getItem(SESSION_STORAGE_ID); + +let isScriptExecuted = false; +if (!isScriptExecuted) { + isScriptExecuted = true; + document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".google-drive-button").forEach(setupGoogleDrivePicker); + }); +} + +function gisLoaded() { + tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: window.stirlingPDF.GoogleDriveClientId, + scope: SCOPES, + callback: "", // defined later + }); +} + +// add more as needed. +// Google picker is limited on what mimeTypes are supported +// Wild card are not supported +const expandableMimeTypes = { + "image/*" : ["image/jpeg", "image/png","image/svg+xml" ] +} + +function fileInputToGooglePickerMimeTypes(accept) { + + if(accept == null || accept == "" || accept.includes("*/*")){ + + // Setting null will accept all supported mimetypes + return null; + } + + let mimeTypes = []; + accept.split(',').forEach(part => { + if(!(part in expandableMimeTypes)){ + mimeTypes.push(part); + return; + } + + expandableMimeTypes[part].forEach(mimeType => { + mimeTypes.push(mimeType); + }); + }); + + const mimeString = mimeTypes.join(",").replace(/\s+/g, ''); + console.log([accept, "became", mimeString]); + return mimeString; +} + +/** + * Callback after api.js is loaded. + */ +function gapiLoaded() { + gapi.load("client:picker", initializePicker); +} + +/** + * Callback after the API client is loaded. Loads the + * discovery doc to initialize the API. + */ +async function initializePicker() { + await gapi.client.load("https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"); +} + +function setupGoogleDrivePicker(picker) { + + const name = picker.getAttribute('data-name'); + const accept = picker.getAttribute('data-accept'); + const multiple = picker.getAttribute('data-multiple') === "true"; + const mimeTypes = fileInputToGooglePickerMimeTypes(accept); + + picker.addEventListener("click", onGoogleDriveButtonClick); + + function onGoogleDriveButtonClick(e) { + e.stopPropagation(); + + tokenClient.callback = (response) => { + if (response.error !== undefined) { + throw response; + } + accessToken = response.access_token; + sessionStorage.setItem(SESSION_STORAGE_ID, accessToken); + createGooglePicker(); + }; + + tokenClient.requestAccessToken({ prompt: accessToken === null ? "consent" : "" }); + } + + /** + * Sign out the user upon button click. + */ + function signOut() { + if (accessToken) { + sessionStorage.removeItem(SESSION_STORAGE_ID); + google.accounts.oauth2.revoke(accessToken); + accessToken = null; + } + } + + function createGooglePicker() { + let builder = new google.picker.PickerBuilder() + .setDeveloperKey(window.stirlingPDF.GoogleDriveApiKey) + .setAppId(window.stirlingPDF.GoogleDriveAppId) + .setOAuthToken(accessToken) + .addView( + new google.picker.DocsView() + .setIncludeFolders(true) + .setMimeTypes(mimeTypes) + ) + .addView( + new google.picker.DocsView() + .setIncludeFolders(true) + .setEnableDrives(true) + .setMimeTypes(mimeTypes) + ) + .setCallback(pickerCallback); + + if(multiple) { + builder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED); + } + const picker = builder.build(); + + picker.setVisible(true); + } + + /** + * Displays the file details of the user's selection. + * @param {object} data - Containers the user selection from the picker + */ + async function pickerCallback(data) { + if (data.action === google.picker.Action.PICKED) { + const files = await Promise.all( + data[google.picker.Response.DOCUMENTS].map(async (pickedFile) => { + const fileId = pickedFile[google.picker.Document.ID]; + console.log(fileId); + const res = await gapi.client.drive.files.get({ + fileId: fileId, + alt: "media", + }); + + let file = new File([new Uint8Array(res.body.length).map((_, i) => res.body.charCodeAt(i))], pickedFile.name, { + type: pickedFile.mimeType, + lastModified: pickedFile.lastModified, + endings: pickedFile.endings, + }); + return file; + }) + ); + + document.body.dispatchEvent(new CustomEvent(name+"GoogleDriveDrivePicked", { detail: files })); + } + } +} diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index a407f60ee..f42b013bf 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -228,8 +228,9 @@ loading: '[[#{loading}]]' };