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

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

// ==UserScript==
// @name          阿里云盘、夸克网盘树状目录
// @version       1.0
// @description   分享页显示树状列表,点击logo旁边笑脸即可
// @author        sunzehui
// @license       MIT
// @match         https://www.alipan.com/s/*
// @match         https://pan.quark.cn/s/*
// @grant         GM_xmlhttpRequest
// @require       https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @require       https://cdnjs.cloudflare.com/ajax/libs/jquery.fancytree/2.38.3/jquery.fancytree-all-deps.min.js
// @namespace     https://github.com/sunzehui/alipan_treefolder
// ==/UserScript==

// 等待dom加载
;(function() {
  var listeners = []
  var doc = window.document
  varMutationObserver = window.MutationObserver || window.WebKitMutationObserver
  var observer

  function domReady(selector, fn) {
    // 储存选择器和回调函数
    listeners.push({
      selector: selector,
      fn: fn,
    })
    if (!observer) {
      // 监听document变化
      observer = new MutationObserver(check)
      observer.observe(doc.documentElement, {
        childList: true,
        subtree: true,
      })
    }
    // 检查该节点是否已经在DOM中
    check()
  }

  function check() {
    // 检查是否匹配已储存的节点
    for (var i = 0; i < listeners.length; i++) {
      var listener = listeners[i]
      // 检查指定节点是否有匹配
      var elements = doc.querySelectorAll(listener.selector)
      for (var j = 0; j < elements.length; j++) {
        var element = elements[j]
        // 确保回调函数只会对该元素调用一次
        if (!element.ready) {
          element.ready = true
          // 对该节点调用回调函数
          listener.fn.call(element, element)
        }
      }
    }
  }

  // 对外暴露ready
  window.domReady = domReady
})()

class AliPanTree {
  constructor() {
    this.tokenStorage = JSON.parse(localStorage.getItem('shareToken'))
    this.token = this.tokenStorage.share_token ? this.tokenStorage.share_token : ''
    this.device_id = this.parseCookie(document.cookie)['cna']
    this.share_id = this.getShareId()
    this.headers = {
      accept: 'application/json, text/plain, */*',
      'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
      'content-type': 'application/json;charset=UTF-8',
      'sec-fetch-mode': 'cors',
      'sec-fetch-site': 'same-site',
      'x-canary': 'client=web,app=adrive,version=v2.3.1',
      'x-device-id': this.device_id,
      'x-share-token': this.token,
    }
    this.config = {
      insertContainer: 'div.CommonHeader--container--LPZpeBK',
      tagClassname: 'script-tag',
      insertTreeViewContainer: '.DetailLayout--container--264z8Xd',
      lazyLoad: true,
      fancytreeCSS_CDN: 'https://cdnjs.cloudflare.com/ajax/libs/jquery.fancytree/2.27.0/skin-win8/ui.fancytree.css',
    }
    this.api = {
      fileList: 'https://api.aliyundrive.com/adrive/v3/file/list',
    }
    this.params = {}
    this.isLoading = false
    this.nowSelectNode = null
  }

  parseCookie(str) {
    return str.split(';').map(v => v.split('=')).reduce((acc, v) => {
      acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim())
      return acc
    }, {})
  }
  getShareId() {
    const url = location.pathname
    return url.match(/(?<=\/s\/)(\w+)(?=\/folder)?/g)[0]
  }
  loading(type = 'start') {
    const tag = $('.' + this.config.tagClassname)

    if (this.isLoading == false && type == 'start') {
      this.isLoading = true
      setTimeout(() => {
        if (!this.isLoading) return
        if (tag.html() == 'o') {
          tag.html('0')
        } else {
          tag.html('o')
        }
        this.loading('start')
      }, 500)
    }

    if (this.isLoading == true && type == 'stop') {
      this.isLoading = false
      tag.html('&#128515;')
    }
  }

  renderTag() {
    const tag = document.createElement('div')
    tag.classList.add(this.config.tagClassname)
    tag.innerHTML = '&#128515;'

    let that = this
    $(document).on('click', '.' + this.config.tagClassname, function() {
      that.handleTagClick()
    })

    domReady('div.banner--7Ux0y', function() {
      document.querySelector('#root > div > div.page--W3d1U > .banner--7Ux0y').appendChild(tag)
    })
  }

  listAdapter(list, isFirst = true) {
    return list.map(item => {
      const hasFolder = !!item.children
      const obj = {
        title: item.name,
        folder: hasFolder,
        expanded: isFirst,
      }
      if (hasFolder) {
        obj.children = this.listAdapter(item.children, false)
      }
      return obj
    })
  }
  async buildFancytreeCfg() {
    const that = this
    const cfg = {
      selectMode: 1,
      autoScroll: true,
      activate: function(event, data) {
        that.nowSelectNode = data.node
      },
    }

    const loadRootNode = async (event, data) => {
      const list = await that.getList({ parent_file_id: 'root' })
      const children = await Promise.all(
        list.items.map(async pItem => {
          const cList = await that.getList({ parent_file_id: pItem.file_id })
          return cList.items.map(cItem => {
            return {
              title: cItem.name,
              folder: cItem.type === 'folder',
              key: cItem.file_id,
              lazy: true,
            }
          })
        })
      )
      return list.items.map(item => ({
        title: item.name,
        folder: item.type === 'folder',
        key: item.file_id,
        expanded: true,
        lazy: true,
        children: children.flat(1),
      }))
    }

    const loadNode = function(event, data) {
      data.result = that.getList({ parent_file_id: data.node.key }).then(list => {
        return list.items.map(item => ({
          title: item.name,
          folder: item.type === 'folder',
          key: item.file_id,
          lazy: item.type === 'folder',
        }))
      })
    }
    if (this.config.lazyLoad) {
      cfg['source'] = loadRootNode()
      cfg['lazyLoad'] = loadNode
    } else {
      const tree = await this.buildTree()
      cfg['source'] = await this.listAdapter(tree.children)
    }
    return cfg
  }
  async handleTagClick() {
    console.log('clicked')
    const $existsView = $('.tree-container')
    if ($existsView.length > 0) {
      return $existsView.show()
    }
    this.loading()
    await this.renderView()
    this.loading('stop')
  }
  // 显示侧边栏
  async renderView() {
    const cfg = await this.buildFancytreeCfg()
    const $treeContainer = $(`
      <div class="tree-container">
        <div class="bar">
          <button class="btn sunzehuiBtn">进入选中文件夹</button>
          <button class="btn close-btn">X</button>
        </div>
        <div class="tree"></div>
      </div>
    `)
    $treeContainer.find('.tree').fancytree(cfg)

    const that = this
    $(document).on('click', '.tree-container .bar .sunzehuiBtn', function() {
      let link = null
      const nowSelectNode = that.nowSelectNode
      if (nowSelectNode && nowSelectNode.folder) {
        link = `/s/${that.getShareId()}/folder/${nowSelectNode.key}`
      }
      if (link == null) alert('请选择文件夹')
      window.open(link, '_blank')
    })

    $(document).on('click', '.tree-container .bar .close-btn', function() {
      $('.tree-container').hide()
    })

    domReady('div.content--t4XI8', function() {
      $('#root > div > div.page--W3d1U').append($treeContainer)
    })
  }

  // 获取文件列表
  async getList({ parent_file_id }) {
    const result = await fetch(this.api.fileList, {
      headers: this.headers,
      referrerPolicy: 'origin',
      body: JSON.stringify({
        share_id: this.share_id,
        parent_file_id: parent_file_id || 'root',
        limit: 100,
        image_thumbnail_process: 'image/resize,w_160/format,jpeg',
        image_url_process: 'image/resize,w_1920/format,jpeg',
        video_thumbnail_process: 'video/snapshot,t_1000,f_jpg,ar_auto,w_300',
        order_by: 'name',
        order_direction: 'DESC',
      }),
      method: 'POST',
      mode: 'cors',
      credentials: 'omit',
    })
    return await result.json()
  }

  async buildTree(parent_file_id) {
    const treeNode = {}
    const root = await this.getList({ parent_file_id })
    treeNode.children = []
    for (let i = 0; i < root.items.length; i++) {
      let node = void 0
      if (root.items[i].type === 'folder') {
        node = await this.buildTree(root.items[i].file_id)
        node.name = root.items[i].name
      } else {
        node = root.items[i]
      }
      treeNode.children.push(node)
    }
    return treeNode
  }

  insertCSS() {
    const cssElem = document.createElement('link')
    cssElem.setAttribute('rel', 'stylesheet')
    cssElem.setAttribute('href', this.config.fancytreeCSS_CDN)
    document.body.appendChild(cssElem)
    const cssElem2 = document.createElement('style')
    cssElem2.innerHTML = `
    .tree-container{
      height: 100%;
      background: #ecf0f1;
      position: fixed;
      top: 60px;
      z-index: 9999;
      left: 0;
      overflow-y:scroll
    }
    .tree-container .bar{
      background: #bdc3c7;
      padding: 0 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 40px;
    }
    .btn{
      padding: 0;
      height: 30px;
    }
    .close-btn{
      background: transparent;
      border: none;
    }
    .sunzehuiBtn{
      display: inline-block;
      font-weight: 400;
      text-align: center;
      vertical-align: middle;
      user-select: none;
      border: 1px solid transparent;
      transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
      padding: 0 8px;
      font-size: 14px;
      border-radius: .2rem;
      color: #fff;
      background-color: #6c757d;
      border-color: #6c757d;
      cursor: pointer;
    }
    .sunzehuiBtn:hover{
      text-decoration: none;
      background-color: #5a6268;
      border-color: #545b62;
    }
    .sunzehuiBtn:focus {
      box-shadow: 0 0 0 0.2rem rgb(130 138 145 / 50%);
    }
    ul.fancytree-container{
      background-color:transparent !important;
      border:none !important;
    }
    .${this.config.tagClassname}{
      width: 20px;
      height: 20px;
      margin-right: auto;
      transform: translateY(-3px);
      margin-left: 20px;
      cursor: pointer;
    }
    `
    document.body.appendChild(cssElem2)
  }
  async init() {
    this.insertCSS()
    this.renderTag()
  }
}

class QuarkPanTree {
  constructor() {
    this.api = {
      fileList: 'https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail',
    }
    this.config = {
      insertContainer: 'div.CommonHeader--container--LPZpeBK',
      tagClassname: 'script-tag',
      insertTreeViewContainer: '.DetailLayout--container--264z8Xd',
      lazyLoad: true,
      fancytreeCSS_CDN: 'https://cdnjs.cloudflare.com/ajax/libs/jquery.fancytree/2.27.0/skin-win8/ui.fancytree.css',
    }
    this.headers = {
      accept: 'application/json, text/plain, */*',
      'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
      'content-type': 'application/json;charset=UTF-8',
      'sec-fetch-mode': 'cors',
      'sec-fetch-site': 'same-site',
      'x-canary': 'client=web,app=adrive,version=v2.3.1',
    }
    this.params = {
      pwd_id: this.getPwdId(),
    }
    this.nowSelectNode = null
    this.isLoading = false
  }

  // ... existing code ...

  parseCookie(str) {
    // ... existing code ...
  }

  getPwdId() {
    const url = location.pathname
    return url.match(/(?<=\/s\/)(\w+)(?=#)?/g)[0]
  }
  getStoken() {
    const tokenStorage = JSON.parse(sessionStorage.getItem('_share_args'))
    return tokenStorage.value.stoken ? tokenStorage.value.stoken : ''
  }

  loading(type = 'start') {
    const tag = $('.' + this.config.tagClassname)

    if (this.isLoading == false && type == 'start') {
      this.isLoading = true
      setTimeout(() => {
        if (!this.isLoading) return
        if (tag.html() == 'o') {
          tag.html('0')
        } else {
          tag.html('o')
        }
        this.loading('start')
      }, 500)
    }

    if (this.isLoading == true && type == 'stop') {
      this.isLoading = false
      tag.html('&#128515;')
    }
  }
  async handleTagClick() {
    const $existsView = $('.tree-container')
    if ($existsView.length > 0) {
      return $existsView.show()
    }
    this.loading()
    await this.renderView()
    this.loading('stop')
  }

  renderTag() {
    const tag = document.createElement('div')
    tag.classList.add(this.config.tagClassname)
    tag.innerHTML = '&#128515;'

    let that = this
    $(document).on('click', '.' + this.config.tagClassname, function() {
      that.handleTagClick()
    })

    const insertContainer = this.config.insertContainer
    domReady(insertContainer, function() {
      document.querySelector(insertContainer).appendChild(tag)
    })
  }

  listAdapter(list, isFirst = true) {
    return list.map(item => {
      const hasFolder = !!item.children
      const obj = {
        title: item.name,
        folder: hasFolder,
        expanded: isFirst,
      }
      if (hasFolder) {
        obj.children = this.listAdapter(item.children, false)
      }
      return obj
    })
  }

  async buildFancytreeCfg() {
    const that = this
    const cfg = {
      selectMode: 1,
      autoScroll: true,
      activate: function(event, data) {
        that.nowSelectNode = data.node
      },
    }
    const loadRootNode = async (event, data) => {
      const list = await this.getList({ parent_file_id: 0 })

      const children = await Promise.all(
        list.map(async pItem => {
          const cList = await this.getList({ parent_file_id: pItem.fid })
          return cList.map(cItem => {
            return {
              title: cItem.file_name,
              folder: cItem.dir,
              key: cItem.fid,
              lazy: true,
            }
          })
        })
      )
      return list.map(item => ({
        title: item.file_name,
        folder: item.dir,
        key: item.fid,
        expanded: true,
        lazy: true,
        children: children.flat(1),
      }))
    }

    const loadNode = function(event, data) {
      data.result = that.getList({ parent_file_id: data.node.key }).then(list => {
        return list.map(item => ({
          title: item.file_name,
          folder: item.dir,
          key: item.fid,
          lazy: item.dir,
        }))
      })
    }
    if (this.config.lazyLoad) {
      cfg['source'] = loadRootNode()
      cfg['lazyLoad'] = loadNode
    } else {
      const tree = await this.buildTree()
      cfg['source'] = await this.listAdapter(tree.children)
    }
    return cfg
  }

  async renderView() {
    const cfg = await this.buildFancytreeCfg()
    const $treeContainer = $(`
      <div class="tree-container">
        <div class="bar">
          <button class="btn sunzehuiBtn">进入选中文件夹</button>
          <button class="btn close-btn">X</button>
        </div>
        <div class="tree"></div>
      </div>
    `)
    $treeContainer.find('.tree').fancytree(cfg)

    const that = this
    $(document).on('click', '.tree-container .bar .sunzehuiBtn', function() {
      const selectedNode = that.nowSelectNode
      if (!selectedNode || !selectedNode.folder) return alert('未选中文件夹')
      // 文件路径 = https://pan.quark.cn/s/{pwd_id}#/list/share/{文件id}-{文件名}/{文件id}-{文件名}/
      const pList = [...selectedNode.getParentList(), selectedNode]
      let filePath = `https://pan.quark.cn/s/${that.getPwdId()}#/list/share/`

      const link = pList.reduce((acc, cur) => {
        return `${acc}${cur.key}-${cur.title}/`
      }, filePath)
      window.open(link, '_blank')
    })

    $(document).on('click', '.tree-container .bar .close-btn', function() {
      $('.tree-container').hide()
    })

    const insertTreeViewContainer = this.config.insertTreeViewContainer
    domReady(insertTreeViewContainer, function() {
      $(insertTreeViewContainer).append($treeContainer)
    })
  }

  async getList({ parent_file_id }) {
    let url = new URL(this.api.fileList)
    let params = {
      pr: 'ucpro',
      fr: 'pc',
      uc_param_str: '',
      pwd_id: this.getPwdId(),
      stoken: this.getStoken(),
      pdir_fid: parent_file_id || 0,
      force: 0,
      _page: 1,
      _size: 50,
      _fetch_banner: 0,
      _fetch_share: 0,
      _fetch_total: 1,
      _sort: 'file_type:asc,updated_at:desc',
      __dt: 959945,
      __t: +new Date(),
    }
    Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
    const result = await fetch(url, {
      headers: this.headers,
      referrerPolicy: 'origin',
      method: 'GET',
      mode: 'cors',
      credentials: 'omit',
    })
    const resp = await result.json()

    return resp.data.list
  }

  async buildTree(parent_file_id) {
    const treeNode = {}
    const list = await this.getList({ parent_file_id })
    treeNode.children = []
    for (let i = 0; i < list.length; i++) {
      let node = void 0
      const item = list[i]
      if (item.dir) {
        node = await this.buildTree(item.fid)
        node.name = item.file_name
      } else {
        node = item
      }
      treeNode.children.push(node)
    }
    return treeNode
  }

  insertCSS() {
    const cssElem = document.createElement('link')
    cssElem.setAttribute('rel', 'stylesheet')
    cssElem.setAttribute('href', this.config.fancytreeCSS_CDN)
    document.body.appendChild(cssElem)
    const cssElem2 = document.createElement('style')
    cssElem2.innerHTML = `
    .tree-container{
      height: 100%;
      background: #ecf0f1;
      position: fixed;
      top: 60px;
      z-index: 9999;
      left: 0;
      overflow-y:scroll
    }
    .tree-container .bar{
      background: #bdc3c7;
      padding: 0 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 40px;
    }
    .btn{
      padding: 0;
      height: 30px;
    }
    .sunzehuiBtn{
      display: inline-block;
      font-weight: 400;
      text-align: center;
      vertical-align: middle;
      user-select: none;
      border: 1px solid transparent;
      transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
      padding: 0 8px;
      font-size: 14px;
      border-radius: .2rem;
      color: #fff;
      background-color: #6c757d;
      border-color: #6c757d;
      cursor: pointer;
    }
    .sunzehuiBtn:hover{
      text-decoration: none;
      background-color: #5a6268;
      border-color: #545b62;
    }
    .sunzehuiBtn:focus {
      box-shadow: 0 0 0 0.2rem rgb(130 138 145 / 50%);
    }
    ul.fancytree-container{
      background-color:transparent !important;
      border:none !important;
    }
    .${this.config.tagClassname}{
      width: 20px;
      height: 20px;
      margin-right: auto;
      transform: translateY(-3px);
      margin-left: 20px;
      cursor: pointer;
    }
    `
    document.body.appendChild(cssElem2)
  }

  async init() {
    this.insertCSS()
    this.renderTag()
  }
}
$(async function() {
  const thisHost = window.location.host
  switch (thisHost) {
    case 'www.alipan.com':
      const aliScript = new AliPanTree()
      aliScript.init()
      break
    case 'pan.quark.cn':
      const quarkScript = new QuarkPanTree()
      quarkScript.init()
      break
    default:
      console.error('暂不支持该网站')
      break
  }
})