阿里云盘、夸克网盘树状目录

分享页显示树状列表,点击logo旁边笑脸即可

  1. // ==UserScript==
  2. // @name 阿里云盘、夸克网盘树状目录
  3. // @version 1.0
  4. // @description 分享页显示树状列表,点击logo旁边笑脸即可
  5. // @author sunzehui
  6. // @license MIT
  7. // @match https://www.alipan.com/s/*
  8. // @match https://pan.quark.cn/s/*
  9. // @grant GM_xmlhttpRequest
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery.fancytree/2.38.3/jquery.fancytree-all-deps.min.js
  12. // @namespace https://github.com/sunzehui/alipan_treefolder
  13. // ==/UserScript==
  14.  
  15. // 等待dom加载
  16. ;(function() {
  17. var listeners = []
  18. var doc = window.document
  19. varMutationObserver = window.MutationObserver || window.WebKitMutationObserver
  20. var observer
  21.  
  22. function domReady(selector, fn) {
  23. // 储存选择器和回调函数
  24. listeners.push({
  25. selector: selector,
  26. fn: fn,
  27. })
  28. if (!observer) {
  29. // 监听document变化
  30. observer = new MutationObserver(check)
  31. observer.observe(doc.documentElement, {
  32. childList: true,
  33. subtree: true,
  34. })
  35. }
  36. // 检查该节点是否已经在DOM中
  37. check()
  38. }
  39.  
  40. function check() {
  41. // 检查是否匹配已储存的节点
  42. for (var i = 0; i < listeners.length; i++) {
  43. var listener = listeners[i]
  44. // 检查指定节点是否有匹配
  45. var elements = doc.querySelectorAll(listener.selector)
  46. for (var j = 0; j < elements.length; j++) {
  47. var element = elements[j]
  48. // 确保回调函数只会对该元素调用一次
  49. if (!element.ready) {
  50. element.ready = true
  51. // 对该节点调用回调函数
  52. listener.fn.call(element, element)
  53. }
  54. }
  55. }
  56. }
  57.  
  58. // 对外暴露ready
  59. window.domReady = domReady
  60. })()
  61.  
  62. class AliPanTree {
  63. constructor() {
  64. this.tokenStorage = JSON.parse(localStorage.getItem('shareToken'))
  65. this.token = this.tokenStorage.share_token ? this.tokenStorage.share_token : ''
  66. this.device_id = this.parseCookie(document.cookie)['cna']
  67. this.share_id = this.getShareId()
  68. this.headers = {
  69. accept: 'application/json, text/plain, */*',
  70. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
  71. 'content-type': 'application/json;charset=UTF-8',
  72. 'sec-fetch-mode': 'cors',
  73. 'sec-fetch-site': 'same-site',
  74. 'x-canary': 'client=web,app=adrive,version=v2.3.1',
  75. 'x-device-id': this.device_id,
  76. 'x-share-token': this.token,
  77. }
  78. this.config = {
  79. insertContainer: 'div.CommonHeader--container--LPZpeBK',
  80. tagClassname: 'script-tag',
  81. insertTreeViewContainer: '.DetailLayout--container--264z8Xd',
  82. lazyLoad: true,
  83. fancytreeCSS_CDN: 'https://cdnjs.cloudflare.com/ajax/libs/jquery.fancytree/2.27.0/skin-win8/ui.fancytree.css',
  84. }
  85. this.api = {
  86. fileList: 'https://api.aliyundrive.com/adrive/v3/file/list',
  87. }
  88. this.params = {}
  89. this.isLoading = false
  90. this.nowSelectNode = null
  91. }
  92.  
  93. parseCookie(str) {
  94. return str.split(';').map(v => v.split('=')).reduce((acc, v) => {
  95. acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim())
  96. return acc
  97. }, {})
  98. }
  99. getShareId() {
  100. const url = location.pathname
  101. return url.match(/(?<=\/s\/)(\w+)(?=\/folder)?/g)[0]
  102. }
  103. loading(type = 'start') {
  104. const tag = $('.' + this.config.tagClassname)
  105.  
  106. if (this.isLoading == false && type == 'start') {
  107. this.isLoading = true
  108. setTimeout(() => {
  109. if (!this.isLoading) return
  110. if (tag.html() == 'o') {
  111. tag.html('0')
  112. } else {
  113. tag.html('o')
  114. }
  115. this.loading('start')
  116. }, 500)
  117. }
  118.  
  119. if (this.isLoading == true && type == 'stop') {
  120. this.isLoading = false
  121. tag.html('&#128515;')
  122. }
  123. }
  124.  
  125. renderTag() {
  126. const tag = document.createElement('div')
  127. tag.classList.add(this.config.tagClassname)
  128. tag.innerHTML = '&#128515;'
  129.  
  130. let that = this
  131. $(document).on('click', '.' + this.config.tagClassname, function() {
  132. that.handleTagClick()
  133. })
  134.  
  135. domReady('div.banner--7Ux0y', function() {
  136. document.querySelector('#root > div > div.page--W3d1U > .banner--7Ux0y').appendChild(tag)
  137. })
  138. }
  139.  
  140. listAdapter(list, isFirst = true) {
  141. return list.map(item => {
  142. const hasFolder = !!item.children
  143. const obj = {
  144. title: item.name,
  145. folder: hasFolder,
  146. expanded: isFirst,
  147. }
  148. if (hasFolder) {
  149. obj.children = this.listAdapter(item.children, false)
  150. }
  151. return obj
  152. })
  153. }
  154. async buildFancytreeCfg() {
  155. const that = this
  156. const cfg = {
  157. selectMode: 1,
  158. autoScroll: true,
  159. activate: function(event, data) {
  160. that.nowSelectNode = data.node
  161. },
  162. }
  163.  
  164. const loadRootNode = async (event, data) => {
  165. const list = await that.getList({ parent_file_id: 'root' })
  166. const children = await Promise.all(
  167. list.items.map(async pItem => {
  168. const cList = await that.getList({ parent_file_id: pItem.file_id })
  169. return cList.items.map(cItem => {
  170. return {
  171. title: cItem.name,
  172. folder: cItem.type === 'folder',
  173. key: cItem.file_id,
  174. lazy: true,
  175. }
  176. })
  177. })
  178. )
  179. return list.items.map(item => ({
  180. title: item.name,
  181. folder: item.type === 'folder',
  182. key: item.file_id,
  183. expanded: true,
  184. lazy: true,
  185. children: children.flat(1),
  186. }))
  187. }
  188.  
  189. const loadNode = function(event, data) {
  190. data.result = that.getList({ parent_file_id: data.node.key }).then(list => {
  191. return list.items.map(item => ({
  192. title: item.name,
  193. folder: item.type === 'folder',
  194. key: item.file_id,
  195. lazy: item.type === 'folder',
  196. }))
  197. })
  198. }
  199. if (this.config.lazyLoad) {
  200. cfg['source'] = loadRootNode()
  201. cfg['lazyLoad'] = loadNode
  202. } else {
  203. const tree = await this.buildTree()
  204. cfg['source'] = await this.listAdapter(tree.children)
  205. }
  206. return cfg
  207. }
  208. async handleTagClick() {
  209. console.log('clicked')
  210. const $existsView = $('.tree-container')
  211. if ($existsView.length > 0) {
  212. return $existsView.show()
  213. }
  214. this.loading()
  215. await this.renderView()
  216. this.loading('stop')
  217. }
  218. // 显示侧边栏
  219. async renderView() {
  220. const cfg = await this.buildFancytreeCfg()
  221. const $treeContainer = $(`
  222. <div class="tree-container">
  223. <div class="bar">
  224. <button class="btn sunzehuiBtn">进入选中文件夹</button>
  225. <button class="btn close-btn">X</button>
  226. </div>
  227. <div class="tree"></div>
  228. </div>
  229. `)
  230. $treeContainer.find('.tree').fancytree(cfg)
  231.  
  232. const that = this
  233. $(document).on('click', '.tree-container .bar .sunzehuiBtn', function() {
  234. let link = null
  235. const nowSelectNode = that.nowSelectNode
  236. if (nowSelectNode && nowSelectNode.folder) {
  237. link = `/s/${that.getShareId()}/folder/${nowSelectNode.key}`
  238. }
  239. if (link == null) alert('请选择文件夹')
  240. window.open(link, '_blank')
  241. })
  242.  
  243. $(document).on('click', '.tree-container .bar .close-btn', function() {
  244. $('.tree-container').hide()
  245. })
  246.  
  247. domReady('div.content--t4XI8', function() {
  248. $('#root > div > div.page--W3d1U').append($treeContainer)
  249. })
  250. }
  251.  
  252. // 获取文件列表
  253. async getList({ parent_file_id }) {
  254. const result = await fetch(this.api.fileList, {
  255. headers: this.headers,
  256. referrerPolicy: 'origin',
  257. body: JSON.stringify({
  258. share_id: this.share_id,
  259. parent_file_id: parent_file_id || 'root',
  260. limit: 100,
  261. image_thumbnail_process: 'image/resize,w_160/format,jpeg',
  262. image_url_process: 'image/resize,w_1920/format,jpeg',
  263. video_thumbnail_process: 'video/snapshot,t_1000,f_jpg,ar_auto,w_300',
  264. order_by: 'name',
  265. order_direction: 'DESC',
  266. }),
  267. method: 'POST',
  268. mode: 'cors',
  269. credentials: 'omit',
  270. })
  271. return await result.json()
  272. }
  273.  
  274. async buildTree(parent_file_id) {
  275. const treeNode = {}
  276. const root = await this.getList({ parent_file_id })
  277. treeNode.children = []
  278. for (let i = 0; i < root.items.length; i++) {
  279. let node = void 0
  280. if (root.items[i].type === 'folder') {
  281. node = await this.buildTree(root.items[i].file_id)
  282. node.name = root.items[i].name
  283. } else {
  284. node = root.items[i]
  285. }
  286. treeNode.children.push(node)
  287. }
  288. return treeNode
  289. }
  290.  
  291. insertCSS() {
  292. const cssElem = document.createElement('link')
  293. cssElem.setAttribute('rel', 'stylesheet')
  294. cssElem.setAttribute('href', this.config.fancytreeCSS_CDN)
  295. document.body.appendChild(cssElem)
  296. const cssElem2 = document.createElement('style')
  297. cssElem2.innerHTML = `
  298. .tree-container{
  299. height: 100%;
  300. background: #ecf0f1;
  301. position: fixed;
  302. top: 60px;
  303. z-index: 9999;
  304. left: 0;
  305. overflow-y:scroll
  306. }
  307. .tree-container .bar{
  308. background: #bdc3c7;
  309. padding: 0 20px;
  310. display: flex;
  311. justify-content: space-between;
  312. align-items: center;
  313. height: 40px;
  314. }
  315. .btn{
  316. padding: 0;
  317. height: 30px;
  318. }
  319. .close-btn{
  320. background: transparent;
  321. border: none;
  322. }
  323. .sunzehuiBtn{
  324. display: inline-block;
  325. font-weight: 400;
  326. text-align: center;
  327. vertical-align: middle;
  328. user-select: none;
  329. border: 1px solid transparent;
  330. transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
  331. padding: 0 8px;
  332. font-size: 14px;
  333. border-radius: .2rem;
  334. color: #fff;
  335. background-color: #6c757d;
  336. border-color: #6c757d;
  337. cursor: pointer;
  338. }
  339. .sunzehuiBtn:hover{
  340. text-decoration: none;
  341. background-color: #5a6268;
  342. border-color: #545b62;
  343. }
  344. .sunzehuiBtn:focus {
  345. box-shadow: 0 0 0 0.2rem rgb(130 138 145 / 50%);
  346. }
  347. ul.fancytree-container{
  348. background-color:transparent !important;
  349. border:none !important;
  350. }
  351. .${this.config.tagClassname}{
  352. width: 20px;
  353. height: 20px;
  354. margin-right: auto;
  355. transform: translateY(-3px);
  356. margin-left: 20px;
  357. cursor: pointer;
  358. }
  359. `
  360. document.body.appendChild(cssElem2)
  361. }
  362. async init() {
  363. this.insertCSS()
  364. this.renderTag()
  365. }
  366. }
  367.  
  368. class QuarkPanTree {
  369. constructor() {
  370. this.api = {
  371. fileList: 'https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail',
  372. }
  373. this.config = {
  374. insertContainer: 'div.CommonHeader--container--LPZpeBK',
  375. tagClassname: 'script-tag',
  376. insertTreeViewContainer: '.DetailLayout--container--264z8Xd',
  377. lazyLoad: true,
  378. fancytreeCSS_CDN: 'https://cdnjs.cloudflare.com/ajax/libs/jquery.fancytree/2.27.0/skin-win8/ui.fancytree.css',
  379. }
  380. this.headers = {
  381. accept: 'application/json, text/plain, */*',
  382. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
  383. 'content-type': 'application/json;charset=UTF-8',
  384. 'sec-fetch-mode': 'cors',
  385. 'sec-fetch-site': 'same-site',
  386. 'x-canary': 'client=web,app=adrive,version=v2.3.1',
  387. }
  388. this.params = {
  389. pwd_id: this.getPwdId(),
  390. }
  391. this.nowSelectNode = null
  392. this.isLoading = false
  393. }
  394.  
  395. // ... existing code ...
  396.  
  397. parseCookie(str) {
  398. // ... existing code ...
  399. }
  400.  
  401. getPwdId() {
  402. const url = location.pathname
  403. return url.match(/(?<=\/s\/)(\w+)(?=#)?/g)[0]
  404. }
  405. getStoken() {
  406. const tokenStorage = JSON.parse(sessionStorage.getItem('_share_args'))
  407. return tokenStorage.value.stoken ? tokenStorage.value.stoken : ''
  408. }
  409.  
  410. loading(type = 'start') {
  411. const tag = $('.' + this.config.tagClassname)
  412.  
  413. if (this.isLoading == false && type == 'start') {
  414. this.isLoading = true
  415. setTimeout(() => {
  416. if (!this.isLoading) return
  417. if (tag.html() == 'o') {
  418. tag.html('0')
  419. } else {
  420. tag.html('o')
  421. }
  422. this.loading('start')
  423. }, 500)
  424. }
  425.  
  426. if (this.isLoading == true && type == 'stop') {
  427. this.isLoading = false
  428. tag.html('&#128515;')
  429. }
  430. }
  431. async handleTagClick() {
  432. const $existsView = $('.tree-container')
  433. if ($existsView.length > 0) {
  434. return $existsView.show()
  435. }
  436. this.loading()
  437. await this.renderView()
  438. this.loading('stop')
  439. }
  440.  
  441. renderTag() {
  442. const tag = document.createElement('div')
  443. tag.classList.add(this.config.tagClassname)
  444. tag.innerHTML = '&#128515;'
  445.  
  446. let that = this
  447. $(document).on('click', '.' + this.config.tagClassname, function() {
  448. that.handleTagClick()
  449. })
  450.  
  451. const insertContainer = this.config.insertContainer
  452. domReady(insertContainer, function() {
  453. document.querySelector(insertContainer).appendChild(tag)
  454. })
  455. }
  456.  
  457. listAdapter(list, isFirst = true) {
  458. return list.map(item => {
  459. const hasFolder = !!item.children
  460. const obj = {
  461. title: item.name,
  462. folder: hasFolder,
  463. expanded: isFirst,
  464. }
  465. if (hasFolder) {
  466. obj.children = this.listAdapter(item.children, false)
  467. }
  468. return obj
  469. })
  470. }
  471.  
  472. async buildFancytreeCfg() {
  473. const that = this
  474. const cfg = {
  475. selectMode: 1,
  476. autoScroll: true,
  477. activate: function(event, data) {
  478. that.nowSelectNode = data.node
  479. },
  480. }
  481. const loadRootNode = async (event, data) => {
  482. const list = await this.getList({ parent_file_id: 0 })
  483.  
  484. const children = await Promise.all(
  485. list.map(async pItem => {
  486. const cList = await this.getList({ parent_file_id: pItem.fid })
  487. return cList.map(cItem => {
  488. return {
  489. title: cItem.file_name,
  490. folder: cItem.dir,
  491. key: cItem.fid,
  492. lazy: true,
  493. }
  494. })
  495. })
  496. )
  497. return list.map(item => ({
  498. title: item.file_name,
  499. folder: item.dir,
  500. key: item.fid,
  501. expanded: true,
  502. lazy: true,
  503. children: children.flat(1),
  504. }))
  505. }
  506.  
  507. const loadNode = function(event, data) {
  508. data.result = that.getList({ parent_file_id: data.node.key }).then(list => {
  509. return list.map(item => ({
  510. title: item.file_name,
  511. folder: item.dir,
  512. key: item.fid,
  513. lazy: item.dir,
  514. }))
  515. })
  516. }
  517. if (this.config.lazyLoad) {
  518. cfg['source'] = loadRootNode()
  519. cfg['lazyLoad'] = loadNode
  520. } else {
  521. const tree = await this.buildTree()
  522. cfg['source'] = await this.listAdapter(tree.children)
  523. }
  524. return cfg
  525. }
  526.  
  527. async renderView() {
  528. const cfg = await this.buildFancytreeCfg()
  529. const $treeContainer = $(`
  530. <div class="tree-container">
  531. <div class="bar">
  532. <button class="btn sunzehuiBtn">进入选中文件夹</button>
  533. <button class="btn close-btn">X</button>
  534. </div>
  535. <div class="tree"></div>
  536. </div>
  537. `)
  538. $treeContainer.find('.tree').fancytree(cfg)
  539.  
  540. const that = this
  541. $(document).on('click', '.tree-container .bar .sunzehuiBtn', function() {
  542. const selectedNode = that.nowSelectNode
  543. if (!selectedNode || !selectedNode.folder) return alert('未选中文件夹')
  544. // 文件路径 = https://pan.quark.cn/s/{pwd_id}#/list/share/{文件id}-{文件名}/{文件id}-{文件名}/
  545. const pList = [...selectedNode.getParentList(), selectedNode]
  546. let filePath = `https://pan.quark.cn/s/${that.getPwdId()}#/list/share/`
  547.  
  548. const link = pList.reduce((acc, cur) => {
  549. return `${acc}${cur.key}-${cur.title}/`
  550. }, filePath)
  551. window.open(link, '_blank')
  552. })
  553.  
  554. $(document).on('click', '.tree-container .bar .close-btn', function() {
  555. $('.tree-container').hide()
  556. })
  557.  
  558. const insertTreeViewContainer = this.config.insertTreeViewContainer
  559. domReady(insertTreeViewContainer, function() {
  560. $(insertTreeViewContainer).append($treeContainer)
  561. })
  562. }
  563.  
  564. async getList({ parent_file_id }) {
  565. let url = new URL(this.api.fileList)
  566. let params = {
  567. pr: 'ucpro',
  568. fr: 'pc',
  569. uc_param_str: '',
  570. pwd_id: this.getPwdId(),
  571. stoken: this.getStoken(),
  572. pdir_fid: parent_file_id || 0,
  573. force: 0,
  574. _page: 1,
  575. _size: 50,
  576. _fetch_banner: 0,
  577. _fetch_share: 0,
  578. _fetch_total: 1,
  579. _sort: 'file_type:asc,updated_at:desc',
  580. __dt: 959945,
  581. __t: +new Date(),
  582. }
  583. Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
  584. const result = await fetch(url, {
  585. headers: this.headers,
  586. referrerPolicy: 'origin',
  587. method: 'GET',
  588. mode: 'cors',
  589. credentials: 'omit',
  590. })
  591. const resp = await result.json()
  592.  
  593. return resp.data.list
  594. }
  595.  
  596. async buildTree(parent_file_id) {
  597. const treeNode = {}
  598. const list = await this.getList({ parent_file_id })
  599. treeNode.children = []
  600. for (let i = 0; i < list.length; i++) {
  601. let node = void 0
  602. const item = list[i]
  603. if (item.dir) {
  604. node = await this.buildTree(item.fid)
  605. node.name = item.file_name
  606. } else {
  607. node = item
  608. }
  609. treeNode.children.push(node)
  610. }
  611. return treeNode
  612. }
  613.  
  614. insertCSS() {
  615. const cssElem = document.createElement('link')
  616. cssElem.setAttribute('rel', 'stylesheet')
  617. cssElem.setAttribute('href', this.config.fancytreeCSS_CDN)
  618. document.body.appendChild(cssElem)
  619. const cssElem2 = document.createElement('style')
  620. cssElem2.innerHTML = `
  621. .tree-container{
  622. height: 100%;
  623. background: #ecf0f1;
  624. position: fixed;
  625. top: 60px;
  626. z-index: 9999;
  627. left: 0;
  628. overflow-y:scroll
  629. }
  630. .tree-container .bar{
  631. background: #bdc3c7;
  632. padding: 0 20px;
  633. display: flex;
  634. justify-content: space-between;
  635. align-items: center;
  636. height: 40px;
  637. }
  638. .btn{
  639. padding: 0;
  640. height: 30px;
  641. }
  642. .sunzehuiBtn{
  643. display: inline-block;
  644. font-weight: 400;
  645. text-align: center;
  646. vertical-align: middle;
  647. user-select: none;
  648. border: 1px solid transparent;
  649. transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
  650. padding: 0 8px;
  651. font-size: 14px;
  652. border-radius: .2rem;
  653. color: #fff;
  654. background-color: #6c757d;
  655. border-color: #6c757d;
  656. cursor: pointer;
  657. }
  658. .sunzehuiBtn:hover{
  659. text-decoration: none;
  660. background-color: #5a6268;
  661. border-color: #545b62;
  662. }
  663. .sunzehuiBtn:focus {
  664. box-shadow: 0 0 0 0.2rem rgb(130 138 145 / 50%);
  665. }
  666. ul.fancytree-container{
  667. background-color:transparent !important;
  668. border:none !important;
  669. }
  670. .${this.config.tagClassname}{
  671. width: 20px;
  672. height: 20px;
  673. margin-right: auto;
  674. transform: translateY(-3px);
  675. margin-left: 20px;
  676. cursor: pointer;
  677. }
  678. `
  679. document.body.appendChild(cssElem2)
  680. }
  681.  
  682. async init() {
  683. this.insertCSS()
  684. this.renderTag()
  685. }
  686. }
  687. $(async function() {
  688. const thisHost = window.location.host
  689. switch (thisHost) {
  690. case 'www.alipan.com':
  691. const aliScript = new AliPanTree()
  692. aliScript.init()
  693. break
  694. case 'pan.quark.cn':
  695. const quarkScript = new QuarkPanTree()
  696. quarkScript.init()
  697. break
  698. default:
  699. console.error('暂不支持该网站')
  700. break
  701. }
  702. })

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址