├── 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 |
2 |
3 |
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 |
19 |
`;
20 | }else{
21 | html += `
22 |
23 |
`;
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 += `share ${names[cur]} `;
103 | html += ``;*/
108 |
109 | // change into select
110 | html += ``;
111 | names.forEach((name, idx) => {
112 | html += `${name} `;
113 | });
114 | 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 |
136 |
137 | search
138 |
139 |
142 | close
143 |
`;
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 |
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 |
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 |
717 |
718 | Download Link
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 => ``)
780 | .join('');
781 | player_items += `
782 | `;
783 | const playBtn = `
784 |
785 | Play From External Player
786 |
787 |
788 | `;
789 |
790 | const content = `
791 |
792 |
793 |
794 |
795 |
796 |
797 |
${playBtn}
798 |
799 |
800 | Download Link
801 |
802 |
803 |
804 | HTML Refrence Adress
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 |
824 |
825 |
826 |
827 |
828 |
829 | Download link
830 |
831 |
832 |
833 |
HTML Reference address
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 ? `Previous ` : `Previous `}
888 |
889 |
890 | ${next_child ? `Next ` : `Next `}
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 | Download link
911 |
912 |
913 |
914 | HTML Reference address
915 |
916 |
917 |
918 | Markdown Reference address
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 |
--------------------------------------------------------------------------------