├── img ├── logo.png └── preview.png ├── css ├── mail-box-style.css └── classic-button.css ├── LICENSE ├── README.md └── Search ├── index.js └── themes └── material └── app.js /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-E-N-W-A-Y/GD-Index-Dark/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-E-N-W-A-Y/GD-Index-Dark/HEAD/img/preview.png -------------------------------------------------------------------------------- /css/mail-box-style.css: -------------------------------------------------------------------------------- 1 | html > body { 2 | /* override bootstrap background */ 3 | background: #f9f9f9; 4 | } 5 | 6 | footer p { 7 | margin-top: 50px; 8 | text-align: center; 9 | } 10 | 11 | .card-block { 12 | padding: 1.25rem; 13 | } 14 | 15 | .email-table { 16 | margin-top: 20px; 17 | } 18 | 19 | div.min-height { 20 | min-height: 400px; 21 | } 22 | 23 | header { 24 | background-color: #D9E2E9; 25 | padding-top: 5px; 26 | padding-bottom: 5px; 27 | margin-bottom: 15px; 28 | } 29 | 30 | #openRandomButton { 31 | 32 | margin-top: 6px; 33 | } 34 | 35 | .email-table > .email { 36 | border-top: 5px solid #7C96AB; 37 | 38 | } 39 | 40 | .header-shadow { 41 | box-shadow: 0 2px 2px rgba(182, 182, 182, 0.75); 42 | } 43 | 44 | .waiting-screen { 45 | padding: 40px 15px; 46 | text-align: center; 47 | } 48 | 49 | .random-column { 50 | border-left: 1px dashed #333; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /css/classic-button.css: -------------------------------------------------------------------------------- 1 | a.button7{ 2 | display:inline-block; 3 | padding:0.7em 1.7em; 4 | margin:0 0.3em 0.3em 0; 5 | border-radius:0.2em; 6 | box-sizing: border-box; 7 | text-decoration:none; 8 | font-family:'Roboto',sans-serif; 9 | font-weight:400; 10 | color:#FFFFFF; 11 | background-color:#3369ff; 12 | box-shadow:inset 0 -0.6em 1em -0.35em rgba(0,0,0,0.17),inset 0 0.6em 2em -0.3em rgba(255,255,255,0.15),inset 0 0 0em 0.05em rgba(255,255,255,0.12); 13 | text-align:center; 14 | position:relative; 15 | } 16 | a.button7:active{ 17 | box-shadow:inset 0 0.6em 2em -0.3em rgba(0,0,0,0.15),inset 0 0 0em 0.05em rgba(255,255,255,0.12); 18 | } 19 | @media all and (max-width:30em){ 20 | a.button7{ 21 | display:block; 22 | margin:0.4em auto; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jeffrey Harrison 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

LOGO

2 |
3 |

PREVIEW

4 | 5 | # Google Drive Index + Guide + Team Drive Maker [If you have G-suite Account] 6 | 7 | ## Full Guide to Deploy: 8 | 9 | [GUIDE](https://telegra.ph/G-Index-DarkMode--MultiAuth--English--TD-Maker--Custom-Domainga-Tutorial-04-29) 10 | 11 | ## SAMPLE 12 | 13 | https://g-d.rive.workers.dev/ 14 | 15 | ## Search Implemented 16 | - Features 17 | - Video Player - | mp4 | webm | avi | mpg | mpeg | mkv | rm | rmvb | mov | wmv | asf | ts | flv 18 | - Music Player - | mp3 | flac | wav | ogg | m4a 19 | - Document Viewer - | html | php | css | go | java | js | json | txt | sh | md | pdf 20 | - Image Viewer - | bmp | jpg | jpeg | png | gif 21 | - Multi drive encryption 22 | - Mobile Friendly 23 | -

ENGLISH LANGUAGE

24 | - Multi-level Search within the team drive 25 | - Dark Theme 26 | - Main Color: 27 | - red | pink | purple | deep-purple | indigo | blue | light-blue | cyan | teal | green | light-green | lime yellow | amber orange | deep-orange | brown | greyblue-grey 28 | 29 | - Accent Color: 30 | - red | pink | purple | deep-purple | indigo | blue | light-blue | cyan | teal | green | light-green | lime | yellow | amber | orange | deep-orange 31 | 32 | - darkmode: true/false 33 | 34 | # How to use? 35 | 36 | ## Deployment 37 | 38 | 39 | ### Manual 40 | 41 | 1.Install `rclone` software locally 42 | 2.Follow [https://rclone.org/drive/]( https://rclone.org/drive/) bind a drive 43 | 3.Execute the command`rclone config file` to find the file `rclone.conf` path 44 | 4.Open `rclone.conf`,find the configuration `root_folder_id` and `refresh_token` 45 | 5.Download index.js in https://github.com/LeeluPradhan/Search/ and fill in root and refresh_token 46 | 6.Deploy the code to [Cloudflare Workers](https://www.cloudflare.com/) 47 | 48 | ### Want to host to your repository? 49 | 1. Simply fork this 50 | or 51 | 2. Download this repo and upload files as per your choice 52 | 3. Use https://www.jsdelivr.com/?docs=gh to use your own `app.js` and change path accordingly in `index.js` 53 | 4. Copy your completed `index.js` to [Cloudflare Workers](https://www.cloudflare.com/) 54 | 4. Enjoy :) 55 | 56 | ### Credits 57 | https://github.com/yanzai/goindex 58 | https://github.com/LeeluPradhan/G-Index 59 | -------------------------------------------------------------------------------- /Search/index.js: -------------------------------------------------------------------------------- 1 | var authConfig = { 2 | siteName: 'GD-Index-Dark', // WebSite Name 3 | version: '1.0', // VersionControl, do not modify manually 4 | // Only material! 5 | theme: 'material', // material classic 6 | //add themes color, darkmode 7 | main_color: '', // red | pink | purple | deep-purple | indigo | blue | light-blue | cyan | teal | green | light-green | lime yellow | amber orange | deep-orange | brown | greyblue-grey 8 | accent_color: '', // red | pink | purple | deep-purple | indigo | blue | light-blue | cyan | teal | green | light-green | lime | yellow | amber | orange | deep-orange 9 | dark_theme: true, // true for dark theme 10 | // client_id & client_secret - PLEASE USE YOUR OWN! 11 | client_id: '', 12 | client_secret: '', 13 | refresh_token: '', // Refresh token 14 | 15 | /** 16 | * Set up multiple Drives to display; add multiples by format 17 | * [id] can be team disk id, subfolder id, or "root" (representing the root directory of personal disk); 18 |    * [name] The displayed name 19 |    * [user] Basic Auth username 20 |    * [pass] Basic Auth password 21 |    * Basic Auth of each disk can be set separately. Basic Auth takes effect on all paths under the disk, including subfolders, file chains on the disk, etc. 22 |    * No need for Basic Auth disk, just keep user and pass empty at the same time. (No need to set it directly) 23 | * [Note] For the disk whose id is set to the subfolder id, the search function will not be supported (it does not affect other disks) 24 | */ 25 | // It is possible to set only the password, only the user name, and the user name and password at the same time 26 | roots: [ 27 | { 28 | id: '', 29 | name: 'Sample1', 30 | user: 'admin', 31 | pass: '' 32 | }, 33 | { 34 | id: 'drive_id', 35 | name: 'Sample2', 36 | user: 'admin2', 37 | pass: 'index' 38 | }, 39 | { 40 | id: 'folder_id', 41 | name: 'Sample3', 42 | user: 'admin3', 43 | pass: 'index2' 44 | } 45 | ], 46 | /** 47 | * The number displayed on each page of the file list page. [Recommended setting value is between 100 and 1000]; 48 |     * If the setting is greater than 1000, it will cause an error when requesting drive api; 49 |     * If the set value is too small, the incremental loading (page loading) of the scroll bar of the file list page will be invalid 50 |     * Another effect of this value is that if the number of files in the directory is greater than this setting (that is, multiple pages need to be displayed), the results of the first listing directory will be cached. 51 | */ 52 | files_list_page_size: 500, 53 | /** 54 | * The number displayed on each page of the search results page. [Recommended setting value is between 50 and 1000]; 55 |     * If the setting is greater than 1000, it will cause an error when requesting drive api; 56 |     * If the set value is too small, it will cause the incremental loading (page loading) of the scroll bar of the search results page to fail; 57 |     * The size of this value affects the response speed of the search operation 58 | */ 59 | search_result_list_page_size: 50, 60 | // Confirm that cors can be opened 61 | enable_cors_file_down: false, 62 | // user_drive_real_root_id 63 | /** 64 |     * The above basic auth already contains the function of global protection in the disk. So by default, the password in the .password file is no longer authenticated; 65 |     * If you still need to verify the password in the .password file for certain directories based on global authentication, set this option to true; 66 |     * [Note] If the password verification of the .password file is enabled, the overhead of querying whether the .password file in the directory will be added each time the directory is listed. 67 |   */ 68 | 69 | "enable_password_file_verify": false 70 | 71 | }; 72 | 73 | 74 | /** 75 | * global functions 76 | */ 77 | const FUNCS = { 78 | /** 79 | * Transform into relatively safe search keywords for Google search morphology 80 | */ 81 | formatSearchKeyword: function (keyword) { 82 | let nothing = ""; 83 | let space = " "; 84 | if (!keyword) return nothing; 85 | return keyword.replace(/(!=)|['"=<>/\\:]/g, nothing) 86 | .replace(/[,,|(){}]/g, space) 87 | .trim() 88 | } 89 | 90 | }; 91 | 92 | /** 93 | * global consts 94 | * @type {{folder_mime_type: string, default_file_fields: string, gd_root_type: {share_drive: number, user_drive: number, sub_folder: number}}} 95 | */ 96 | const CONSTS = new (class { 97 | default_file_fields = 'parents,id,name,mimeType,modifiedTime,createdTime,fileExtension,size'; 98 | gd_root_type = { 99 | user_drive: 0, 100 | share_drive: 1, 101 | sub_folder: 2 102 | }; 103 | folder_mime_type = 'application/vnd.google-apps.folder'; 104 | })(); 105 | 106 | 107 | // gd instances 108 | var gds = []; 109 | 110 | function html(current_drive_order = 0, model = {}) { 111 | return ` 112 | 113 | 114 | 115 | 116 | 117 | ${authConfig.siteName} 118 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | `; 131 | }; 132 | 133 | addEventListener('fetch', event => { 134 | event.respondWith(handleRequest(event.request)); 135 | }); 136 | 137 | /** 138 | * Fetch and log a request 139 | * @param {Request} request 140 | */ 141 | async function handleRequest(request) { 142 | if (gds.length === 0) { 143 | for (let i = 0; i < authConfig.roots.length; i++) { 144 | const gd = new googleDrive(authConfig, i); 145 | await gd.init(); 146 | gds.push(gd) 147 | } 148 | // This operation is parallel to improve efficiency 149 | let tasks = []; 150 | gds.forEach(gd => { 151 | tasks.push(gd.initRootType()); 152 | }); 153 | for (let task of tasks) { 154 | await task; 155 | } 156 | } 157 | 158 | // Extract drive order from path 159 | // and get the corresponding gd instance according to the drive order 160 | let gd; 161 | let url = new URL(request.url); 162 | let path = url.pathname; 163 | 164 | /** 165 | * Redirect to start page 166 | * @returns {Response} 167 | */ 168 | function redirectToIndexPage() { 169 | return new Response('', {status: 301, headers: {'Location': `${url.origin}/0:/`}}); 170 | } 171 | 172 | if (path == '/') return redirectToIndexPage(); 173 | if (path.toLowerCase() == '/favicon.ico') { 174 | // You can find one later favicon 175 | return new Response('', {status: 404}) 176 | } 177 | 178 | // Special command format 179 | const command_reg = /^\/(?\d+):(?[a-zA-Z0-9]+)$/g; 180 | const match = command_reg.exec(path); 181 | if (match) { 182 | const num = match.groups.num; 183 | const order = Number(num); 184 | if (order >= 0 && order < gds.length) { 185 | gd = gds[order]; 186 | } else { 187 | return redirectToIndexPage() 188 | } 189 | const command = match.groups.command; 190 | // Search 191 | if (command === 'search') { 192 | if (request.method === 'POST') { 193 | // search results 194 | return handleSearch(request, gd); 195 | } else { 196 | const params = url.searchParams; 197 | // Search Page 198 | return new Response(html(gd.order, { 199 | q: params.get("q") || '', 200 | is_search_page: true, 201 | root_type: gd.root_type 202 | }), 203 | { 204 | status: 200, 205 | headers: {'Content-Type': 'text/html; charset=utf-8'} 206 | }); 207 | } 208 | } else if (command === 'id2path' && request.method === 'POST') { 209 | return handleId2Path(request, gd) 210 | } 211 | } 212 | 213 | // Desired path format 214 | const common_reg = /^\/\d+:\/.*$/g; 215 | try { 216 | if (!path.match(common_reg)) { 217 | return redirectToIndexPage(); 218 | } 219 | let split = path.split("/"); 220 | let order = Number(split[1].slice(0, -1)); 221 | if (order >= 0 && order < gds.length) { 222 | gd = gds[order]; 223 | } else { 224 | return redirectToIndexPage() 225 | } 226 | } catch (e) { 227 | return redirectToIndexPage() 228 | } 229 | 230 | // basic auth 231 | for (const r = gd.basicAuthResponse(request); r;) return r; 232 | 233 | path = path.replace(gd.url_path_prefix, '') || '/'; 234 | if (request.method == 'POST') { 235 | return apiRequest(request, gd); 236 | } 237 | 238 | let action = url.searchParams.get('a'); 239 | 240 | if (path.substr(-1) == '/' || action != null) { 241 | return new Response(html(gd.order, {root_type: gd.root_type}), { 242 | status: 200, 243 | headers: {'Content-Type': 'text/html; charset=utf-8'} 244 | }); 245 | } else { 246 | if (path.split('/').pop().toLowerCase() == ".password") { 247 | return new Response("", {status: 404}); 248 | } 249 | let file = await gd.file(path); 250 | let range = request.headers.get('Range'); 251 | const inline_down = 'true' === url.searchParams.get('inline'); 252 | return gd.down(file.id, range, inline_down); 253 | } 254 | } 255 | 256 | 257 | async function apiRequest(request, gd) { 258 | let url = new URL(request.url); 259 | let path = url.pathname; 260 | path = path.replace(gd.url_path_prefix, '') || '/'; 261 | 262 | let option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}} 263 | 264 | if (path.substr(-1) == '/') { 265 | let form = await request.formData(); 266 | // This can increase the speed of the first listing. The disadvantage is that if the password verification fails, the overhead of listing directories will still be incurred 267 | let deferred_list_result = gd.list(path, form.get('page_token'), Number(form.get('page_index'))); 268 | 269 | // check .password file, if `enable_password_file_verify` is true 270 | if (authConfig['enable_password_file_verify']) { 271 | let password = await gd.password(path); 272 | // console.log("dir password", password); 273 | if (password && password.replace("\n", "") !== form.get('password')) { 274 | let html = `{"error": {"code": 401,"message": "password error."}}`; 275 | return new Response(html, option); 276 | } 277 | } 278 | 279 | let list_result = await deferred_list_result; 280 | return new Response(JSON.stringify(list_result), option); 281 | } else { 282 | let file = await gd.file(path); 283 | let range = request.headers.get('Range'); 284 | return new Response(JSON.stringify(file)); 285 | } 286 | } 287 | 288 | // Deal With search 289 | async function handleSearch(request, gd) { 290 | const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}; 291 | let form = await request.formData(); 292 | let search_result = await 293 | gd.search(form.get('q') || '', form.get('page_token'), Number(form.get('page_index'))); 294 | return new Response(JSON.stringify(search_result), option); 295 | } 296 | 297 | /** 298 | * Deal With id2path 299 | * @param request Id parameter required 300 | * @param gd 301 | * @returns {Promise} [Note] If the item represented by the id received from the front desk is not under the target gd disk, then the response will be returned to the front desk with an empty string "" 302 | */ 303 | async function handleId2Path(request, gd) { 304 | const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}; 305 | let form = await request.formData(); 306 | let path = await gd.findPathById(form.get('id')); 307 | return new Response(path || '', option); 308 | } 309 | 310 | class googleDrive { 311 | constructor(authConfig, order) { 312 | // Each disk corresponds to an order, corresponding to a gd instance 313 | this.order = order; 314 | this.root = authConfig.roots[order]; 315 | this.url_path_prefix = `/${order}:`; 316 | this.authConfig = authConfig; 317 | // TODO: The invalid refresh strategy of these caches can be formulated later 318 | // path id 319 | this.paths = []; 320 | // path file 321 | this.files = []; 322 | // path pass 323 | this.passwords = []; 324 | // id <-> path 325 | this.id_path_cache = {}; 326 | this.id_path_cache[this.root['id']] = '/'; 327 | this.paths["/"] = this.root['id']; 328 | /*if (this.root['pass'] != "") { 329 | this.passwords['/'] = this.root['pass']; 330 | }*/ 331 | // this.init(); 332 | } 333 | 334 | /** 335 | * Initial authorization; then obtain user_drive_real_root_id 336 | * @returns {Promise} 337 | */ 338 | async init() { 339 | await this.accessToken(); 340 | /*await (async () => { 341 | // Get only 1 time 342 | if (authConfig.user_drive_real_root_id) return; 343 | const root_obj = await (gds[0] || this).findItemById('root'); 344 | if (root_obj && root_obj.id) { 345 | authConfig.user_drive_real_root_id = root_obj.id 346 | } 347 | })();*/ 348 | // Wait for user_drive_real_root_id and only get it once 349 | if (authConfig.user_drive_real_root_id) return; 350 | const root_obj = await (gds[0] || this).findItemById('root'); 351 | if (root_obj && root_obj.id) { 352 | authConfig.user_drive_real_root_id = root_obj.id 353 | } 354 | } 355 | 356 | /** 357 | * Get the root directory type, set to root_type 358 | * @returns {Promise} 359 | */ 360 | async initRootType() { 361 | const root_id = this.root['id']; 362 | const types = CONSTS.gd_root_type; 363 | if (root_id === 'root' || root_id === authConfig.user_drive_real_root_id) { 364 | this.root_type = types.user_drive; 365 | } else { 366 | const obj = await this.getShareDriveObjById(root_id); 367 | this.root_type = obj ? types.share_drive : types.sub_folder; 368 | } 369 | } 370 | /** 371 | * Returns a response that requires authorization, or null 372 | * @param request 373 | * @returns {Response|null} 374 | */ 375 | basicAuthResponse(request) { 376 | const user = this.root.user || '', 377 | pass = this.root.pass || '', 378 | _401 = new Response('Unauthorized', { 379 | headers: {'WWW-Authenticate': `Basic realm="goindex:drive:${this.order}"`}, 380 | status: 401 381 | }); 382 | if (user || pass) { 383 | const auth = request.headers.get('Authorization') 384 | if (auth) { 385 | try { 386 | const [received_user, received_pass] = atob(auth.split(' ').pop()).split(':'); 387 | return (received_user === user && received_pass === pass) ? null : _401; 388 | } catch (e) { 389 | } 390 | } 391 | } else return null; 392 | return _401; 393 | } 394 | 395 | async down(id, range = '', inline = false) { 396 | let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`; 397 | let requestOption = await this.requestOption(); 398 | requestOption.headers['Range'] = range; 399 | let res = await fetch(url, requestOption); 400 | const {headers} = res = new Response(res.body, res) 401 | this.authConfig.enable_cors_file_down && headers.append('Access-Control-Allow-Origin', '*'); 402 | inline === true && headers.set('Content-Disposition', 'inline'); 403 | return res; 404 | } 405 | 406 | async file(path) { 407 | if (typeof this.files[path] == 'undefined') { 408 | this.files[path] = await this._file(path); 409 | } 410 | return this.files[path]; 411 | } 412 | 413 | async _file(path) { 414 | let arr = path.split('/'); 415 | let name = arr.pop(); 416 | name = decodeURIComponent(name).replace(/\'/g, "\\'"); 417 | let dir = arr.join('/') + '/'; 418 | // console.log(name, dir); 419 | let parent = await this.findPathId(dir); 420 | // console.log(parent); 421 | let url = 'https://www.googleapis.com/drive/v3/files'; 422 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true}; 423 | params.q = `'${parent}' in parents and name = '${name}' and trashed = false`; 424 | params.fields = "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink)"; 425 | url += '?' + this.enQuery(params); 426 | let requestOption = await this.requestOption(); 427 | let response = await fetch(url, requestOption); 428 | let obj = await response.json(); 429 | // console.log(obj); 430 | return obj.files[0]; 431 | } 432 | 433 | // Cache through reqeust cache 434 | async list(path, page_token = null, page_index = 0) { 435 | if (this.path_children_cache == undefined) { 436 | // { :[ {nextPageToken:'',data:{}}, {nextPageToken:'',data:{}} ...], ...} 437 | this.path_children_cache = {}; 438 | } 439 | 440 | if (this.path_children_cache[path] 441 | && this.path_children_cache[path][page_index] 442 | && this.path_children_cache[path][page_index].data 443 | ) { 444 | let child_obj = this.path_children_cache[path][page_index]; 445 | return { 446 | nextPageToken: child_obj.nextPageToken || null, 447 | curPageIndex: page_index, 448 | data: child_obj.data 449 | }; 450 | } 451 | 452 | let id = await this.findPathId(path); 453 | let result = await this._ls(id, page_token, page_index); 454 | let data = result.data; 455 | // Cache multiple pages 456 | if (result.nextPageToken && data.files) { 457 | if (!Array.isArray(this.path_children_cache[path])) { 458 | this.path_children_cache[path] = [] 459 | } 460 | this.path_children_cache[path][Number(result.curPageIndex)] = { 461 | nextPageToken: result.nextPageToken, 462 | data: data 463 | }; 464 | } 465 | 466 | return result 467 | } 468 | 469 | 470 | async _ls(parent, page_token = null, page_index = 0) { 471 | // console.log("_ls", parent); 472 | 473 | if (parent == undefined) { 474 | return null; 475 | } 476 | let obj; 477 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true}; 478 | params.q = `'${parent}' in parents and trashed = false AND name !='.password'`; 479 | params.orderBy = 'folder,name,modifiedTime desc'; 480 | params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)"; 481 | params.pageSize = this.authConfig.files_list_page_size; 482 | 483 | if (page_token) { 484 | params.pageToken = page_token; 485 | } 486 | let url = 'https://www.googleapis.com/drive/v3/files'; 487 | url += '?' + this.enQuery(params); 488 | let requestOption = await this.requestOption(); 489 | let response = await fetch(url, requestOption); 490 | obj = await response.json(); 491 | 492 | return { 493 | nextPageToken: obj.nextPageToken || null, 494 | curPageIndex: page_index, 495 | data: obj 496 | }; 497 | 498 | /*do { 499 | if (pageToken) { 500 | params.pageToken = pageToken; 501 | } 502 | let url = 'https://www.googleapis.com/drive/v3/files'; 503 | url += '?' + this.enQuery(params); 504 | let requestOption = await this.requestOption(); 505 | let response = await fetch(url, requestOption); 506 | obj = await response.json(); 507 | files.push(...obj.files); 508 | pageToken = obj.nextPageToken; 509 | } while (pageToken);*/ 510 | 511 | } 512 | 513 | async password(path) { 514 | if (this.passwords[path] !== undefined) { 515 | return this.passwords[path]; 516 | } 517 | 518 | // console.log("load", path, ".password", this.passwords[path]); 519 | 520 | let file = await this.file(path + '.password'); 521 | if (file == undefined) { 522 | this.passwords[path] = null; 523 | } else { 524 | let url = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`; 525 | let requestOption = await this.requestOption(); 526 | let response = await this.fetch200(url, requestOption); 527 | this.passwords[path] = await response.text(); 528 | } 529 | 530 | return this.passwords[path]; 531 | } 532 | 533 | 534 | /** 535 | * Get share drive information by id 536 | * @param any_id 537 | * @returns {Promise} Any abnormal situation returns null 538 | */ 539 | async getShareDriveObjById(any_id) { 540 | if (!any_id) return null; 541 | if ('string' !== typeof any_id) return null; 542 | 543 | let url = `https://www.googleapis.com/drive/v3/drives/${any_id}`; 544 | let requestOption = await this.requestOption(); 545 | let res = await fetch(url, requestOption); 546 | let obj = await res.json(); 547 | if (obj && obj.id) return obj; 548 | 549 | return null 550 | } 551 | 552 | 553 | /** 554 | * search for 555 | * @returns {Promise<{data: null, nextPageToken: null, curPageIndex: number}>} 556 | */ 557 | async search(origin_keyword, page_token = null, page_index = 0) { 558 | const types = CONSTS.gd_root_type; 559 | const is_user_drive = this.root_type === types.user_drive; 560 | const is_share_drive = this.root_type === types.share_drive; 561 | 562 | const empty_result = { 563 | nextPageToken: null, 564 | curPageIndex: page_index, 565 | data: null 566 | }; 567 | 568 | if (!is_user_drive && !is_share_drive) { 569 | return empty_result; 570 | } 571 | let keyword = FUNCS.formatSearchKeyword(origin_keyword); 572 | if (!keyword) { 573 | // The keyword is empty, return 574 | return empty_result; 575 | } 576 | let words = keyword.split(/\s+/); 577 | let name_search_str = `name contains '${words.join("' AND name contains '")}'`; 578 | 579 | // corpora is a personal drive for user and a team drive for drive. With driveId 580 | let params = {}; 581 | if (is_user_drive) { 582 | params.corpora = 'user' 583 | } 584 | if (is_share_drive) { 585 | params.corpora = 'drive'; 586 | params.driveId = this.root.id; 587 | // This parameter will only be effective until June 1, 2020. Afterwards shared drive items will be included in the results. 588 | params.includeItemsFromAllDrives = true; 589 | params.supportsAllDrives = true; 590 | } 591 | if (page_token) { 592 | params.pageToken = page_token; 593 | } 594 | params.q = `trashed = false AND name !='.password' AND (${name_search_str})`; 595 | params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)"; 596 | params.pageSize = this.authConfig.search_result_list_page_size; 597 | // params.orderBy = 'folder,name,modifiedTime desc'; 598 | 599 | let url = 'https://www.googleapis.com/drive/v3/files'; 600 | url += '?' + this.enQuery(params); 601 | // console.log(params) 602 | let requestOption = await this.requestOption(); 603 | let response = await fetch(url, requestOption); 604 | let res_obj = await response.json(); 605 | 606 | return { 607 | nextPageToken: res_obj.nextPageToken || null, 608 | curPageIndex: page_index, 609 | data: res_obj 610 | }; 611 | } 612 | 613 | 614 | /** 615 | * Get the file object of the superior folder of this file or folder layer by layer. Note: It will be very slow! ! ! 616 | * Up to find the root directory (root id) of the current gd object 617 | * Only consider a single upward chain. 618 | * [Note] If the item represented by this id is not under the target gd disk, then this function will return null 619 | * 620 | * @param child_id 621 | * @param contain_myself 622 | * @returns {Promise<[]>} 623 | */ 624 | async findParentFilesRecursion(child_id, contain_myself = true) { 625 | const gd = this; 626 | const gd_root_id = gd.root.id; 627 | const user_drive_real_root_id = authConfig.user_drive_real_root_id; 628 | const is_user_drive = gd.root_type === CONSTS.gd_root_type.user_drive; 629 | 630 | // End point query id from bottom to top 631 | const target_top_id = is_user_drive ? user_drive_real_root_id : gd_root_id; 632 | const fields = CONSTS.default_file_fields; 633 | 634 | // [{},{},...] 635 | const parent_files = []; 636 | let meet_top = false; 637 | 638 | async function addItsFirstParent(file_obj) { 639 | if (!file_obj) return; 640 | if (!file_obj.parents) return; 641 | if (file_obj.parents.length < 1) return; 642 | 643 | // ['','',...] 644 | let p_ids = file_obj.parents; 645 | if (p_ids && p_ids.length > 0) { 646 | // its first parent 647 | const first_p_id = p_ids[0]; 648 | if (first_p_id === target_top_id) { 649 | meet_top = true; 650 | return; 651 | } 652 | const p_file_obj = await gd.findItemById(first_p_id); 653 | if (p_file_obj && p_file_obj.id) { 654 | parent_files.push(p_file_obj); 655 | await addItsFirstParent(p_file_obj); 656 | } 657 | } 658 | } 659 | 660 | const child_obj = await gd.findItemById(child_id); 661 | if (contain_myself) { 662 | parent_files.push(child_obj); 663 | } 664 | await addItsFirstParent(child_obj); 665 | 666 | return meet_top ? parent_files : null 667 | } 668 | 669 | /** 670 | * Get the path relative to the root directory of this disk 671 | * @param child_id 672 | * @returns {Promise} [Note] If the item represented by this id is not in the target gd disk, then this method will return an empty string "" 673 | */ 674 | async findPathById(child_id) { 675 | if (this.id_path_cache[child_id]) { 676 | return this.id_path_cache[child_id]; 677 | } 678 | 679 | const p_files = await this.findParentFilesRecursion(child_id); 680 | if (!p_files || p_files.length < 1) return ''; 681 | 682 | let cache = []; 683 | // Cache the path and id of each level found 684 | p_files.forEach((value, idx) => { 685 | const is_folder = idx === 0 ? (p_files[idx].mimeType === CONSTS.folder_mime_type) : true; 686 | let path = '/' + p_files.slice(idx).map(it => it.name).reverse().join('/'); 687 | if (is_folder) path += '/'; 688 | cache.push({id: p_files[idx].id, path: path}) 689 | }); 690 | 691 | cache.forEach((obj) => { 692 | this.id_path_cache[obj.id] = obj.path; 693 | this.paths[obj.path] = obj.id 694 | }); 695 | 696 | /*const is_folder = p_files[0].mimeType === CONSTS.folder_mime_type; 697 | let path = '/' + p_files.map(it => it.name).reverse().join('/'); 698 | if (is_folder) path += '/';*/ 699 | 700 | return cache[0].path; 701 | } 702 | 703 | 704 | // Get file item based on id 705 | async findItemById(id) { 706 | const is_user_drive = this.root_type === CONSTS.gd_root_type.user_drive; 707 | let url = `https://www.googleapis.com/drive/v3/files/${id}?fields=${CONSTS.default_file_fields}${is_user_drive ? '' : '&supportsAllDrives=true'}`; 708 | let requestOption = await this.requestOption(); 709 | let res = await fetch(url, requestOption); 710 | return await res.json() 711 | } 712 | 713 | async findPathId(path) { 714 | let c_path = '/'; 715 | let c_id = this.paths[c_path]; 716 | 717 | let arr = path.trim('/').split('/'); 718 | for (let name of arr) { 719 | c_path += name + '/'; 720 | 721 | if (typeof this.paths[c_path] == 'undefined') { 722 | let id = await this._findDirId(c_id, name); 723 | this.paths[c_path] = id; 724 | } 725 | 726 | c_id = this.paths[c_path]; 727 | if (c_id == undefined || c_id == null) { 728 | break; 729 | } 730 | } 731 | // console.log(this.paths); 732 | return this.paths[path]; 733 | } 734 | 735 | async _findDirId(parent, name) { 736 | name = decodeURIComponent(name).replace(/\'/g, "\\'"); 737 | 738 | // console.log("_findDirId", parent, name); 739 | 740 | if (parent == undefined) { 741 | return null; 742 | } 743 | 744 | let url = 'https://www.googleapis.com/drive/v3/files'; 745 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true}; 746 | params.q = `'${parent}' in parents and mimeType = 'application/vnd.google-apps.folder' and name = '${name}' and trashed = false`; 747 | params.fields = "nextPageToken, files(id, name, mimeType)"; 748 | url += '?' + this.enQuery(params); 749 | let requestOption = await this.requestOption(); 750 | let response = await fetch(url, requestOption); 751 | let obj = await response.json(); 752 | if (obj.files[0] == undefined) { 753 | return null; 754 | } 755 | return obj.files[0].id; 756 | } 757 | 758 | async accessToken() { 759 | console.log("accessToken"); 760 | if (this.authConfig.expires == undefined || this.authConfig.expires < Date.now()) { 761 | const obj = await this.fetchAccessToken(); 762 | if (obj.access_token != undefined) { 763 | this.authConfig.accessToken = obj.access_token; 764 | this.authConfig.expires = Date.now() + 3500 * 1000; 765 | } 766 | } 767 | return this.authConfig.accessToken; 768 | } 769 | 770 | async fetchAccessToken() { 771 | console.log("fetchAccessToken"); 772 | const url = "https://www.googleapis.com/oauth2/v4/token"; 773 | const headers = { 774 | 'Content-Type': 'application/x-www-form-urlencoded' 775 | }; 776 | const post_data = { 777 | 'client_id': this.authConfig.client_id, 778 | 'client_secret': this.authConfig.client_secret, 779 | 'refresh_token': this.authConfig.refresh_token, 780 | 'grant_type': 'refresh_token' 781 | } 782 | 783 | let requestOption = { 784 | 'method': 'POST', 785 | 'headers': headers, 786 | 'body': this.enQuery(post_data) 787 | }; 788 | 789 | const response = await fetch(url, requestOption); 790 | return await response.json(); 791 | } 792 | 793 | async fetch200(url, requestOption) { 794 | let response; 795 | for (let i = 0; i < 3; i++) { 796 | response = await fetch(url, requestOption); 797 | console.log(response.status); 798 | if (response.status != 403) { 799 | break; 800 | } 801 | await this.sleep(800 * (i + 1)); 802 | } 803 | return response; 804 | } 805 | 806 | async requestOption(headers = {}, method = 'GET') { 807 | const accessToken = await this.accessToken(); 808 | headers['authorization'] = 'Bearer ' + accessToken; 809 | return {'method': method, 'headers': headers}; 810 | } 811 | 812 | enQuery(data) { 813 | const ret = []; 814 | for (let d in data) { 815 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])); 816 | } 817 | return ret.join('&'); 818 | } 819 | 820 | sleep(ms) { 821 | return new Promise(function (resolve, reject) { 822 | let i = 0; 823 | setTimeout(function () { 824 | console.log('sleep' + ms); 825 | i++; 826 | if (i >= 2) reject(new Error('i>=2')); 827 | else resolve(i); 828 | }, ms); 829 | }) 830 | } 831 | } 832 | 833 | String.prototype.trim = function (char) { 834 | if (char) { 835 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), ''); 836 | } 837 | return this.replace(/^\s+|\s+$/g, ''); 838 | }; 839 | -------------------------------------------------------------------------------- /Search/themes/material/app.js: -------------------------------------------------------------------------------- 1 | // Load the necessary static in the head 2 | document.write(''); 3 | // markdown Standby 4 | document.write(''); 5 | document.write(''); 6 | 7 | if(dark){document.write('');} 8 | 9 | // Initialize the page and load the necessary resources 10 | function init(){ 11 | document.siteName = $('title').html(); 12 | $('body').addClass("mdui-theme-primary-"+main_color+" mdui-theme-accent-"+accent_color); 13 | var html = ""; 14 | html += ` 15 |
` 16 | if(dark){ 17 | html += ` 18 | `; 20 | }else{ 21 | html += ` 22 | `; 24 | } 25 | html += ` 26 |
27 |
28 |
`; 29 | $('body').html(html); 30 | } 31 | 32 | const Os = { 33 | isWindows: navigator.platform.toUpperCase().indexOf('WIN') > -1, // .includes 34 | isMac: navigator.platform.toUpperCase().indexOf('MAC') > -1, 35 | isMacLike: /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform), 36 | isIos: /(iPhone|iPod|iPad)/i.test(navigator.platform), 37 | isMobile: /Android|webOS|iPhone|iPad|iPod|iOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) 38 | }; 39 | 40 | function getDocumentHeight() { 41 | var D = document; 42 | return Math.max( 43 | D.body.scrollHeight, D.documentElement.scrollHeight, 44 | D.body.offsetHeight, D.documentElement.offsetHeight, 45 | D.body.clientHeight, D.documentElement.clientHeight 46 | ); 47 | } 48 | 49 | function render(path) { 50 | if (path.indexOf("?") > 0) { 51 | path = path.substr(0, path.indexOf("?")); 52 | } 53 | title(path); 54 | nav(path); 55 | // .../0: This 56 | var reg = /\/\d+:$/g; 57 | if (window.MODEL.is_search_page) { 58 | // Used to store the state of some scroll events 59 | window.scroll_status = { 60 | // Whether the scroll event is already bound 61 | event_bound: false, 62 | // "Scroll to the bottom, loading more data" event lock 63 | loading_lock: false 64 | }; 65 | render_search_result_list() 66 | } else if (path.match(reg) || path.substr(-1) == '/') { 67 | // Used to store the state of some scroll events 68 | window.scroll_status = { 69 | // Whether the scroll event is already bound 70 | event_bound: false, 71 | // "Scroll to the bottom, loading more data" event lock 72 | loading_lock: false 73 | }; 74 | list(path); 75 | } else { 76 | file(path); 77 | } 78 | } 79 | 80 | 81 | // Rendering title 82 | function title(path) { 83 | path = decodeURI(path); 84 | var cur = window.current_drive_order || 0; 85 | var drive_name = window.drive_names[cur]; 86 | path = path.replace(`/${cur}:`, ''); 87 | // $('title').html(document.siteName + ' - ' + path); 88 | var model = window.MODEL; 89 | if (model.is_search_page) 90 | $('title').html(`${document.siteName} - ${drive_name} - Search Result for ${model.q} `); 91 | else 92 | $('title').html(`${document.siteName} - ${drive_name} - ${path}`); 93 | } 94 | 95 | // Render the navigation bar 96 | function nav(path) { 97 | var model = window.MODEL; 98 | var html = ""; 99 | var cur = window.current_drive_order || 0; 100 | html += `${document.siteName}`; 101 | var names = window.drive_names; 102 | /*html += ``; 103 | html += `
    `; 104 | names.forEach((name, idx) => { 105 | html += `
  • ${name}
  • `; 106 | }); 107 | html += `
`;*/ 108 | 109 | // change into select 110 | html += ``; 115 | 116 | if (!model.is_search_page) { 117 | var arr = path.trim('/').split('/'); 118 | var p = '/'; 119 | if (arr.length > 1) { 120 | arr.shift(); 121 | for (i in arr) { 122 | var n = arr[i]; 123 | n = decodeURI(n); 124 | p += n + '/'; 125 | if (n == '') { 126 | break; 127 | } 128 | html += `chevron_right${n}`; 129 | } 130 | } 131 | } 132 | var search_text = model.is_search_page ? (model.q || '') : ''; 133 | const isMobile = Os.isMobile; 134 | var search_bar = `
135 | `; 144 | 145 | // Personal or team 146 | if (model.root_type < 2) { 147 | // Show search box 148 | html += search_bar; 149 | } 150 | 151 | $('#nav').html(html); 152 | mdui.mutation(); 153 | mdui.updateTextFields(); 154 | } 155 | 156 | /** 157 | * Initiate POST request for listing 158 | * @param path Path 159 | * @param params Form params 160 | * @param resultCallback Success Result Callback 161 | * @param authErrorCallback Pass Error Callback 162 | */ 163 | function requestListPath(path, params, resultCallback, authErrorCallback) { 164 | var p = { 165 | password: params['password'] || null, 166 | page_token: params['page_token'] || null, 167 | page_index: params['page_index'] || 0 168 | }; 169 | $.post(path, p, function (data, status) { 170 | var res = jQuery.parseJSON(data); 171 | if (res && res.error && res.error.code == '401') { 172 | // Password verification failed 173 | if (authErrorCallback) authErrorCallback(path) 174 | } else if (res && res.data) { 175 | if (resultCallback) resultCallback(res, path, p) 176 | } 177 | }) 178 | } 179 | 180 | /** 181 | * Search POST request 182 | * @param params Form params 183 | * @param resultCallback Success callback 184 | */ 185 | function requestSearch(params, resultCallback) { 186 | var p = { 187 | q: params['q'] || null, 188 | page_token: params['page_token'] || null, 189 | page_index: params['page_index'] || 0 190 | }; 191 | $.post(`/${window.current_drive_order}:search`, p, function (data, status) { 192 | var res = jQuery.parseJSON(data); 193 | if (res && res.data) { 194 | if (resultCallback) resultCallback(res, p) 195 | } 196 | }) 197 | } 198 | 199 | 200 | // Render file list 201 | function list(path) { 202 | var content = ` 203 | 204 |
205 |
    206 |
  • 207 |
    208 | File 209 | expand_more 210 |
    211 |
    212 | Time 213 | expand_more 214 |
    215 |
    216 | Size 217 | expand_more 218 |
    219 |
  • 220 |
221 |
222 |
223 |
    224 |
225 |
Total Item
226 |
227 | 228 | `; 229 | $('#content').html(content); 230 | 231 | var password = localStorage.getItem('password' + path); 232 | $('#list').html(`
`); 233 | $('#readme_md').hide().html(''); 234 | $('#head_md').hide().html(''); 235 | 236 | /** 237 | * Callback after successful data return from column directory request 238 | * @param res Returned result (object) 239 | * @param path Requested path 240 | * @param prevReqParams Parameters used in the request 241 | */ 242 | function successResultCallback(res, path, prevReqParams) { 243 | 244 | // Temporarily store nextPageToken and currentPageIndex in the list element 245 | $('#list') 246 | .data('nextPageToken', res['nextPageToken']) 247 | .data('curPageIndex', res['curPageIndex']); 248 | 249 | // Remove loading spinner 250 | $('#spinner').remove(); 251 | 252 | if (res['nextPageToken'] === null) { 253 | // If it is the last page, unbind the scroll event, reset scroll_status, and append data 254 | $(window).off('scroll'); 255 | window.scroll_status.event_bound = false; 256 | window.scroll_status.loading_lock = false; 257 | append_files_to_list(path, res['data']['files']); 258 | } else { 259 | // If it is not the last page, append data and bind the scroll event (if not already bound), update scroll_status 260 | append_files_to_list(path, res['data']['files']); 261 | if (window.scroll_status.event_bound !== true) { 262 | // Bind event, if not yet bound 263 | $(window).on('scroll', function () { 264 | var scrollTop = $(this).scrollTop(); 265 | var scrollHeight = getDocumentHeight(); 266 | var windowHeight = $(this).height(); 267 | // Roll to the bottom 268 | if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) { 269 | /* 270 | When the event of scrolling to the bottom is triggered, if it is already loading at this time, the event is ignored; 271 | Otherwise, go to loading and occupy the loading lock, indicating that loading is in progress 272 | */ 273 | if (window.scroll_status.loading_lock === true) { 274 | return; 275 | } 276 | window.scroll_status.loading_lock = true; 277 | 278 | // Show one loading spinner 279 | $(`
`) 280 | .insertBefore('#readme_md'); 281 | mdui.updateSpinners(); 282 | // mdui.mutation(); 283 | 284 | let $list = $('#list'); 285 | requestListPath(path, { 286 | password: prevReqParams['password'], 287 | page_token: $list.data('nextPageToken'), 288 | // Request next page 289 | page_index: $list.data('curPageIndex') + 1 290 | }, 291 | successResultCallback, 292 | // The password is the same as before. Will not appear authError 293 | null 294 | ) 295 | } 296 | }); 297 | window.scroll_status.event_bound = true 298 | } 299 | } 300 | 301 | // After loading successfully and rendering new data successfully, release the loading lock so that you can continue to process the "scroll to bottom" event 302 | if (window.scroll_status.loading_lock === true) { 303 | window.scroll_status.loading_lock = false 304 | } 305 | } 306 | 307 | // Start requesting data from page 1 308 | requestListPath(path, {password: password}, 309 | successResultCallback, 310 | function (path) { 311 | $('#spinner').remove(); 312 | var pass = prompt("Access Denied, please enter the password", ""); 313 | localStorage.setItem('password' + path, pass); 314 | if (pass != null && pass != "") { 315 | list(path); 316 | } else { 317 | history.go(-1); 318 | } 319 | }); 320 | } 321 | 322 | /** 323 | * Append the data of the new page requested to the list 324 | * @param path path 325 | * @param files Requested results 326 | */ 327 | function append_files_to_list(path, files) { 328 | var $list = $('#list'); 329 | // Is it the last page of data? 330 | var is_lastpage_loaded = null === $list.data('nextPageToken'); 331 | var is_firstpage = '0' == $list.data('curPageIndex'); 332 | 333 | html = ""; 334 | let targetFiles = []; 335 | for (i in files) { 336 | var item = files[i]; 337 | var p = path + item.name + '/'; 338 | if (item['size'] == undefined) { 339 | item['size'] = ""; 340 | } 341 | 342 | item['modifiedTime'] = utc2beijing(item['modifiedTime']); 343 | item['size'] = formatFileSize(item['size']); 344 | if (item['mimeType'] == 'application/vnd.google-apps.folder') { 345 | html += `
  • 346 |
    347 | folder_open 348 | ${item.name} 349 |
    350 |
    ${item['modifiedTime']}
    351 |
    ${item['size']}
    352 |
    353 |
  • `; 354 | } else { 355 | var p = path + item.name; 356 | const filepath = path + item.name; 357 | var c = "file"; 358 | // README is displayed after the last page is loaded, otherwise it will affect the scroll event 359 | if (is_lastpage_loaded && item.name == "README.md") { 360 | get_file(p, item, function (data) { 361 | markdown("#readme_md", data); 362 | }); 363 | } 364 | if (item.name == "HEAD.md") { 365 | get_file(p, item, function (data) { 366 | markdown("#head_md", data); 367 | }); 368 | } 369 | var ext = p.split('.').pop().toLowerCase(); 370 | if ("|html|php|css|go|java|js|json|txt|sh|md|mp4|webm|avi|bmp|jpg|jpeg|png|gif|m4a|mp3|flac|wav|ogg|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|pdf|".indexOf(`|${ext}|`) >= 0) { 371 | targetFiles.push(filepath); 372 | p += "?a=view"; 373 | c += " view"; 374 | } 375 | html += `
  • 376 |
    377 | insert_drive_file 378 | ${item.name} 379 |
    380 |
    ${item['modifiedTime']}
    381 |
    ${item['size']}
    382 |
    383 |
  • `; 384 | } 385 | } 386 | 387 | /*let targetObj = {}; 388 | targetFiles.forEach((myFilepath, myIndex) => { 389 | if (!targetObj[myFilepath]) { 390 | targetObj[myFilepath] = { 391 | filepath: myFilepath, 392 | prev: myIndex === 0 ? null : targetFiles[myIndex - 1], 393 | next: myIndex === targetFiles.length - 1 ? null : targetFiles[myIndex + 1], 394 | } 395 | } 396 | }) 397 | // console.log(targetObj) 398 | if (Object.keys(targetObj).length) { 399 | localStorage.setItem(path, JSON.stringify(targetObj)); 400 | // console.log(path) 401 | }*/ 402 | 403 | if (targetFiles.length > 0) { 404 | let old = localStorage.getItem(path); 405 | let new_children = targetFiles; 406 | // Reset on page 1; otherwise append 407 | if (!is_firstpage && old) { 408 | let old_children; 409 | try { 410 | old_children = JSON.parse(old); 411 | if (!Array.isArray(old_children)) { 412 | old_children = [] 413 | } 414 | } catch (e) { 415 | old_children = []; 416 | } 417 | new_children = old_children.concat(targetFiles) 418 | } 419 | 420 | localStorage.setItem(path, JSON.stringify(new_children)) 421 | } 422 | 423 | // When it is page 1, remove the horizontal loading bar 424 | $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html); 425 | // When it is the last page, count and display the total number of items 426 | if (is_lastpage_loaded) { 427 | $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length); 428 | } 429 | } 430 | 431 | /** 432 | * Render the search result list. There is a lot of repetitive code, but there are different logics in it. 433 | */ 434 | function render_search_result_list() { 435 | var content = ` 436 | 437 |
    438 |
      439 |
    • 440 |
      441 | File 442 | expand_more 443 |
      444 |
      445 | Time 446 | expand_more 447 |
      448 |
      449 | Size 450 | expand_more 451 |
      452 |
    • 453 |
    454 |
    455 |
    456 |
      457 |
    458 |
    Total Item
    459 |
    460 | 461 | `; 462 | $('#content').html(content); 463 | 464 | $('#list').html(`
    `); 465 | $('#readme_md').hide().html(''); 466 | $('#head_md').hide().html(''); 467 | 468 | /** 469 | * The callback after the search request successfully returns data 470 | * @param res Results returned(object) 471 | * @param path Requested path 472 | * @param prevReqParams Parameters used in the request 473 | */ 474 | function searchSuccessCallback(res, prevReqParams) { 475 | 476 | // Temporarily store nextPageToken and currentPageIndex in the list element 477 | $('#list') 478 | .data('nextPageToken', res['nextPageToken']) 479 | .data('curPageIndex', res['curPageIndex']); 480 | 481 | // Removeloading spinner 482 | $('#spinner').remove(); 483 | 484 | if (res['nextPageToken'] === null) { 485 | // If it is the last page, unbind the scroll event, reset scroll_status, and append data 486 | $(window).off('scroll'); 487 | window.scroll_status.event_bound = false; 488 | window.scroll_status.loading_lock = false; 489 | append_search_result_to_list(res['data']['files']); 490 | } else { 491 | // If it is not the last page, append data and bind the scroll event (if not already bound), update scroll_status 492 | append_search_result_to_list(res['data']['files']); 493 | if (window.scroll_status.event_bound !== true) { 494 | // Bind event, if not yet bound 495 | $(window).on('scroll', function () { 496 | var scrollTop = $(this).scrollTop(); 497 | var scrollHeight = getDocumentHeight(); 498 | var windowHeight = $(this).height(); 499 | // Roll to the bottom 500 | if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) { 501 | /* 502 | When the event of scrolling to the bottom is triggered, if it is already loading at this time, the event is ignored; 503 | Otherwise, go to loading and occupy the loading lock, indicating that loading is in progress 504 | */ 505 | if (window.scroll_status.loading_lock === true) { 506 | return; 507 | } 508 | window.scroll_status.loading_lock = true; 509 | 510 | // Show one loading spinner 511 | $(`
    `) 512 | .insertBefore('#readme_md'); 513 | mdui.updateSpinners(); 514 | // mdui.mutation(); 515 | 516 | let $list = $('#list'); 517 | requestSearch({ 518 | q: window.MODEL.q, 519 | page_token: $list.data('nextPageToken'), 520 | // Request next page 521 | page_index: $list.data('curPageIndex') + 1 522 | }, 523 | searchSuccessCallback 524 | ) 525 | } 526 | }); 527 | window.scroll_status.event_bound = true 528 | } 529 | } 530 | 531 | // After loading successfully and rendering new data successfully, release the loading lock so that you can continue to process the "scroll to bottom" event 532 | if (window.scroll_status.loading_lock === true) { 533 | window.scroll_status.loading_lock = false 534 | } 535 | } 536 | 537 | // Start requesting data from page 1 538 | requestSearch({q: window.MODEL.q}, searchSuccessCallback); 539 | } 540 | 541 | /** 542 | * Append a new page of search results 543 | * @param files 544 | */ 545 | function append_search_result_to_list(files) { 546 | var $list = $('#list'); 547 | // Is it the last page of data? 548 | var is_lastpage_loaded = null === $list.data('nextPageToken'); 549 | // var is_firstpage = '0' == $list.data('curPageIndex'); 550 | 551 | html = ""; 552 | 553 | for (i in files) { 554 | var item = files[i]; 555 | if (item['size'] == undefined) { 556 | item['size'] = ""; 557 | } 558 | 559 | item['modifiedTime'] = utc2beijing(item['modifiedTime']); 560 | item['size'] = formatFileSize(item['size']); 561 | if (item['mimeType'] == 'application/vnd.google-apps.folder') { 562 | html += `
  • 563 |
    564 | folder_open 565 | ${item.name} 566 |
    567 |
    ${item['modifiedTime']}
    568 |
    ${item['size']}
    569 |
    570 |
  • `; 571 | } else { 572 | var c = "file"; 573 | var ext = item.name.split('.').pop().toLowerCase(); 574 | if ("|html|php|css|go|java|js|json|txt|sh|md|mp4|webm|avi|bmp|jpg|jpeg|png|gif|m4a|mp3|flac|wav|ogg|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|".indexOf(`|${ext}|`) >= 0) { 575 | c += " view"; 576 | } 577 | html += `
  • 578 |
    579 | insert_drive_file 580 | ${item.name} 581 |
    582 |
    ${item['modifiedTime']}
    583 |
    ${item['size']}
    584 |
    585 |
  • `; 586 | } 587 | } 588 | 589 | // When it is page 1, remove the horizontal loading bar 590 | $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html); 591 | // When it is the last page, count and display the total number of items 592 | if (is_lastpage_loaded) { 593 | $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length); 594 | } 595 | } 596 | 597 | /** 598 | * Search result item click event 599 | * @param a_ele Clicked element 600 | */ 601 | function onSearchResultItemClick(a_ele) { 602 | var me = $(a_ele); 603 | var can_preview = me.hasClass('view'); 604 | var cur = window.current_drive_order; 605 | var dialog = mdui.dialog({ 606 | title: '', 607 | content: '
    Getting target path...
    ', 608 | // content: '
    ', 609 | history: false, 610 | modal: true, 611 | closeOnEsc: true 612 | }); 613 | mdui.updateSpinners(); 614 | 615 | // Request to get the path 616 | $.post(`/${cur}:id2path`, {id: a_ele.id}, function (data) { 617 | if (data) { 618 | dialog.close(); 619 | var href = `/${cur}:${data}${can_preview ? '?a=view' : ''}`; 620 | dialog = mdui.dialog({ 621 | title: 'Target path', 622 | content: `${data}`, 623 | history: false, 624 | modal: true, 625 | closeOnEsc: true, 626 | buttons: [ 627 | { 628 | text: 'Open', onClick: function () { 629 | window.location.href = href 630 | } 631 | }, { 632 | text: 'Open in new tab', onClick: function () { 633 | window.open(href) 634 | } 635 | } 636 | , {text: 'Cancel'} 637 | ] 638 | }); 639 | return; 640 | } 641 | dialog.close(); 642 | dialog = mdui.dialog({ 643 | title: 'Failed to get the target path', 644 | content: 'o(╯□╰)o It may be because this item does not exist in the disk! It may also be because the file [Shared with me] has not been added to Personal Drive!', 645 | history: false, 646 | modal: true, 647 | closeOnEsc: true, 648 | buttons: [ 649 | {text: 'WTF ???'} 650 | ] 651 | }); 652 | }) 653 | } 654 | 655 | function get_file(path, file, callback) { 656 | var key = "file_path_" + path + file['modifiedTime']; 657 | var data = localStorage.getItem(key); 658 | if (data != undefined) { 659 | return callback(data); 660 | } else { 661 | $.get(path, function (d) { 662 | localStorage.setItem(key, d); 663 | callback(d); 664 | }); 665 | } 666 | } 667 | 668 | 669 | // File display? A = view 670 | function file(path) { 671 | var name = path.split('/').pop(); 672 | var ext = name.split('.').pop().toLowerCase().replace(`?a=view`, "").toLowerCase(); 673 | if ("|html|php|css|go|java|js|json|txt|sh|md|".indexOf(`|${ext}|`) >= 0) { 674 | return file_code(path); 675 | } 676 | 677 | if ("|mp4|webm|avi|".indexOf(`|${ext}|`) >= 0) { 678 | return file_video(path); 679 | } 680 | 681 | if ("|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|".indexOf(`|${ext}|`) >= 0) { 682 | return file_video(path); 683 | } 684 | 685 | if ("|mp3|flac|wav|ogg|m4a|".indexOf(`|${ext}|`) >= 0) { 686 | return file_audio(path); 687 | } 688 | 689 | if ("|bmp|jpg|jpeg|png|gif|".indexOf(`|${ext}|`) >= 0) { 690 | return file_image(path); 691 | } 692 | 693 | if ('pdf' === ext) return file_pdf(path); 694 | } 695 | 696 | // Document display |html|php|css|go|java|js|json|txt|sh|md| 697 | function file_code(path) { 698 | var type = { 699 | "html": "html", 700 | "php": "php", 701 | "css": "css", 702 | "go": "golang", 703 | "java": "java", 704 | "js": "javascript", 705 | "json": "json", 706 | "txt": "Text", 707 | "sh": "sh", 708 | "md": "Markdown", 709 | }; 710 | var name = path.split('/').pop(); 711 | var ext = name.split('.').pop().toLowerCase(); 712 | var href = window.location.origin + path; 713 | var content = ` 714 |
    715 |
    
     716 | 
    717 |
    718 | 719 | 720 |
    721 | file_download 722 | 723 | 724 | `; 725 | $('#content').html(content); 726 | 727 | $.get(path, function (data) { 728 | $('#editor').html($('
    ').text(data).html()); 729 | var code_type = "Text"; 730 | if (type[ext] != undefined) { 731 | code_type = type[ext]; 732 | } 733 | var editor = ace.edit("editor"); 734 | editor.setTheme("ace/theme/ambiance"); 735 | editor.setFontSize(18); 736 | editor.session.setMode("ace/mode/" + code_type); 737 | 738 | //Autocompletion 739 | editor.setOptions({ 740 | enableBasicAutocompletion: true, 741 | enableSnippets: true, 742 | enableLiveAutocompletion: true, 743 | maxLines: Infinity 744 | }); 745 | }); 746 | } 747 | function copyToClipboard(str) { 748 | const $temp = $(""); 749 | $("body").append($temp); 750 | $temp.val(str).select(); 751 | document.execCommand("copy"); 752 | $temp.remove(); 753 | } 754 | // Document display video |mp4|webm|avi| 755 | function file_video(path) { 756 | const url = window.location.origin + path; 757 | let player_items = [ 758 | { 759 | text: 'MXPlayer(Free)', 760 | href: `intent:${url}#Intent;package=com.mxtech.videoplayer.ad;S.title=${path};end`, 761 | }, 762 | { 763 | text: 'MXPlayer(Pro)', 764 | href: `intent:${url}#Intent;package=com.mxtech.videoplayer.pro;S.title=${path};end`, 765 | }, 766 | { 767 | text: 'nPlayer', 768 | href: `nplayer-${url}`, 769 | }, 770 | { 771 | text: 'VLC', 772 | href: `vlc://${url}`, 773 | }, 774 | { 775 | text: 'PotPlayer', 776 | href: `potplayer://${url}` 777 | } 778 | ] 779 | .map(it => `
  • ${it.text}
  • `) 780 | .join(''); 781 | player_items += `
  • 782 |
  • Copy Link
  • `; 783 | const playBtn = ` 784 | 787 | 788 |
      ${player_items}
    `; 789 | 790 | const content = ` 791 | 792 |
    793 |
    794 | 797 |
    ${playBtn} 798 | 799 |
    800 | 801 | 802 |
    803 |
    804 | 805 | 806 |
    807 |
    808 | file_download 809 | `; 810 | $('#content').html(content); 811 | $('#copy-link').on('click', () => { 812 | copyToClipboard(url); 813 | mdui.snackbar('Copied To Clipboard!'); 814 | }); 815 | } 816 | 817 | // File display Audio |mp3|flac|m4a|wav|ogg| 818 | function file_audio(path) { 819 | var url = window.location.origin + path; 820 | var content = ` 821 |
    822 |
    823 | 826 |
    827 | 828 |
    829 | 830 | 831 |
    832 |
    833 | 834 | 835 |
    836 |
    837 | file_download 838 | `; 839 | $('#content').html(content); 840 | } 841 | 842 | // Document display pdf pdf 843 | function file_pdf(path) { 844 | const url = window.location.origin + path; 845 | const inline_url = `${url}?inline=true` 846 | const file_name = decodeURI(path.slice(path.lastIndexOf('/') + 1, path.length)) 847 | var content = ` 848 | 849 | file_download 850 | `; 851 | $('#content').removeClass('mdui-container').addClass('mdui-container-fluid').css({padding: 0}).html(content); 852 | } 853 | 854 | // picture display 855 | function file_image(path) { 856 | var url = window.location.origin + path; 857 | // console.log(window.location.pathname) 858 | const currentPathname = window.location.pathname 859 | const lastIndex = currentPathname.lastIndexOf('/'); 860 | const fatherPathname = currentPathname.slice(0, lastIndex + 1); 861 | // console.log(fatherPathname) 862 | let target_children = localStorage.getItem(fatherPathname); 863 | // console.log(`fatherPathname: ${fatherPathname}`); 864 | // console.log(target_children) 865 | let targetText = ''; 866 | if (target_children) { 867 | try { 868 | target_children = JSON.parse(target_children); 869 | if (!Array.isArray(target_children)) { 870 | target_children = [] 871 | } 872 | } catch (e) { 873 | console.error(e); 874 | target_children = []; 875 | } 876 | if (target_children.length > 0 && target_children.includes(path)) { 877 | let len = target_children.length; 878 | let cur = target_children.indexOf(path); 879 | // console.log(`len = ${len}`) 880 | // console.log(`cur = ${cur}`) 881 | let prev_child = (cur - 1 > -1) ? target_children[cur - 1] : null; 882 | let next_child = (cur + 1 < len) ? target_children[cur + 1] : null; 883 | targetText = ` 884 |
    885 |
    886 |
    887 | ${prev_child ? `` : ``} 888 |
    889 |
    890 | ${next_child ? `` : ``} 891 |
    892 |
    893 |
    894 | `; 895 | } 896 | //
    897 | // ${targetObj[path].prev ? `Prev` : `Prev`} 898 | // ${targetObj[path].next ? `Next` : `Prev`} 899 | //
    900 | } 901 | var content = ` 902 |
    903 |
    904 |
    905 | ${targetText} 906 | 907 |
    908 |
    909 |
    910 | 911 | 912 |
    913 |
    914 | 915 | 916 |
    917 |
    918 | 919 | 920 |
    921 |
    922 |
    923 | file_download 924 | `; 925 | //my code 926 | $('#content').html(content); 927 | $('#leftBtn, #rightBtn').click((e) => { 928 | let target = $(e.target); 929 | if (['I', 'SPAN'].includes(e.target.nodeName)) { 930 | target = $(e.target).parent(); 931 | } 932 | const filepath = target.attr('data-filepath'); 933 | const direction = target.attr('data-direction'); 934 | //console.log (`$ {direction} turn page $ {filepath}`); 935 | file(filepath) 936 | }); 937 | } 938 | 939 | 940 | //Time conversion 941 | function utc2beijing(utc_datetime) { 942 | // Convert to normal time format year-month-day hour: minute: second 943 | var T_pos = utc_datetime.indexOf('T'); 944 | var Z_pos = utc_datetime.indexOf('Z'); 945 | var year_month_day = utc_datetime.substr(0, T_pos); 946 | var hour_minute_second = utc_datetime.substr(T_pos + 1, Z_pos - T_pos - 1); 947 | var new_datetime = year_month_day + " " + hour_minute_second; // 2017-03-31 08:02:06 948 | 949 | // Processing becomes timestamp 950 | timestamp = new Date(Date.parse(new_datetime)); 951 | timestamp = timestamp.getTime(); 952 | timestamp = timestamp / 1000; 953 | 954 | // 8 hours more, Beijing time is eight more time zones than UTC time 955 | var unixtimestamp = timestamp + 8 * 60 * 60; 956 | 957 | // Timestamp to time 958 | var unixtimestamp = new Date(unixtimestamp * 1000); 959 | var year = 1900 + unixtimestamp.getYear(); 960 | var month = "0" + (unixtimestamp.getMonth() + 1); 961 | var date = "0" + unixtimestamp.getDate(); 962 | var hour = "0" + unixtimestamp.getHours(); 963 | var minute = "0" + unixtimestamp.getMinutes(); 964 | var second = "0" + unixtimestamp.getSeconds(); 965 | return year + "-" + month.substring(month.length - 2, month.length) + "-" + date.substring(date.length - 2, date.length) 966 | + " " + hour.substring(hour.length - 2, hour.length) + ":" 967 | + minute.substring(minute.length - 2, minute.length) + ":" 968 | + second.substring(second.length - 2, second.length); 969 | } 970 | 971 | // bytes Adaptive conversion to KB, MB, GB 972 | function formatFileSize(bytes) { 973 | if (bytes >= 1000000000) { 974 | bytes = (bytes / 1000000000).toFixed(2) + ' GB'; 975 | } else if (bytes >= 1000000) { 976 | bytes = (bytes / 1000000).toFixed(2) + ' MB'; 977 | } else if (bytes >= 1000) { 978 | bytes = (bytes / 1000).toFixed(2) + ' KB'; 979 | } else if (bytes > 1) { 980 | bytes = bytes + ' bytes'; 981 | } else if (bytes == 1) { 982 | bytes = bytes + ' byte'; 983 | } else { 984 | bytes = ''; 985 | } 986 | return bytes; 987 | } 988 | 989 | String.prototype.trim = function (char) { 990 | if (char) { 991 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), ''); 992 | } 993 | return this.replace(/^\s+|\s+$/g, ''); 994 | }; 995 | 996 | 997 | // README.md HEAD.md stand by 998 | function markdown(el, data) { 999 | if (window.md == undefined) { 1000 | //$.getScript('https://cdn.jsdelivr.net/npm/markdown-it@10.0.0/dist/markdown-it.min.js',function(){ 1001 | window.md = window.markdownit(); 1002 | markdown(el, data); 1003 | //}); 1004 | } else { 1005 | var html = md.render(data); 1006 | $(el).show().html(html); 1007 | } 1008 | } 1009 | 1010 | // Listen for fallback events 1011 | window.onpopstate = function () { 1012 | var path = window.location.pathname; 1013 | render(path); 1014 | } 1015 | 1016 | 1017 | $(function () { 1018 | init(); 1019 | var path = window.location.pathname; 1020 | /*$("body").on("click", '.folder', function () { 1021 | var url = $(this).attr('href'); 1022 | history.pushState(null, null, url); 1023 | render(url); 1024 | return false; 1025 | }); 1026 | $("body").on("click", '.view', function () { 1027 | var url = $(this).attr('href'); 1028 | history.pushState(null, null, url); 1029 | render(url); 1030 | return false; 1031 | });*/ 1032 | 1033 | render(path); 1034 | }); 1035 | --------------------------------------------------------------------------------