// ==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('😃')
}
}
renderTag() {
const tag = document.createElement('div')
tag.classList.add(this.config.tagClassname)
tag.innerHTML = '😃'
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('😃')
}
}
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 = '😃'
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
}
})