Notion.so v3 Trash Cleaner

Provides a pop up where you can select a workspace in Notion.so to clear its trash

  1. // ==UserScript==
  2. // @name Notion.so v3 Trash Cleaner
  3. // @namespace https://github.com/bjxpen
  4. // @version 0.4
  5. // @description Provides a pop up where you can select a workspace in Notion.so to clear its trash
  6. // @author Jiaxing Peng
  7. // @license MIT
  8. // @match *://www.notion.so/*
  9. // @require https://cdn.jsdelivr.net/npm/redom@3.24.0/dist/redom.min.js
  10. // @require https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js
  11. // @run-at document-idle
  12. // @grant GM_xmlhttpRequest
  13. // ==/UserScript==
  14.  
  15. /*jshint esversion: 6 */
  16. class Component {
  17. setState(state) {
  18. if (this.state === undefined) {
  19. this.state = {}
  20. }
  21. Object.assign(this.state, state)
  22. this.update()
  23. }
  24.  
  25. update() {}
  26. }
  27.  
  28. class Menu extends Component {
  29. constructor() {
  30. super()
  31. this.state = {
  32. msg: ""
  33. }
  34. this.render()
  35. this.fetchSpaces()
  36. }
  37.  
  38. fetchSpaces() {
  39. this.setState({
  40. fetchSpaceState: "fetching"
  41. })
  42. postJSON('/api/v3/loadUserContent')
  43. .catch(err => {
  44. console.log(err)
  45. this.setState({
  46. fetchSpaceState: "error"
  47. })
  48. })
  49. .then(res => [... function*() {
  50. const spaceView = res.recordMap.space_view
  51. for (let _ in spaceView) {
  52. yield res.recordMap.space[spaceView[_].value.space_id].value
  53. }
  54. }()])
  55. .then(spaces => {
  56. this.spaces = spaces
  57. this.setState({
  58. fetchSpaceState: "fetched"
  59. })
  60. })
  61. }
  62.  
  63. render() {
  64. this.el = redom.el("div#_del-trash-menu")
  65. }
  66.  
  67. setMsg(msg) {
  68. setTimeout(() => this.setState({
  69. msg
  70. }), 0)
  71. }
  72.  
  73. update() {
  74. const msg = (() => {
  75. if (this.state.fetchSpaceState === "fetched" && this.state.msg !== "") {
  76. return this.state.msg
  77. }
  78. switch (this.state.fetchSpaceState) {
  79. case "fetching":
  80. return "Fetching workspace metadata..."
  81. case "fetched":
  82. return "Choose workspace to delete:"
  83. case "error":
  84. return "Network error: Failed fetching workspace data"
  85. default:
  86. return this.state.msg
  87. }
  88. })()
  89.  
  90. redom.setChildren(this.el, [
  91. redom.el("div", "(Turn off this script to close the pop up)"),
  92. redom.el("pre", msg),
  93. this.state.fetchSpaceState === "fetched" &&
  94. redom.el("ul", this.spaces.map(space => new Space({
  95. space,
  96. setMsg: this.setMsg.bind(this)
  97. }))),
  98. ]);
  99. }
  100. }
  101.  
  102. class Space extends Component {
  103. constructor({
  104. space,
  105. setMsg
  106. }) {
  107. super()
  108. this.space = space
  109. this.setMsg = setMsg
  110. this.render()
  111. }
  112.  
  113. render() {
  114. this.el = redom.el("li", this.space.name)
  115. this.el.addEventListener("click", this.onClick.bind(this))
  116. }
  117.  
  118. deleteBlocks(query, currentQueryCount, totalQueryCount) {
  119. return new Promise(res => {
  120. this.setMsg(`Workspace "${this.space.name}":\nFetching block ids in trash for block query "${query}" (${currentQueryCount}/${totalQueryCount}) ...`)
  121. postJSON("/api/v3/searchTrashPages", {
  122. query: query,
  123. limit: 0,
  124. spaceId: this.space.id
  125. }, {
  126. onDownloadProgress: (ev) => {
  127. this.setMsg(`Workspace "${this.space.name}":\nFetching block ids in trash (${ev.loaded/1024} KB) for block query "${query}" ...`)
  128. }
  129. })
  130. .catch(err => {
  131. console.log(err)
  132. this.setMsg(`Workspace "${this.space.name}":\nNetwork error: Failed fetching trash posts for block query "${query}"`)
  133. })
  134. .then(res => res.results)
  135. .then(blockIds => {
  136. if (blockIds.length === 0) {
  137. this.setMsg(`Workspace "${this.space.name}":\nTrash is cleared for block query "${query}"`)
  138. return res()
  139. }
  140. const total = blockIds.length
  141. const chunkSize = 1000
  142. const recurDel = () => {
  143. if (blockIds.length > 0) {
  144. const deleted = total - blockIds.length
  145. this.setMsg(`Workspace "${this.space.name}":\nDeleting blocks for block query "${query}" (${deleted}/${total}) ...`)
  146. postJSON("/api/v3/deleteBlocks", {
  147. blockIds: blockIds.splice(0, chunkSize),
  148. permanentlyDelete: true
  149. })
  150. .catch(err => {
  151. console.log(err)
  152. this.setMsg(`Workspace "${this.space.name}":\nNetwork error: Failed deleting posts for block query "${query}"`)
  153. })
  154. .then(_ => {
  155. recurDel()
  156. })
  157. } else {
  158. this.setMsg(`Workspace "${this.space.name}":\nRetry to delete leftover posts for block query "${query}" ...`)
  159. setTimeout(() => this.deleteBlocks(query, currentQueryCount, totalQueryCount).then(res), 300)
  160. }
  161. }
  162. recurDel()
  163. })
  164. })
  165. }
  166.  
  167. onClick(ev) {
  168. const queries = ["", ..." abcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?]"]
  169. runSerial(queries.map((query, index) => () => this.deleteBlocks(query, index + 1, queries.length)))
  170. .then(() => this.setMsg("Done"))
  171. }
  172. }
  173.  
  174. function runSerial(promises) {
  175. let result = Promise.resolve()
  176. for (let i = 0; i < promises.length; i++) {
  177. result = result.then(() => promises[i]())
  178. }
  179. return result
  180. }
  181.  
  182. function loadScript(url) {
  183. return new Promise((res, rej) => {
  184. const script = document.createElement("script")
  185. document.body.appendChild(script)
  186. script.addEventListener("load", (ev) => {
  187. res(url)
  188. })
  189. script.src = url
  190. })
  191. }
  192.  
  193. function loadCSS(css) {
  194. const elm = document.createElement("style")
  195. elm.innerHTML = css
  196. document.body.appendChild(elm)
  197. }
  198.  
  199. function postJSON(url, jsonPayload = null, config = {}) {
  200. return axios.post(url, jsonPayload, config).then(res => res.data)
  201. }
  202.  
  203. Promise.all([
  204. window.redom || loadScript("https://cdn.jsdelivr.net/npm/redom@3.24.0/dist/redom.min.js"),
  205. window.axios || loadScript("https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js")
  206. ])
  207. .then(() => {
  208. loadCSS(`
  209. #_del-trash-menu {
  210. position: absolute;
  211. color: rgba(55, 53, 47, 0.6);
  212. background: rgb(247, 246, 243);
  213. padding: 1em;
  214. top: 0;
  215. left: calc(50% - 200px);
  216. width: 400px;
  217. min-height: 200px;
  218. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
  219. font-size: 14px;
  220. z-index: 9999;
  221. }
  222. #_del-trash-menu ul, #_del-trash-menu li {
  223. color: black;
  224. list-style: none;
  225. margin: 0;
  226. padding: 0;
  227. }
  228. #_del-trash-menu li {
  229. margin: 12px 0;
  230. padding: 6px;
  231. }
  232. #_del-trash-menu li:hover {
  233. cursor: pointer;
  234. background: white;
  235. }
  236. `)
  237. const root = document.querySelector("#_del-trash-menu")
  238. root && root.remove()
  239. document.body.appendChild(new Menu().el)
  240. })

QingJ © 2025

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