├── imgs ├── 1.png ├── 2.png └── 3.png ├── themes ├── logo.png ├── classic │ └── app.js └── material │ └── app.js ├── README.md ├── README_zh.md └── index.js /imgs/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzai/goindex/HEAD/imgs/1.png -------------------------------------------------------------------------------- /imgs/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzai/goindex/HEAD/imgs/2.png -------------------------------------------------------------------------------- /imgs/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzai/goindex/HEAD/imgs/3.png -------------------------------------------------------------------------------- /themes/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzai/goindex/HEAD/themes/logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GoIndex](https://raw.githubusercontent.com/donwa/goindex/master/themes/logo.png) 2 | 3 | GoIndex 4 | ==== 5 | 6 | 这是一个 [修改版的goindex](https://github.com/yanzai/goindex) ,在 [原版goindex](https://github.com/donwa/goindex) 基础上添加了多盘支持、搜索、分页加载等功能。 7 | 8 | `index.js` 包含 Workers 所需的代码. 9 | 10 | ## 预览 11 | 12 | Demo: https://yanzai-goindex.java.workers.dev 13 | 14 | 15 | 16 | 多盘: 17 | ![多盘](imgs/1.png) 18 | 19 | 20 | 21 | 搜索: 22 | ![搜索](imgs/2.png) 23 | 24 | 25 | 26 | 分页: 27 | ![分页](imgs/3.png) 28 | 29 | 30 | 31 | ## 更新日志 32 | 33 | ### 2020-4-28 34 | 35 | - 添加 Basic Auth 认证,每个盘符可单独配置用户名和密码,可以保护该盘下所有子文件和子文件夹 36 | 37 | - 支持自定义 web 界面主题色,添加了 dark_mode ; 在 `uiConfig` 中可以配置 38 | 39 | - 原 goindex 的 .password 验证方式作为后备验证方式得以保留,但默认不开启 40 | 41 | 以上,详见 `index.js` 中的配置项的注释。 42 | 43 | ### 2020-4-23 44 | 45 | - 支持调用 nPlayer / MXPlayer Free / MXPlayer Pro / PotPlayer / VLC 播放,支持直接复制直链 46 | - 简单支持 PDF 文件预览 47 | - 可以配置是否允许其他 web 前端 cors 方式获取文件 48 | 49 | ### 2020-3-9 50 | 51 | - flac file play support 52 | 53 | ### 2020-3-7 54 | 55 | - 添加搜索功能,搜索结果分页增量展示,并支持跳转到对应路径浏览 56 | - 搜索功能支持个人盘和团队盘全盘搜索 57 | - 搜索分页大小可配置,具体见 `index.js` 注释 58 | - 尝试解决移动端滚动到底部时的增量加载问题 59 | - UI优化,盘符选择改为下拉框展示 60 | 61 | ### 2020-3-5 62 | 63 | - 文件列表页分页增量加载,支持自定义分页大小,多页内容的可以缓存,配置见 `index.js` 注释 64 | - 图片浏览页 下一张/上一张 导航 65 | - 优化列目录时的速度 66 | 67 | ### 2020-3-4 68 | 69 | 在原版基础上修改: 70 | 71 | - 添加多盘支持,自主设置要显示的多盘及各自密码 72 | - 前端只修改了 material ,故不支持 classic 主题 73 | - 配置见 `index.js` 注释 74 | 75 | 76 | --- 77 | 78 | 79 | 80 | > **安装部署可以参考原版,以下摘自原版 goindex 的部署说明:** 81 | 82 | 83 | 84 | ## Demo 85 | material: [https://index.gd.workers.dev/](https://index.gd.workers.dev/) 86 | classic: [https://indexc.gd.workers.dev/](https://indexc.gd.workers.dev/) 87 | 88 | ## Deployment 89 | 1.Install `rclone` software locally 90 | 2.Follow [https://rclone.org/drive/]( https://rclone.org/drive/) bind a drive 91 | 3.Execute the command`rclone config file` to find the file `rclone.conf` path 92 | 4.Open `rclone.conf`,find the configuration `root_folder_id` and `refresh_token` 93 | 5.Download index.js in https://github.com/donwa/goindex and fill in root and refresh_token 94 | 6.Deploy the code to [Cloudflare Workers](https://www.cloudflare.com/) 95 | 96 | ## Quick Deployment 97 | 1.Open https://installen.gd.workers.dev/ 98 | 2.Auth and get the code 99 | 3.Deploy the code to [Cloudflare Workers](https://www.cloudflare.com/) 100 | 101 | 102 | 103 | ## About 104 | Cloudflare Workers allow you to write JavaScript which runs on all of Cloudflare's 150+ global data centers. 105 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | ![GoIndex](https://raw.githubusercontent.com/donwa/goindex/master/themes/logo.png) 2 | 3 | GoIndex 4 | ==== 5 | 6 | 这是一个 [修改版的goindex](https://github.com/yanzai/goindex) ,在 [原版goindex](https://github.com/donwa/goindex) 基础上添加了多盘支持、搜索、分页加载等功能。 7 | 8 | `index.js` 包含 Workers 所需的代码. 9 | 10 | ## 预览 11 | 12 | Demo: https://yanzai-goindex.java.workers.dev 13 | 14 | 15 | 16 | 多盘: 17 | ![多盘](imgs/1.png) 18 | 19 | 20 | 21 | 搜索: 22 | ![搜索](imgs/2.png) 23 | 24 | 25 | 26 | 分页: 27 | ![分页](imgs/3.png) 28 | 29 | 30 | 31 | ## 更新日志 32 | 33 | ### 2020-4-28 34 | 35 | - 添加 Basic Auth 认证,每个盘符可单独配置用户名和密码,可以保护该盘下所有子文件和子文件夹 36 | 37 | - 支持自定义 web 界面主题色,添加了 dark_mode ; 在 `uiConfig` 中可以配置 38 | 39 | - 原 goindex 的 .password 验证方式作为后备验证方式得以保留,但默认不开启 40 | 41 | 以上,详见 `index.js` 中的配置项的注释。 42 | 43 | ### 2020-4-23 44 | 45 | - 支持调用 nPlayer / MXPlayer Free / MXPlayer Pro / PotPlayer / VLC 播放,支持直接复制直链 46 | - 简单支持 PDF 文件预览 47 | - 可以配置是否允许其他 web 前端 cors 方式获取文件 48 | 49 | ### 2020-3-9 50 | 51 | - flac file play support 52 | 53 | ### 2020-3-7 54 | 55 | - 添加搜索功能,搜索结果分页增量展示,并支持跳转到对应路径浏览 56 | - 搜索功能支持个人盘和团队盘全盘搜索 57 | - 搜索分页大小可配置,具体见 `index.js` 注释 58 | - 尝试解决移动端滚动到底部时的增量加载问题 59 | - UI优化,盘符选择改为下拉框展示 60 | 61 | ### 2020-3-5 62 | 63 | - 文件列表页分页增量加载,支持自定义分页大小,多页内容的可以缓存,配置见 `index.js` 注释 64 | - 图片浏览页 下一张/上一张 导航 65 | - 优化列目录时的速度 66 | 67 | ### 2020-3-4 68 | 69 | 在原版基础上修改: 70 | 71 | - 添加多盘支持,自主设置要显示的多盘及各自密码 72 | - 前端只修改了 material ,故不支持 classic 主题 73 | - 配置见 `index.js` 注释 74 | 75 | 76 | --- 77 | 78 | 79 | 80 | > **安装部署可以参考原版,以下摘自原版 goindex 的部署说明:** 81 | 82 | ## Demo 83 | material: 84 | [https://index.gd.workers.dev/](https://index.gd.workers.dev/) 85 | classic: 86 | [https://indexc.gd.workers.dev/](https://indexc.gd.workers.dev/) 87 | 88 | ## 安装部署方案1 89 | 1、在本地安装 rclone 90 | 2、按照 https://rclone.org/drive/ 流程进行授权。 91 | 3、执行 rclone config file 查看 rclone.conf 路径。找到root_folder_id和refresh_token记录下来。 92 | 4、下载 https://github.com/donwa/goindex 中的 index.js 并填入 root 和 refresh_token 93 | 5、复制代码 到 CloudFlare 部署。 94 | 95 | ## 安装部署方案2 96 | 作者不会记录refresh_token,但为避免纠纷,建议有条件的同学使用方案1进行部署 97 | 1、访问[https://install.gd.workers.dev/](https://install.gd.workers.dev/) 98 | 2、授权认证后,生成部署代码。 99 | 3、复制代码 到 CloudFlare 部署。 100 | 101 | ## 文件夹密码: 102 | 在google drive 文件中放置 `.password` 文件来设置密码。 103 | 密码文件只能保护该文件不被列举,不能保护该文件夹的子文件夹不被列举。 104 | 也不保护文件夹下文件不被下载。 105 | 106 | 程序文件中 `root_pass` 只为根目录密码,优先于 `.password` 文件 107 | 108 | 109 | ## 更新日志 110 | 111 | 1.0.6 112 | 添加 classic 模板 113 | 114 | 1.0.5 115 | 添加文件展示页 116 | 117 | 1.0.4 118 | 修复 注入问题。 119 | 120 | 1.0.3 121 | 修复 `.password` 绕过下载问题。 122 | 123 | 1.0.2 124 | 优化前端逻辑 125 | 添加文件预览功能(临时) 126 | 添加前端文件缓存功能 127 | 128 | 1.0.1 129 | 添加 README.md 、 HEAD.md 支持 130 | 131 | 1.0.0 132 | 前后端分离,确定基本架构 133 | 添加.password 支持 134 | -------------------------------------------------------------------------------- /themes/classic/app.js: -------------------------------------------------------------------------------- 1 | document.write(''); 2 | // 初始化页面,并载入必要资源 3 | function init(){ 4 | document.siteName = $('title').html(); 5 | $('body').addClass("mdui-theme-primary-blue-grey mdui-theme-accent-blue"); 6 | var html = ` 7 |

Index of

8 | 9 |
10 | `; 11 | $('body').html(html); 12 | } 13 | 14 | function render(path){ 15 | if(path.indexOf("?") > 0){ 16 | path = path.substr(0,path.indexOf("?")); 17 | } 18 | title(path); 19 | nav(path); 20 | if(path.substr(-1) == '/'){ 21 | list(path); 22 | }else{ 23 | file(path); 24 | } 25 | } 26 | 27 | 28 | // 渲染 title 29 | function title(path){ 30 | path = decodeURI(path); 31 | $('title').html(document.siteName+' - '+path); 32 | } 33 | 34 | // 渲染导航栏 35 | function nav(path){ 36 | path = decodeURI(path); 37 | $('#heading').html('Index of '+path); 38 | } 39 | 40 | // 渲染文件列表 41 | function list(path){ 42 | var content = ` 43 | NameSizeDate Modified 44 | `; 45 | 46 | if(path != '/'){ 47 | var up = path.split('/'); 48 | up.pop();up.pop(); 49 | up = up.join('/')+'/'; 50 | content += ` 51 | 52 | 53 | .. 54 | 55 | 56 | 57 | 58 | `; 59 | } 60 | $('#table').html(content); 61 | 62 | var password = localStorage.getItem('password'+path); 63 | $.post(path,'{"password":"'+password+'"}', function(data,status){ 64 | var obj = jQuery.parseJSON(data); 65 | if(typeof obj != 'null' && obj.hasOwnProperty('error') && obj.error.code == '401'){ 66 | var pass = prompt("password",""); 67 | localStorage.setItem('password'+path, pass); 68 | if(pass != null && pass != ""){ 69 | list(path); 70 | }else{ 71 | history.go(-1); 72 | } 73 | }else if(typeof obj != 'null'){ 74 | list_files(path,obj.files); 75 | } 76 | }); 77 | } 78 | 79 | function list_files(path,files){ 80 | html = ""; 81 | for(i in files){ 82 | var item = files[i]; 83 | if(item['size']==undefined){ 84 | item['size'] = ""; 85 | } 86 | item['modifiedTime'] = utc2beijing(item['modifiedTime']); 87 | item['size'] = formatFileSize(item['size']); 88 | if(item['mimeType'] == 'application/vnd.google-apps.folder'){ 89 | var p = path+item.name+'/'; 90 | html +=` 91 | 92 | ${item.name}/ 93 | ${item['size']} 94 | ${item['modifiedTime']} 95 | 96 | `; 97 | }else{ 98 | var p = path+item.name; 99 | html += ` 100 | 101 | ${item.name} 102 | ${item['size']} 103 | ${item['modifiedTime']} 104 | 105 | `; 106 | } 107 | } 108 | $('#table').append(html); 109 | } 110 | 111 | //时间转换 112 | function utc2beijing(utc_datetime) { 113 | // 转为正常的时间格式 年-月-日 时:分:秒 114 | var T_pos = utc_datetime.indexOf('T'); 115 | var Z_pos = utc_datetime.indexOf('Z'); 116 | var year_month_day = utc_datetime.substr(0,T_pos); 117 | var hour_minute_second = utc_datetime.substr(T_pos+1,Z_pos-T_pos-1); 118 | var new_datetime = year_month_day+" "+hour_minute_second; // 2017-03-31 08:02:06 119 | 120 | // 处理成为时间戳 121 | timestamp = new Date(Date.parse(new_datetime)); 122 | timestamp = timestamp.getTime(); 123 | timestamp = timestamp/1000; 124 | 125 | // 增加8个小时,北京时间比utc时间多八个时区 126 | var unixtimestamp = timestamp+8*60*60; 127 | 128 | // 时间戳转为时间 129 | var unixtimestamp = new Date(unixtimestamp*1000); 130 | var year = 1900 + unixtimestamp.getYear(); 131 | var month = "0" + (unixtimestamp.getMonth() + 1); 132 | var date = "0" + unixtimestamp.getDate(); 133 | var hour = "0" + unixtimestamp.getHours(); 134 | var minute = "0" + unixtimestamp.getMinutes(); 135 | var second = "0" + unixtimestamp.getSeconds(); 136 | return year + "-" + month.substring(month.length-2, month.length) + "-" + date.substring(date.length-2, date.length) 137 | + " " + hour.substring(hour.length-2, hour.length) + ":" 138 | + minute.substring(minute.length-2, minute.length) + ":" 139 | + second.substring(second.length-2, second.length); 140 | } 141 | 142 | // bytes自适应转换到KB,MB,GB 143 | function formatFileSize(bytes) { 144 | if (bytes>=1000000000) {bytes=(bytes/1000000000).toFixed(2)+' GB';} 145 | else if (bytes>=1000000) {bytes=(bytes/1000000).toFixed(2)+' MB';} 146 | else if (bytes>=1000) {bytes=(bytes/1000).toFixed(2)+' KB';} 147 | else if (bytes>1) {bytes=bytes+' bytes';} 148 | else if (bytes==1) {bytes=bytes+' byte';} 149 | else {bytes='';} 150 | return bytes; 151 | } 152 | 153 | // 监听回退事件 154 | window.onpopstate = function(){ 155 | var path = window.location.pathname; 156 | render(path); 157 | } 158 | 159 | 160 | $(function(){ 161 | init(); 162 | var path = window.location.pathname; 163 | $("body").on("click",'.folder',function(){ 164 | var url = $(this).attr('href'); 165 | history.pushState(null, null, url); 166 | render(url); 167 | return false; 168 | }); 169 | 170 | render(path); 171 | }); 172 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const authConfig = { 2 | "siteName": "GoIndex", // 网站名称 3 | "version": "_4.28", // 程序版本。用户不要手动修改 4 | /*"client_id": "202264815644.apps.googleusercontent.com", 5 | "client_secret": "X4Z3ca8xfWDb1Voo-F9a7ZxJ",*/ 6 | // 【注意】强烈推荐使用自己的 client_id 和 client_secret 7 | "client_id": "", 8 | "client_secret": "", 9 | "refresh_token": "", // 授权 token 10 | /** 11 | * 设置要显示的多个云端硬盘;按格式添加多个 12 | * [id]: 可以是 团队盘id、子文件夹id、或者"root"(代表个人盘根目录); 13 | * [name]: 显示的名称 14 | * [user]: Basic Auth 的用户名 15 | * [pass]: Basic Auth 的密码 16 | * [protect_file_link]: Basic Auth 是否用于保护文件链接,默认值(不设置时)为 false,即不保护文件链接(方便 直链下载/外部播放 等) 17 | * 每个盘的 Basic Auth 都可以单独设置。Basic Auth 默认保护该盘下所有文件夹/子文件夹路径 18 | * 【注意】默认不保护文件链接,这样可以方便 直链下载/外部播放; 19 | * 如果要保护文件链接,需要将 protect_file_link 设置为 true,此时如果要进行外部播放等操作,需要将 host 替换为 user:pass@host 的 形式 20 | * 不需要 Basic Auth 的盘,保持 user 和 pass 同时为空即可。(直接不设置也可以) 21 | * 【注意】对于id设置为为子文件夹id的盘将不支持搜索功能(不影响其他盘)。 22 | */ 23 | "roots": [ 24 | { 25 | id: "root", 26 | name: "个人盘" 27 | }, 28 | { 29 | id: "drive_id", 30 | name: "团队盘1", 31 | user: 'user1', 32 | pass: "111", 33 | protect_file_link: true 34 | }, 35 | { 36 | id: "folder_id", 37 | name: "文件夹", 38 | // 只设置密码、只设置用户名、同时设置用户名密码,都是可以的 39 | user: '', 40 | pass: "222", 41 | protect_file_link: false 42 | } 43 | ], 44 | /** 45 | * 文件列表页面每页显示的数量。【推荐设置值为 100 到 1000 之间】; 46 | * 如果设置大于1000,会导致请求 drive api 时出错; 47 | * 如果设置的值过小,会导致文件列表页面滚动条增量加载(分页加载)失效; 48 | * 此值的另一个作用是,如果目录内文件数大于此设置值(即需要多页展示的),将会对首次列目录结果进行缓存。 49 | */ 50 | "files_list_page_size": 500, 51 | /** 52 | * 搜索结果页面每页显示的数量。【推荐设置值为 50 到 1000 之间】; 53 | * 如果设置大于1000,会导致请求 drive api 时出错; 54 | * 如果设置的值过小,会导致搜索结果页面滚动条增量加载(分页加载)失效; 55 | * 此值的大小影响搜索操作的响应速度。 56 | */ 57 | "search_result_list_page_size": 50, 58 | // 确认有 cors 用途的可以开启 59 | "enable_cors_file_down": false, 60 | /** 61 | * 上面的 basic auth 已经包含了盘内全局保护的功能。所以默认不再去认证 .password 文件内的密码; 62 | * 如果在全局认证的基础上,仍需要给某些目录单独进行 .password 文件内的密码验证的话,将此选项设置为 true; 63 | * 【注意】如果开启了 .password 文件密码验证,每次列目录都会额外增加查询目录内 .password 文件是否存在的开销。 64 | */ 65 | "enable_password_file_verify": false 66 | }; 67 | 68 | /** 69 | * web ui 设置 70 | */ 71 | const uiConfig = { 72 | // 此版本只支持 material 73 | "theme": "material", // DO NOT set it to classic 74 | "dark_mode": false, 75 | "main_color": "blue-grey", 76 | "accent_color": "blue", 77 | /*"main_color": "light-green", 78 | "accent_color": "green",*/ 79 | "fluid_navigation_bar": true, 80 | }; 81 | 82 | /** 83 | * global functions 84 | */ 85 | const FUNCS = { 86 | /** 87 | * 转换成针对谷歌搜索词法相对安全的搜索关键词 88 | */ 89 | formatSearchKeyword: function (keyword) { 90 | let nothing = ""; 91 | let space = " "; 92 | if (!keyword) return nothing; 93 | return keyword.replace(/(!=)|['"=<>/\\:]/g, nothing) 94 | .replace(/[,,|(){}]/g, space) 95 | .trim() 96 | } 97 | 98 | }; 99 | 100 | /** 101 | * global consts 102 | * @type {{folder_mime_type: string, default_file_fields: string, gd_root_type: {share_drive: number, user_drive: number, sub_folder: number}}} 103 | */ 104 | const CONSTS = new (class { 105 | default_file_fields = 'parents,id,name,mimeType,modifiedTime,createdTime,fileExtension,size'; 106 | gd_root_type = { 107 | user_drive: 0, 108 | share_drive: 1, 109 | sub_folder: 2 110 | }; 111 | folder_mime_type = 'application/vnd.google-apps.folder'; 112 | })(); 113 | 114 | 115 | // gd instances 116 | var gds = []; 117 | 118 | function html(current_drive_order = 0, model = {}) { 119 | return ` 120 | 121 | 122 | 123 | 124 | 125 | ${authConfig.siteName} 126 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | `; 139 | }; 140 | 141 | addEventListener('fetch', event => { 142 | event.respondWith(handleRequest(event.request)); 143 | }); 144 | 145 | /** 146 | * Fetch and log a request 147 | * @param {Request} request 148 | */ 149 | async function handleRequest(request) { 150 | if (gds.length === 0) { 151 | for (let i = 0; i < authConfig.roots.length; i++) { 152 | const gd = new googleDrive(authConfig, i); 153 | await gd.init(); 154 | gds.push(gd) 155 | } 156 | // 这个操作并行,提高效率 157 | let tasks = []; 158 | gds.forEach(gd => { 159 | tasks.push(gd.initRootType()); 160 | }); 161 | for (let task of tasks) { 162 | await task; 163 | } 164 | } 165 | 166 | // 从 path 中提取 drive order 167 | // 并根据 drive order 获取对应的 gd instance 168 | let gd; 169 | let url = new URL(request.url); 170 | let path = url.pathname; 171 | 172 | /** 173 | * 重定向至起始页 174 | * @returns {Response} 175 | */ 176 | function redirectToIndexPage() { 177 | return new Response('', {status: 301, headers: {'Location': `${url.origin}/0:/`}}); 178 | } 179 | 180 | if (path == '/') return redirectToIndexPage(); 181 | if (path.toLowerCase() == '/favicon.ico') { 182 | // 后面可以找一个 favicon 183 | return new Response('', {status: 404}) 184 | } 185 | 186 | // 特殊命令格式 187 | const command_reg = /^\/(?\d+):(?[a-zA-Z0-9]+)$/g; 188 | const match = command_reg.exec(path); 189 | if (match) { 190 | const num = match.groups.num; 191 | const order = Number(num); 192 | if (order >= 0 && order < gds.length) { 193 | gd = gds[order]; 194 | } else { 195 | return redirectToIndexPage() 196 | } 197 | // basic auth 198 | for (const r = gd.basicAuthResponse(request); r;) return r; 199 | const command = match.groups.command; 200 | // 搜索 201 | if (command === 'search') { 202 | if (request.method === 'POST') { 203 | // 搜索结果 204 | return handleSearch(request, gd); 205 | } else { 206 | const params = url.searchParams; 207 | // 搜索页面 208 | return new Response(html(gd.order, { 209 | q: params.get("q") || '', 210 | is_search_page: true, 211 | root_type: gd.root_type 212 | }), 213 | { 214 | status: 200, 215 | headers: {'Content-Type': 'text/html; charset=utf-8'} 216 | }); 217 | } 218 | } else if (command === 'id2path' && request.method === 'POST') { 219 | return handleId2Path(request, gd) 220 | } 221 | } 222 | 223 | // 期望的 path 格式 224 | const common_reg = /^\/\d+:\/.*$/g; 225 | try { 226 | if (!path.match(common_reg)) { 227 | return redirectToIndexPage(); 228 | } 229 | let split = path.split("/"); 230 | let order = Number(split[1].slice(0, -1)); 231 | if (order >= 0 && order < gds.length) { 232 | gd = gds[order]; 233 | } else { 234 | return redirectToIndexPage() 235 | } 236 | } catch (e) { 237 | return redirectToIndexPage() 238 | } 239 | 240 | // basic auth 241 | // for (const r = gd.basicAuthResponse(request); r;) return r; 242 | const basic_auth_res = gd.basicAuthResponse(request); 243 | 244 | path = path.replace(gd.url_path_prefix, '') || '/'; 245 | if (request.method == 'POST') { 246 | return basic_auth_res || apiRequest(request, gd); 247 | } 248 | 249 | let action = url.searchParams.get('a'); 250 | 251 | if (path.substr(-1) == '/' || action != null) { 252 | return basic_auth_res || new Response(html(gd.order, {root_type: gd.root_type}), { 253 | status: 200, 254 | headers: {'Content-Type': 'text/html; charset=utf-8'} 255 | }); 256 | } else { 257 | if (path.split('/').pop().toLowerCase() == ".password") { 258 | return basic_auth_res || new Response("", {status: 404}); 259 | } 260 | let file = await gd.file(path); 261 | let range = request.headers.get('Range'); 262 | const inline_down = 'true' === url.searchParams.get('inline'); 263 | if (gd.root.protect_file_link && basic_auth_res) return basic_auth_res; 264 | return gd.down(file.id, range, inline_down); 265 | } 266 | } 267 | 268 | 269 | async function apiRequest(request, gd) { 270 | let url = new URL(request.url); 271 | let path = url.pathname; 272 | path = path.replace(gd.url_path_prefix, '') || '/'; 273 | 274 | let option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}} 275 | 276 | if (path.substr(-1) == '/') { 277 | let form = await request.formData(); 278 | // 这样可以提升首次列目录时的速度。缺点是,如果password验证失败,也依然会产生列目录的开销 279 | let deferred_list_result = gd.list(path, form.get('page_token'), Number(form.get('page_index'))); 280 | 281 | // check .password file, if `enable_password_file_verify` is true 282 | if (authConfig['enable_password_file_verify']) { 283 | let password = await gd.password(path); 284 | // console.log("dir password", password); 285 | if (password && password.replace("\n", "") !== form.get('password')) { 286 | let html = `{"error": {"code": 401,"message": "password error."}}`; 287 | return new Response(html, option); 288 | } 289 | } 290 | 291 | let list_result = await deferred_list_result; 292 | return new Response(JSON.stringify(list_result), option); 293 | } else { 294 | let file = await gd.file(path); 295 | let range = request.headers.get('Range'); 296 | return new Response(JSON.stringify(file)); 297 | } 298 | } 299 | 300 | // 处理 search 301 | async function handleSearch(request, gd) { 302 | const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}; 303 | let form = await request.formData(); 304 | let search_result = await 305 | gd.search(form.get('q') || '', form.get('page_token'), Number(form.get('page_index'))); 306 | return new Response(JSON.stringify(search_result), option); 307 | } 308 | 309 | /** 310 | * 处理 id2path 311 | * @param request 需要 id 参数 312 | * @param gd 313 | * @returns {Promise} 【注意】如果从前台接收的id代表的项目不在目标gd盘下,那么response会返回给前台一个空字符串"" 314 | */ 315 | async function handleId2Path(request, gd) { 316 | const option = {status: 200, headers: {'Access-Control-Allow-Origin': '*'}}; 317 | let form = await request.formData(); 318 | let path = await gd.findPathById(form.get('id')); 319 | return new Response(path || '', option); 320 | } 321 | 322 | class googleDrive { 323 | constructor(authConfig, order) { 324 | // 每个盘对应一个order,对应一个gd实例 325 | this.order = order; 326 | this.root = authConfig.roots[order]; 327 | this.root.protect_file_link = this.root.protect_file_link || false; 328 | this.url_path_prefix = `/${order}:`; 329 | this.authConfig = authConfig; 330 | // TODO: 这些缓存的失效刷新策略,后期可以制定一下 331 | // path id 332 | this.paths = []; 333 | // path file 334 | this.files = []; 335 | // path pass 336 | this.passwords = []; 337 | // id <-> path 338 | this.id_path_cache = {}; 339 | this.id_path_cache[this.root['id']] = '/'; 340 | this.paths["/"] = this.root['id']; 341 | /*if (this.root['pass'] != "") { 342 | this.passwords['/'] = this.root['pass']; 343 | }*/ 344 | // this.init(); 345 | } 346 | 347 | /** 348 | * 初次授权;然后获取 user_drive_real_root_id 349 | * @returns {Promise} 350 | */ 351 | async init() { 352 | await this.accessToken(); 353 | /*await (async () => { 354 | // 只获取1次 355 | if (authConfig.user_drive_real_root_id) return; 356 | const root_obj = await (gds[0] || this).findItemById('root'); 357 | if (root_obj && root_obj.id) { 358 | authConfig.user_drive_real_root_id = root_obj.id 359 | } 360 | })();*/ 361 | // 等待 user_drive_real_root_id ,只获取1次 362 | if (authConfig.user_drive_real_root_id) return; 363 | const root_obj = await (gds[0] || this).findItemById('root'); 364 | if (root_obj && root_obj.id) { 365 | authConfig.user_drive_real_root_id = root_obj.id 366 | } 367 | } 368 | 369 | /** 370 | * 获取根目录类型,设置到 root_type 371 | * @returns {Promise} 372 | */ 373 | async initRootType() { 374 | const root_id = this.root['id']; 375 | const types = CONSTS.gd_root_type; 376 | if (root_id === 'root' || root_id === authConfig.user_drive_real_root_id) { 377 | this.root_type = types.user_drive; 378 | } else { 379 | const obj = await this.getShareDriveObjById(root_id); 380 | this.root_type = obj ? types.share_drive : types.sub_folder; 381 | } 382 | } 383 | 384 | /** 385 | * Returns a response that requires authorization, or null 386 | * @param request 387 | * @returns {Response|null} 388 | */ 389 | basicAuthResponse(request) { 390 | const user = this.root.user || '', 391 | pass = this.root.pass || '', 392 | _401 = new Response('Unauthorized', { 393 | headers: {'WWW-Authenticate': `Basic realm="goindex:drive:${this.order}"`}, 394 | status: 401 395 | }); 396 | if (user || pass) { 397 | const auth = request.headers.get('Authorization') 398 | if (auth) { 399 | try { 400 | const [received_user, received_pass] = atob(auth.split(' ').pop()).split(':'); 401 | return (received_user === user && received_pass === pass) ? null : _401; 402 | } catch (e) { 403 | } 404 | } 405 | } else return null; 406 | return _401; 407 | } 408 | 409 | async down(id, range = '', inline = false) { 410 | let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`; 411 | let requestOption = await this.requestOption(); 412 | requestOption.headers['Range'] = range; 413 | let res = await fetch(url, requestOption); 414 | const {headers} = res = new Response(res.body, res) 415 | this.authConfig.enable_cors_file_down && headers.append('Access-Control-Allow-Origin', '*'); 416 | inline === true && headers.set('Content-Disposition', 'inline'); 417 | return res; 418 | } 419 | 420 | async file(path) { 421 | if (typeof this.files[path] == 'undefined') { 422 | this.files[path] = await this._file(path); 423 | } 424 | return this.files[path]; 425 | } 426 | 427 | async _file(path) { 428 | let arr = path.split('/'); 429 | let name = arr.pop(); 430 | name = decodeURIComponent(name).replace(/\'/g, "\\'"); 431 | let dir = arr.join('/') + '/'; 432 | // console.log(name, dir); 433 | let parent = await this.findPathId(dir); 434 | // console.log(parent); 435 | let url = 'https://www.googleapis.com/drive/v3/files'; 436 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true}; 437 | params.q = `'${parent}' in parents and name = '${name}' and trashed = false`; 438 | params.fields = "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink)"; 439 | url += '?' + this.enQuery(params); 440 | let requestOption = await this.requestOption(); 441 | let response = await fetch(url, requestOption); 442 | let obj = await response.json(); 443 | // console.log(obj); 444 | return obj.files[0]; 445 | } 446 | 447 | // 通过reqeust cache 来缓存 448 | async list(path, page_token = null, page_index = 0) { 449 | if (this.path_children_cache == undefined) { 450 | // { :[ {nextPageToken:'',data:{}}, {nextPageToken:'',data:{}} ...], ...} 451 | this.path_children_cache = {}; 452 | } 453 | 454 | if (this.path_children_cache[path] 455 | && this.path_children_cache[path][page_index] 456 | && this.path_children_cache[path][page_index].data 457 | ) { 458 | let child_obj = this.path_children_cache[path][page_index]; 459 | return { 460 | nextPageToken: child_obj.nextPageToken || null, 461 | curPageIndex: page_index, 462 | data: child_obj.data 463 | }; 464 | } 465 | 466 | let id = await this.findPathId(path); 467 | let result = await this._ls(id, page_token, page_index); 468 | let data = result.data; 469 | // 对有多页的,进行缓存 470 | if (result.nextPageToken && data.files) { 471 | if (!Array.isArray(this.path_children_cache[path])) { 472 | this.path_children_cache[path] = [] 473 | } 474 | this.path_children_cache[path][Number(result.curPageIndex)] = { 475 | nextPageToken: result.nextPageToken, 476 | data: data 477 | }; 478 | } 479 | 480 | return result 481 | } 482 | 483 | 484 | async _ls(parent, page_token = null, page_index = 0) { 485 | // console.log("_ls", parent); 486 | 487 | if (parent == undefined) { 488 | return null; 489 | } 490 | let obj; 491 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true}; 492 | params.q = `'${parent}' in parents and trashed = false AND name !='.password'`; 493 | params.orderBy = 'folder,name,modifiedTime desc'; 494 | params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)"; 495 | params.pageSize = this.authConfig.files_list_page_size; 496 | 497 | if (page_token) { 498 | params.pageToken = page_token; 499 | } 500 | let url = 'https://www.googleapis.com/drive/v3/files'; 501 | url += '?' + this.enQuery(params); 502 | let requestOption = await this.requestOption(); 503 | let response = await fetch(url, requestOption); 504 | obj = await response.json(); 505 | 506 | return { 507 | nextPageToken: obj.nextPageToken || null, 508 | curPageIndex: page_index, 509 | data: obj 510 | }; 511 | 512 | /*do { 513 | if (pageToken) { 514 | params.pageToken = pageToken; 515 | } 516 | let url = 'https://www.googleapis.com/drive/v3/files'; 517 | url += '?' + this.enQuery(params); 518 | let requestOption = await this.requestOption(); 519 | let response = await fetch(url, requestOption); 520 | obj = await response.json(); 521 | files.push(...obj.files); 522 | pageToken = obj.nextPageToken; 523 | } while (pageToken);*/ 524 | 525 | } 526 | 527 | async password(path) { 528 | if (this.passwords[path] !== undefined) { 529 | return this.passwords[path]; 530 | } 531 | 532 | // console.log("load", path, ".password", this.passwords[path]); 533 | 534 | let file = await this.file(path + '.password'); 535 | if (file == undefined) { 536 | this.passwords[path] = null; 537 | } else { 538 | let url = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`; 539 | let requestOption = await this.requestOption(); 540 | let response = await this.fetch200(url, requestOption); 541 | this.passwords[path] = await response.text(); 542 | } 543 | 544 | return this.passwords[path]; 545 | } 546 | 547 | 548 | /** 549 | * 通过 id 获取 share drive 信息 550 | * @param any_id 551 | * @returns {Promise} 任何非正常情况都返回 null 552 | */ 553 | async getShareDriveObjById(any_id) { 554 | if (!any_id) return null; 555 | if ('string' !== typeof any_id) return null; 556 | 557 | let url = `https://www.googleapis.com/drive/v3/drives/${any_id}`; 558 | let requestOption = await this.requestOption(); 559 | let res = await fetch(url, requestOption); 560 | let obj = await res.json(); 561 | if (obj && obj.id) return obj; 562 | 563 | return null 564 | } 565 | 566 | 567 | /** 568 | * 搜索 569 | * @returns {Promise<{data: null, nextPageToken: null, curPageIndex: number}>} 570 | */ 571 | async search(origin_keyword, page_token = null, page_index = 0) { 572 | const types = CONSTS.gd_root_type; 573 | const is_user_drive = this.root_type === types.user_drive; 574 | const is_share_drive = this.root_type === types.share_drive; 575 | 576 | const empty_result = { 577 | nextPageToken: null, 578 | curPageIndex: page_index, 579 | data: null 580 | }; 581 | 582 | if (!is_user_drive && !is_share_drive) { 583 | return empty_result; 584 | } 585 | let keyword = FUNCS.formatSearchKeyword(origin_keyword); 586 | if (!keyword) { 587 | // 关键词为空,返回 588 | return empty_result; 589 | } 590 | let words = keyword.split(/\s+/); 591 | let name_search_str = `name contains '${words.join("' AND name contains '")}'`; 592 | 593 | // corpora 为 user 是个人盘 ,为 drive 是团队盘。配合 driveId 594 | let params = {}; 595 | if (is_user_drive) { 596 | params.corpora = 'user' 597 | } 598 | if (is_share_drive) { 599 | params.corpora = 'drive'; 600 | params.driveId = this.root.id; 601 | // This parameter will only be effective until June 1, 2020. Afterwards shared drive items will be included in the results. 602 | params.includeItemsFromAllDrives = true; 603 | params.supportsAllDrives = true; 604 | } 605 | if (page_token) { 606 | params.pageToken = page_token; 607 | } 608 | params.q = `trashed = false AND name !='.password' AND (${name_search_str})`; 609 | params.fields = "nextPageToken, files(id, name, mimeType, size , modifiedTime)"; 610 | params.pageSize = this.authConfig.search_result_list_page_size; 611 | // params.orderBy = 'folder,name,modifiedTime desc'; 612 | 613 | let url = 'https://www.googleapis.com/drive/v3/files'; 614 | url += '?' + this.enQuery(params); 615 | // console.log(params) 616 | let requestOption = await this.requestOption(); 617 | let response = await fetch(url, requestOption); 618 | let res_obj = await response.json(); 619 | 620 | return { 621 | nextPageToken: res_obj.nextPageToken || null, 622 | curPageIndex: page_index, 623 | data: res_obj 624 | }; 625 | } 626 | 627 | 628 | /** 629 | * 一层一层的向上获取这个文件或文件夹的上级文件夹的 file 对象。注意:会很慢!!! 630 | * 最多向上寻找到当前 gd 对象的根目录 (root id) 631 | * 只考虑一条单独的向上链。 632 | * 【注意】如果此id代表的项目不在目标gd盘下,那么此函数会返回null 633 | * 634 | * @param child_id 635 | * @param contain_myself 636 | * @returns {Promise<[]>} 637 | */ 638 | async findParentFilesRecursion(child_id, contain_myself = true) { 639 | const gd = this; 640 | const gd_root_id = gd.root.id; 641 | const user_drive_real_root_id = authConfig.user_drive_real_root_id; 642 | const is_user_drive = gd.root_type === CONSTS.gd_root_type.user_drive; 643 | 644 | // 自下向上查询的终点目标id 645 | const target_top_id = is_user_drive ? user_drive_real_root_id : gd_root_id; 646 | const fields = CONSTS.default_file_fields; 647 | 648 | // [{},{},...] 649 | const parent_files = []; 650 | let meet_top = false; 651 | 652 | async function addItsFirstParent(file_obj) { 653 | if (!file_obj) return; 654 | if (!file_obj.parents) return; 655 | if (file_obj.parents.length < 1) return; 656 | 657 | // ['','',...] 658 | let p_ids = file_obj.parents; 659 | if (p_ids && p_ids.length > 0) { 660 | // its first parent 661 | const first_p_id = p_ids[0]; 662 | if (first_p_id === target_top_id) { 663 | meet_top = true; 664 | return; 665 | } 666 | const p_file_obj = await gd.findItemById(first_p_id); 667 | if (p_file_obj && p_file_obj.id) { 668 | parent_files.push(p_file_obj); 669 | await addItsFirstParent(p_file_obj); 670 | } 671 | } 672 | } 673 | 674 | const child_obj = await gd.findItemById(child_id); 675 | if (contain_myself) { 676 | parent_files.push(child_obj); 677 | } 678 | await addItsFirstParent(child_obj); 679 | 680 | return meet_top ? parent_files : null 681 | } 682 | 683 | /** 684 | * 获取相对于本盘根目录的path 685 | * @param child_id 686 | * @returns {Promise} 【注意】如果此id代表的项目不在目标gd盘下,那么此方法会返回空字符串"" 687 | */ 688 | async findPathById(child_id) { 689 | if (this.id_path_cache[child_id]) { 690 | return this.id_path_cache[child_id]; 691 | } 692 | 693 | const p_files = await this.findParentFilesRecursion(child_id); 694 | if (!p_files || p_files.length < 1) return ''; 695 | 696 | let cache = []; 697 | // 把查出来的每一级的path和id都缓存一下 698 | p_files.forEach((value, idx) => { 699 | const is_folder = idx === 0 ? (p_files[idx].mimeType === CONSTS.folder_mime_type) : true; 700 | let path = '/' + p_files.slice(idx).map(it => it.name).reverse().join('/'); 701 | if (is_folder) path += '/'; 702 | cache.push({id: p_files[idx].id, path: path}) 703 | }); 704 | 705 | cache.forEach((obj) => { 706 | this.id_path_cache[obj.id] = obj.path; 707 | this.paths[obj.path] = obj.id 708 | }); 709 | 710 | /*const is_folder = p_files[0].mimeType === CONSTS.folder_mime_type; 711 | let path = '/' + p_files.map(it => it.name).reverse().join('/'); 712 | if (is_folder) path += '/';*/ 713 | 714 | return cache[0].path; 715 | } 716 | 717 | 718 | // 根据id获取file item 719 | async findItemById(id) { 720 | const is_user_drive = this.root_type === CONSTS.gd_root_type.user_drive; 721 | let url = `https://www.googleapis.com/drive/v3/files/${id}?fields=${CONSTS.default_file_fields}${is_user_drive ? '' : '&supportsAllDrives=true'}`; 722 | let requestOption = await this.requestOption(); 723 | let res = await fetch(url, requestOption); 724 | return await res.json() 725 | } 726 | 727 | async findPathId(path) { 728 | let c_path = '/'; 729 | let c_id = this.paths[c_path]; 730 | 731 | let arr = path.trim('/').split('/'); 732 | for (let name of arr) { 733 | c_path += name + '/'; 734 | 735 | if (typeof this.paths[c_path] == 'undefined') { 736 | let id = await this._findDirId(c_id, name); 737 | this.paths[c_path] = id; 738 | } 739 | 740 | c_id = this.paths[c_path]; 741 | if (c_id == undefined || c_id == null) { 742 | break; 743 | } 744 | } 745 | // console.log(this.paths); 746 | return this.paths[path]; 747 | } 748 | 749 | async _findDirId(parent, name) { 750 | name = decodeURIComponent(name).replace(/\'/g, "\\'"); 751 | 752 | // console.log("_findDirId", parent, name); 753 | 754 | if (parent == undefined) { 755 | return null; 756 | } 757 | 758 | let url = 'https://www.googleapis.com/drive/v3/files'; 759 | let params = {'includeItemsFromAllDrives': true, 'supportsAllDrives': true}; 760 | params.q = `'${parent}' in parents and mimeType = 'application/vnd.google-apps.folder' and name = '${name}' and trashed = false`; 761 | params.fields = "nextPageToken, files(id, name, mimeType)"; 762 | url += '?' + this.enQuery(params); 763 | let requestOption = await this.requestOption(); 764 | let response = await fetch(url, requestOption); 765 | let obj = await response.json(); 766 | if (obj.files[0] == undefined) { 767 | return null; 768 | } 769 | return obj.files[0].id; 770 | } 771 | 772 | async accessToken() { 773 | console.log("accessToken"); 774 | if (this.authConfig.expires == undefined || this.authConfig.expires < Date.now()) { 775 | const obj = await this.fetchAccessToken(); 776 | if (obj.access_token != undefined) { 777 | this.authConfig.accessToken = obj.access_token; 778 | this.authConfig.expires = Date.now() + 3500 * 1000; 779 | } 780 | } 781 | return this.authConfig.accessToken; 782 | } 783 | 784 | async fetchAccessToken() { 785 | console.log("fetchAccessToken"); 786 | const url = "https://www.googleapis.com/oauth2/v4/token"; 787 | const headers = { 788 | 'Content-Type': 'application/x-www-form-urlencoded' 789 | }; 790 | const post_data = { 791 | 'client_id': this.authConfig.client_id, 792 | 'client_secret': this.authConfig.client_secret, 793 | 'refresh_token': this.authConfig.refresh_token, 794 | 'grant_type': 'refresh_token' 795 | } 796 | 797 | let requestOption = { 798 | 'method': 'POST', 799 | 'headers': headers, 800 | 'body': this.enQuery(post_data) 801 | }; 802 | 803 | const response = await fetch(url, requestOption); 804 | return await response.json(); 805 | } 806 | 807 | async fetch200(url, requestOption) { 808 | let response; 809 | for (let i = 0; i < 3; i++) { 810 | response = await fetch(url, requestOption); 811 | console.log(response.status); 812 | if (response.status != 403) { 813 | break; 814 | } 815 | await this.sleep(800 * (i + 1)); 816 | } 817 | return response; 818 | } 819 | 820 | async requestOption(headers = {}, method = 'GET') { 821 | const accessToken = await this.accessToken(); 822 | headers['authorization'] = 'Bearer ' + accessToken; 823 | return {'method': method, 'headers': headers}; 824 | } 825 | 826 | enQuery(data) { 827 | const ret = []; 828 | for (let d in data) { 829 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])); 830 | } 831 | return ret.join('&'); 832 | } 833 | 834 | sleep(ms) { 835 | return new Promise(function (resolve, reject) { 836 | let i = 0; 837 | setTimeout(function () { 838 | console.log('sleep' + ms); 839 | i++; 840 | if (i >= 2) reject(new Error('i>=2')); 841 | else resolve(i); 842 | }, ms); 843 | }) 844 | } 845 | } 846 | 847 | String.prototype.trim = function (char) { 848 | if (char) { 849 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), ''); 850 | } 851 | return this.replace(/^\s+|\s+$/g, ''); 852 | }; 853 | -------------------------------------------------------------------------------- /themes/material/app.js: -------------------------------------------------------------------------------- 1 | // 在head 中 加载 必要静态 2 | document.write(''); 3 | // markdown支持 4 | document.write(''); 5 | document.write(''); 6 | // add custome theme and darkmode 7 | if (UI.dark_mode) { 8 | document.write(``); 9 | } 10 | 11 | // 初始化页面,并载入必要资源 12 | function init() { 13 | document.siteName = $('title').html(); 14 | $('body').addClass(`mdui-theme-primary-${UI.main_color} mdui-theme-accent-${UI.accent_color}`); 15 | var html = ` 16 |
17 | 19 |
20 |
21 |
22 | `; 23 | $('body').html(html); 24 | } 25 | 26 | const Os = { 27 | isWindows: navigator.platform.toUpperCase().indexOf('WIN') > -1, // .includes 28 | isMac: navigator.platform.toUpperCase().indexOf('MAC') > -1, 29 | isMacLike: /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform), 30 | isIos: /(iPhone|iPod|iPad)/i.test(navigator.platform), 31 | isMobile: /Android|webOS|iPhone|iPad|iPod|iOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) 32 | }; 33 | 34 | function getDocumentHeight() { 35 | var D = document; 36 | return Math.max( 37 | D.body.scrollHeight, D.documentElement.scrollHeight, 38 | D.body.offsetHeight, D.documentElement.offsetHeight, 39 | D.body.clientHeight, D.documentElement.clientHeight 40 | ); 41 | } 42 | 43 | function render(path) { 44 | if (path.indexOf("?") > 0) { 45 | path = path.substr(0, path.indexOf("?")); 46 | } 47 | title(path); 48 | nav(path); 49 | // .../0: 这种 50 | var reg = /\/\d+:$/g; 51 | if (window.MODEL.is_search_page) { 52 | // 用来存储一些滚动事件的状态 53 | window.scroll_status = { 54 | // 滚动事件是否已经绑定 55 | event_bound: false, 56 | // "滚动到底部,正在加载更多数据" 事件的锁 57 | loading_lock: false 58 | }; 59 | render_search_result_list() 60 | } else if (path.match(reg) || path.substr(-1) == '/') { 61 | // 用来存储一些滚动事件的状态 62 | window.scroll_status = { 63 | // 滚动事件是否已经绑定 64 | event_bound: false, 65 | // "滚动到底部,正在加载更多数据" 事件的锁 66 | loading_lock: false 67 | }; 68 | list(path); 69 | } else { 70 | file(path); 71 | } 72 | } 73 | 74 | 75 | // 渲染 title 76 | function title(path) { 77 | path = decodeURI(path); 78 | var cur = window.current_drive_order || 0; 79 | var drive_name = window.drive_names[cur]; 80 | path = path.replace(`/${cur}:`, ''); 81 | // $('title').html(document.siteName + ' - ' + path); 82 | var model = window.MODEL; 83 | if (model.is_search_page) 84 | $('title').html(`${document.siteName} - ${drive_name} - 搜索 ${model.q} 的结果`); 85 | else 86 | $('title').html(`${document.siteName} - ${drive_name} - ${path}`); 87 | } 88 | 89 | // 渲染导航栏 90 | function nav(path) { 91 | var model = window.MODEL; 92 | var html = ""; 93 | var cur = window.current_drive_order || 0; 94 | html += `${document.siteName}`; 95 | var names = window.drive_names; 96 | /*html += ``; 97 | html += `
    `; 98 | names.forEach((name, idx) => { 99 | html += `
  • ${name}
  • `; 100 | }); 101 | html += `
`;*/ 102 | 103 | // 修改为 select 104 | html += ``; 109 | 110 | if (!model.is_search_page) { 111 | var arr = path.trim('/').split('/'); 112 | var p = '/'; 113 | if (arr.length > 1) { 114 | arr.shift(); 115 | for (i in arr) { 116 | var n = arr[i]; 117 | n = decodeURI(n); 118 | p += n + '/'; 119 | if (n == '') { 120 | break; 121 | } 122 | html += `chevron_right${n}`; 123 | } 124 | } 125 | } 126 | var search_text = model.is_search_page ? (model.q || '') : ''; 127 | const isMobile = Os.isMobile; 128 | var search_bar = `
129 | `; 138 | 139 | // 个人盘 或 团队盘 140 | if (model.root_type < 2) { 141 | // 显示搜索框 142 | html += search_bar; 143 | } 144 | 145 | $('#nav').html(html); 146 | mdui.mutation(); 147 | mdui.updateTextFields(); 148 | } 149 | 150 | /** 151 | * 发起列目录的 POST 请求 152 | * @param path Path 153 | * @param params Form params 154 | * @param resultCallback Success Result Callback 155 | * @param authErrorCallback Pass Error Callback 156 | */ 157 | function requestListPath(path, params, resultCallback, authErrorCallback) { 158 | var p = { 159 | password: params['password'] || null, 160 | page_token: params['page_token'] || null, 161 | page_index: params['page_index'] || 0 162 | }; 163 | $.post(path, p, function (data, status) { 164 | var res = jQuery.parseJSON(data); 165 | if (res && res.error && res.error.code == '401') { 166 | // 密码验证失败 167 | if (authErrorCallback) authErrorCallback(path) 168 | } else if (res && res.data) { 169 | if (resultCallback) resultCallback(res, path, p) 170 | } 171 | }) 172 | } 173 | 174 | /** 175 | * 搜索 POST 请求 176 | * @param params Form params 177 | * @param resultCallback Success callback 178 | */ 179 | function requestSearch(params, resultCallback) { 180 | var p = { 181 | q: params['q'] || null, 182 | page_token: params['page_token'] || null, 183 | page_index: params['page_index'] || 0 184 | }; 185 | $.post(`/${window.current_drive_order}:search`, p, function (data, status) { 186 | var res = jQuery.parseJSON(data); 187 | if (res && res.data) { 188 | if (resultCallback) resultCallback(res, p) 189 | } 190 | }) 191 | } 192 | 193 | 194 | // 渲染文件列表 195 | function list(path) { 196 | var content = ` 197 | 198 | 199 |
200 |
    201 |
  • 202 |
    203 | 文件 204 | expand_more 205 |
    206 |
    207 | 修改时间 208 | expand_more 209 |
    210 |
    211 | 大小 212 | expand_more 213 |
    214 |
  • 215 |
216 |
217 |
218 |
    219 |
220 |
221 |
222 | 223 | `; 224 | $('#content').html(content); 225 | 226 | var password = localStorage.getItem('password' + path); 227 | $('#list').html(`
`); 228 | $('#readme_md').hide().html(''); 229 | $('#head_md').hide().html(''); 230 | 231 | /** 232 | * 列目录请求成功返回数据后的回调 233 | * @param res 返回的结果(object) 234 | * @param path 请求的路径 235 | * @param prevReqParams 请求时所用的参数 236 | */ 237 | function successResultCallback(res, path, prevReqParams) { 238 | 239 | // 把 nextPageToken 和 currentPageIndex 暂存在 list元素 中 240 | $('#list') 241 | .data('nextPageToken', res['nextPageToken']) 242 | .data('curPageIndex', res['curPageIndex']); 243 | 244 | // 移除 loading spinner 245 | $('#spinner').remove(); 246 | 247 | if (res['nextPageToken'] === null) { 248 | // 如果是最后一页,取消绑定 scroll 事件,重置 scroll_status ,并 append 数据 249 | $(window).off('scroll'); 250 | window.scroll_status.event_bound = false; 251 | window.scroll_status.loading_lock = false; 252 | append_files_to_list(path, res['data']['files']); 253 | } else { 254 | // 如果不是最后一页,append数据 ,并绑定 scroll 事件(如果还未绑定),更新 scroll_status 255 | append_files_to_list(path, res['data']['files']); 256 | if (window.scroll_status.event_bound !== true) { 257 | // 绑定事件,如果还未绑定 258 | $(window).on('scroll', function () { 259 | var scrollTop = $(this).scrollTop(); 260 | var scrollHeight = getDocumentHeight(); 261 | var windowHeight = $(this).height(); 262 | // 滚到底部 263 | if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) { 264 | /* 265 | 滚到底部事件触发时,如果此时已经正在 loading 中,则忽略此次事件; 266 | 否则,去 loading,并占据 loading锁,表明 正在 loading 中 267 | */ 268 | if (window.scroll_status.loading_lock === true) { 269 | return; 270 | } 271 | window.scroll_status.loading_lock = true; 272 | 273 | // 展示一个 loading spinner 274 | $(`
`) 275 | .insertBefore('#readme_md'); 276 | mdui.updateSpinners(); 277 | // mdui.mutation(); 278 | 279 | let $list = $('#list'); 280 | requestListPath(path, { 281 | password: prevReqParams['password'], 282 | page_token: $list.data('nextPageToken'), 283 | // 请求下一页 284 | page_index: $list.data('curPageIndex') + 1 285 | }, 286 | successResultCallback, 287 | // 密码和之前相同。不会出现 authError 288 | null 289 | ) 290 | } 291 | }); 292 | window.scroll_status.event_bound = true 293 | } 294 | } 295 | 296 | // loading 成功,并成功渲染了新数据之后,释放 loading 锁,以便能继续处理 "滚动到底部" 事件 297 | if (window.scroll_status.loading_lock === true) { 298 | window.scroll_status.loading_lock = false 299 | } 300 | } 301 | 302 | // 开始从第1页请求数据 303 | requestListPath(path, {password: password}, 304 | successResultCallback, 305 | function (path) { 306 | $('#spinner').remove(); 307 | var pass = prompt("目录加密, 请输入密码", ""); 308 | localStorage.setItem('password' + path, pass); 309 | if (pass != null && pass != "") { 310 | list(path); 311 | } else { 312 | history.go(-1); 313 | } 314 | }); 315 | } 316 | 317 | /** 318 | * 把请求得来的新一页的数据追加到 list 中 319 | * @param path 路径 320 | * @param files 请求得来的结果 321 | */ 322 | function append_files_to_list(path, files) { 323 | var $list = $('#list'); 324 | // 是最后一页数据了吗? 325 | var is_lastpage_loaded = null === $list.data('nextPageToken'); 326 | var is_firstpage = '0' == $list.data('curPageIndex'); 327 | 328 | html = ""; 329 | let targetFiles = []; 330 | for (i in files) { 331 | var item = files[i]; 332 | var p = path + item.name + '/'; 333 | if (item['size'] == undefined) { 334 | item['size'] = ""; 335 | } 336 | 337 | item['modifiedTime'] = utc2beijing(item['modifiedTime']); 338 | item['size'] = formatFileSize(item['size']); 339 | if (item['mimeType'] == 'application/vnd.google-apps.folder') { 340 | html += `
  • 341 |
    342 | folder_open 343 | ${item.name} 344 |
    345 |
    ${item['modifiedTime']}
    346 |
    ${item['size']}
    347 |
    348 |
  • `; 349 | } else { 350 | var p = path + item.name; 351 | const filepath = path + item.name; 352 | var c = "file"; 353 | // 当加载完最后一页后,才显示 README ,否则会影响滚动事件 354 | if (is_lastpage_loaded && item.name == "README.md") { 355 | get_file(p, item, function (data) { 356 | markdown("#readme_md", data); 357 | }); 358 | } 359 | if (item.name == "HEAD.md") { 360 | get_file(p, item, function (data) { 361 | markdown("#head_md", data); 362 | }); 363 | } 364 | var ext = p.split('.').pop().toLowerCase(); 365 | 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) { 366 | targetFiles.push(filepath); 367 | p += "?a=view"; 368 | c += " view"; 369 | } 370 | html += `
  • 371 |
    372 | insert_drive_file 373 | ${item.name} 374 |
    375 |
    ${item['modifiedTime']}
    376 |
    ${item['size']}
    377 |
    378 |
  • `; 379 | } 380 | } 381 | 382 | /*let targetObj = {}; 383 | targetFiles.forEach((myFilepath, myIndex) => { 384 | if (!targetObj[myFilepath]) { 385 | targetObj[myFilepath] = { 386 | filepath: myFilepath, 387 | prev: myIndex === 0 ? null : targetFiles[myIndex - 1], 388 | next: myIndex === targetFiles.length - 1 ? null : targetFiles[myIndex + 1], 389 | } 390 | } 391 | }) 392 | // console.log(targetObj) 393 | if (Object.keys(targetObj).length) { 394 | localStorage.setItem(path, JSON.stringify(targetObj)); 395 | // console.log(path) 396 | }*/ 397 | 398 | if (targetFiles.length > 0) { 399 | let old = localStorage.getItem(path); 400 | let new_children = targetFiles; 401 | // 第1页重设;否则追加 402 | if (!is_firstpage && old) { 403 | let old_children; 404 | try { 405 | old_children = JSON.parse(old); 406 | if (!Array.isArray(old_children)) { 407 | old_children = [] 408 | } 409 | } catch (e) { 410 | old_children = []; 411 | } 412 | new_children = old_children.concat(targetFiles) 413 | } 414 | 415 | localStorage.setItem(path, JSON.stringify(new_children)) 416 | } 417 | 418 | // 是第1页时,去除横向loading条 419 | $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html); 420 | // 是最后一页时,统计并显示出总项目数 421 | if (is_lastpage_loaded) { 422 | $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length); 423 | } 424 | } 425 | 426 | /** 427 | * 渲染搜索结果列表。有大量重复代码,但是里面有不一样的逻辑,暂时先这样分开弄吧 428 | */ 429 | function render_search_result_list() { 430 | var content = ` 431 | 432 | 433 |
    434 |
      435 |
    • 436 |
      437 | 文件 438 | expand_more 439 |
      440 |
      441 | 修改时间 442 | expand_more 443 |
      444 |
      445 | 大小 446 | expand_more 447 |
      448 |
    • 449 |
    450 |
    451 |
    452 |
      453 |
    454 |
    455 |
    456 | 457 | `; 458 | $('#content').html(content); 459 | 460 | $('#list').html(`
    `); 461 | $('#readme_md').hide().html(''); 462 | $('#head_md').hide().html(''); 463 | 464 | /** 465 | * 搜索请求成功返回数据后的回调 466 | * @param res 返回的结果(object) 467 | * @param path 请求的路径 468 | * @param prevReqParams 请求时所用的参数 469 | */ 470 | function searchSuccessCallback(res, prevReqParams) { 471 | 472 | // 把 nextPageToken 和 currentPageIndex 暂存在 list元素 中 473 | $('#list') 474 | .data('nextPageToken', res['nextPageToken']) 475 | .data('curPageIndex', res['curPageIndex']); 476 | 477 | // 移除 loading spinner 478 | $('#spinner').remove(); 479 | 480 | if (res['nextPageToken'] === null) { 481 | // 如果是最后一页,取消绑定 scroll 事件,重置 scroll_status ,并 append 数据 482 | $(window).off('scroll'); 483 | window.scroll_status.event_bound = false; 484 | window.scroll_status.loading_lock = false; 485 | append_search_result_to_list(res['data']['files']); 486 | } else { 487 | // 如果不是最后一页,append数据 ,并绑定 scroll 事件(如果还未绑定),更新 scroll_status 488 | append_search_result_to_list(res['data']['files']); 489 | if (window.scroll_status.event_bound !== true) { 490 | // 绑定事件,如果还未绑定 491 | $(window).on('scroll', function () { 492 | var scrollTop = $(this).scrollTop(); 493 | var scrollHeight = getDocumentHeight(); 494 | var windowHeight = $(this).height(); 495 | // 滚到底部 496 | if (scrollTop + windowHeight > scrollHeight - (Os.isMobile ? 130 : 80)) { 497 | /* 498 | 滚到底部事件触发时,如果此时已经正在 loading 中,则忽略此次事件; 499 | 否则,去 loading,并占据 loading锁,表明 正在 loading 中 500 | */ 501 | if (window.scroll_status.loading_lock === true) { 502 | return; 503 | } 504 | window.scroll_status.loading_lock = true; 505 | 506 | // 展示一个 loading spinner 507 | $(`
    `) 508 | .insertBefore('#readme_md'); 509 | mdui.updateSpinners(); 510 | // mdui.mutation(); 511 | 512 | let $list = $('#list'); 513 | requestSearch({ 514 | q: window.MODEL.q, 515 | page_token: $list.data('nextPageToken'), 516 | // 请求下一页 517 | page_index: $list.data('curPageIndex') + 1 518 | }, 519 | searchSuccessCallback 520 | ) 521 | } 522 | }); 523 | window.scroll_status.event_bound = true 524 | } 525 | } 526 | 527 | // loading 成功,并成功渲染了新数据之后,释放 loading 锁,以便能继续处理 "滚动到底部" 事件 528 | if (window.scroll_status.loading_lock === true) { 529 | window.scroll_status.loading_lock = false 530 | } 531 | } 532 | 533 | // 开始从第1页请求数据 534 | requestSearch({q: window.MODEL.q}, searchSuccessCallback); 535 | } 536 | 537 | /** 538 | * 追加新一页的搜索结果 539 | * @param files 540 | */ 541 | function append_search_result_to_list(files) { 542 | var $list = $('#list'); 543 | // 是最后一页数据了吗? 544 | var is_lastpage_loaded = null === $list.data('nextPageToken'); 545 | // var is_firstpage = '0' == $list.data('curPageIndex'); 546 | 547 | html = ""; 548 | 549 | for (i in files) { 550 | var item = files[i]; 551 | if (item['size'] == undefined) { 552 | item['size'] = ""; 553 | } 554 | 555 | item['modifiedTime'] = utc2beijing(item['modifiedTime']); 556 | item['size'] = formatFileSize(item['size']); 557 | if (item['mimeType'] == 'application/vnd.google-apps.folder') { 558 | html += `
  • 559 |
    560 | folder_open 561 | ${item.name} 562 |
    563 |
    ${item['modifiedTime']}
    564 |
    ${item['size']}
    565 |
    566 |
  • `; 567 | } else { 568 | var c = "file"; 569 | var ext = item.name.split('.').pop().toLowerCase(); 570 | 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) { 571 | c += " view"; 572 | } 573 | html += `
  • 574 |
    575 | insert_drive_file 576 | ${item.name} 577 |
    578 |
    ${item['modifiedTime']}
    579 |
    ${item['size']}
    580 |
    581 |
  • `; 582 | } 583 | } 584 | 585 | // 是第1页时,去除横向loading条 586 | $list.html(($list.data('curPageIndex') == '0' ? '' : $list.html()) + html); 587 | // 是最后一页时,统计并显示出总项目数 588 | if (is_lastpage_loaded) { 589 | $('#count').removeClass('mdui-hidden').find('.number').text($list.find('li.mdui-list-item').length); 590 | } 591 | } 592 | 593 | /** 594 | * 搜索结果项目点击事件 595 | * @param a_ele 点击的元素 596 | */ 597 | function onSearchResultItemClick(a_ele) { 598 | var me = $(a_ele); 599 | var can_preview = me.hasClass('view'); 600 | var cur = window.current_drive_order; 601 | var dialog = mdui.dialog({ 602 | title: '', 603 | content: '
    正在获取目标路径...
    ', 604 | // content: '
    ', 605 | history: false, 606 | modal: true, 607 | closeOnEsc: true 608 | }); 609 | mdui.updateSpinners(); 610 | 611 | // 请求获取路径 612 | $.post(`/${cur}:id2path`, {id: a_ele.id}, function (data) { 613 | if (data) { 614 | dialog.close(); 615 | var href = `/${cur}:${data}${can_preview ? '?a=view' : ''}`; 616 | dialog = mdui.dialog({ 617 | title: '目标路径', 618 | content: `${data}`, 619 | history: false, 620 | modal: true, 621 | closeOnEsc: true, 622 | buttons: [ 623 | { 624 | text: '打开', onClick: function () { 625 | window.location.href = href 626 | } 627 | }, { 628 | text: '新标签中打开', onClick: function () { 629 | window.open(href) 630 | } 631 | } 632 | , {text: '取消'} 633 | ] 634 | }); 635 | return; 636 | } 637 | dialog.close(); 638 | dialog = mdui.dialog({ 639 | title: '获取目标路径失败', 640 | content: 'o(╯□╰)o 可能是因为该盘中并不存在此项!也可能因为没有把【与我共享】的文件添加到个人云端硬盘中!', 641 | history: false, 642 | modal: true, 643 | closeOnEsc: true, 644 | buttons: [ 645 | {text: 'WTF ???'} 646 | ] 647 | }); 648 | }) 649 | } 650 | 651 | function get_file(path, file, callback) { 652 | var key = "file_path_" + path + file['modifiedTime']; 653 | var data = localStorage.getItem(key); 654 | if (data != undefined) { 655 | return callback(data); 656 | } else { 657 | $.get(path, function (d) { 658 | localStorage.setItem(key, d); 659 | callback(d); 660 | }); 661 | } 662 | } 663 | 664 | 665 | // 文件展示 ?a=view 666 | function file(path) { 667 | var name = path.split('/').pop(); 668 | var ext = name.split('.').pop().toLowerCase().replace(`?a=view`, "").toLowerCase(); 669 | if ("|html|php|css|go|java|js|json|txt|sh|md|".indexOf(`|${ext}|`) >= 0) { 670 | return file_code(path); 671 | } 672 | 673 | if ("|mp4|webm|avi|".indexOf(`|${ext}|`) >= 0) { 674 | return file_video(path); 675 | } 676 | 677 | if ("|mpg|mpeg|mkv|rm|rmvb|mov|wmv|asf|ts|flv|".indexOf(`|${ext}|`) >= 0) { 678 | return file_video(path); 679 | } 680 | 681 | if ("|mp3|flac|wav|ogg|m4a|".indexOf(`|${ext}|`) >= 0) { 682 | return file_audio(path); 683 | } 684 | 685 | if ("|bmp|jpg|jpeg|png|gif|".indexOf(`|${ext}|`) >= 0) { 686 | return file_image(path); 687 | } 688 | 689 | if ('pdf' === ext) return file_pdf(path); 690 | } 691 | 692 | // 文件展示 |html|php|css|go|java|js|json|txt|sh|md| 693 | function file_code(path) { 694 | var type = { 695 | "html": "html", 696 | "php": "php", 697 | "css": "css", 698 | "go": "golang", 699 | "java": "java", 700 | "js": "javascript", 701 | "json": "json", 702 | "txt": "Text", 703 | "sh": "sh", 704 | "md": "Markdown", 705 | }; 706 | var name = path.split('/').pop(); 707 | var ext = name.split('.').pop().toLowerCase(); 708 | var href = window.location.origin + path; 709 | var content = ` 710 |
    711 |
    
     712 | 
    713 |
    714 | 715 | 716 |
    717 | file_download 718 | 719 | 720 | 721 | `; 722 | $('#content').html(content); 723 | 724 | $.get(path, function (data) { 725 | $('#editor').html($('
    ').text(data).html()); 726 | var code_type = "Text"; 727 | if (type[ext] != undefined) { 728 | code_type = type[ext]; 729 | } 730 | var editor = ace.edit("editor"); 731 | editor.setTheme("ace/theme/ambiance"); 732 | editor.setFontSize(18); 733 | editor.session.setMode("ace/mode/" + code_type); 734 | 735 | //Autocompletion 736 | editor.setOptions({ 737 | enableBasicAutocompletion: true, 738 | enableSnippets: true, 739 | enableLiveAutocompletion: true, 740 | maxLines: Infinity 741 | }); 742 | }); 743 | } 744 | 745 | function copyToClipboard(str) { 746 | const $temp = $(""); 747 | $("body").append($temp); 748 | $temp.val(str).select(); 749 | document.execCommand("copy"); 750 | $temp.remove(); 751 | } 752 | 753 | // 文件展示 视频 |mp4|webm|avi| 754 | function file_video(path) { 755 | const url = window.location.origin + path; 756 | let player_items = [ 757 | { 758 | text: 'MXPlayer(Free)', 759 | href: `intent:${url}#Intent;package=com.mxtech.videoplayer.ad;S.title=${path};end`, 760 | }, 761 | { 762 | text: 'MXPlayer(Pro)', 763 | href: `intent:${url}#Intent;package=com.mxtech.videoplayer.pro;S.title=${path};end`, 764 | }, 765 | { 766 | text: 'nPlayer', 767 | href: `nplayer-${url}`, 768 | }, 769 | { 770 | text: 'VLC', 771 | href: `vlc://${url}`, 772 | }, 773 | { 774 | text: 'PotPlayer', 775 | href: `potplayer://${url}` 776 | } 777 | ] 778 | .map(it => `
  • ${it.text}
  • `) 779 | .join(''); 780 | player_items += `
  • 781 |
  • 复制链接
  • `; 782 | const playBtn = ` 783 | 786 |
      ${player_items}
    `; 787 | 788 | const content = ` 789 |
    790 |
    791 | 794 |
    ${playBtn} 795 | 796 |
    797 | 798 | 799 |
    800 |
    801 | 802 | 803 |
    804 |
    805 | file_download 806 | `; 807 | $('#content').html(content); 808 | $('#copy-link').on('click', () => { 809 | copyToClipboard(url); 810 | mdui.snackbar('已复制到剪切板!'); 811 | }); 812 | } 813 | 814 | // 文件展示 音频 |mp3|flac|m4a|wav|ogg| 815 | function file_audio(path) { 816 | var url = window.location.origin + path; 817 | var content = ` 818 |
    819 |
    820 | 823 |
    824 | 825 |
    826 | 827 | 828 |
    829 |
    830 | 831 | 832 |
    833 |
    834 | file_download 835 | `; 836 | $('#content').html(content); 837 | } 838 | 839 | // 文件展示 pdf pdf 840 | function file_pdf(path) { 841 | const url = window.location.origin + path; 842 | const inline_url = `${url}?inline=true` 843 | const file_name = decodeURI(path.slice(path.lastIndexOf('/') + 1, path.length)) 844 | var content = ` 845 | 846 | file_download 847 | `; 848 | $('#content').removeClass('mdui-container').addClass('mdui-container-fluid').css({padding: 0}).html(content); 849 | } 850 | 851 | // 图片展示 852 | function file_image(path) { 853 | var url = window.location.origin + path; 854 | // console.log(window.location.pathname) 855 | const currentPathname = window.location.pathname 856 | const lastIndex = currentPathname.lastIndexOf('/'); 857 | const fatherPathname = currentPathname.slice(0, lastIndex + 1); 858 | // console.log(fatherPathname) 859 | let target_children = localStorage.getItem(fatherPathname); 860 | // console.log(`fatherPathname: ${fatherPathname}`); 861 | // console.log(target_children) 862 | let targetText = ''; 863 | if (target_children) { 864 | try { 865 | target_children = JSON.parse(target_children); 866 | if (!Array.isArray(target_children)) { 867 | target_children = [] 868 | } 869 | } catch (e) { 870 | console.error(e); 871 | target_children = []; 872 | } 873 | if (target_children.length > 0 && target_children.includes(path)) { 874 | let len = target_children.length; 875 | let cur = target_children.indexOf(path); 876 | // console.log(`len = ${len}`) 877 | // console.log(`cur = ${cur}`) 878 | let prev_child = (cur - 1 > -1) ? target_children[cur - 1] : null; 879 | let next_child = (cur + 1 < len) ? target_children[cur + 1] : null; 880 | targetText = ` 881 |
    882 |
    883 |
    884 | ${prev_child ? `` : ``} 885 |
    886 |
    887 | ${next_child ? `` : ``} 888 |
    889 |
    890 |
    891 | `; 892 | } 893 | //
    894 | // ${targetObj[path].prev ? `Prev` : `Prev`} 895 | // ${targetObj[path].next ? `Next` : `Prev`} 896 | //
    897 | } 898 | var content = ` 899 |
    900 |
    901 |
    902 | ${targetText} 903 | 904 |
    905 |
    906 |
    907 | 908 | 909 |
    910 |
    911 | 912 | 913 |
    914 |
    915 | 916 | 917 |
    918 |
    919 |
    920 | file_download 921 | `; 922 | //my code 923 | $('#content').html(content); 924 | $('#leftBtn, #rightBtn').click((e) => { 925 | let target = $(e.target); 926 | if (['I', 'SPAN'].includes(e.target.nodeName)) { 927 | target = $(e.target).parent(); 928 | } 929 | const filepath = target.attr('data-filepath'); 930 | const direction = target.attr('data-direction'); 931 | //console.log(`${direction}翻页 ${filepath}`); 932 | file(filepath) 933 | }); 934 | } 935 | 936 | 937 | //时间转换 938 | function utc2beijing(utc_datetime) { 939 | // 转为正常的时间格式 年-月-日 时:分:秒 940 | var T_pos = utc_datetime.indexOf('T'); 941 | var Z_pos = utc_datetime.indexOf('Z'); 942 | var year_month_day = utc_datetime.substr(0, T_pos); 943 | var hour_minute_second = utc_datetime.substr(T_pos + 1, Z_pos - T_pos - 1); 944 | var new_datetime = year_month_day + " " + hour_minute_second; // 2017-03-31 08:02:06 945 | 946 | // 处理成为时间戳 947 | timestamp = new Date(Date.parse(new_datetime)); 948 | timestamp = timestamp.getTime(); 949 | timestamp = timestamp / 1000; 950 | 951 | // 增加8个小时,北京时间比utc时间多八个时区 952 | var unixtimestamp = timestamp + 8 * 60 * 60; 953 | 954 | // 时间戳转为时间 955 | var unixtimestamp = new Date(unixtimestamp * 1000); 956 | var year = 1900 + unixtimestamp.getYear(); 957 | var month = "0" + (unixtimestamp.getMonth() + 1); 958 | var date = "0" + unixtimestamp.getDate(); 959 | var hour = "0" + unixtimestamp.getHours(); 960 | var minute = "0" + unixtimestamp.getMinutes(); 961 | var second = "0" + unixtimestamp.getSeconds(); 962 | return year + "-" + month.substring(month.length - 2, month.length) + "-" + date.substring(date.length - 2, date.length) 963 | + " " + hour.substring(hour.length - 2, hour.length) + ":" 964 | + minute.substring(minute.length - 2, minute.length) + ":" 965 | + second.substring(second.length - 2, second.length); 966 | } 967 | 968 | // bytes自适应转换到KB,MB,GB 969 | function formatFileSize(bytes) { 970 | if (bytes >= 1000000000) { 971 | bytes = (bytes / 1000000000).toFixed(2) + ' GB'; 972 | } else if (bytes >= 1000000) { 973 | bytes = (bytes / 1000000).toFixed(2) + ' MB'; 974 | } else if (bytes >= 1000) { 975 | bytes = (bytes / 1000).toFixed(2) + ' KB'; 976 | } else if (bytes > 1) { 977 | bytes = bytes + ' bytes'; 978 | } else if (bytes == 1) { 979 | bytes = bytes + ' byte'; 980 | } else { 981 | bytes = ''; 982 | } 983 | return bytes; 984 | } 985 | 986 | String.prototype.trim = function (char) { 987 | if (char) { 988 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), ''); 989 | } 990 | return this.replace(/^\s+|\s+$/g, ''); 991 | }; 992 | 993 | 994 | // README.md HEAD.md 支持 995 | function markdown(el, data) { 996 | if (window.md == undefined) { 997 | //$.getScript('https://cdn.jsdelivr.net/npm/markdown-it@10.0.0/dist/markdown-it.min.js',function(){ 998 | window.md = window.markdownit(); 999 | markdown(el, data); 1000 | //}); 1001 | } else { 1002 | var html = md.render(data); 1003 | $(el).show().html(html); 1004 | } 1005 | } 1006 | 1007 | // 监听回退事件 1008 | window.onpopstate = function () { 1009 | var path = window.location.pathname; 1010 | render(path); 1011 | } 1012 | 1013 | 1014 | $(function () { 1015 | init(); 1016 | var path = window.location.pathname; 1017 | /*$("body").on("click", '.folder', function () { 1018 | var url = $(this).attr('href'); 1019 | history.pushState(null, null, url); 1020 | render(url); 1021 | return false; 1022 | }); 1023 | 1024 | $("body").on("click", '.view', function () { 1025 | var url = $(this).attr('href'); 1026 | history.pushState(null, null, url); 1027 | render(url); 1028 | return false; 1029 | });*/ 1030 | 1031 | render(path); 1032 | }); 1033 | --------------------------------------------------------------------------------