├── Plugins ├── DriveBrowser │ ├── README.txt │ ├── Libraries │ │ ├── Google.Apis.dll │ │ ├── Google.Apis.Auth.dll │ │ ├── Google.Apis.Core.dll │ │ ├── Newtonsoft.Json.dll │ │ ├── Google.Apis.Drive.v3.dll │ │ ├── Google.Apis.DriveActivity.v2.dll │ │ ├── Google.Apis.PeopleService.v1.dll │ │ ├── Google.Apis.Auth.PlatformServices.dll │ │ ├── Google.Apis.dll.meta │ │ ├── Google.Apis.Auth.dll.meta │ │ ├── Google.Apis.Core.dll.meta │ │ ├── Newtonsoft.Json.dll.meta │ │ ├── Google.Apis.Drive.v3.dll.meta │ │ ├── Google.Apis.DriveActivity.v2.dll.meta │ │ ├── Google.Apis.PeopleService.v1.dll.meta │ │ └── Google.Apis.Auth.PlatformServices.dll.meta │ ├── README.txt.meta │ ├── Core.meta │ ├── Downloads.meta │ ├── FileActivity.meta │ ├── FileBrowser.meta │ ├── GlobalSearch.meta │ ├── Libraries.meta │ ├── DriveBrowser.Editor.asmdef.meta │ ├── Core │ │ ├── DriveAPI.cs.meta │ │ ├── DriveFile.cs.meta │ │ ├── HelperFunctions.cs.meta │ │ ├── GoogleCloudCredentials.cs.meta │ │ ├── DriveFile.cs │ │ ├── GoogleCloudCredentials.cs │ │ ├── HelperFunctions.cs │ │ └── DriveAPI.cs │ ├── Downloads │ │ ├── DownloadRequest.cs.meta │ │ ├── DownloadProgressViewer.cs.meta │ │ ├── DownloadRequest.cs │ │ └── DownloadProgressViewer.cs │ ├── FileBrowser │ │ ├── FileBrowser.cs.meta │ │ ├── FilesTreeView.cs.meta │ │ ├── FilePreviewPopup.cs.meta │ │ ├── FilePreviewPopup.cs │ │ ├── FilesTreeView.cs │ │ └── FileBrowser.cs │ ├── FileActivity │ │ ├── ActivityEntry.cs.meta │ │ ├── ActivityTreeView.cs.meta │ │ ├── ActivityViewer.cs.meta │ │ ├── ActivityEntry.cs │ │ ├── ActivityTreeView.cs │ │ └── ActivityViewer.cs │ ├── GlobalSearch │ │ ├── GlobalSearchTreeView.cs.meta │ │ ├── GlobalSearchWindow.cs.meta │ │ ├── GlobalSearchTreeView.cs │ │ └── GlobalSearchWindow.cs │ └── DriveBrowser.Editor.asmdef └── DriveBrowser.meta ├── .github ├── Images │ ├── DriveBrowserWindow.png │ └── FileActivityWindow.png └── README.md ├── LICENSE.txt.meta ├── package.json.meta ├── Plugins.meta ├── package.json └── LICENSE.txt /Plugins/DriveBrowser/README.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/README.txt -------------------------------------------------------------------------------- /.github/Images/DriveBrowserWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/.github/Images/DriveBrowserWindow.png -------------------------------------------------------------------------------- /.github/Images/FileActivityWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/.github/Images/FileActivityWindow.png -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.dll -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Auth.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.Auth.dll -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Core.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.Core.dll -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Drive.v3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.Drive.v3.dll -------------------------------------------------------------------------------- /LICENSE.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e840331df0521944e97ab3461c710ed7 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8aa7b170f7aac7c48aa4dcd00e782ca8 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.DriveActivity.v2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.DriveActivity.v2.dll -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.PeopleService.v1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.PeopleService.v1.dll -------------------------------------------------------------------------------- /Plugins.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7e8a92a7293f0e4da92df8451c03087 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Auth.PlatformServices.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityEditorGoogleDriveIntegration/HEAD/Plugins/DriveBrowser/Libraries/Google.Apis.Auth.PlatformServices.dll -------------------------------------------------------------------------------- /Plugins/DriveBrowser/README.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 740bf16d813c5d349b14cdc613a3352d 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a06f23ab65d11d047b61af3ba1177f5d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35d7fe3c27a17f34696d68b1abd4106e 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Downloads.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c195a0b77183a104985388d34ced40dd 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b234b0e26149ab441beded6f61d8014c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e70a6beacf67cc54cbf476b5a2bc1808 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/GlobalSearch.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f1d396a1e524ed54780603122dffeae6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 07271da178081ae4cbf5ea3592f455ba 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/DriveBrowser.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e76c125deb08d8f44a0784936d37169e 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/DriveAPI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c9fa2e10c93961548b1a5c5e6888682a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/DriveFile.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 604551f4c243220468181928f7f063a7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/HelperFunctions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c5e2ba5505076e94c84f271b1f335634 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Downloads/DownloadRequest.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b90c7282a81af940a2fed28efc3c8f4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser/FileBrowser.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 43aa86551b083be4cb0a41decd65bb71 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser/FilesTreeView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7b7a2aea204e33a418b0165bac2f4d3e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/GoogleCloudCredentials.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 67317a080e982b741b6a9c1b456ceae5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity/ActivityEntry.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 473d6174e1f275d458b8aa1b74d35202 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity/ActivityTreeView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5f1783b82d5fb2d42bd7965265d6faf6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity/ActivityViewer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ca508d429139b29468d1d5cb1cf6b2aa 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser/FilePreviewPopup.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d57160a4c090de84c93c93aed013dfce 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Downloads/DownloadProgressViewer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3fad004c19a19074e837e1ce9bcab3da 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/GlobalSearch/GlobalSearchTreeView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b1832691857f5a6478bdb86fb526ce41 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/GlobalSearch/GlobalSearchWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 24cd8858f82bb314eb1004ad9407a4da 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/DriveBrowser.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DriveBrowser.Editor", 3 | "references": [], 4 | "includePlatforms": [ 5 | "Editor" 6 | ], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.yasirkula.driveintegration", 3 | "displayName": "Google Drive Integration for Unity Editor", 4 | "version": "1.0.1", 5 | "documentationUrl": "https://github.com/yasirkula/UnityEditorGoogleDriveIntegration", 6 | "changelogUrl": "https://github.com/yasirkula/UnityEditorGoogleDriveIntegration/releases", 7 | "licensesUrl": "https://github.com/yasirkula/UnityEditorGoogleDriveIntegration/blob/master/LICENSE.txt", 8 | "description": "This plugin helps you access your Google Drive files from within Unity editor. You can easily download these files to your Unity project or see the recent changes made to a file/folder." 9 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity/ActivityEntry.cs: -------------------------------------------------------------------------------- 1 | namespace DriveBrowser 2 | { 3 | // Entries are sorted which makes it easier to sort ActivityTreeView by activity type 4 | public enum FileActivityType { Create, Delete, Edit, Move, Rename, Restore, Unknown }; 5 | 6 | public delegate void ActivityEntryDelegate( ActivityEntry activityEntry ); 7 | 8 | [System.Serializable] 9 | public class ActivityEntry 10 | { 11 | public FileActivityType type; 12 | public string fileID; 13 | public string relativePath; 14 | public string username; 15 | public bool isFolder; 16 | public long size, timeTicks; 17 | 18 | private System.DateTime? m_time; 19 | public System.DateTime time 20 | { 21 | get 22 | { 23 | if( !m_time.HasValue ) 24 | m_time = new System.DateTime( timeTicks ).ToLocalTime(); // This DateTime seems to be UTC whereas DriveFile's DateTime is already localized 25 | 26 | return m_time.Value; 27 | } 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return type + " " + relativePath; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Süleyman Yasir KULA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Downloads/DownloadRequest.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | #if UNITY_2020_2_OR_NEWER 3 | using UnityEditor.AssetImporters; 4 | #else 5 | using UnityEditor.Experimental.AssetImporters; 6 | #endif 7 | using UnityEngine; 8 | using System.IO; 9 | 10 | namespace DriveBrowser 11 | { 12 | [System.Serializable] 13 | public class DownloadRequest 14 | { 15 | public string[] fileIDs; 16 | public string path; 17 | } 18 | 19 | [ScriptedImporter( 1, EXTENSION )] 20 | public class DownloadRequestImporter : ScriptedImporter 21 | { 22 | public const string EXTENSION = "drivedl"; 23 | public const string DOWNLOAD_REQUEST_TEMP_PATH = "Library/DriveDownload." + DownloadRequestImporter.EXTENSION; 24 | 25 | public override void OnImportAsset( AssetImportContext ctx ) 26 | { 27 | string assetPath = ctx.assetPath; 28 | 29 | DownloadRequest downloadRequest = JsonUtility.FromJson( File.ReadAllText( assetPath ) ); 30 | downloadRequest.path = Path.GetDirectoryName( assetPath ); 31 | 32 | EditorApplication.delayCall += () => 33 | { 34 | // Can't delete the asset immediately, wait for 1 frame 35 | AssetDatabase.DeleteAsset( assetPath ); 36 | 37 | // Initiate the download 38 | downloadRequest.DownloadAsync(); 39 | }; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/DriveFile.cs: -------------------------------------------------------------------------------- 1 | using DFile = Google.Apis.Drive.v3.Data.File; 2 | 3 | namespace DriveBrowser 4 | { 5 | public enum FolderChildrenState { Unknown = 0, HasChildren = 1, NoChildren = 2 }; 6 | 7 | [System.Serializable] 8 | public class DriveFile 9 | { 10 | public string id, name, parentID; 11 | public long size, modifiedTimeTicks; 12 | public bool isFolder; 13 | public string[] children = new string[0]; 14 | public FolderChildrenState childrenState; 15 | 16 | private System.DateTime? m_modifiedTime; 17 | public System.DateTime modifiedTime 18 | { 19 | get 20 | { 21 | if( !m_modifiedTime.HasValue ) 22 | m_modifiedTime = new System.DateTime( modifiedTimeTicks ); 23 | 24 | return m_modifiedTime.Value; 25 | } 26 | } 27 | 28 | public DriveFile( DFile file ) 29 | { 30 | id = file.Id; 31 | name = file.Name; 32 | parentID = ( file.Parents != null && file.Parents.Count > 0 ) ? file.Parents[0] : null; 33 | size = file.Size ?? 0L; 34 | modifiedTimeTicks = file.ModifiedTime.HasValue ? file.ModifiedTime.Value.Ticks : 0L; 35 | isFolder = ( file.MimeType == "application/vnd.google-apps.folder" ); 36 | childrenState = ( isFolder ? FolderChildrenState.Unknown : FolderChildrenState.NoChildren ); 37 | } 38 | 39 | public override string ToString() 40 | { 41 | return name; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 70eee6c83f7c71542985a9d3da8c74fd 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Auth.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c9eb7b5c90d902545b23acc610470de4 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Core.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 014e7c5270ea3064ea52ca1072d8e53e 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Newtonsoft.Json.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8f87e3854159d274bb1f1b89dde82075 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Drive.v3.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b875652094addca438780c914552c6dd 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.DriveActivity.v2.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4a92b0042894ae94bb23ccc7864836f6 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.PeopleService.v1.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 22b2afa0ace541d43b0e1b775a31d772 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Libraries/Google.Apis.Auth.PlatformServices.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6046fb47423e7b04a8076afe8a9c56ba 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 0 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | '': Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Editor: 0 20 | Exclude Linux: 1 21 | Exclude Linux64: 1 22 | Exclude LinuxUniversal: 1 23 | Exclude OSXUniversal: 1 24 | Exclude Win: 1 25 | Exclude Win64: 1 26 | - first: 27 | Any: 28 | second: 29 | enabled: 0 30 | settings: {} 31 | - first: 32 | Editor: Editor 33 | second: 34 | enabled: 1 35 | settings: 36 | DefaultValueInitialized: true 37 | - first: 38 | Facebook: Win 39 | second: 40 | enabled: 0 41 | settings: 42 | CPU: None 43 | - first: 44 | Facebook: Win64 45 | second: 46 | enabled: 0 47 | settings: 48 | CPU: None 49 | - first: 50 | Standalone: Linux 51 | second: 52 | enabled: 0 53 | settings: 54 | CPU: None 55 | - first: 56 | Standalone: Linux64 57 | second: 58 | enabled: 0 59 | settings: 60 | CPU: None 61 | - first: 62 | Standalone: LinuxUniversal 63 | second: 64 | enabled: 0 65 | settings: 66 | CPU: None 67 | - first: 68 | Standalone: OSXUniversal 69 | second: 70 | enabled: 0 71 | settings: 72 | CPU: x86 73 | - first: 74 | Standalone: Win 75 | second: 76 | enabled: 0 77 | settings: 78 | CPU: None 79 | - first: 80 | Standalone: Win64 81 | second: 82 | enabled: 0 83 | settings: 84 | CPU: None 85 | - first: 86 | Windows Store Apps: WindowsStoreApps 87 | second: 88 | enabled: 0 89 | settings: 90 | CPU: AnyCPU 91 | userData: 92 | assetBundleName: 93 | assetBundleVariant: 94 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # Google Drive™ Integration for Unity Editor 2 | 3 | This plugin helps you access your Google Drive™ files from within Unity editor. You can easily download these files to your Unity project or see the recent changes made to a file/folder. Please note that this plugin accesses Drive™ storage in read-only mode and doesn't allow uploading files to Drive™ or modifying the existing Drive™ files. 4 | 5 | The plugin requires at least **.NET Standard 2.0** or **.NET 4.x** *Api Compatibility Level* in *Player Settings*. Tested on Unity 2018.4.34f1 and 2019.4.26f1. 6 | 7 | **[GitHub Sponsors ☕](https://github.com/sponsors/yasirkula)** 8 | 9 | ## INSTALLATION 10 | 11 | There are 4 ways to install this plugin: 12 | 13 | - import [DriveIntegration.unitypackage](https://github.com/yasirkula/UnityEditorGoogleDriveIntegration/releases) via *Assets-Import Package* 14 | - clone/[download](https://github.com/yasirkula/UnityEditorGoogleDriveIntegration/archive/master.zip) this repository and move the *Plugins* folder to your Unity project's *Assets* folder 15 | - *(via Package Manager)* click the + button and install the package from the following git URL: 16 | - `https://github.com/yasirkula/UnityEditorGoogleDriveIntegration.git` 17 | - *(via [OpenUPM](https://openupm.com))* after installing [openupm-cli](https://github.com/openupm/openupm-cli), run the following command: 18 | - `openupm add com.yasirkula.driveintegration` 19 | 20 | ## HOW TO 21 | 22 | - Create a *Google Cloud project*: https://github.com/yasirkula/UnityEditorGoogleDriveIntegration/wiki/Creating-Google-Cloud-Project 23 | - Open the *Drive Browser* window via **Window-Drive Browser**. The first time this window is opened, it will prompt you to enter your Google Cloud project's credentials. Then, you'll be prompted to grant read-only access to your Drive™ files 24 | 25 | ![screenshot](Images/DriveBrowserWindow.png) 26 | 27 | - Drag&drop files/folders from the Drive Browser window to Project window to download them 28 | - Right click a folder and select **View Activity** to see the recent changes made in that folder 29 | 30 | ![screenshot](Images/FileActivityWindow.png) 31 | 32 | - Right click the Drive Browser tab and select **Help** to learn more 33 | -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/GoogleCloudCredentials.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace DriveBrowser 6 | { 7 | [HelpURL( "https://github.com/yasirkula/UnityEditorGoogleDriveIntegration/wiki/Creating-Google-Cloud-Project" )] 8 | public class GoogleCloudCredentials : ScriptableObject 9 | { 10 | private const string INITIAL_SAVE_PATH = "Assets/Plugins/DriveBrowser/GoogleCloudCredentials.asset"; 11 | 12 | public string ClientID, ClientSecret; 13 | 14 | private static GoogleCloudCredentials m_instance; 15 | public static GoogleCloudCredentials Instance 16 | { 17 | get 18 | { 19 | if( !m_instance ) 20 | { 21 | string[] instances = AssetDatabase.FindAssets( "t:GoogleCloudCredentials" ); 22 | if( instances != null && instances.Length > 0 ) 23 | m_instance = AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath( instances[0] ) ); 24 | 25 | if( !m_instance ) 26 | { 27 | Directory.CreateDirectory( Path.GetDirectoryName( INITIAL_SAVE_PATH ) ); 28 | 29 | AssetDatabase.CreateAsset( CreateInstance(), INITIAL_SAVE_PATH ); 30 | AssetDatabase.SaveAssets(); 31 | m_instance = AssetDatabase.LoadAssetAtPath( INITIAL_SAVE_PATH ); 32 | 33 | Debug.Log( "Created Google Cloud credentials file at " + INITIAL_SAVE_PATH + ". You can move this file around freely.", m_instance ); 34 | } 35 | } 36 | 37 | return m_instance; 38 | } 39 | } 40 | } 41 | 42 | [CustomEditor( typeof( GoogleCloudCredentials ) )] 43 | public class GoogleCloudCredentialsEditor : Editor 44 | { 45 | public override void OnInspectorGUI() 46 | { 47 | GoogleCloudCredentials credentials = (GoogleCloudCredentials) target; 48 | bool credentialsMissing = ( string.IsNullOrEmpty( credentials.ClientID ) || string.IsNullOrEmpty( credentials.ClientSecret ) ); 49 | 50 | if( credentialsMissing ) 51 | EditorGUILayout.HelpBox( "Fill in the credentials first!", MessageType.Error ); 52 | else 53 | { 54 | EditorGUILayout.HelpBox( 55 | "Anyone who has access to these credentials can use them to call Drive APIs using your daily quota.\n\n" + 56 | "It is not a major deal since these credentials can be regenerated but if you don't want others to see/use your credentials, consider excluding this file from your repository (e.g. '.gitignore').\n\n" + 57 | "In that case, other people who have access to this project will have to generate their own credentials (i.e. create their own Google Cloud projects).", MessageType.Info ); 58 | } 59 | 60 | serializedObject.Update(); 61 | DrawPropertiesExcluding( serializedObject, "m_Script" ); 62 | serializedObject.ApplyModifiedProperties(); 63 | 64 | if( !credentialsMissing ) 65 | { 66 | EditorGUILayout.Space(); 67 | 68 | if( GUILayout.Button( "Open Drive Browser" ) ) 69 | FileBrowser.Initialize(); 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Downloads/DownloadProgressViewer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace DriveBrowser 7 | { 8 | public class DownloadProgressViewer : EditorWindow 9 | { 10 | [System.Serializable] 11 | private class DownloadedFile 12 | { 13 | public DriveFile file; 14 | public long downloadedBytes; 15 | } 16 | 17 | private int totalFileCount, downloadedFileCount; 18 | private CancellationTokenSource cancellationTokenSource; 19 | private List downloadedFiles = new List( 4 ); 20 | 21 | private bool shouldRepositionSelf = true; 22 | 23 | private Vector2 scrollPos; 24 | 25 | public static DownloadProgressViewer Initialize( int totalFileCount, CancellationTokenSource cancellationTokenSource ) 26 | { 27 | DownloadProgressViewer window = CreateInstance(); 28 | window.titleContent = new GUIContent( "Download Progress" ); 29 | window.minSize = new Vector2( 200f, 80f ); 30 | window.maxSize = new Vector2( 500f, 150f ); 31 | 32 | window.cancellationTokenSource = cancellationTokenSource; 33 | window.totalFileCount = totalFileCount; 34 | 35 | window.ShowUtility(); 36 | window.Repaint(); 37 | 38 | return window; 39 | } 40 | 41 | private void OnDisable() 42 | { 43 | if( cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested ) 44 | { 45 | EditorUtility.DisplayDialog( "Warning", "Closing this window automatically cancels the download.", "OK" ); 46 | cancellationTokenSource.Cancel(); 47 | } 48 | } 49 | 50 | public void AddDownload( DriveFile downloadedFile ) 51 | { 52 | downloadedFiles.Add( new DownloadedFile() { file = downloadedFile } ); 53 | EditorApplication.delayCall += Repaint; // See: SetProgress 54 | } 55 | 56 | public void RemoveDownload( DriveFile downloadedFile ) 57 | { 58 | downloadedFileCount += downloadedFiles.RemoveAll( ( download ) => download.file == downloadedFile ); 59 | EditorApplication.delayCall += Repaint; // See: SetProgress 60 | } 61 | 62 | public void SetProgress( DriveFile downloadedFile, long downloadedBytes ) 63 | { 64 | foreach( DownloadedFile download in downloadedFiles ) 65 | { 66 | if( download.file == downloadedFile ) 67 | { 68 | download.downloadedBytes = downloadedBytes; 69 | EditorApplication.delayCall += Repaint; // SetProgress can be called from a separate thread whereas Repaint must be called from the main thread 70 | 71 | return; 72 | } 73 | } 74 | } 75 | 76 | public void IncrementTotalFileCount( int delta ) 77 | { 78 | totalFileCount += delta; 79 | } 80 | 81 | public void DownloadCompleted() 82 | { 83 | cancellationTokenSource = null; 84 | Repaint(); 85 | } 86 | 87 | private void OnGUI() 88 | { 89 | if( shouldRepositionSelf ) 90 | { 91 | shouldRepositionSelf = false; 92 | HelperFunctions.MoveWindowOverCursor( this, maxSize.y ); 93 | } 94 | 95 | if( cancellationTokenSource != null ) 96 | { 97 | EditorGUILayout.LabelField( string.Concat( "Downloaded: ", downloadedFileCount.ToString(), "/", totalFileCount.ToString() ) ); 98 | 99 | EditorGUILayout.Space(); 100 | 101 | scrollPos = EditorGUILayout.BeginScrollView( scrollPos ); 102 | 103 | foreach( DownloadedFile download in downloadedFiles ) 104 | { 105 | string progressLabel; 106 | if( download.file.size == 0L ) 107 | progressLabel = download.file.name; 108 | else 109 | progressLabel = string.Concat( download.file.name, " (", EditorUtility.FormatBytes( download.downloadedBytes ), " / ", EditorUtility.FormatBytes( download.file.size ), ")" ); 110 | 111 | float progress = ( download.file.size == 0L ) ? 0f : (float) ( (double) download.downloadedBytes / download.file.size ); 112 | EditorGUI.ProgressBar( EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight ), progress, progressLabel ); 113 | } 114 | 115 | EditorGUILayout.EndScrollView(); 116 | 117 | GUILayout.FlexibleSpace(); 118 | 119 | GUI.enabled = !cancellationTokenSource.IsCancellationRequested; 120 | if( GUILayout.Button( "Cancel" ) ) 121 | cancellationTokenSource.Cancel(); 122 | GUI.enabled = true; 123 | } 124 | else 125 | { 126 | EditorGUILayout.LabelField( "Download finished" ); 127 | 128 | GUILayout.FlexibleSpace(); 129 | 130 | if( GUILayout.Button( "Close" ) ) 131 | Close(); 132 | } 133 | 134 | EditorGUILayout.Space(); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/GlobalSearch/GlobalSearchTreeView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace DriveBrowser 8 | { 9 | public class GlobalSearchTreeView : TreeView 10 | { 11 | private readonly List searchResults; 12 | 13 | private readonly CompareInfo textComparer; 14 | private readonly CompareOptions textCompareOptions; 15 | 16 | private readonly System.Action onFilesRightClicked; 17 | 18 | private readonly GUIContent sharedGUIContent = new GUIContent(); 19 | 20 | public GlobalSearchTreeView( TreeViewState treeViewState, MultiColumnHeader header, List searchResults, System.Action onFilesRightClicked ) : base( treeViewState, header ) 21 | { 22 | this.searchResults = searchResults; 23 | this.onFilesRightClicked = onFilesRightClicked; 24 | 25 | textComparer = new CultureInfo( "en-US" ).CompareInfo; 26 | textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace; 27 | 28 | showBorder = true; 29 | 30 | header.visibleColumnsChanged += ( _header ) => _header.ResizeToFit(); 31 | header.sortingChanged += ( _header ) => 32 | { 33 | Reload(); 34 | 35 | if( HasSelection() ) 36 | SetSelection( GetSelection(), TreeViewSelectionOptions.RevealAndFrame ); 37 | }; 38 | 39 | Reload(); 40 | } 41 | 42 | protected override TreeViewItem BuildRoot() 43 | { 44 | return new TreeViewItem { id = 0, depth = -1 }; 45 | } 46 | 47 | protected override IList BuildRows( TreeViewItem root ) 48 | { 49 | List rows = ( GetRows() as List ) ?? new List( 64 ); 50 | rows.Clear(); 51 | 52 | bool isSearching = !string.IsNullOrEmpty( searchString ); 53 | 54 | for( int i = 0; i < searchResults.Count; i++ ) 55 | { 56 | DriveFile file = searchResults[i]; 57 | if( !isSearching || textComparer.IndexOf( file.name, searchString, textCompareOptions ) >= 0 ) 58 | rows.Add( new TreeViewItem( i, 0, file.name ) ); 59 | } 60 | 61 | if( rows.Count > 0 ) 62 | { 63 | rows.Sort( ( r1, r2 ) => 64 | { 65 | DriveFile f1 = searchResults[r1.id]; 66 | DriveFile f2 = searchResults[r2.id]; 67 | 68 | switch( multiColumnHeader.sortedColumnIndex ) 69 | { 70 | case 0: // Sort by name 71 | { 72 | if( f1.isFolder && !f2.isFolder ) 73 | return -1; 74 | else if( !f1.isFolder && f2.isFolder ) 75 | return 1; 76 | 77 | return f1.name.CompareTo( f2.name ); 78 | } 79 | case 1: // Sort by file size 80 | { 81 | if( f1.isFolder && !f2.isFolder ) 82 | return -1; 83 | else if( !f1.isFolder && f2.isFolder ) 84 | return 1; 85 | 86 | return f1.size.CompareTo( f2.size ); 87 | } 88 | case 2: // Sort by last modified date 89 | { 90 | return f1.modifiedTimeTicks.CompareTo( f2.modifiedTimeTicks ); 91 | } 92 | default: return 0; 93 | } 94 | } ); 95 | 96 | if( !multiColumnHeader.IsSortedAscending( multiColumnHeader.sortedColumnIndex ) ) 97 | rows.Reverse(); 98 | 99 | foreach( TreeViewItem row in rows ) 100 | root.AddChild( row ); 101 | } 102 | else if( root.children == null ) // Otherwise: "InvalidOperationException: TreeView: 'rootItem.children == null'" 103 | root.children = new List( 0 ); 104 | 105 | return rows; 106 | } 107 | 108 | protected override void RowGUI( RowGUIArgs args ) 109 | { 110 | DriveFile file = searchResults[args.item.id]; 111 | 112 | if( Event.current.type == EventType.MouseDown && args.rowRect.Contains( Event.current.mousePosition ) ) 113 | { 114 | Rect previewSourceRect = args.rowRect; 115 | previewSourceRect.width = EditorGUIUtility.currentViewWidth; 116 | 117 | FilePreviewPopup.Show( previewSourceRect, file ); 118 | } 119 | 120 | for( int i = 0; i < args.GetNumVisibleColumns(); ++i ) 121 | { 122 | Rect cellRect = args.GetCellRect( i ); 123 | switch( args.GetColumn( i ) ) 124 | { 125 | case 0: // Filename 126 | { 127 | cellRect.xMin += GetContentIndent( args.item ); 128 | cellRect.y -= 2f; 129 | cellRect.height += 4f; // Incrementing height fixes cropped icon issue on Unity 2019.2 or earlier 130 | 131 | sharedGUIContent.text = file.name; 132 | sharedGUIContent.image = file.isFolder ? AssetDatabase.GetCachedIcon( "Assets" ) as Texture2D : UnityEditorInternal.InternalEditorUtility.GetIconForFile( file.name ); 133 | sharedGUIContent.tooltip = file.name; 134 | 135 | GUI.Label( cellRect, sharedGUIContent ); 136 | break; 137 | } 138 | case 1: // File size 139 | { 140 | if( !file.isFolder ) 141 | GUI.Label( cellRect, EditorUtility.FormatBytes( file.size ) ); 142 | 143 | break; 144 | } 145 | case 2: // Last modified date 146 | { 147 | GUI.Label( cellRect, file.modifiedTime.ToString( "dd.MM.yy HH:mm" ) ); 148 | break; 149 | } 150 | } 151 | } 152 | } 153 | 154 | protected override bool CanStartDrag( CanStartDragArgs args ) 155 | { 156 | return true; 157 | } 158 | 159 | protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) 160 | { 161 | IList sortedItemIDs = SortItemIDsInRowOrder( args.draggedItemIDs ); 162 | if( sortedItemIDs.Count == 0 ) 163 | return; 164 | 165 | string[] fileIDs = new string[sortedItemIDs.Count]; 166 | for( int i = 0; i < sortedItemIDs.Count; i++ ) 167 | fileIDs[i] = searchResults[sortedItemIDs[i]].id; 168 | 169 | HelperFunctions.InitiateDragDropDownload( fileIDs ); 170 | } 171 | 172 | protected override void ContextClickedItem( int id ) 173 | { 174 | IList selection = GetSelection(); 175 | if( selection == null || selection.Count == 0 ) 176 | return; 177 | 178 | DriveFile[] selectedFiles = new DriveFile[selection.Count]; 179 | for( int i = 0; i < selection.Count; i++ ) 180 | selectedFiles[i] = searchResults[selection[i]]; 181 | 182 | onFilesRightClicked?.Invoke( selectedFiles ); 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser/FilePreviewPopup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading; 5 | using UnityEditor; 6 | using UnityEngine; 7 | 8 | namespace DriveBrowser 9 | { 10 | public static class FilePreviewPopup 11 | { 12 | private class PopupContent : PopupWindowContent 13 | { 14 | private const float LOADING_BAR_SIZE = 32f; 15 | 16 | public override Vector2 GetWindowSize() 17 | { 18 | return new Vector2( 128f, 128f ); 19 | } 20 | 21 | public override void OnGUI( Rect rect ) 22 | { 23 | if( loadingThumbnail ) 24 | { 25 | Rect loadingBarRect = new Rect( rect.center - new Vector2( LOADING_BAR_SIZE * 0.5f, LOADING_BAR_SIZE * 0.5f ), new Vector2( LOADING_BAR_SIZE, LOADING_BAR_SIZE ) ); 26 | GUI.Label( loadingBarRect, loadingBar ); 27 | } 28 | else if( thumbnail ) 29 | GUI.DrawTexture( rect, thumbnail, ScaleMode.ScaleToFit ); 30 | } 31 | 32 | public override void OnClose() 33 | { 34 | if( cancellationTokenSource != null ) 35 | { 36 | cancellationTokenSource.Cancel(); 37 | cancellationTokenSource = null; 38 | } 39 | } 40 | } 41 | 42 | // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/61f92bd79ae862c4465d35270f9d1d57befd1761/Editor/Mono/InternalEditorUtility.cs#L90-L111 43 | private static GUIContent[] loadingBarImages; 44 | private static GUIContent loadingBar 45 | { 46 | get 47 | { 48 | if( loadingBarImages == null ) 49 | { 50 | loadingBarImages = new GUIContent[12]; 51 | for( int i = 0; i < 12; i++ ) 52 | { 53 | loadingBarImages[i] = new GUIContent { image = EditorGUIUtility.IconContent( "WaitSpin" + i.ToString( "00" ) ).image }; 54 | loadingBarImages[i].image.hideFlags = HideFlags.HideAndDontSave; 55 | loadingBarImages[i].image.name = "Spinner"; 56 | } 57 | } 58 | 59 | int frame = (int) Mathf.Repeat( Time.realtimeSinceStartup * 10, 11.99f ); 60 | return loadingBarImages[frame]; 61 | } 62 | } 63 | 64 | private static readonly PopupContent popupContent = new PopupContent(); 65 | private static EditorWindow ActiveWindow { get { return popupContent.editorWindow; } } 66 | //private static EditorWindow ActiveWindow { get { return activeWindowGetter.GetValue( null ) as EditorWindow; } } 67 | 68 | private static Texture2D thumbnail; 69 | private static bool loadingThumbnail; 70 | private static string loadingThumbnailForFileID; 71 | 72 | private static CancellationTokenSource cancellationTokenSource; 73 | 74 | private static readonly System.Type popupWindowType = typeof( EditorWindow ).Assembly.GetType( "UnityEditor.PopupWindowWithoutFocus" ); 75 | private static readonly System.Type popupLocationType = typeof( EditorWindow ).Assembly.GetType( "UnityEditor.PopupLocation" ); 76 | 77 | //private static readonly FieldInfo activeWindowGetter = popupWindowType.GetField( "s_PopupWindowWithoutFocus", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ); 78 | private static readonly MethodInfo showWindowFunction = popupWindowType.GetMethod( "Show", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new System.Type[3] { typeof( Rect ), typeof( PopupWindowContent ), popupLocationType.MakeArrayType() }, null ); 79 | 80 | private static System.Array preferredPopupLocations; 81 | 82 | public static void Show( Rect rect, DriveFile file ) 83 | { 84 | if( file == null || string.IsNullOrEmpty( file.id ) || file.isFolder || file.name.EndsWith( ".meta", System.StringComparison.OrdinalIgnoreCase ) ) 85 | return; 86 | 87 | if( preferredPopupLocations == null ) 88 | { 89 | preferredPopupLocations = System.Array.CreateInstance( popupLocationType, 2 ); 90 | // Omitted Right because preview doesn't want to show up at right in 'Drive Browser' window for unknown reasons 91 | //preferredPopupLocations.SetValue( System.Enum.Parse( popupLocationType, "Right" ), 0 ); 92 | preferredPopupLocations.SetValue( System.Enum.Parse( popupLocationType, "Left" ), 0 ); 93 | preferredPopupLocations.SetValue( System.Enum.Parse( popupLocationType, "Below" ), 1 ); 94 | } 95 | 96 | showWindowFunction.Invoke( null, new object[3] { rect, popupContent, preferredPopupLocations } ); 97 | ShowThumbnailAsync( file ); 98 | 99 | EditorApplication.update -= RepaintPopupContentWhileLoading; 100 | EditorApplication.update += RepaintPopupContentWhileLoading; 101 | } 102 | 103 | public static void Hide() 104 | { 105 | EditorWindow activeWindow = ActiveWindow; 106 | if( activeWindow ) 107 | { 108 | activeWindow.Close(); 109 | 110 | if( cancellationTokenSource != null ) 111 | { 112 | cancellationTokenSource.Cancel(); 113 | cancellationTokenSource = null; 114 | } 115 | 116 | EditorApplication.update -= RepaintPopupContentWhileLoading; 117 | } 118 | } 119 | 120 | public static void Dispose() 121 | { 122 | if( thumbnail ) 123 | { 124 | Object.DestroyImmediate( thumbnail ); 125 | thumbnail = null; 126 | } 127 | 128 | if( cancellationTokenSource != null ) 129 | { 130 | cancellationTokenSource.Cancel(); 131 | cancellationTokenSource = null; 132 | } 133 | 134 | Hide(); 135 | } 136 | 137 | private static async void ShowThumbnailAsync( DriveFile file ) 138 | { 139 | if( cancellationTokenSource != null ) 140 | { 141 | cancellationTokenSource.Cancel(); 142 | cancellationTokenSource = null; 143 | } 144 | 145 | loadingThumbnail = true; 146 | loadingThumbnailForFileID = file.id; 147 | 148 | using( CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource() ) 149 | { 150 | cancellationTokenSource = _cancellationTokenSource; 151 | string thumbnailPath = await file.GetThumbnailAsync( _cancellationTokenSource.Token ); 152 | if( cancellationTokenSource == _cancellationTokenSource ) 153 | cancellationTokenSource = null; 154 | 155 | // We may have requested another thumbnail before this thumbnail was downloaded from the server 156 | if( loadingThumbnailForFileID == file.id ) 157 | { 158 | loadingThumbnail = false; 159 | 160 | if( string.IsNullOrEmpty( thumbnailPath ) ) 161 | Hide(); 162 | else 163 | { 164 | if( !thumbnail ) 165 | thumbnail = new Texture2D( 256, 256, TextureFormat.RGBA32, false ) { hideFlags = HideFlags.HideAndDontSave }; 166 | 167 | thumbnail.LoadImage( File.ReadAllBytes( thumbnailPath ) ); 168 | } 169 | 170 | EditorWindow activeWindow = ActiveWindow; 171 | if( activeWindow ) 172 | activeWindow.Repaint(); 173 | } 174 | } 175 | } 176 | 177 | private static void RepaintPopupContentWhileLoading() 178 | { 179 | EditorWindow activeWindow = ActiveWindow; 180 | if( activeWindow && loadingThumbnail ) 181 | activeWindow.Repaint(); 182 | else 183 | EditorApplication.update -= RepaintPopupContentWhileLoading; 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/GlobalSearch/GlobalSearchWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace DriveBrowser 8 | { 9 | public delegate void SearchResultEntryDelegate( DriveFile searchResultEntry ); 10 | 11 | public class GlobalSearchWindow : EditorWindow 12 | { 13 | private const int MINIMUM_ENTRY_COUNT_PER_FETCH = 50; 14 | 15 | private FileBrowser fileBrowser; 16 | private string searchTerm; 17 | private string pageToken; 18 | 19 | private List searchResults = new List( 64 ); 20 | 21 | private GlobalSearchTreeView searchResultsTreeView; 22 | private TreeViewState searchResultsTreeViewState; 23 | private MultiColumnHeaderState searchResultsTreeViewHeaderState; 24 | private SearchField searchField; 25 | 26 | private CancellationTokenSource cancellationTokenSource; 27 | 28 | private bool shouldRepositionSelf = true; 29 | 30 | private Vector2 scrollPos; 31 | 32 | private bool m_isBusy; 33 | private bool IsBusy 34 | { 35 | get { return m_isBusy; } 36 | set 37 | { 38 | if( m_isBusy != value ) 39 | { 40 | m_isBusy = value; 41 | 42 | if( m_isBusy ) 43 | HelperFunctions.LockAssemblyReload(); 44 | else 45 | HelperFunctions.UnlockAssemblyReload(); 46 | } 47 | } 48 | } 49 | 50 | public static GlobalSearchWindow Initialize( FileBrowser fileBrowser, string searchTerm ) 51 | { 52 | GlobalSearchWindow window = CreateInstance(); 53 | #if UNITY_2019_3_OR_NEWER 54 | window.titleContent = new GUIContent( "Search", EditorGUIUtility.IconContent( "Search Icon" ).image ); 55 | #else 56 | window.titleContent = new GUIContent( "Search" ); 57 | #endif 58 | window.minSize = new Vector2( 400f, 175f ); 59 | 60 | window.fileBrowser = fileBrowser; 61 | window.searchTerm = searchTerm; 62 | 63 | window.Show(); 64 | EditorApplication.delayCall += () => window.LoadMoreSearchResultsAsync(); 65 | 66 | return window; 67 | } 68 | 69 | private void OnEnable() 70 | { 71 | if( searchResultsTreeViewState == null ) 72 | searchResultsTreeViewState = new TreeViewState(); 73 | 74 | MultiColumnHeader multiColumnHeader = HelperFunctions.GenerateMultiColumnHeader( ref searchResultsTreeViewHeaderState, 0, 75 | new HelperFunctions.HeaderColumn( "Name", true, true, 30f, 0f, 0f ), 76 | new HelperFunctions.HeaderColumn( "Size", false, false, 30f, 0f, 70f ), 77 | new HelperFunctions.HeaderColumn( "Last Modified", false, false, 30f, 0f, 100f ) ); 78 | 79 | searchResultsTreeView = new GlobalSearchTreeView( searchResultsTreeViewState, multiColumnHeader, searchResults, OnFilesRightClicked ); 80 | 81 | searchField = new SearchField(); 82 | searchField.downOrUpArrowKeyPressed += searchResultsTreeView.SetFocusAndEnsureSelectedItem; 83 | } 84 | 85 | private void OnDisable() 86 | { 87 | if( cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested ) 88 | cancellationTokenSource.Cancel(); 89 | 90 | IsBusy = false; 91 | } 92 | 93 | private void OnFilesRightClicked( DriveFile[] files ) 94 | { 95 | if( files == null || files.Length == 0 ) 96 | return; 97 | 98 | GenericMenu contextMenu = new GenericMenu(); 99 | 100 | contextMenu.AddItem( new GUIContent( "Download" ), false, () => new DownloadRequest() { fileIDs = files.GetFileIDs() }.DownloadAsync() ); 101 | 102 | contextMenu.AddSeparator( "" ); 103 | 104 | contextMenu.AddItem( new GUIContent( "Show in Drive Browser" ), false, () => 105 | { 106 | if( fileBrowser ) 107 | { 108 | fileBrowser.SetSelectionAsync( files ); 109 | fileBrowser.Focus(); 110 | } 111 | } ); 112 | 113 | if( files.Length == 1 && !string.IsNullOrEmpty( files[0].id ) ) 114 | { 115 | contextMenu.AddSeparator( "" ); 116 | contextMenu.AddItem( new GUIContent( "View Activity" ), false, () => ActivityViewer.Initialize( fileBrowser, DriveAPI.GetFileByID( files[0].id ) ) ); 117 | } 118 | 119 | contextMenu.ShowAsContext(); 120 | Repaint(); // Without this, context menu can appear seconds later which is annoying 121 | } 122 | 123 | private void OnGUI() 124 | { 125 | // Close any leftover windows after restarting Unity (this code somehow doesn't work inside OnEnable, condition is valid but Close function does nothing) 126 | if( string.IsNullOrEmpty( searchTerm ) || fileBrowser == null || fileBrowser.Equals( null ) ) 127 | { 128 | Close(); 129 | return; 130 | } 131 | 132 | if( shouldRepositionSelf ) 133 | { 134 | shouldRepositionSelf = false; 135 | HelperFunctions.MoveWindowOverCursor( this, position.height ); 136 | } 137 | 138 | Rect searchTermRect = EditorGUILayout.GetControlRect( true, EditorGUIUtility.singleLineHeight ); 139 | GUI.Label( new Rect( searchTermRect.position, new Vector2( 115f, searchTermRect.height ) ), "Search results for:", EditorStyles.boldLabel ); 140 | searchTermRect.xMin += 120f; 141 | EditorGUI.TextField( searchTermRect, searchTerm ); 142 | 143 | scrollPos = EditorGUILayout.BeginScrollView( scrollPos ); 144 | 145 | searchResultsTreeView.searchString = searchField.OnGUI( searchResultsTreeView.searchString ); 146 | searchResultsTreeView.OnGUI( GUILayoutUtility.GetRect( 0, 100000, 0, 100000 ) ); 147 | 148 | EditorGUILayout.EndScrollView(); 149 | 150 | if( IsBusy ) 151 | { 152 | EditorGUILayout.Space(); 153 | 154 | GUI.enabled = cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested; 155 | if( GUILayout.Button( "Abort Search..." ) ) 156 | cancellationTokenSource.Cancel(); 157 | GUI.enabled = true; 158 | 159 | EditorGUILayout.Space(); 160 | } 161 | else if( !string.IsNullOrEmpty( pageToken ) ) 162 | { 163 | EditorGUILayout.Space(); 164 | 165 | if( GUILayout.Button( "Load More..." ) ) 166 | LoadMoreSearchResultsAsync(); 167 | 168 | EditorGUILayout.Space(); 169 | } 170 | 171 | // This happens only when the mouse click is not captured by the TreeView 172 | // In this case, clear the TreeView's selection 173 | if( Event.current.type == EventType.MouseDown && Event.current.button == 0 ) 174 | { 175 | searchResultsTreeView.SetSelection( new int[0] ); 176 | EditorApplication.delayCall += Repaint; 177 | } 178 | } 179 | 180 | private async void LoadMoreSearchResultsAsync() 181 | { 182 | if( IsBusy ) 183 | return; 184 | 185 | IsBusy = true; 186 | try 187 | { 188 | cancellationTokenSource = new CancellationTokenSource(); 189 | 190 | pageToken = await DriveAPI.PerformGlobalSearchAsync( searchTerm, ( searchResultEntry ) => 191 | { 192 | searchResults.Add( searchResultEntry ); 193 | searchResultsTreeView.Reload(); 194 | 195 | Repaint(); 196 | }, cancellationTokenSource.Token, MINIMUM_ENTRY_COUNT_PER_FETCH, pageToken ); 197 | } 198 | finally 199 | { 200 | IsBusy = false; 201 | 202 | if( cancellationTokenSource != null ) 203 | { 204 | cancellationTokenSource.Dispose(); 205 | cancellationTokenSource = null; 206 | } 207 | } 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/HelperFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Security.Cryptography; 5 | using UnityEditor; 6 | using UnityEditor.IMGUI.Controls; 7 | using UnityEngine; 8 | 9 | namespace DriveBrowser 10 | { 11 | public static class HelperFunctions 12 | { 13 | public class HeaderColumn 14 | { 15 | public readonly string label; 16 | public readonly bool autoResize, sortedAscending; 17 | public readonly float minWidth, maxWidth, width; 18 | 19 | public HeaderColumn( string label, bool autoResize, bool sortedAscending, float minWidth, float maxWidth, float width ) 20 | { 21 | this.label = label; 22 | this.autoResize = autoResize; 23 | this.sortedAscending = sortedAscending; 24 | this.minWidth = minWidth; 25 | this.maxWidth = maxWidth; 26 | this.width = width; 27 | } 28 | } 29 | 30 | private const float WINDOW_REPOSITION_PADDING_TOP = 30f; 31 | 32 | private static bool assemblyLockedHintShown; 33 | private static MethodInfo screenFittedRectGetter; 34 | 35 | public static void InitiateDragDropDownload( string[] fileIDs ) 36 | { 37 | if( fileIDs == null || fileIDs.Length == 0 ) 38 | return; 39 | 40 | DownloadRequest downloadRequest = new DownloadRequest() { fileIDs = fileIDs }; 41 | File.WriteAllText( DownloadRequestImporter.DOWNLOAD_REQUEST_TEMP_PATH, JsonUtility.ToJson( downloadRequest ) ); 42 | 43 | DragAndDrop.PrepareStartDrag(); 44 | DragAndDrop.paths = new string[1] { DownloadRequestImporter.DOWNLOAD_REQUEST_TEMP_PATH }; 45 | DragAndDrop.StartDrag( "Download " + ( fileIDs.Length > 1 ? "Multiple" : DriveAPI.GetFileByID( fileIDs[0] ).name ) ); 46 | } 47 | 48 | public static void MoveWindowOverCursor( EditorWindow window, float preferredHeight ) 49 | { 50 | if( Event.current == null ) 51 | { 52 | Debug.LogError( "MoveOverCursor must be called from OnGUI" ); 53 | return; 54 | } 55 | 56 | Rect windowRect = window.position; 57 | windowRect.height = preferredHeight + WINDOW_REPOSITION_PADDING_TOP; 58 | windowRect.position = GUIUtility.GUIToScreenPoint( Event.current.mousePosition ) - new Vector2( windowRect.width * 0.5f, windowRect.height * 1.15f ); 59 | 60 | // If we don't call FitRectToScreen, EditorWindow can actually spawn outside of the screen 61 | if( screenFittedRectGetter == null ) 62 | screenFittedRectGetter = typeof( EditorWindow ).Assembly.GetType( "UnityEditor.ContainerWindow" ).GetMethod( "FitRectToScreen", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ); 63 | 64 | windowRect = (Rect) screenFittedRectGetter.Invoke( null, new object[3] { windowRect, true, true } ); 65 | windowRect.height = preferredHeight; 66 | windowRect.y += WINDOW_REPOSITION_PADDING_TOP; 67 | 68 | window.position = windowRect; 69 | } 70 | 71 | public static string[] GetFileIDs( this DriveFile[] files ) 72 | { 73 | string[] fileIDs = new string[files.Length]; 74 | for( int i = 0; i < files.Length; i++ ) 75 | fileIDs[i] = files[i].id; 76 | 77 | return fileIDs; 78 | } 79 | 80 | public static string[] GetFileIDs( this ActivityEntry[] entries ) 81 | { 82 | string[] fileIDs = new string[entries.Length]; 83 | for( int i = 0; i < entries.Length; i++ ) 84 | fileIDs[i] = entries[i].fileID; 85 | 86 | return fileIDs; 87 | } 88 | 89 | public static void SerializeToArray( this Dictionary dictionary, out T[] array ) 90 | { 91 | array = new T[dictionary.Count * 2]; 92 | 93 | int index = 0; 94 | foreach( KeyValuePair kvPair in dictionary ) 95 | { 96 | array[index] = kvPair.Key; 97 | array[index + 1] = kvPair.Value; 98 | 99 | index += 2; 100 | } 101 | } 102 | 103 | public static void DeserializeFromArray( this Dictionary dictionary, T[] array ) 104 | { 105 | if( array != null ) 106 | { 107 | for( int i = 0; i < array.Length; i += 2 ) 108 | dictionary[array[i]] = array[i + 1]; 109 | } 110 | } 111 | 112 | public static void SerializeToArray( this Dictionary dictionary, out K[] keys, out V[] values ) 113 | { 114 | keys = new K[dictionary.Count]; 115 | values = new V[dictionary.Count]; 116 | 117 | int index = 0; 118 | foreach( KeyValuePair kvPair in dictionary ) 119 | { 120 | keys[index] = kvPair.Key; 121 | values[index] = kvPair.Value; 122 | 123 | index++; 124 | } 125 | } 126 | 127 | public static void DeserializeFromArray( this Dictionary dictionary, K[] keys, V[] values ) 128 | { 129 | if( keys != null && values != null ) 130 | { 131 | for( int i = 0; i < keys.Length; i++ ) 132 | dictionary[keys[i]] = values[i]; 133 | } 134 | } 135 | 136 | public static MultiColumnHeader GenerateMultiColumnHeader( ref MultiColumnHeaderState headerState, int defaultSortedColumnIndex, params HeaderColumn[] columns ) 137 | { 138 | MultiColumnHeaderState.Column[] _columns = new MultiColumnHeaderState.Column[columns.Length]; 139 | for( int i = 0; i < columns.Length; i++ ) 140 | { 141 | _columns[i] = new MultiColumnHeaderState.Column() 142 | { 143 | headerContent = new GUIContent( columns[i].label, columns[i].label ), 144 | allowToggleVisibility = true, 145 | autoResize = columns[i].autoResize, 146 | canSort = true, 147 | sortedAscending = columns[i].sortedAscending, 148 | headerTextAlignment = TextAlignment.Left, 149 | sortingArrowAlignment = TextAlignment.Center, 150 | }; 151 | 152 | if( columns[i].minWidth > 0f ) 153 | _columns[i].minWidth = columns[i].minWidth; 154 | if( columns[i].maxWidth > 0f ) 155 | _columns[i].maxWidth = columns[i].maxWidth; 156 | if( columns[i].width > 0f ) 157 | _columns[i].width = columns[i].width; 158 | } 159 | 160 | // IDK most of the technical stuff done here. Credit: https://docs.unity3d.com/Manual/TreeViewAPI.html 161 | MultiColumnHeaderState newHeaderState = new MultiColumnHeaderState( _columns ); 162 | 163 | if( MultiColumnHeaderState.CanOverwriteSerializedFields( headerState, newHeaderState ) ) 164 | MultiColumnHeaderState.OverwriteSerializedFields( headerState, newHeaderState ); 165 | 166 | MultiColumnHeader multiColumnHeader = new MultiColumnHeader( newHeaderState ); 167 | if( headerState == null ) // First initialization 168 | { 169 | multiColumnHeader.ResizeToFit(); 170 | multiColumnHeader.sortedColumnIndex = defaultSortedColumnIndex; 171 | } 172 | 173 | headerState = newHeaderState; 174 | 175 | return multiColumnHeader; 176 | } 177 | 178 | // Credit: https://stackoverflow.com/a/10520086/2373034 179 | public static string CalculateMD5Hash( string filePath ) 180 | { 181 | using( MD5 md5 = MD5.Create() ) 182 | { 183 | using( FileStream stream = File.OpenRead( filePath ) ) 184 | { 185 | byte[] hash = md5.ComputeHash( stream ); 186 | return System.BitConverter.ToString( hash ).Replace( "-", "" ).ToLowerInvariant(); 187 | } 188 | } 189 | } 190 | 191 | public static void LockAssemblyReload() 192 | { 193 | assemblyLockedHintShown = false; 194 | 195 | EditorApplication.LockReloadAssemblies(); 196 | EditorApplication.update -= EnforceAssemblyLock; 197 | EditorApplication.update += EnforceAssemblyLock; 198 | } 199 | 200 | public static void UnlockAssemblyReload() 201 | { 202 | EditorApplication.update -= EnforceAssemblyLock; 203 | EditorApplication.UnlockReloadAssemblies(); 204 | } 205 | 206 | private static void EnforceAssemblyLock() 207 | { 208 | if( EditorApplication.isPlayingOrWillChangePlaymode ) 209 | { 210 | EditorApplication.isPlaying = false; 211 | Debug.LogWarning( "Can't enter Play mode while an asynchronous Drive operation is in progress!" ); 212 | } 213 | 214 | if( !assemblyLockedHintShown && EditorApplication.isCompiling ) 215 | { 216 | assemblyLockedHintShown = true; 217 | Debug.LogWarning( "Can't reload assemblies while an asynchronous Drive operation is in progress!" ); 218 | } 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity/ActivityTreeView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace DriveBrowser 8 | { 9 | public class ActivityTreeView : TreeView 10 | { 11 | private readonly List activity; 12 | 13 | private bool showCreateEntries = true, showDeleteEntries = true, showEditEntries = true, showMoveEntries = true, showRenameEntries = true, showRestoreEntries = true; 14 | 15 | private readonly CompareInfo textComparer; 16 | private readonly CompareOptions textCompareOptions; 17 | 18 | private readonly System.Action onEntriesRightClicked; 19 | 20 | private readonly GUIContent sharedGUIContent = new GUIContent(); 21 | 22 | public ActivityTreeView( TreeViewState treeViewState, MultiColumnHeader header, List activity, System.Action onEntriesRightClicked ) : base( treeViewState, header ) 23 | { 24 | this.activity = activity; 25 | this.onEntriesRightClicked = onEntriesRightClicked; 26 | 27 | textComparer = new CultureInfo( "en-US" ).CompareInfo; 28 | textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace; 29 | 30 | showBorder = true; 31 | 32 | header.visibleColumnsChanged += ( _header ) => _header.ResizeToFit(); 33 | header.sortingChanged += ( _header ) => 34 | { 35 | Reload(); 36 | 37 | if( HasSelection() ) 38 | SetSelection( GetSelection(), TreeViewSelectionOptions.RevealAndFrame ); 39 | }; 40 | } 41 | 42 | public void SetFilters( bool showCreateEntries, bool showDeleteEntries, bool showEditEntries, bool showMoveEntries, bool showRenameEntries, bool showRestoreEntries ) 43 | { 44 | this.showCreateEntries = showCreateEntries; 45 | this.showDeleteEntries = showDeleteEntries; 46 | this.showEditEntries = showEditEntries; 47 | this.showMoveEntries = showMoveEntries; 48 | this.showRenameEntries = showRenameEntries; 49 | this.showRestoreEntries = showRestoreEntries; 50 | 51 | Reload(); 52 | } 53 | 54 | protected override TreeViewItem BuildRoot() 55 | { 56 | return new TreeViewItem { id = 0, depth = -1 }; 57 | } 58 | 59 | protected override IList BuildRows( TreeViewItem root ) 60 | { 61 | List rows = ( GetRows() as List ) ?? new List( 64 ); 62 | rows.Clear(); 63 | 64 | bool isSearching = !string.IsNullOrEmpty( searchString ); 65 | 66 | for( int i = 0; i < activity.Count; i++ ) 67 | { 68 | ActivityEntry entry = activity[i]; 69 | if( !isSearching || textComparer.IndexOf( entry.relativePath, searchString, textCompareOptions ) >= 0 || textComparer.IndexOf( entry.username, searchString, textCompareOptions ) >= 0 || textComparer.IndexOf( entry.type.ToString(), searchString, textCompareOptions ) >= 0 ) 70 | { 71 | switch( entry.type ) 72 | { 73 | case FileActivityType.Create: if( !showCreateEntries ) continue; break; 74 | case FileActivityType.Delete: if( !showDeleteEntries ) continue; break; 75 | case FileActivityType.Edit: if( !showEditEntries ) continue; break; 76 | case FileActivityType.Move: if( !showMoveEntries ) continue; break; 77 | case FileActivityType.Rename: if( !showRenameEntries ) continue; break; 78 | case FileActivityType.Restore: if( !showRestoreEntries ) continue; break; 79 | } 80 | 81 | rows.Add( new TreeViewItem( i, 0, entry.relativePath ) ); 82 | } 83 | } 84 | 85 | if( rows.Count > 0 ) 86 | { 87 | bool descendingSort = !multiColumnHeader.IsSortedAscending( multiColumnHeader.sortedColumnIndex ); 88 | 89 | rows.Sort( ( r1, r2 ) => 90 | { 91 | ActivityEntry f1 = activity[r1.id]; 92 | ActivityEntry f2 = activity[r2.id]; 93 | 94 | switch( multiColumnHeader.sortedColumnIndex ) 95 | { 96 | case 0: // Sort by type 97 | { 98 | int result = f1.type.CompareTo( f2.type ); 99 | return ( result != 0 ) ? result : ( descendingSort ? f1.timeTicks.CompareTo( f2.timeTicks ) : f2.timeTicks.CompareTo( f1.timeTicks ) ); 100 | } 101 | case 1: // Sort by relative path 102 | { 103 | int result = f1.relativePath.CompareTo( f2.relativePath ); 104 | return ( result != 0 ) ? result : ( descendingSort ? f1.timeTicks.CompareTo( f2.timeTicks ) : f2.timeTicks.CompareTo( f1.timeTicks ) ); 105 | } 106 | case 2: // Sort by username 107 | { 108 | int result = f1.username.CompareTo( f2.username ); 109 | return ( result != 0 ) ? result : ( descendingSort ? f1.timeTicks.CompareTo( f2.timeTicks ) : f2.timeTicks.CompareTo( f1.timeTicks ) ); 110 | } 111 | case 3: // Sort by file size 112 | { 113 | int result = f1.size.CompareTo( f2.size ); 114 | return ( result != 0 ) ? result : ( descendingSort ? f1.timeTicks.CompareTo( f2.timeTicks ) : f2.timeTicks.CompareTo( f1.timeTicks ) ); 115 | } 116 | case 4: // Sort by last modified date 117 | { 118 | return f1.timeTicks.CompareTo( f2.timeTicks ); 119 | } 120 | default: return 0; 121 | } 122 | } ); 123 | 124 | if( descendingSort ) 125 | rows.Reverse(); 126 | 127 | foreach( TreeViewItem row in rows ) 128 | root.AddChild( row ); 129 | } 130 | else if( root.children == null ) // Otherwise: "InvalidOperationException: TreeView: 'rootItem.children == null'" 131 | root.children = new List( 0 ); 132 | 133 | return rows; 134 | } 135 | 136 | protected override void RowGUI( RowGUIArgs args ) 137 | { 138 | ActivityEntry entry = activity[args.item.id]; 139 | 140 | // Highlight permanently deleted files in red 141 | if( string.IsNullOrEmpty( entry.fileID ) ) 142 | EditorGUI.DrawRect( args.rowRect, new Color( 1f, 0f, 0f, 0.2f ) ); 143 | else if( Event.current.type == EventType.MouseDown && args.rowRect.Contains( Event.current.mousePosition ) ) 144 | { 145 | Rect previewSourceRect = args.rowRect; 146 | previewSourceRect.width = EditorGUIUtility.currentViewWidth; 147 | 148 | FilePreviewPopup.Show( previewSourceRect, DriveAPI.GetFileByID( entry.fileID ) ); 149 | } 150 | 151 | for( int i = 0; i < args.GetNumVisibleColumns(); ++i ) 152 | { 153 | Rect cellRect = args.GetCellRect( i ); 154 | switch( args.GetColumn( i ) ) 155 | { 156 | case 0: // Activity type 157 | { 158 | GUI.Label( cellRect, entry.type.ToString() ); 159 | break; 160 | } 161 | case 1: // Relative path 162 | { 163 | cellRect.y -= 2f; 164 | cellRect.height += 4f; // Incrementing height fixes cropped icon issue on Unity 2019.2 or earlier 165 | 166 | sharedGUIContent.text = entry.relativePath; 167 | sharedGUIContent.image = entry.isFolder ? AssetDatabase.GetCachedIcon( "Assets" ) as Texture2D : UnityEditorInternal.InternalEditorUtility.GetIconForFile( entry.relativePath ); 168 | sharedGUIContent.tooltip = entry.relativePath; 169 | 170 | GUI.Label( cellRect, sharedGUIContent ); 171 | break; 172 | } 173 | case 2: // Modified user 174 | { 175 | sharedGUIContent.text = entry.username; 176 | sharedGUIContent.image = null; 177 | sharedGUIContent.tooltip = entry.username; 178 | 179 | GUI.Label( cellRect, sharedGUIContent ); 180 | break; 181 | } 182 | case 3: // File size 183 | { 184 | if( !entry.isFolder && entry.size >= 0L ) 185 | GUI.Label( cellRect, EditorUtility.FormatBytes( entry.size ) ); 186 | 187 | break; 188 | } 189 | case 4: // Modified time 190 | { 191 | GUI.Label( cellRect, entry.time.ToString( "dd.MM.yy HH:mm" ) ); 192 | break; 193 | } 194 | } 195 | } 196 | } 197 | 198 | protected override bool CanStartDrag( CanStartDragArgs args ) 199 | { 200 | return true; 201 | } 202 | 203 | protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) 204 | { 205 | IList sortedItemIDs = SortItemIDsInRowOrder( args.draggedItemIDs ); 206 | if( sortedItemIDs.Count == 0 ) 207 | return; 208 | 209 | List fileIDs = new List( sortedItemIDs.Count ); 210 | for( int i = 0; i < sortedItemIDs.Count; i++ ) 211 | { 212 | string fileID = activity[sortedItemIDs[i]].fileID; 213 | 214 | // fileID can be null if this activity entry belongs to a permanently deleted file 215 | if( !string.IsNullOrEmpty( fileID ) ) 216 | fileIDs.Add( fileID ); 217 | } 218 | 219 | HelperFunctions.InitiateDragDropDownload( fileIDs.ToArray() ); 220 | } 221 | 222 | protected override void ContextClickedItem( int id ) 223 | { 224 | IList selection = GetSelection(); 225 | if( selection == null || selection.Count == 0 ) 226 | return; 227 | 228 | ActivityEntry[] selectedEntries = new ActivityEntry[selection.Count]; 229 | for( int i = 0; i < selection.Count; i++ ) 230 | selectedEntries[i] = activity[selection[i]]; 231 | 232 | onEntriesRightClicked?.Invoke( selectedEntries ); 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileActivity/ActivityViewer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace DriveBrowser 8 | { 9 | public class ActivityViewer : EditorWindow, IHasCustomMenu 10 | { 11 | private const int MINIMUM_ENTRY_COUNT_PER_FETCH = 20; 12 | 13 | private FileBrowser fileBrowser; 14 | private DriveFile inspectedFile; 15 | private string pageToken; 16 | 17 | private List activity = new List( 64 ); 18 | 19 | private bool showCreateEntries = true, showDeleteEntries = true, showEditEntries = true, showMoveEntries = true, showRenameEntries = true, showRestoreEntries = true; 20 | 21 | private ActivityTreeView activityTreeView; 22 | private TreeViewState activityTreeViewState; 23 | private MultiColumnHeaderState activityTreeViewHeaderState; 24 | private SearchField searchField; 25 | 26 | private CancellationTokenSource cancellationTokenSource; 27 | 28 | private bool shouldRepositionSelf = true; 29 | 30 | private Vector2 scrollPos; 31 | 32 | private bool m_isBusy; 33 | private bool IsBusy 34 | { 35 | get { return m_isBusy; } 36 | set 37 | { 38 | if( m_isBusy != value ) 39 | { 40 | m_isBusy = value; 41 | 42 | if( m_isBusy ) 43 | HelperFunctions.LockAssemblyReload(); 44 | else 45 | HelperFunctions.UnlockAssemblyReload(); 46 | } 47 | } 48 | } 49 | 50 | public static ActivityViewer Initialize( FileBrowser fileBrowser, DriveFile inspectedFile ) 51 | { 52 | ActivityViewer window = CreateInstance(); 53 | #if UNITY_2019_3_OR_NEWER 54 | window.titleContent = new GUIContent( "File Activity", inspectedFile.isFolder ? AssetDatabase.GetCachedIcon( "Assets" ) as Texture2D : UnityEditorInternal.InternalEditorUtility.GetIconForFile( inspectedFile.name ) ); 55 | #else 56 | window.titleContent = new GUIContent( "File Activity" ); 57 | #endif 58 | window.minSize = new Vector2( 400f, 175f ); 59 | 60 | window.fileBrowser = fileBrowser; 61 | window.inspectedFile = inspectedFile; 62 | 63 | window.Show(); 64 | EditorApplication.delayCall += () => window.LoadMoreFileActivityAsync(); 65 | 66 | return window; 67 | } 68 | 69 | private void OnEnable() 70 | { 71 | if( activityTreeViewState == null ) 72 | activityTreeViewState = new TreeViewState(); 73 | 74 | MultiColumnHeader multiColumnHeader = HelperFunctions.GenerateMultiColumnHeader( ref activityTreeViewHeaderState, 4, 75 | new HelperFunctions.HeaderColumn( "Type", false, true, 40f, 55f, 55f ), 76 | new HelperFunctions.HeaderColumn( "Name", true, true, 30f, 0f, 0f ), 77 | new HelperFunctions.HeaderColumn( "User", false, true, 30f, 0f, 70f ), 78 | new HelperFunctions.HeaderColumn( "Size", false, false, 30f, 0f, 70f ), 79 | new HelperFunctions.HeaderColumn( "Last Modified", false, false, 30f, 0f, 100f ) ); 80 | 81 | activityTreeView = new ActivityTreeView( activityTreeViewState, multiColumnHeader, activity, OnEntriesRightClicked ); 82 | activityTreeView.SetFilters( showCreateEntries, showDeleteEntries, showEditEntries, showMoveEntries, showRenameEntries, showRestoreEntries ); 83 | 84 | searchField = new SearchField(); 85 | searchField.downOrUpArrowKeyPressed += activityTreeView.SetFocusAndEnsureSelectedItem; 86 | } 87 | 88 | private void OnDisable() 89 | { 90 | if( cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested ) 91 | cancellationTokenSource.Cancel(); 92 | 93 | IsBusy = false; 94 | } 95 | 96 | void IHasCustomMenu.AddItemsToMenu( GenericMenu menu ) 97 | { 98 | menu.AddItem( new GUIContent( "Show 'Create' Activity" ), showCreateEntries, () => ToggleFilter( ref showCreateEntries ) ); 99 | menu.AddItem( new GUIContent( "Show 'Delete' Activity" ), showDeleteEntries, () => ToggleFilter( ref showDeleteEntries ) ); 100 | menu.AddItem( new GUIContent( "Show 'Edit' Activity" ), showEditEntries, () => ToggleFilter( ref showEditEntries ) ); 101 | menu.AddItem( new GUIContent( "Show 'Move' Activity" ), showMoveEntries, () => ToggleFilter( ref showMoveEntries ) ); 102 | menu.AddItem( new GUIContent( "Show 'Rename' Activity" ), showRenameEntries, () => ToggleFilter( ref showRenameEntries ) ); 103 | menu.AddItem( new GUIContent( "Show 'Restore' Activity" ), showRestoreEntries, () => ToggleFilter( ref showRestoreEntries ) ); 104 | } 105 | 106 | private void ToggleFilter( ref bool filter ) 107 | { 108 | filter = !filter; 109 | activityTreeView.SetFilters( showCreateEntries, showDeleteEntries, showEditEntries, showMoveEntries, showRenameEntries, showRestoreEntries ); 110 | } 111 | 112 | private void OnEntriesRightClicked( ActivityEntry[] entries ) 113 | { 114 | if( entries == null || entries.Length == 0 ) 115 | return; 116 | 117 | GenericMenu contextMenu = new GenericMenu(); 118 | 119 | contextMenu.AddItem( new GUIContent( "Download" ), false, () => new DownloadRequest() { fileIDs = entries.GetFileIDs() }.DownloadAsync() ); 120 | 121 | contextMenu.AddSeparator( "" ); 122 | 123 | contextMenu.AddItem( new GUIContent( "Show in Drive Browser" ), false, () => 124 | { 125 | if( fileBrowser ) 126 | { 127 | List files = new List( entries.Length ); 128 | for( int i = 0; i < entries.Length; i++ ) 129 | { 130 | if( !string.IsNullOrEmpty( entries[i].fileID ) ) 131 | files.Add( DriveAPI.GetFileByID( entries[i].fileID ) ); 132 | } 133 | 134 | fileBrowser.SetSelectionAsync( files ); 135 | fileBrowser.Focus(); 136 | } 137 | } ); 138 | 139 | if( entries.Length == 1 && !string.IsNullOrEmpty( entries[0].fileID ) ) 140 | { 141 | contextMenu.AddSeparator( "" ); 142 | contextMenu.AddItem( new GUIContent( "View Activity" ), false, () => Initialize( fileBrowser, DriveAPI.GetFileByID( entries[0].fileID ) ) ); 143 | } 144 | 145 | contextMenu.ShowAsContext(); 146 | Repaint(); // Without this, context menu can appear seconds later which is annoying 147 | } 148 | 149 | private void OnGUI() 150 | { 151 | // Close any leftover windows after restarting Unity (this code somehow doesn't work inside OnEnable, condition is valid but Close function does nothing) 152 | if( inspectedFile == null || fileBrowser == null || fileBrowser.Equals( null ) ) 153 | { 154 | Close(); 155 | return; 156 | } 157 | 158 | if( shouldRepositionSelf ) 159 | { 160 | shouldRepositionSelf = false; 161 | HelperFunctions.MoveWindowOverCursor( this, position.height ); 162 | } 163 | 164 | Rect inspectedFileRect = EditorGUILayout.GetControlRect( true, EditorGUIUtility.singleLineHeight ); 165 | GUI.Label( new Rect( inspectedFileRect.position, new Vector2( 80f, inspectedFileRect.height ) ), "Activity of:", EditorStyles.boldLabel ); 166 | inspectedFileRect.xMin += 85f; 167 | GUI.Label( inspectedFileRect, new GUIContent 168 | { 169 | text = inspectedFile.name, 170 | image = inspectedFile.isFolder ? AssetDatabase.GetCachedIcon( "Assets" ) as Texture2D : UnityEditorInternal.InternalEditorUtility.GetIconForFile( inspectedFile.name ) 171 | } ); 172 | 173 | if( Event.current.type == EventType.MouseDown && inspectedFileRect.Contains( Event.current.mousePosition ) ) 174 | fileBrowser.PingFile( inspectedFile ); 175 | 176 | scrollPos = EditorGUILayout.BeginScrollView( scrollPos ); 177 | 178 | activityTreeView.searchString = searchField.OnGUI( activityTreeView.searchString ); 179 | activityTreeView.OnGUI( GUILayoutUtility.GetRect( 0, 100000, 0, 100000 ) ); 180 | 181 | EditorGUILayout.EndScrollView(); 182 | 183 | if( IsBusy ) 184 | { 185 | EditorGUILayout.Space(); 186 | 187 | GUI.enabled = cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested; 188 | if( GUILayout.Button( "Abort Operation..." ) ) 189 | cancellationTokenSource.Cancel(); 190 | GUI.enabled = true; 191 | 192 | EditorGUILayout.Space(); 193 | } 194 | else if( !string.IsNullOrEmpty( pageToken ) ) 195 | { 196 | EditorGUILayout.Space(); 197 | 198 | if( GUILayout.Button( "Load More..." ) ) 199 | LoadMoreFileActivityAsync(); 200 | 201 | EditorGUILayout.Space(); 202 | } 203 | 204 | // This happens only when the mouse click is not captured by the TreeView 205 | // In this case, clear the TreeView's selection 206 | if( Event.current.type == EventType.MouseDown && Event.current.button == 0 ) 207 | { 208 | activityTreeView.SetSelection( new int[0] ); 209 | EditorApplication.delayCall += Repaint; 210 | } 211 | } 212 | 213 | private async void LoadMoreFileActivityAsync() 214 | { 215 | if( IsBusy ) 216 | return; 217 | 218 | IsBusy = true; 219 | try 220 | { 221 | cancellationTokenSource = new CancellationTokenSource(); 222 | 223 | pageToken = await inspectedFile.GetActivityAsync( ( activityEntry ) => 224 | { 225 | activity.Add( activityEntry ); 226 | activityTreeView.Reload(); 227 | 228 | Repaint(); 229 | }, cancellationTokenSource.Token, MINIMUM_ENTRY_COUNT_PER_FETCH, pageToken ); 230 | } 231 | finally 232 | { 233 | IsBusy = false; 234 | 235 | if( cancellationTokenSource != null ) 236 | { 237 | cancellationTokenSource.Dispose(); 238 | cancellationTokenSource = null; 239 | } 240 | } 241 | } 242 | } 243 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser/FilesTreeView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace DriveBrowser 8 | { 9 | public class FilesTreeView : TreeView 10 | { 11 | private readonly DriveFile rootFolder; 12 | 13 | // We use a unique string's hash code as id, so the id we use here isn't really guaranteed to be unique. Fingers crossed :| 14 | private readonly Dictionary fileIDToFile = new Dictionary( 1024 ); 15 | 16 | private readonly System.Action onFolderExplored; 17 | private readonly System.Action onFilesRightClicked; 18 | 19 | private readonly CompareInfo textComparer; 20 | private readonly CompareOptions textCompareOptions; 21 | 22 | private readonly GUIContent sharedGUIContent = new GUIContent(); 23 | 24 | public FilesTreeView( TreeViewState treeViewState, MultiColumnHeader header, DriveFile rootFolder, System.Action onFolderExplored, System.Action onFilesRightClicked ) : base( treeViewState, header ) 25 | { 26 | this.rootFolder = rootFolder; 27 | 28 | this.onFolderExplored = onFolderExplored; 29 | this.onFilesRightClicked = onFilesRightClicked; 30 | 31 | textComparer = new CultureInfo( "en-US" ).CompareInfo; 32 | textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace; 33 | 34 | showBorder = true; 35 | 36 | header.visibleColumnsChanged += ( _header ) => _header.ResizeToFit(); 37 | header.sortingChanged += ( _header ) => 38 | { 39 | Reload(); 40 | 41 | if( HasSelection() ) 42 | SetSelection( GetSelection(), TreeViewSelectionOptions.RevealAndFrame ); 43 | }; 44 | 45 | Reload(); 46 | } 47 | 48 | protected override TreeViewItem BuildRoot() 49 | { 50 | return new TreeViewItem { id = 0, depth = -1 }; 51 | } 52 | 53 | protected override IList BuildRows( TreeViewItem root ) 54 | { 55 | List rows = ( GetRows() as List ) ?? new List( 256 ); 56 | rows.Clear(); 57 | 58 | if( !string.IsNullOrEmpty( searchString ) ) 59 | root.children = new List( 256 ); 60 | 61 | AddChildrenRecursive( rootFolder, root ); 62 | SortRowsRecursive( root.children, rows ); 63 | SetupDepthsFromParentsAndChildren( root ); 64 | 65 | return rows; 66 | } 67 | 68 | private void AddChildrenRecursive( DriveFile folder, TreeViewItem item ) 69 | { 70 | bool isSearching = !string.IsNullOrEmpty( searchString ); 71 | if( !isSearching ) // While searching, all files are added to the root item and its children is initialized in BuildRows 72 | item.children = new List( folder.children.Length ); 73 | 74 | for( int i = 0; i < folder.children.Length; i++ ) 75 | { 76 | DriveFile file = DriveAPI.GetFileByID( folder.children[i] ); 77 | int id = file.id.GetHashCode(); 78 | fileIDToFile[id] = file; 79 | 80 | if( !isSearching ) 81 | { 82 | TreeViewItem childItem = new TreeViewItem( id, -1, file.name ); 83 | item.AddChild( childItem ); 84 | 85 | if( file.children.Length > 0 && IsExpanded( id ) ) 86 | AddChildrenRecursive( file, childItem ); 87 | } 88 | else 89 | { 90 | if( textComparer.IndexOf( file.name, searchString, textCompareOptions ) >= 0 ) 91 | { 92 | TreeViewItem childItem = new TreeViewItem( id, -1, file.name ); 93 | item.AddChild( childItem ); 94 | } 95 | 96 | if( file.children.Length > 0 ) 97 | AddChildrenRecursive( file, item ); 98 | } 99 | } 100 | } 101 | 102 | public void Refresh( DriveFile refreshedFile ) 103 | { 104 | // SetExpanded internally rebuilds the TreeView so there is no need to call SetExpanded and Reload together 105 | if( !IsExpanded( refreshedFile.id.GetHashCode() ) ) 106 | SetExpanded( refreshedFile.id.GetHashCode(), true ); 107 | else 108 | Reload(); 109 | } 110 | 111 | protected override void RowGUI( RowGUIArgs args ) 112 | { 113 | DriveFile file = fileIDToFile[args.item.id]; 114 | 115 | if( Event.current.type == EventType.MouseDown && args.rowRect.Contains( Event.current.mousePosition ) ) 116 | { 117 | Rect previewSourceRect = args.rowRect; 118 | previewSourceRect.width = EditorGUIUtility.currentViewWidth; 119 | 120 | FilePreviewPopup.Show( previewSourceRect, file ); 121 | } 122 | 123 | for( int i = 0; i < args.GetNumVisibleColumns(); ++i ) 124 | { 125 | Rect cellRect = args.GetCellRect( i ); 126 | switch( args.GetColumn( i ) ) 127 | { 128 | case 0: // Filename 129 | { 130 | cellRect.xMin += GetContentIndent( args.item ); 131 | cellRect.y -= 2f; 132 | cellRect.height += 4f; // Incrementing height fixes cropped icon issue on Unity 2019.2 or earlier 133 | 134 | // Draw foldout arrow 135 | if( file.childrenState != FolderChildrenState.NoChildren && string.IsNullOrEmpty( searchString ) ) 136 | { 137 | Rect foldoutRect = new Rect( cellRect.x - foldoutWidth, cellRect.center.y - EditorGUIUtility.singleLineHeight * 0.5f, foldoutWidth, EditorGUIUtility.singleLineHeight ); 138 | 139 | // Folders that aren't explored yet will have green foldout arrows 140 | Color backgroundColor = GUI.backgroundColor; 141 | if( file.childrenState == FolderChildrenState.Unknown ) 142 | { 143 | if( EditorGUIUtility.isProSkin ) 144 | GUI.backgroundColor = Color.green; 145 | else // Foldout arrows don't turn green in light skin, we can show a green background icon instead 146 | GUI.DrawTexture( foldoutRect, EditorGUIUtility.Load( "d_greenLight" ) as Texture, ScaleMode.ScaleToFit ); 147 | } 148 | 149 | EditorGUI.BeginChangeCheck(); 150 | bool isExpanded = EditorGUI.Foldout( foldoutRect, file.childrenState != FolderChildrenState.Unknown && IsExpanded( args.item.id ), GUIContent.none ); 151 | if( EditorGUI.EndChangeCheck() ) 152 | { 153 | if( !isExpanded || file.childrenState == FolderChildrenState.HasChildren ) 154 | SetExpanded( args.item.id, isExpanded ); 155 | else 156 | onFolderExplored?.Invoke( file ); 157 | } 158 | 159 | GUI.backgroundColor = backgroundColor; 160 | } 161 | 162 | sharedGUIContent.text = file.name; 163 | sharedGUIContent.image = file.isFolder ? AssetDatabase.GetCachedIcon( "Assets" ) as Texture2D : UnityEditorInternal.InternalEditorUtility.GetIconForFile( file.name ); 164 | sharedGUIContent.tooltip = file.name; 165 | 166 | GUI.Label( cellRect, sharedGUIContent ); 167 | break; 168 | } 169 | case 1: // File size 170 | { 171 | if( !file.isFolder ) 172 | GUI.Label( cellRect, EditorUtility.FormatBytes( file.size ) ); 173 | 174 | break; 175 | } 176 | case 2: // Last modified date 177 | { 178 | GUI.Label( cellRect, file.modifiedTime.ToString( "dd.MM.yy HH:mm" ) ); 179 | break; 180 | } 181 | } 182 | } 183 | } 184 | 185 | private void SortRowsRecursive( List rows, List flattenedRows ) 186 | { 187 | if( rows == null || rows.Count == 0 ) 188 | return; 189 | 190 | if( rows.Count > 1 && multiColumnHeader.sortedColumnIndex != -1 ) 191 | { 192 | rows.Sort( ( r1, r2 ) => 193 | { 194 | DriveFile f1 = fileIDToFile[r1.id]; 195 | DriveFile f2 = fileIDToFile[r2.id]; 196 | 197 | switch( multiColumnHeader.sortedColumnIndex ) 198 | { 199 | case 0: // Sort by name 200 | { 201 | if( f1.isFolder && !f2.isFolder ) 202 | return -1; 203 | else if( !f1.isFolder && f2.isFolder ) 204 | return 1; 205 | 206 | return f1.name.CompareTo( f2.name ); 207 | } 208 | case 1: // Sort by file size 209 | { 210 | if( f1.isFolder && !f2.isFolder ) 211 | return -1; 212 | else if( !f1.isFolder && f2.isFolder ) 213 | return 1; 214 | 215 | return f1.size.CompareTo( f2.size ); 216 | } 217 | case 2: // Sort by last modified date 218 | { 219 | return f1.modifiedTimeTicks.CompareTo( f2.modifiedTimeTicks ); 220 | } 221 | default: return 0; 222 | } 223 | } ); 224 | 225 | if( !multiColumnHeader.IsSortedAscending( multiColumnHeader.sortedColumnIndex ) ) 226 | rows.Reverse(); 227 | } 228 | 229 | foreach( TreeViewItem childRow in rows ) 230 | { 231 | flattenedRows.Add( childRow ); 232 | SortRowsRecursive( childRow.children, flattenedRows ); 233 | } 234 | } 235 | 236 | protected override bool CanStartDrag( CanStartDragArgs args ) 237 | { 238 | return true; 239 | } 240 | 241 | protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) 242 | { 243 | IList sortedItemIDs = SortItemIDsInRowOrder( args.draggedItemIDs ); 244 | if( sortedItemIDs.Count == 0 ) 245 | return; 246 | 247 | string[] fileIDs = new string[sortedItemIDs.Count]; 248 | for( int i = 0; i < sortedItemIDs.Count; i++ ) 249 | fileIDs[i] = fileIDToFile[sortedItemIDs[i]].id; 250 | 251 | HelperFunctions.InitiateDragDropDownload( fileIDs ); 252 | } 253 | 254 | protected override void ContextClickedItem( int id ) 255 | { 256 | IList selection = GetSelection(); 257 | if( selection == null || selection.Count == 0 ) 258 | return; 259 | 260 | DriveFile[] selectedFiles = new DriveFile[selection.Count]; 261 | for( int i = 0; i < selection.Count; i++ ) 262 | selectedFiles[i] = fileIDToFile[selection[i]]; 263 | 264 | onFilesRightClicked?.Invoke( selectedFiles ); 265 | } 266 | 267 | protected override bool CanChangeExpandedState( TreeViewItem item ) 268 | { 269 | return false; // We draw the foldout arrow manually inside RowGUI 270 | } 271 | 272 | protected override IList GetDescendantsThatHaveChildren( int id ) 273 | { 274 | return new int[1] { id }; 275 | } 276 | 277 | protected override IList GetAncestors( int id ) 278 | { 279 | if( !fileIDToFile.TryGetValue( id, out DriveFile file ) ) 280 | return new int[1] { id }; 281 | 282 | List result = new List( 4 ) { id }; 283 | while( !string.IsNullOrEmpty( file.parentID ) ) 284 | { 285 | int _parentID = file.parentID.GetHashCode(); 286 | result.Add( _parentID ); 287 | file = fileIDToFile[_parentID]; 288 | } 289 | 290 | return result; 291 | } 292 | } 293 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/FileBrowser/FileBrowser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEditor.IMGUI.Controls; 4 | using UnityEngine; 5 | 6 | namespace DriveBrowser 7 | { 8 | public class FileBrowser : EditorWindow, IHasCustomMenu, ISerializationCallbackReceiver 9 | { 10 | private FilesTreeView filesTreeView; 11 | private TreeViewState filesTreeViewState; 12 | private MultiColumnHeaderState filesTreeViewHeaderState; 13 | private SearchField searchField; 14 | 15 | private string[] fileIDToFileSerializedKeys; 16 | private DriveFile[] fileIDToFileSerializedValues; 17 | private string[] userIDToUsernameSerialized; 18 | 19 | private Vector2 scrollPos; 20 | 21 | private bool m_isBusy; 22 | private bool IsBusy 23 | { 24 | get { return m_isBusy; } 25 | set 26 | { 27 | if( m_isBusy != value ) 28 | { 29 | m_isBusy = value; 30 | 31 | if( m_isBusy ) 32 | { 33 | #if UNITY_2019_1_OR_NEWER 34 | ShowNotification( new GUIContent( "Loading..." ), 10000.0 ); 35 | #else 36 | ShowNotification( new GUIContent( "Loading..." ) ); 37 | #endif 38 | HelperFunctions.LockAssemblyReload(); 39 | } 40 | else 41 | { 42 | HelperFunctions.UnlockAssemblyReload(); 43 | RemoveNotification(); 44 | } 45 | 46 | Repaint(); 47 | } 48 | } 49 | } 50 | 51 | [MenuItem( "Window/Drive Browser" )] 52 | public static void Initialize() 53 | { 54 | // Don't show this window until Google Cloud credentials are entered 55 | if( string.IsNullOrEmpty( GoogleCloudCredentials.Instance.ClientID ) || string.IsNullOrEmpty( GoogleCloudCredentials.Instance.ClientSecret ) ) 56 | Selection.activeObject = GoogleCloudCredentials.Instance; 57 | else 58 | { 59 | FileBrowser window = GetWindow(); 60 | window.titleContent = new GUIContent( "Drive Browser" ); 61 | window.minSize = new Vector2( 300f, 150f ); 62 | window.Show(); 63 | } 64 | } 65 | 66 | private void Awake() 67 | { 68 | // Wait for OnEnable to initialize the TreeView 69 | EditorApplication.delayCall += () => RefreshFolderAsync( DriveAPI.RootFolder ); 70 | } 71 | 72 | private void OnEnable() 73 | { 74 | if( filesTreeViewState == null ) 75 | filesTreeViewState = new TreeViewState(); 76 | 77 | MultiColumnHeader multiColumnHeader = HelperFunctions.GenerateMultiColumnHeader( ref filesTreeViewHeaderState, 0, 78 | new HelperFunctions.HeaderColumn( "Name", true, true, 30f, 0f, 0f ), 79 | new HelperFunctions.HeaderColumn( "Size", false, false, 30f, 0f, 70f ), 80 | new HelperFunctions.HeaderColumn( "Last Modified", false, false, 30f, 0f, 100f ) ); 81 | 82 | filesTreeView = new FilesTreeView( filesTreeViewState, multiColumnHeader, DriveAPI.RootFolder, RefreshFolderAsync, OnFilesRightClicked ); 83 | 84 | searchField = new SearchField(); 85 | searchField.downOrUpArrowKeyPressed += filesTreeView.SetFocusAndEnsureSelectedItem; 86 | } 87 | 88 | private void OnDisable() 89 | { 90 | FilePreviewPopup.Dispose(); 91 | } 92 | 93 | private void OnDestroy() 94 | { 95 | // Close all ActivityViewer and GlobalSearchWindow windows with this window since they are tied to this window 96 | ActivityViewer[] activityViewerWindows = Resources.FindObjectsOfTypeAll(); 97 | if( activityViewerWindows != null ) 98 | { 99 | foreach( ActivityViewer activityViewerWindow in activityViewerWindows ) 100 | { 101 | if( activityViewerWindow != null && !activityViewerWindow.Equals( null ) ) 102 | activityViewerWindow.Close(); 103 | } 104 | } 105 | 106 | GlobalSearchWindow[] globalSearchWindows = Resources.FindObjectsOfTypeAll(); 107 | if( globalSearchWindows != null ) 108 | { 109 | foreach( GlobalSearchWindow globalSearchWindow in globalSearchWindows ) 110 | { 111 | if( globalSearchWindow != null && !globalSearchWindow.Equals( null ) ) 112 | globalSearchWindow.Close(); 113 | } 114 | } 115 | 116 | IsBusy = false; 117 | } 118 | 119 | void IHasCustomMenu.AddItemsToMenu( GenericMenu menu ) 120 | { 121 | menu.AddItem( new GUIContent( "Help" ), false, () => 122 | { 123 | EditorUtility.DisplayDialog( "Help", 124 | "- To download files, either drag them to the Project window or right click them and select 'Download'\n" + 125 | "- Downloads for files larger than 5 MB can sometimes take ~20 seconds to initialize, unfortunately\n" + 126 | "- Green folders aren't explored yet and when they are expanded, their contents will asynchronously be fetched from the Drive servers\n" + 127 | "- Green folders aren't included in search since their contents aren't known until they are expanded\n" + 128 | "- To refresh a folder's contents, right click the folder and select 'Refresh'\n" + 129 | "- To switch Google accounts, click the 'Reauthenticate' button\n" + 130 | "- Google documents aren't actual files and thus, their file sizes will be displayed as 0 bytes\n" + 131 | "- In File Activity window, files highlighted in red color are permanently deleted and can't be downloaded\n" + 132 | "- Asynchronous operations usually prevent code compilation until the operation is completed but if you feel like your code won't compile or " + 133 | "Unity won't enter Play mode although all asynchronous operations are completed, click the 'Unstuck Compilation Pipeline' button", "OK" ); 134 | } ); 135 | 136 | menu.AddSeparator( "" ); 137 | 138 | menu.AddItem( new GUIContent( "Refresh Root" ), false, () => RefreshFolderAsync( DriveAPI.RootFolder ) ); 139 | 140 | menu.AddSeparator( "" ); 141 | 142 | menu.AddItem( new GUIContent( "Reauthenticate" ), false, () => 143 | { 144 | DriveAPI.RevokeAuthentication(); 145 | RefreshFolderAsync( DriveAPI.RootFolder ); 146 | } ); 147 | 148 | menu.AddSeparator( "" ); 149 | 150 | menu.AddItem( new GUIContent( "Clear Preview Cache" ), false, () => DriveAPI.ClearThumbnailCache() ); 151 | 152 | menu.AddItem( new GUIContent( "Unstuck Compilation Pipeline" ), false, () => HelperFunctions.UnlockAssemblyReload() ); 153 | } 154 | 155 | // Serialize DriveAPI's Dictionaries in this EditorWindow's arrays 156 | void ISerializationCallbackReceiver.OnBeforeSerialize() 157 | { 158 | DriveAPI.Serialize( out userIDToUsernameSerialized, out fileIDToFileSerializedKeys, out fileIDToFileSerializedValues ); 159 | } 160 | 161 | void ISerializationCallbackReceiver.OnAfterDeserialize() 162 | { 163 | DriveAPI.Deserialize( userIDToUsernameSerialized, fileIDToFileSerializedKeys, fileIDToFileSerializedValues ); 164 | } 165 | 166 | private void OnFilesRightClicked( DriveFile[] files ) 167 | { 168 | if( IsBusy || files == null || files.Length == 0 ) 169 | return; 170 | 171 | GenericMenu contextMenu = new GenericMenu(); 172 | 173 | if( files.Length == 1 ) 174 | { 175 | if( files[0].isFolder ) 176 | { 177 | contextMenu.AddItem( new GUIContent( "Refresh" ), false, () => RefreshFolderAsync( files[0] ) ); 178 | contextMenu.AddSeparator( "" ); 179 | } 180 | 181 | contextMenu.AddItem( new GUIContent( "Download" ), false, () => new DownloadRequest() { fileIDs = new string[1] { files[0].id } }.DownloadAsync() ); 182 | contextMenu.AddSeparator( "" ); 183 | contextMenu.AddItem( new GUIContent( "Open in Browser" ), false, () => files[0].OpenInBrowserAsync() ); 184 | 185 | if( files[0].size > 0L ) 186 | { 187 | contextMenu.AddSeparator( "" ); 188 | contextMenu.AddItem( new GUIContent( "MD5/Print" ), false, async () => Debug.Log( await files[0].GetMD5HashAsync() ) ); 189 | contextMenu.AddItem( new GUIContent( "MD5/Compare" ), false, async () => 190 | { 191 | string comparedFilePath = EditorUtility.OpenFilePanel( "Compare MD5 hash with file", Application.dataPath, "" ); 192 | if( string.IsNullOrEmpty( comparedFilePath ) ) 193 | return; 194 | 195 | string driveFileHash = await files[0].GetMD5HashAsync(); 196 | string localFileHash = HelperFunctions.CalculateMD5Hash( comparedFilePath ); 197 | Debug.Log( string.Concat( ( driveFileHash == localFileHash ) ? "MD5 hashes match:\n" : "MD5 hashes don't match:\n", 198 | "(Drive) ", files[0].name, ": ", driveFileHash, "\n", 199 | "(Local) ", comparedFilePath, ": ", localFileHash, "" ) ); 200 | } ); 201 | } 202 | 203 | contextMenu.AddSeparator( "" ); 204 | contextMenu.AddItem( new GUIContent( "View Activity" ), false, () => ActivityViewer.Initialize( this, files[0] ) ); 205 | } 206 | else 207 | contextMenu.AddItem( new GUIContent( "Download" ), false, () => new DownloadRequest() { fileIDs = files.GetFileIDs() }.DownloadAsync() ); 208 | 209 | contextMenu.ShowAsContext(); 210 | Repaint(); // Without this, context menu can appear seconds later which is annoying 211 | } 212 | 213 | private void OnGUI() 214 | { 215 | // Remove preview popup on mouse scroll wheel events 216 | if( Event.current.type == EventType.ScrollWheel ) 217 | FilePreviewPopup.Hide(); 218 | 219 | GUI.enabled = !IsBusy; 220 | 221 | scrollPos = EditorGUILayout.BeginScrollView( scrollPos ); 222 | 223 | EditorGUI.BeginChangeCheck(); 224 | bool wasSearching = !string.IsNullOrEmpty( filesTreeView.searchString ); 225 | filesTreeView.searchString = searchField.OnGUI( filesTreeView.searchString ); 226 | if( EditorGUI.EndChangeCheck() && filesTreeView.HasSelection() ) 227 | { 228 | bool isSearching = !string.IsNullOrEmpty( filesTreeView.searchString ); 229 | if( isSearching && !wasSearching ) // Clear selection when entering search because otherwise it can cause false-positives with the next 'else if' condition 230 | filesTreeView.SetSelection( new int[0] ); 231 | else if( !isSearching && wasSearching ) // Focus on the selected item when exiting search so that we can see the file's location 232 | filesTreeView.SetSelection( filesTreeView.GetSelection(), TreeViewSelectionOptions.RevealAndFrame ); 233 | } 234 | 235 | filesTreeView.OnGUI( GUILayoutUtility.GetRect( 0, 100000, 0, 100000 ) ); 236 | 237 | EditorGUILayout.EndScrollView(); 238 | 239 | if( !string.IsNullOrWhiteSpace( filesTreeView.searchString ) ) 240 | { 241 | EditorGUILayout.Space(); 242 | 243 | if( GUILayout.Button( $"Search '{filesTreeView.searchString}' In All Drive..." ) ) 244 | GlobalSearchWindow.Initialize( this, filesTreeView.searchString ); 245 | 246 | EditorGUILayout.Space(); 247 | } 248 | 249 | // This happens only when the mouse click is not captured by the TreeView 250 | // In this case, clear the TreeView's selection 251 | if( Event.current.type == EventType.MouseDown && Event.current.button == 0 ) 252 | { 253 | filesTreeView.SetSelection( new int[0] ); 254 | EditorApplication.delayCall += Repaint; 255 | } 256 | 257 | GUI.enabled = true; 258 | } 259 | 260 | private async void RefreshFolderAsync( DriveFile folder ) 261 | { 262 | if( IsBusy ) 263 | return; 264 | 265 | IsBusy = true; 266 | try 267 | { 268 | await folder.RefreshContentsAsync(); 269 | 270 | filesTreeView.searchString = ""; 271 | filesTreeView.Refresh( folder ); 272 | } 273 | finally 274 | { 275 | IsBusy = false; 276 | } 277 | } 278 | 279 | public void PingFile( DriveFile file ) 280 | { 281 | filesTreeView.SetSelection( new int[1] { file.id.GetHashCode() }, TreeViewSelectionOptions.RevealAndFrame ); 282 | Repaint(); 283 | } 284 | 285 | public async void SetSelectionAsync( IList files ) 286 | { 287 | if( IsBusy || files == null || files.Count == 0 ) 288 | return; 289 | 290 | IsBusy = true; 291 | try 292 | { 293 | HashSet exploredFolders = new HashSet(); 294 | List exploredFolderIDs = new List( files.Count * 4 ); 295 | List fileIDs = new List( files.Count ); 296 | 297 | filesTreeView.searchString = ""; 298 | 299 | await DriveAPI.RootFolder.RefreshContentsAsync(); 300 | 301 | foreach( DriveFile file in files ) 302 | { 303 | if( file == null || string.IsNullOrEmpty( file.id ) ) 304 | continue; 305 | 306 | // Explore the contents of all directories leading to this file so that they can be expanded in FilesTreeView 307 | DriveFile[] fileHierarchy = await file.LoadFileHierarchyAsync(); 308 | for( int i = 0; i < fileHierarchy.Length - 1; i++ ) 309 | { 310 | if( !exploredFolders.Contains( fileHierarchy[i].id ) ) 311 | { 312 | exploredFolders.Add( fileHierarchy[i].id ); 313 | exploredFolderIDs.Add( fileHierarchy[i].id.GetHashCode() ); 314 | 315 | await fileHierarchy[i].RefreshContentsAsync(); 316 | } 317 | } 318 | 319 | if( !fileIDs.Contains( file.id.GetHashCode() ) ) 320 | fileIDs.Add( file.id.GetHashCode() ); 321 | } 322 | 323 | filesTreeView.SetExpanded( exploredFolderIDs ); 324 | 325 | if( fileIDs.Count > 0 ) 326 | { 327 | filesTreeView.Reload(); 328 | filesTreeView.SetSelection( fileIDs, TreeViewSelectionOptions.RevealAndFrame ); 329 | 330 | Repaint(); 331 | } 332 | } 333 | finally 334 | { 335 | IsBusy = false; 336 | } 337 | } 338 | } 339 | } -------------------------------------------------------------------------------- /Plugins/DriveBrowser/Core/DriveAPI.cs: -------------------------------------------------------------------------------- 1 | using Google.Apis.Auth.OAuth2; 2 | using Google.Apis.Auth.OAuth2.Responses; 3 | using Google.Apis.Drive.v3; 4 | using Google.Apis.Drive.v3.Data; 5 | using Google.Apis.DriveActivity.v2; 6 | using Google.Apis.DriveActivity.v2.Data; 7 | using Google.Apis.PeopleService.v1; 8 | using Google.Apis.Services; 9 | using Google.Apis.Util.Store; 10 | using System.Collections.Generic; 11 | using System.Globalization; 12 | using System.IO; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using UnityEditor; 17 | using UnityEngine; 18 | using File = System.IO.File; 19 | using DFile = Google.Apis.Drive.v3.Data.File; 20 | using PPerson = Google.Apis.PeopleService.v1.Data.Person; 21 | 22 | namespace DriveBrowser 23 | { 24 | public static class DriveAPI 25 | { 26 | private enum DownloadConflictResolution { Undetermined = 0, AlwaysAsk = 1, AlwaysOverwrite = 2, AlwaysUseUniqueName = 3, AlwaysSkip = 4 }; 27 | 28 | private const string AUTH_TOKEN_PATH = "Library/DriveTokens"; 29 | private const string THUMBNAILS_DOWNLOAD_PATH = "Library/DriveThumbnails"; 30 | private const string REQUIRED_FILE_FIELDS = "id, name, mimeType, size, modifiedTime, parents"; 31 | private const string ROOT_FOLDER_ID = "Hello world, my old friend"; 32 | 33 | private const int MAX_CONCURRENT_DOWNLOAD_COUNT = 3; 34 | private const int DOWNLOAD_PROGRESS_REPORT_INTERVAL = 1_048_576; // Progress changes will be reported every 1 MB 35 | 36 | private static DriveService driveAPI; 37 | private static DriveActivityService driveActivityAPI; 38 | private static PeopleServiceService peopleAPI; 39 | 40 | private static readonly Dictionary fileIDToFile = new Dictionary( 1024 ); 41 | private static readonly Dictionary userIDToUsername = new Dictionary( 64 ); 42 | 43 | public static DriveFile RootFolder 44 | { 45 | get 46 | { 47 | if( !fileIDToFile.TryGetValue( ROOT_FOLDER_ID, out DriveFile rootFolder ) ) 48 | { 49 | rootFolder = new DriveFile( new DFile() ) 50 | { 51 | id = ROOT_FOLDER_ID, 52 | isFolder = true 53 | }; 54 | 55 | fileIDToFile[ROOT_FOLDER_ID] = rootFolder; 56 | } 57 | 58 | return rootFolder; 59 | } 60 | } 61 | 62 | private static readonly List folderContents = new List( 64 ); 63 | 64 | private static DownloadConflictResolution downloadConflictResolution; 65 | 66 | public static void Serialize( out string[] userIDToUsernameSerialized, out string[] fileIDToFileSerializedKeys, out DriveFile[] fileIDToFileSerializedValues ) 67 | { 68 | userIDToUsername.SerializeToArray( out userIDToUsernameSerialized ); 69 | fileIDToFile.SerializeToArray( out fileIDToFileSerializedKeys, out fileIDToFileSerializedValues ); 70 | } 71 | 72 | public static void Deserialize( string[] userIDToUsernameSerialized, string[] fileIDToFileSerializedKeys, DriveFile[] fileIDToFileSerializedValues ) 73 | { 74 | userIDToUsername.DeserializeFromArray( userIDToUsernameSerialized ); 75 | fileIDToFile.DeserializeFromArray( fileIDToFileSerializedKeys, fileIDToFileSerializedValues ); 76 | } 77 | 78 | public static async Task RefreshContentsAsync( this DriveFile folder ) 79 | { 80 | // We may call this function from anywhere using a cached DriveFile. During serialization or a prior RefreshContentsAsync call, 81 | // the fileIdToFile[folder.id] may no longer point to the DriveFile we have but rather an updated version of it. We always want to 82 | // refresh the contents of the up-to-date folder because we are modifying its children and childrenState; and modifying these variables 83 | // for a DriveFile that is no longer a part of the file hierarchy wouldn't make sense 84 | folder = fileIDToFile[folder.id]; 85 | 86 | bool isRootFolder = folder == RootFolder; 87 | folderContents.Clear(); 88 | 89 | try 90 | { 91 | string pageToken = null; 92 | do 93 | { 94 | FilesResource.ListRequest request = ( await GetDriveAPIAsync() ).Files.List(); 95 | request.PageSize = 50; 96 | request.Fields = $"nextPageToken, files({REQUIRED_FILE_FIELDS})"; 97 | request.PageToken = pageToken; 98 | 99 | if( isRootFolder ) 100 | request.Q = "('root' in parents or sharedWithMe = true) and trashed = false and mimeType != 'application/vnd.google-apps.shortcut'"; 101 | else 102 | request.Q = "'" + folder.id + "' in parents and trashed = false"; 103 | 104 | FileList result = await request.ExecuteAsync(); 105 | if( result.Files != null ) 106 | { 107 | foreach( DFile file in result.Files ) 108 | { 109 | // Root files should have their parentID set to null 110 | file.Parents = isRootFolder ? null : new string[1] { folder.id }; 111 | 112 | DriveFile _file = new DriveFile( file ); 113 | folderContents.Add( _file ); 114 | 115 | // Try not to lose the whole cached hierarchy while refreshing a root folder 116 | if( fileIDToFile.TryGetValue( file.Id, out DriveFile previouslyCachedFile ) ) 117 | { 118 | _file.children = previouslyCachedFile.children; 119 | _file.childrenState = previouslyCachedFile.childrenState; 120 | } 121 | 122 | fileIDToFile[file.Id] = _file; 123 | } 124 | } 125 | 126 | pageToken = result.NextPageToken; 127 | } while( pageToken != null ); 128 | 129 | folder.childrenState = ( folderContents.Count > 0 ) ? FolderChildrenState.HasChildren : FolderChildrenState.NoChildren; 130 | folder.children = new string[folderContents.Count]; 131 | for( int i = 0; i < folderContents.Count; i++ ) 132 | folder.children[i] = folderContents[i].id; 133 | } 134 | catch( System.Exception e ) 135 | { 136 | Debug.LogException( e ); 137 | folderContents.Clear(); 138 | } 139 | } 140 | 141 | public static async Task GetActivityAsync( this DriveFile file, ActivityEntryDelegate onEntryReceived, CancellationToken cancellationToken, int minimumEntryCount = 20, string pageToken = null ) 142 | { 143 | try 144 | { 145 | int receivedEntryCount = 0; 146 | do 147 | { 148 | QueryDriveActivityRequest request = new QueryDriveActivityRequest() 149 | { 150 | PageSize = minimumEntryCount, 151 | Filter = "detail.action_detail_case:(CREATE EDIT RENAME MOVE DELETE RESTORE)", 152 | PageToken = string.IsNullOrEmpty( pageToken ) ? null : pageToken 153 | }; 154 | 155 | if( file.isFolder ) 156 | request.AncestorName = "items/" + file.id; 157 | else 158 | request.ItemName = "items/" + file.id; 159 | 160 | QueryDriveActivityResponse result = await ( await GetDriveActivityAPIAsync() ).Activity.Query( request ).ExecuteAsync( cancellationToken ); 161 | if( result.Activities != null ) 162 | { 163 | StringBuilder sb = new StringBuilder( 200 ); 164 | 165 | foreach( DriveActivity activity in result.Activities ) 166 | { 167 | if( activity.Targets == null ) 168 | continue; 169 | 170 | foreach( Target targetFile in activity.Targets ) 171 | { 172 | if( targetFile.DriveItem == null ) 173 | continue; 174 | 175 | if( cancellationToken.IsCancellationRequested ) 176 | return null; 177 | 178 | ActivityEntry activityEntry = new ActivityEntry(); 179 | 180 | if( activity.Timestamp != null && activity.Timestamp is System.DateTime ) 181 | activityEntry.timeTicks = ( (System.DateTime) activity.Timestamp ).Ticks; 182 | else if( activity.TimeRange != null && activity.TimeRange.EndTime is System.DateTime ) 183 | activityEntry.timeTicks = ( (System.DateTime) activity.TimeRange.EndTime ).Ticks; 184 | 185 | activityEntry.username = ( activity.Actors != null && activity.Actors.Count > 0 ) ? await activity.Actors[0].GetUsernameAsync() : "Unknown User"; 186 | 187 | if( activity.PrimaryActionDetail.Create != null ) 188 | activityEntry.type = FileActivityType.Create; 189 | else if( activity.PrimaryActionDetail.Edit != null ) 190 | activityEntry.type = FileActivityType.Edit; 191 | else if( activity.PrimaryActionDetail.Rename != null ) 192 | activityEntry.type = FileActivityType.Rename; 193 | else if( activity.PrimaryActionDetail.Move != null ) 194 | activityEntry.type = FileActivityType.Move; 195 | else if( activity.PrimaryActionDetail.Delete != null ) 196 | activityEntry.type = FileActivityType.Delete; 197 | else if( activity.PrimaryActionDetail.Restore != null ) 198 | activityEntry.type = FileActivityType.Restore; 199 | else 200 | continue; // We aren't interested in other event types 201 | 202 | DriveFile changedFile = await GetFileByIDAsync( targetFile.DriveItem.Name.Replace( "items/", "" ) ); 203 | if( changedFile == null ) 204 | { 205 | activityEntry.isFolder = ( targetFile.DriveItem.DriveFolder != null ); 206 | activityEntry.size = -1L; 207 | 208 | // Alternative method of calculating relativePath that doesn't require DriveFile; but it can't find more than one parent directories 209 | sb.Length = 0; 210 | 211 | if( activity.Actions != null ) 212 | { 213 | foreach( Action activityDetails in activity.Actions ) 214 | { 215 | if( activityDetails.Detail != null && activityDetails.Detail.Move != null && activityDetails.Detail.Move.AddedParents != null && activityDetails.Detail.Move.AddedParents.Count > 0 && activityDetails.Detail.Move.AddedParents[0].DriveItem != null ) 216 | { 217 | sb.Append( activityDetails.Detail.Move.AddedParents[0].DriveItem.Title ).Append( "/" ); 218 | break; 219 | } 220 | } 221 | } 222 | 223 | activityEntry.relativePath = sb.Append( targetFile.DriveItem.Title ).ToString(); 224 | } 225 | else 226 | { 227 | activityEntry.fileID = changedFile.id; 228 | activityEntry.isFolder = changedFile.isFolder; 229 | activityEntry.size = changedFile.size; 230 | 231 | DriveFile[] fileHierarchy = await changedFile.LoadFileHierarchyAsync( file.id ); 232 | if( fileHierarchy.Length == 1 ) 233 | activityEntry.relativePath = targetFile.DriveItem.Title; 234 | else 235 | { 236 | sb.Length = 0; 237 | for( int i = 0; i < fileHierarchy.Length - 1; i++ ) 238 | sb.Append( fileHierarchy[i].name ).Append( "/" ); 239 | 240 | activityEntry.relativePath = sb.Append( targetFile.DriveItem.Title ).ToString(); 241 | } 242 | } 243 | 244 | onEntryReceived?.Invoke( activityEntry ); 245 | receivedEntryCount++; 246 | } 247 | } 248 | } 249 | 250 | pageToken = result.NextPageToken; 251 | } while( pageToken != null && receivedEntryCount < minimumEntryCount ); 252 | } 253 | catch( System.OperationCanceledException ) 254 | { 255 | return null; 256 | } 257 | catch( System.Exception e ) 258 | { 259 | Debug.LogException( e ); 260 | } 261 | 262 | return pageToken; 263 | } 264 | 265 | // The built-in "name contains 'World'" query unfortunately fails for "HelloWorld.txt" (i.e. it doesn't perform substring search) 266 | // Thus, to get the results that a human being would expect to see, we need to go through ALL files on Drive and search their names manually 267 | public static async Task PerformGlobalSearchAsync( string searchTerm, SearchResultEntryDelegate onEntryReceived, CancellationToken cancellationToken, int minimumEntryCount = 50, string pageToken = null ) 268 | { 269 | try 270 | { 271 | CompareInfo textComparer = new CultureInfo( "en-US" ).CompareInfo; 272 | CompareOptions textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace; 273 | 274 | int receivedEntryCount = 0; 275 | do 276 | { 277 | FilesResource.ListRequest request = ( await GetDriveAPIAsync() ).Files.List(); 278 | request.PageSize = 1000; 279 | request.Fields = "nextPageToken, files(id, name)"; 280 | request.Q = "trashed = false and mimeType != 'application/vnd.google-apps.shortcut'"; 281 | request.PageToken = pageToken; 282 | 283 | FileList result = await request.ExecuteAsync( cancellationToken ); 284 | if( result.Files != null ) 285 | { 286 | foreach( DFile file in result.Files ) 287 | { 288 | if( cancellationToken.IsCancellationRequested ) 289 | return null; 290 | 291 | // Manually search the file's name 292 | if( textComparer.IndexOf( file.Name, searchTerm, textCompareOptions ) < 0 ) 293 | continue; 294 | 295 | // Calling GetFileByIDAsync to fetch all of the required fields of the file from the server and not just its name 296 | onEntryReceived?.Invoke( await GetFileByIDAsync( file.Id ) ); 297 | receivedEntryCount++; 298 | } 299 | } 300 | 301 | pageToken = result.NextPageToken; 302 | } while( pageToken != null && receivedEntryCount < minimumEntryCount ); 303 | } 304 | catch( System.OperationCanceledException ) 305 | { 306 | return null; 307 | } 308 | catch( System.Exception e ) 309 | { 310 | Debug.LogException( e ); 311 | } 312 | 313 | return pageToken; 314 | } 315 | 316 | public static async Task GetUsernameAsync( this Actor actor ) 317 | { 318 | if( actor == null || actor.User == null || actor.User.KnownUser == null ) 319 | return "Unknown User"; 320 | 321 | if( userIDToUsername.TryGetValue( actor.User.KnownUser.PersonName, out string cachedResult ) ) 322 | return cachedResult; 323 | 324 | string username = "Unknown User"; 325 | try 326 | { 327 | PeopleResource.GetRequest request = ( await GetPeopleAPIAsync() ).People.Get( actor.User.KnownUser.PersonName ); 328 | request.PersonFields = "names"; 329 | 330 | PPerson result = await request.ExecuteAsync(); 331 | if( result.Names != null && result.Names.Count > 0 ) 332 | username = result.Names[0].DisplayName; 333 | 334 | userIDToUsername[actor.User.KnownUser.PersonName] = username; 335 | } 336 | catch( System.Exception e ) 337 | { 338 | Debug.LogException( e ); 339 | } 340 | 341 | return username; 342 | } 343 | 344 | public static async void DownloadAsync( this DownloadRequest downloadRequest ) 345 | { 346 | // Pick the download folder if it isn't already determined 347 | if( string.IsNullOrEmpty( downloadRequest.path ) ) 348 | { 349 | downloadRequest.path = EditorUtility.OpenFolderPanel( "Download file(s) to", "Assets", "" ); 350 | if( string.IsNullOrEmpty( downloadRequest.path ) ) 351 | return; 352 | } 353 | 354 | // Filter the files so that there are no duplicates or no parent-child relationships (which would result in duplicate downloads) 355 | List filesToDownload = new List(); 356 | for( int i = 0; i < downloadRequest.fileIDs.Length; i++ ) 357 | { 358 | if( string.IsNullOrEmpty( downloadRequest.fileIDs[i] ) ) 359 | continue; 360 | 361 | DriveFile file = fileIDToFile[downloadRequest.fileIDs[i]]; 362 | if( filesToDownload.Contains( file ) ) 363 | continue; 364 | 365 | // 1. If this file is parent of other files in the list, remove those child files from the list 366 | // 2. If another file in the list is parent of this file, don't add this file to the list 367 | bool shouldDownloadFile = true; 368 | for( int j = filesToDownload.Count - 1; j >= 0; j-- ) 369 | { 370 | if( await file.IsAncestorOfAsync( filesToDownload[j] ) ) 371 | filesToDownload.RemoveAt( j ); 372 | else if( await filesToDownload[j].IsAncestorOfAsync( file ) ) 373 | { 374 | shouldDownloadFile = false; 375 | break; 376 | } 377 | } 378 | 379 | if( shouldDownloadFile ) 380 | filesToDownload.Add( file ); 381 | } 382 | 383 | if( filesToDownload.Count == 0 ) 384 | { 385 | Debug.LogWarning( "No files to download..." ); 386 | return; 387 | } 388 | 389 | await GetDriveAPIAsync(); 390 | 391 | // If we are downloading a single file, don't show the "Always Overwrite" dialog. Otherwise, show it after the first conflict 392 | downloadConflictResolution = ( filesToDownload.Count > 1 || filesToDownload[0].isFolder ) ? DownloadConflictResolution.Undetermined : DownloadConflictResolution.AlwaysAsk; 393 | 394 | CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 395 | DownloadProgressViewer downloadProgressViewer = DownloadProgressViewer.Initialize( filesToDownload.Count, cancellationTokenSource ); 396 | 397 | HelperFunctions.LockAssemblyReload(); 398 | try 399 | { 400 | CancellationToken cancellationToken = cancellationTokenSource.Token; 401 | 402 | using( SemaphoreSlim downloadThrottler = new SemaphoreSlim( 0, MAX_CONCURRENT_DOWNLOAD_COUNT ) ) 403 | { 404 | Task[] downloadTasks = new Task[filesToDownload.Count]; 405 | for( int i = 0; i < filesToDownload.Count; i++ ) 406 | downloadTasks[i] = filesToDownload[i].DownloadAsync( downloadRequest.path, downloadProgressViewer, downloadThrottler, cancellationToken ); 407 | 408 | downloadThrottler.Release( MAX_CONCURRENT_DOWNLOAD_COUNT ); 409 | 410 | await Task.WhenAll( downloadTasks ); 411 | } 412 | 413 | if( cancellationToken.IsCancellationRequested ) 414 | Debug.Log( "Download canceled" ); 415 | } 416 | catch( System.Exception e ) 417 | { 418 | Debug.LogError( "FATAL DOWNLOAD ERROR" ); 419 | Debug.LogException( e ); 420 | } 421 | finally 422 | { 423 | HelperFunctions.UnlockAssemblyReload(); 424 | 425 | cancellationTokenSource.Dispose(); 426 | downloadProgressViewer.DownloadCompleted(); 427 | 428 | AssetDatabase.Refresh(); 429 | } 430 | } 431 | 432 | private static async Task DownloadAsync( this DriveFile file, string directory, DownloadProgressViewer downloadProgressViewer, SemaphoreSlim downloadThrottler, CancellationToken cancellationToken ) 433 | { 434 | bool throttledDownload = false; 435 | try 436 | { 437 | if( downloadThrottler != null ) 438 | { 439 | throttledDownload = true; 440 | await downloadThrottler.WaitAsync( cancellationToken ); 441 | } 442 | 443 | if( cancellationToken.IsCancellationRequested ) 444 | return; 445 | 446 | // Credit: https://stackoverflow.com/a/23182807/2373034 447 | string validFilename = string.Concat( file.name.Split( Path.GetInvalidFileNameChars() ) ); 448 | 449 | downloadProgressViewer.AddDownload( file ); 450 | 451 | if( file.isFolder ) 452 | { 453 | if( file.childrenState == FolderChildrenState.Unknown ) 454 | await file.RefreshContentsAsync(); 455 | 456 | if( throttledDownload ) 457 | { 458 | throttledDownload = false; 459 | downloadThrottler.Release(); 460 | } 461 | 462 | downloadProgressViewer.RemoveDownload( file ); 463 | 464 | string downloadPath = ProcessFileDownloadPath( Path.Combine( directory, validFilename ), true, out bool shouldSkipFolder ); 465 | if( shouldSkipFolder ) 466 | return; 467 | 468 | downloadProgressViewer.IncrementTotalFileCount( file.children.Length ); 469 | 470 | Directory.CreateDirectory( downloadPath ); 471 | 472 | Task[] downloadTasks = new Task[file.children.Length]; 473 | for( int i = 0; i < file.children.Length; i++ ) 474 | downloadTasks[i] = fileIDToFile[file.children[i]].DownloadAsync( downloadPath, downloadProgressViewer, downloadThrottler, cancellationToken ); 475 | 476 | await Task.WhenAll( downloadTasks ); 477 | } 478 | else 479 | { 480 | FilesResource.GetRequest request = driveAPI.Files.Get( file.id ); 481 | request.Fields = "copyRequiresWriterPermission, exportLinks"; 482 | 483 | DFile _file = await request.ExecuteAsync( cancellationToken ); 484 | if( _file.CopyRequiresWriterPermission.Value ) 485 | { 486 | Debug.LogWarning( "Can't download '" + file.name + "' because the owner has restricted access to it" ); 487 | return; 488 | } 489 | 490 | if( _file.ExportLinks != null && _file.ExportLinks.Count > 0 ) 491 | { 492 | // This is a Drive document, we need to export it 493 | string mimeType = "application/pdf", exportUrl; 494 | if( !_file.ExportLinks.TryGetValue( mimeType, out exportUrl ) ) 495 | { 496 | // If PDF export isn't supported, fallback to a supported mime type 497 | foreach( KeyValuePair kvPair in _file.ExportLinks ) 498 | { 499 | mimeType = kvPair.Key; 500 | exportUrl = kvPair.Value; 501 | 502 | break; 503 | } 504 | } 505 | 506 | Debug.Log( "Exporting '" + file.name + "' document with mime: " + mimeType ); 507 | 508 | // Determine file path 509 | const string EXPORT_FORMAT_TEXT = "&exportFormat="; 510 | int exportExtensionIndex = exportUrl.IndexOf( EXPORT_FORMAT_TEXT ); 511 | string exportExtension = ( exportExtensionIndex >= 0 ) ? ( "." + exportUrl.Substring( exportExtensionIndex + EXPORT_FORMAT_TEXT.Length ) ) : ""; 512 | 513 | string downloadPath = ProcessFileDownloadPath( Path.Combine( directory, validFilename + exportExtension ), false, out bool shouldSkipFile ); 514 | if( shouldSkipFile ) 515 | return; 516 | 517 | try 518 | { 519 | using( FileStream fileStream = File.Create( downloadPath ) ) 520 | { 521 | FilesResource.ExportRequest exportRequest = driveAPI.Files.Export( file.id, mimeType ); 522 | exportRequest.MediaDownloader.ProgressChanged += ( progress ) => downloadProgressViewer.SetProgress( file, progress.BytesDownloaded ); 523 | exportRequest.MediaDownloader.ChunkSize = DOWNLOAD_PROGRESS_REPORT_INTERVAL; 524 | Google.Apis.Download.IDownloadProgress exportResult = await exportRequest.DownloadAsync( fileStream, cancellationToken ); 525 | if( exportResult.Status == Google.Apis.Download.DownloadStatus.Failed ) 526 | Debug.LogWarning( "Failed to export: " + file.name + " " + exportResult.Exception ); 527 | } 528 | } 529 | catch( System.OperationCanceledException ) { } 530 | 531 | if( cancellationToken.IsCancellationRequested ) 532 | { 533 | File.Delete( downloadPath ); 534 | File.Delete( downloadPath + ".meta" ); 535 | } 536 | } 537 | else 538 | { 539 | // This is a normal file, we need to download it 540 | string downloadPath = ProcessFileDownloadPath( Path.Combine( directory, validFilename ), false, out bool shouldSkipFile ); 541 | if( shouldSkipFile ) 542 | return; 543 | 544 | try 545 | { 546 | using( FileStream fileStream = File.Create( downloadPath ) ) 547 | { 548 | FilesResource.GetRequest downloadRequest = driveAPI.Files.Get( file.id ); 549 | downloadRequest.MediaDownloader.ProgressChanged += ( progress ) => downloadProgressViewer.SetProgress( file, progress.BytesDownloaded ); 550 | downloadRequest.MediaDownloader.ChunkSize = DOWNLOAD_PROGRESS_REPORT_INTERVAL; 551 | Google.Apis.Download.IDownloadProgress downloadResult = await downloadRequest.DownloadAsync( fileStream, cancellationToken ); 552 | if( downloadResult.Status == Google.Apis.Download.DownloadStatus.Failed ) 553 | { 554 | // Drive downloads can fail for large downloads with error message 'cannotDownloadAbusiveFile' 555 | Google.GoogleApiException apiException = downloadResult.Exception as Google.GoogleApiException; 556 | if( apiException == null || !apiException.CheckErrorType( "cannotDownloadAbusiveFile" ) ) 557 | Debug.LogWarning( "Failed to download: " + file.name + " " + downloadResult.Exception ); 558 | else 559 | { 560 | //Debug.Log( "DEBUG: Downloading large file..." ); 561 | 562 | // Setting AcknowledgeAbuse to true gets rid of the 'cannotDownloadAbusiveFile' error 563 | downloadRequest.AcknowledgeAbuse = true; 564 | downloadResult = await downloadRequest.DownloadAsync( fileStream, cancellationToken ); 565 | if( downloadResult.Status == Google.Apis.Download.DownloadStatus.Failed ) 566 | Debug.LogWarning( "Failed to download: " + file.name + " " + downloadResult.Exception ); 567 | } 568 | } 569 | } 570 | } 571 | catch( System.OperationCanceledException ) { } 572 | 573 | if( cancellationToken.IsCancellationRequested ) 574 | { 575 | File.Delete( downloadPath ); 576 | File.Delete( downloadPath + ".meta" ); 577 | } 578 | } 579 | } 580 | } 581 | catch( Google.GoogleApiException e ) 582 | { 583 | if( e.CheckErrorType( "notFound" ) ) 584 | { 585 | Debug.LogWarning( "Can't download '" + file.name + "' because it seems like the file no longer exists" ); 586 | 587 | // Remove the deleted file from cached file hierarchy 588 | DriveFile parentFolder = string.IsNullOrEmpty( file.parentID ) ? RootFolder : fileIDToFile[file.parentID]; 589 | List parentFolderChildren = new List( parentFolder.children ); 590 | parentFolderChildren.Remove( file.id ); 591 | parentFolder.children = parentFolderChildren.ToArray(); 592 | } 593 | else if( e.CheckErrorType( "exportSizeLimitExceeded" ) ) 594 | Debug.LogWarning( "Can't export '" + file.name + "' because its file size exceeds the 10 MB limit" ); 595 | else 596 | Debug.LogException( e ); 597 | } 598 | catch( System.OperationCanceledException ) { } 599 | catch( System.Exception e ) 600 | { 601 | Debug.LogException( e ); 602 | } 603 | finally 604 | { 605 | if( throttledDownload && !cancellationToken.IsCancellationRequested ) 606 | downloadThrottler.Release(); 607 | 608 | downloadProgressViewer.RemoveDownload( file ); 609 | } 610 | } 611 | 612 | private static string ProcessFileDownloadPath( string downloadPath, bool isDirectory, out bool shouldSkipFile ) 613 | { 614 | shouldSkipFile = false; 615 | 616 | if( isDirectory ? Directory.Exists( downloadPath ) : File.Exists( downloadPath ) ) 617 | { 618 | int conflictResolutionStrategy; 619 | switch( downloadConflictResolution ) 620 | { 621 | case DownloadConflictResolution.AlwaysOverwrite: conflictResolutionStrategy = 0; break; 622 | case DownloadConflictResolution.AlwaysSkip: conflictResolutionStrategy = 1; break; 623 | case DownloadConflictResolution.AlwaysUseUniqueName: conflictResolutionStrategy = 2; break; 624 | default: 625 | { 626 | string noun = isDirectory ? "Folder" : "File"; 627 | conflictResolutionStrategy = EditorUtility.DisplayDialogComplex( $"{noun} Conflict", $"{noun} '" + Path.GetFileName( downloadPath ) + "' already exists at path: " + Path.GetDirectoryName( downloadPath ), isDirectory ? "Append" : "Overwrite", "Skip", "Use unique name" ); 628 | 629 | break; 630 | } 631 | } 632 | 633 | switch( conflictResolutionStrategy ) 634 | { 635 | case 0: 636 | { 637 | // Overwrite/Append 638 | if( downloadConflictResolution == DownloadConflictResolution.Undetermined ) 639 | downloadConflictResolution = EditorUtility.DisplayDialog( "Always Append/Overwrite", "When another conflict occurs, should the conflict automatically be resolved with Append (for directories) and Overwrite (for files)?", "Always Append/Overwrite", "Always Ask" ) ? DownloadConflictResolution.AlwaysOverwrite : DownloadConflictResolution.AlwaysAsk; 640 | 641 | break; 642 | } 643 | case 1: 644 | { 645 | // Skip 646 | shouldSkipFile = true; 647 | 648 | if( downloadConflictResolution == DownloadConflictResolution.Undetermined ) 649 | downloadConflictResolution = EditorUtility.DisplayDialog( "Always Skip", "When another conflict occurs, should the conflicted file/folder automatically be skipped?", "Always Skip", "Always Ask" ) ? DownloadConflictResolution.AlwaysSkip : DownloadConflictResolution.AlwaysAsk; 650 | 651 | break; 652 | } 653 | case 2: 654 | { 655 | // Use unique name 656 | string extension = isDirectory ? "" : Path.GetExtension( downloadPath ); 657 | if( extension == null ) 658 | extension = ""; 659 | 660 | string downloadPathWithoutExtension = downloadPath.Substring( 0, downloadPath.Length - extension.Length ) + " "; 661 | int fileSuffix = 1; 662 | do 663 | { 664 | downloadPath = downloadPathWithoutExtension + ( fileSuffix++ ) + extension; 665 | } while( isDirectory ? Directory.Exists( downloadPath ) : File.Exists( downloadPath ) ); 666 | 667 | if( downloadConflictResolution == DownloadConflictResolution.Undetermined ) 668 | downloadConflictResolution = EditorUtility.DisplayDialog( "Always Use Unique Name", "When another conflict occurs, should the conflict automatically be resolved by using a unique name?", "Always Use Unique Name", "Always Ask" ) ? DownloadConflictResolution.AlwaysUseUniqueName : DownloadConflictResolution.AlwaysAsk; 669 | 670 | break; 671 | } 672 | } 673 | } 674 | 675 | return downloadPath; 676 | } 677 | 678 | public static async Task GetThumbnailAsync( this DriveFile file, CancellationToken cancellationToken ) 679 | { 680 | try 681 | { 682 | string thumbnailPath = THUMBNAILS_DOWNLOAD_PATH + "/" + file.id; 683 | FileInfo thumbnailFile = new FileInfo( thumbnailPath ); 684 | if( thumbnailFile.Exists ) 685 | return thumbnailFile.Length > 0L ? thumbnailPath : null; 686 | 687 | Directory.CreateDirectory( THUMBNAILS_DOWNLOAD_PATH ); 688 | 689 | FilesResource.GetRequest request = ( await GetDriveAPIAsync() ).Files.Get( file.id ); 690 | request.Fields = "thumbnailLink"; 691 | 692 | DFile _file = await request.ExecuteAsync( cancellationToken ); 693 | string thumbnailLink = _file.ThumbnailLink; 694 | if( string.IsNullOrEmpty( thumbnailLink ) ) 695 | { 696 | thumbnailFile.Create().Close(); // Create an empty thumbnail file in cache 697 | return null; 698 | } 699 | 700 | if( cancellationToken.IsCancellationRequested ) 701 | return null; 702 | 703 | using( Stream networkStream = await driveAPI.HttpClient.GetStreamAsync( thumbnailLink ) ) 704 | using( FileStream fileStream = thumbnailFile.Create() ) 705 | { 706 | // Not passing CancellationToken to CopyToAsync because once the cached file is created, we don't want to leave 707 | // its contents empty by canceling the copy operation. Since thumbnail files are very small (~10KB on average), 708 | // this copy operation should take negligible time anyways 709 | await networkStream.CopyToAsync( fileStream, 81920 ); 710 | } 711 | 712 | return thumbnailPath; 713 | } 714 | catch( Google.GoogleApiException e ) 715 | { 716 | if( !e.CheckErrorType( "notFound" ) ) 717 | Debug.LogException( e ); 718 | 719 | return null; 720 | } 721 | catch( System.OperationCanceledException ) 722 | { 723 | return null; 724 | } 725 | catch( System.Exception e ) 726 | { 727 | Debug.LogException( e ); 728 | return null; 729 | } 730 | } 731 | 732 | public static async Task GetMD5HashAsync( this DriveFile file ) 733 | { 734 | FilesResource.GetRequest request = ( await GetDriveAPIAsync() ).Files.Get( file.id ); 735 | request.Fields = "md5Checksum"; 736 | 737 | return ( await request.ExecuteAsync() ).Md5Checksum; 738 | } 739 | 740 | public static async void OpenInBrowserAsync( this DriveFile file ) 741 | { 742 | FilesResource.GetRequest request = ( await GetDriveAPIAsync() ).Files.Get( file.id ); 743 | request.Fields = "webViewLink"; 744 | 745 | string url = ( await request.ExecuteAsync() ).WebViewLink; 746 | Application.OpenURL( url ); 747 | } 748 | 749 | // The DriveFile must be loaded before or it will throw Exception! 750 | public static DriveFile GetFileByID( string id ) 751 | { 752 | return fileIDToFile[id]; 753 | } 754 | 755 | public static async Task GetFileByIDAsync( string id ) 756 | { 757 | if( !fileIDToFile.TryGetValue( id, out DriveFile result ) ) 758 | { 759 | FilesResource.GetRequest request = ( await GetDriveAPIAsync() ).Files.Get( id ); 760 | request.Fields = REQUIRED_FILE_FIELDS; 761 | 762 | try 763 | { 764 | DFile file = await request.ExecuteAsync(); 765 | fileIDToFile[file.Id] = result = new DriveFile( file ); 766 | } 767 | catch( Google.GoogleApiException e ) 768 | { 769 | if( e.CheckErrorType( "notFound" ) ) 770 | return null; 771 | 772 | throw; 773 | } 774 | } 775 | 776 | return result; 777 | } 778 | 779 | public static async Task IsAncestorOfAsync( this DriveFile ancestor, DriveFile child ) 780 | { 781 | while( !string.IsNullOrEmpty( child.parentID ) ) 782 | { 783 | string _parentID = child.parentID; 784 | if( _parentID == ancestor.id ) 785 | return true; 786 | 787 | child = await GetFileByIDAsync( _parentID ); 788 | } 789 | 790 | return false; 791 | } 792 | 793 | public static async Task LoadFileHierarchyAsync( this DriveFile file, string relativeToFolderID = null ) 794 | { 795 | if( string.IsNullOrEmpty( file.parentID ) ) 796 | return new DriveFile[1] { file }; 797 | 798 | List pathComponents = new List( 6 ) { file }; 799 | while( !string.IsNullOrEmpty( file.parentID ) && file.parentID != relativeToFolderID ) 800 | { 801 | file = await GetFileByIDAsync( file.parentID ); 802 | if( file == null ) 803 | break; 804 | 805 | pathComponents.Add( file ); 806 | } 807 | 808 | pathComponents.Reverse(); 809 | return pathComponents.ToArray(); 810 | } 811 | 812 | [InitializeOnLoadMethod] 813 | private static void ClearThumbnailsOnExit() 814 | { 815 | // Clear thumbnail cache when exiting Unity 816 | EditorApplication.quitting -= ClearThumbnailCache; 817 | EditorApplication.quitting += ClearThumbnailCache; 818 | } 819 | 820 | public static void ClearThumbnailCache() 821 | { 822 | Directory.Delete( THUMBNAILS_DOWNLOAD_PATH, true ); 823 | } 824 | 825 | private static bool CheckErrorType( this Google.GoogleApiException exception, string expectedErrorType ) 826 | { 827 | if( exception != null && exception.Error != null && exception.Error.Errors != null ) 828 | { 829 | foreach( Google.Apis.Requests.SingleError error in exception.Error.Errors ) 830 | { 831 | if( error.Reason.Equals( expectedErrorType, System.StringComparison.OrdinalIgnoreCase ) ) 832 | return true; 833 | } 834 | } 835 | 836 | return false; 837 | } 838 | 839 | private static async Task GetDriveAPIAsync() 840 | { 841 | if( driveAPI == null ) 842 | await InitializeAPIs(); 843 | 844 | return driveAPI; 845 | } 846 | 847 | private static async Task GetDriveActivityAPIAsync() 848 | { 849 | if( driveActivityAPI == null ) 850 | await InitializeAPIs(); 851 | 852 | return driveActivityAPI; 853 | } 854 | 855 | private static async Task GetPeopleAPIAsync() 856 | { 857 | if( peopleAPI == null ) 858 | await InitializeAPIs(); 859 | 860 | return peopleAPI; 861 | } 862 | 863 | private static async Task InitializeAPIs() 864 | { 865 | ClientSecrets secrets = new ClientSecrets() 866 | { 867 | ClientId = GoogleCloudCredentials.Instance.ClientID, 868 | ClientSecret = GoogleCloudCredentials.Instance.ClientSecret 869 | }; 870 | 871 | string[] scopes = new string[] { DriveService.Scope.DriveReadonly, DriveActivityService.Scope.DriveActivityReadonly, PeopleServiceService.Scope.UserinfoProfile, PeopleServiceService.Scope.ContactsReadonly }; 872 | UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync( secrets, scopes, "user", CancellationToken.None, new FileDataStore( AUTH_TOKEN_PATH, true ) ); 873 | BaseClientService.Initializer apiInitializer = new BaseClientService.Initializer() 874 | { 875 | HttpClientInitializer = credential, 876 | ApplicationName = "Activity Viewer for Drive", 877 | }; 878 | 879 | driveAPI = new DriveService( apiInitializer ); 880 | driveActivityAPI = new DriveActivityService( apiInitializer ); 881 | peopleAPI = new PeopleServiceService( apiInitializer ); 882 | 883 | // Perform a dummy request to verify that our cached access tokens are still valid 884 | try 885 | { 886 | AboutResource.GetRequest request = driveAPI.About.Get(); 887 | request.Fields = "kind"; 888 | await request.ExecuteAsync(); 889 | } 890 | catch( TokenResponseException e ) 891 | { 892 | if( e.Error != null ) 893 | Debug.LogWarning( $"Drive access tokens were invalidated, reauthenticating. Reason:\"{e.Error.Error}\", Description:\"{e.Error.ErrorDescription}\", Uri:\"{e.Error.ErrorUri}\"" ); 894 | else 895 | Debug.LogException( e ); 896 | 897 | RevokeAuthentication(); 898 | await InitializeAPIs(); 899 | } 900 | } 901 | 902 | public static void RevokeAuthentication() 903 | { 904 | if( Directory.Exists( AUTH_TOKEN_PATH ) ) 905 | Directory.Delete( AUTH_TOKEN_PATH, true ); 906 | 907 | driveAPI = null; 908 | driveActivityAPI = null; 909 | peopleAPI = null; 910 | } 911 | } 912 | } --------------------------------------------------------------------------------