1+ // ==UserScript==
2+ // @name V2ex 增强
3+ // @version 1.0.0
4+ // @author X.I.U
5+ // @description 自动签到、自动无缝翻页、回到顶部(右键点击两侧空白处)
6+ // @match *://v2ex.com/*
7+ // @match *://*.v2ex.com/*
8+ // @icon https://www.v2ex.com/static/favicon.ico
9+ // @grant GM_xmlhttpRequest
10+ // @grant GM_registerMenuCommand
11+ // @grant GM_unregisterMenuCommand
12+ // @grant GM_openInTab
13+ // @grant GM_getValue
14+ // @grant GM_setValue
15+ // @grant GM_notification
16+ // @license GPL-3.0 License
17+ // @run -at document-end
18+ // @namespace https://github.com/XIU2/UserScript
19+ // ==/UserScript==
20+
21+ ( function ( ) {
22+ var menu_ALL = [
23+ [ 'menu_autoClockIn' , '自动签到' , '自动签到' , true ] ,
24+ [ 'menu_pageLoading' , '自动无缝翻页' , '自动无缝翻页' , true ] ,
25+ [ 'menu_pageLoading_reply' , '帖子内自动翻页' , '帖子内自动翻页' , false ] ,
26+ [ 'menu_backToTop' , '回到顶部(右键点击两侧空白处)' , '回到顶部' , true ]
27+ ] , menu_ID = [ ] ;
28+ for ( let i = 0 ; i < menu_ALL . length ; i ++ ) { // 如果读取到的值为 null 就写入默认值
29+ if ( GM_getValue ( menu_ALL [ i ] [ 0 ] ) == null ) { GM_setValue ( menu_ALL [ i ] [ 0 ] , menu_ALL [ i ] [ 3 ] ) } ;
30+ }
31+ registerMenuCommand ( ) ;
32+
33+ // 注册脚本菜单
34+ function registerMenuCommand ( ) {
35+ if ( menu_ID . length > menu_ALL . length ) { // 如果菜单ID数组多于菜单数组,说明不是首次添加菜单,需要卸载所有脚本菜单
36+ for ( let i = 0 ; i < menu_ID . length ; i ++ ) {
37+ GM_unregisterMenuCommand ( menu_ID [ i ] ) ;
38+ }
39+ }
40+ for ( let i = 0 ; i < menu_ALL . length ; i ++ ) { // 循环注册脚本菜单
41+ menu_ALL [ i ] [ 3 ] = GM_getValue ( menu_ALL [ i ] [ 0 ] ) ;
42+ menu_ID [ i ] = GM_registerMenuCommand ( `[ ${ menu_ALL [ i ] [ 3 ] ?'√' :'×' } ] ${ menu_ALL [ i ] [ 1 ] } ` , function ( ) { menu_switch ( `${ menu_ALL [ i ] [ 3 ] } ` , `${ menu_ALL [ i ] [ 0 ] } ` , `${ menu_ALL [ i ] [ 2 ] } ` ) } ) ;
43+ }
44+ menu_ID [ menu_ID . length ] = GM_registerMenuCommand ( '反馈 & 建议' , function ( ) { window . GM_openInTab ( 'https://github.com/XIU2/UserScript#xiu2userscript' , { active : true , insert : true , setParent : true } ) ; } ) ;
45+ }
46+
47+ // 菜单开关
48+ function menu_switch ( menu_status , Name , Tips ) {
49+ if ( menu_status == 'true' ) {
50+ GM_setValue ( `${ Name } ` , false ) ;
51+ GM_notification ( { text : `已关闭 [${ Tips } ] 功能\n(刷新网页后生效)` , title : '吾爱破解论坛增强' , timeout : 3000 } ) ;
52+ } else {
53+ GM_setValue ( `${ Name } ` , true ) ;
54+ GM_notification ( { text : `已开启 [${ Tips } ] 功能\n(刷新网页后生效)` , title : '吾爱破解论坛增强' , timeout : 3000 } ) ;
55+ }
56+ registerMenuCommand ( ) ; // 重新注册脚本菜单
57+ } ;
58+
59+ // 返回菜单值
60+ function menu_value ( menuName ) {
61+ for ( let menu of menu_ALL ) {
62+ if ( menu [ 0 ] == menuName ) {
63+ return menu [ 3 ]
64+ }
65+ }
66+ }
67+
68+
69+ // 默认 ID 为 0
70+ var curSite = { SiteTypeID : 0 } ;
71+
72+ // 自动翻页规则,HT_insert:1 = 插入该元素的前面;2 = 插入该元素内的最后面
73+ let DBSite = {
74+ recent : { // 最近主题页
75+ SiteTypeID : 1 ,
76+ pager : {
77+ type : 1 ,
78+ nextLink : '//a[@class="page_current"]/following-sibling::a[1][@href]' ,
79+ pageElement : 'css;.cell.item' ,
80+ HT_insert : [ '//*[@id="Main"]//div[@class="box"]//div[@class="cell"][last()]' , 1 ] ,
81+ replaceE : 'css;#Main .box .cell:not(.item) > table' ,
82+ scrollDelta : 600
83+ }
84+ } ,
85+ notifications : { // 提醒消息页
86+ SiteTypeID : 2 ,
87+ pager : {
88+ type : 1 ,
89+ nextLink : '//a[@class="page_current"]/following-sibling::a[1][@href]' ,
90+ pageElement : 'css;#notifications > div' ,
91+ HT_insert : [ 'css;#notifications' , 2 ] ,
92+ replaceE : 'css;#Main .box > .cell:not(.item) > table' ,
93+ scrollDelta : 600
94+ }
95+ } ,
96+ replies : { // 用户回复页
97+ SiteTypeID : 3 ,
98+ pager : {
99+ type : 1 ,
100+ nextLink : '//a[@class="page_current"]/following-sibling::a[1][@href]' ,
101+ pageElement : '//*[@id="Main"]//div[@class="box"]//div[@class="dock_area"] | //*[@id="Main"]//div[@class="box"]//div[@class="inner"] | //*[@id="Main"]//div[@class="box"]//div[@class="dock_area"][last()]/following-sibling::div[@class="cell"][1]' ,
102+ HT_insert : [ '//*[@id="Main"]//div[@class="box"]//div[@class="cell"][last()]' , 1 ] ,
103+ replaceE : 'css;#Main .box .cell:not(.item) > table' ,
104+ scrollDelta : 1000
105+ }
106+ } ,
107+ go : { // 分类主题页
108+ SiteTypeID : 4 ,
109+ pager : {
110+ type : 1 ,
111+ nextLink : '//a[@class="page_current"]/following-sibling::a[1][@href]' ,
112+ pageElement : 'css;#TopicsNode > div' ,
113+ HT_insert : [ 'css;#TopicsNode' , 2 ] ,
114+ replaceE : 'css;#Main .box > .cell:not(.item) > table' ,
115+ scrollDelta : 700
116+ }
117+ } ,
118+ reply : { // 帖子内容页
119+ SiteTypeID : 5 ,
120+ pager : {
121+ type : 1 ,
122+ nextLink : '//a[@class="page_current"]/preceding-sibling::a[1][@href]' ,
123+ pageElement : 'css;.cell[id^="r_"]' ,
124+ HT_insert : [ '//div[starts-with(@id, "r_")][last()]/following-sibling::div[@class="cell"][1]' , 1 ] ,
125+ replaceE : 'css;#Main .box > .cell:not(.normalUser) > table' ,
126+ scrollDelta : 700
127+ }
128+ } ,
129+ reply_positive : { // 帖子内容页(正序)
130+ SiteTypeID : 6 ,
131+ pager : {
132+ type : 1 ,
133+ nextLink : '//a[@class="page_current"]/preceding-sibling::a[1][@href]' ,
134+ pageElement : 'css;.cell[id^="r_"]' ,
135+ HT_insert : [ '//div[starts-with(@id, "r_")][position()=1]' , 1 ] ,
136+ replaceE : 'css;#Main .box > .cell:not(.normalUser) > table' ,
137+ scrollDelta : 700
138+ }
139+ }
140+ } ;
141+
142+
143+ switch ( location . pathname ) {
144+ case "/recent" : // 最近主题页
145+ curSite = DBSite . recent ;
146+ break ;
147+ case "/notifications" : // 提醒消息页
148+ curSite = DBSite . notifications ;
149+ break ;
150+ case "/member/XIU2/replies" : // 用户回复页
151+ curSite = DBSite . replies ;
152+ break ;
153+ default :
154+ if ( location . pathname . indexOf ( '/go/' ) > - 1 ) { // 分类主题页
155+ curSite = DBSite . go ;
156+ } else if ( location . pathname . indexOf ( '/t/' ) > - 1 ) { // 帖子内容页
157+ if ( menu_value ( 'menu_pageLoading_reply' ) ) curSite = DBSite . reply_positive ;
158+ }
159+ }
160+
161+ curSite . pageUrl = "" ; // 下一页URL
162+ if ( menu_value ( 'menu_autoClockIn' ) ) setTimeout ( qianDao , 1000 ) // 自动签到(后台),延迟 1 秒执行是为了兼容 [V2ex Plus] 扩展
163+ if ( menu_value ( 'menu_pageLoading' ) ) pageLoading // 自动翻页(无缝)
164+ if ( menu_value ( 'menu_backToTop' ) ) backToTop ( ) ; // 回到顶部(右键点击空白处)
165+
166+
167+ // 自动签到(后台)
168+ function qianDao ( ) {
169+ let qiandao = document . querySelector ( '.box .inner a[href="/mission/daily"]' ) ;
170+ if ( qiandao ) {
171+ let url = ( location . origin + "/mission/daily/redeem?" + RegExp ( "once\\=(\\d+)" ) . exec ( document . querySelector ( 'div#Top .tools' ) . innerHTML ) [ 0 ] ) ;
172+ GM_xmlhttpRequest ( {
173+ url : url ,
174+ method : "GET" ,
175+ timeout : 5000 ,
176+ onload : function ( response ) {
177+ let html = ShowPager . createDocumentByString ( response . responseText ) ;
178+ console . log ( html )
179+ if ( html . querySelector ( 'li.fa.fa-ok-sign' ) ) {
180+ html = html . getElementById ( 'Main' ) . innerText . match ( / 已 连 续 登 录 ( \d + ?) 天 / ) [ 0 ] ;
181+ qiandao . innerText = `自动签到成功!${ html } ` ;
182+ qiandao . href = '#' ;
183+ } else {
184+ GM_notification ( { text : '自动签到失败!请联系作者解决!' , timeout : 4000 } ) ;
185+ }
186+ }
187+ } ) ;
188+ } else if ( document . getElementById ( 'gift_v2excellent' ) ) { // 兼容 [V2ex Plus] 扩展
189+ document . getElementById ( 'gift_v2excellent' ) . click ( ) ;
190+ }
191+ }
192+
193+
194+ // 回到顶部(右键点击空白处)
195+ function backToTop ( ) {
196+ document . getElementById ( "Wrapper" ) . oncontextmenu = function ( event ) {
197+ if ( event . target == this ) {
198+ event . preventDefault ( ) ;
199+ window . scrollTo ( 0 , 0 )
200+ }
201+ }
202+ /*let backToTop = `<div style="position: fixed;bottom: 0;right: 0;opacity: 0.1;"><div style="transition: height .2s ease;"><button type="button" style="display: flex;align-items: center;justify-content: center;background: #fff;border-radius: 3px;width: 200px;height: 200px;color: #8590a6;box-shadow: 0 1px 3px rgba(18,18,18,.1);border: none;cursor: pointer;outline: none;" title="回到顶部" onclick="window.scrollTo(0,0)"><svg fill="currentColor" viewBox="0 0 24 24" width="24" height="24"><path d="M16.036 19.59a1 1 0 0 1-.997.995H9.032a.996.996 0 0 1-.997-.996v-7.005H5.03c-1.1 0-1.36-.633-.578-1.416L11.33 4.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.005z"></path></svg></button></div></div>`
203+ let backToTop_add = document.createElement('div');
204+ backToTop_add.innerHTML = `${backToTop}`;
205+ document.body.appendChild(backToTop_add);*/
206+ }
207+
208+
209+ // 自动无缝翻页
210+ function pageLoading ( ) {
211+ if ( curSite . SiteTypeID > 0 ) {
212+ windowScroll ( function ( direction , e ) {
213+ if ( direction === "down" ) { // 下滑才准备翻页
214+ let scrollTop = document . documentElement . scrollTop || window . pageYOffset || document . body . scrollTop ;
215+ //console.log(document.documentElement.scrollHeight)
216+ let scrollDelta = curSite . pager . scrollDelta ;
217+ if ( document . documentElement . scrollHeight <= document . documentElement . clientHeight + scrollTop + scrollDelta ) {
218+ if ( curSite . pager . type === 1 ) {
219+ ShowPager . loadMorePage ( ) ;
220+ } else {
221+ let autopbn = document . querySelector ( curSite . pager . nextLink ) ;
222+ if ( autopbn ) {
223+ autopbn . click ( ) ;
224+ }
225+ }
226+ }
227+ }
228+ } ) ;
229+ }
230+ }
231+
232+
233+ // 滚动条事件
234+ function windowScroll ( fn1 ) {
235+ var beforeScrollTop = document . documentElement . scrollTop ,
236+ fn = fn1 || function ( ) { } ;
237+ setTimeout ( function ( ) { // 延时执行,避免刚载入到页面就触发翻页事件
238+ window . addEventListener ( "scroll" , function ( e ) {
239+ var afterScrollTop = document . documentElement . scrollTop ,
240+ delta = afterScrollTop - beforeScrollTop ;
241+ if ( delta == 0 ) return false ;
242+ fn ( delta > 0 ? "down" : "up" , e ) ;
243+ beforeScrollTop = afterScrollTop ;
244+ } , false ) ;
245+ } , 1000 )
246+ }
247+
248+
249+ var ShowPager = { // 修改自 https://greasyfork.org/scripts/14178
250+ getFullHref : function ( e ) {
251+ if ( e == null ) return '' ;
252+ "string" != typeof e && ( e = e . getAttribute ( "href" ) ) ;
253+ var t = this . getFullHref . a ;
254+ return t || ( this . getFullHref . a = t = document . createElement ( "a" ) ) , t . href = e , t . href ;
255+ } ,
256+ createDocumentByString : function ( e ) {
257+ if ( e ) {
258+ if ( "HTML" !== document . documentElement . nodeName ) return ( new DOMParser ) . parseFromString ( e , "application/xhtml+xml" ) ;
259+ var t ;
260+ try {
261+ t = ( new DOMParser ) . parseFromString ( e , "text/html" ) ;
262+ } catch ( e ) {
263+ }
264+ if ( t ) return t ;
265+ if ( document . implementation . createHTMLDocument ) t = document . implementation . createHTMLDocument ( "ADocument" ) ; else try {
266+ ( t = document . cloneNode ( ! 1 ) ) . appendChild ( t . importNode ( document . documentElement , ! 1 ) ) ,
267+ t . documentElement . appendChild ( t . createElement ( "head" ) ) , t . documentElement . appendChild ( t . createElement ( "body" ) ) ;
268+ } catch ( e ) {
269+ }
270+ if ( t ) {
271+ var r = document . createRange ( ) ;
272+ r . selectNodeContents ( document . body ) ;
273+ var n = r . createContextualFragment ( e ) ;
274+ t . body . appendChild ( n ) ;
275+ for ( var a , o = {
276+ TITLE : ! 0 ,
277+ META : ! 0 ,
278+ LINK : ! 0 ,
279+ STYLE : ! 0 ,
280+ BASE : ! 0
281+ } , i = t . body , s = i . childNodes , c = s . length - 1 ; c >= 0 ; c -- ) o [ ( a = s [ c ] ) . nodeName ] && i . removeChild ( a ) ;
282+ return t ;
283+ }
284+ } else console . error ( "没有找到要转成DOM的字符串" ) ;
285+ } ,
286+ loadMorePage : function ( ) {
287+ if ( curSite . pager ) {
288+ let curPageEle = getElementByXpath ( curSite . pager . nextLink ) ;
289+ var url = this . getFullHref ( curPageEle ) ;
290+ //console.log(`${url} ${curPageEle} ${curSite.pageUrl}`);
291+ if ( url === '' ) return ;
292+ if ( curSite . pageUrl === url ) return ; // 不会重复加载相同的页面
293+ curSite . pageUrl = url ;
294+ // 读取下一页的数据
295+ curSite . pager . startFilter && curSite . pager . startFilter ( ) ;
296+ GM_xmlhttpRequest ( {
297+ url : url ,
298+ method : "GET" ,
299+ timeout : 5000 ,
300+ onload : function ( response ) {
301+ try {
302+ var newBody = ShowPager . createDocumentByString ( response . responseText ) ;
303+ let pageElems = getAllElements ( curSite . pager . pageElement , newBody , newBody ) ;
304+ let toElement = getAllElements ( curSite . pager . HT_insert [ 0 ] ) [ 0 ] ;
305+ if ( pageElems . length >= 0 ) {
306+ let addTo = "beforeend" ;
307+ if ( curSite . pager . HT_insert [ 1 ] == 1 ) addTo = "beforebegin" ;
308+ // 插入新页面元素
309+ pageElems . forEach ( function ( one ) {
310+ toElement . insertAdjacentElement ( addTo , one ) ;
311+ } ) ;
312+ // 替换待替换元素
313+ try {
314+ let oriE = getAllElements ( curSite . pager . replaceE ) ;
315+ let repE = getAllElements ( curSite . pager . replaceE , newBody , newBody ) ;
316+ if ( oriE . length === repE . length ) {
317+ for ( var i = 0 ; i < oriE . length ; i ++ ) {
318+ oriE [ i ] . outerHTML = repE [ i ] . outerHTML ;
319+ }
320+ }
321+ } catch ( e ) {
322+ console . log ( e ) ;
323+ }
324+ }
325+ } catch ( e ) {
326+ console . log ( e ) ;
327+ }
328+ }
329+ } ) ;
330+ }
331+ } ,
332+ } ;
333+
334+
335+ function getElementByXpath ( e , t , r ) {
336+ r = r || document , t = t || r ;
337+ try {
338+ return r . evaluate ( e , t , null , XPathResult . FIRST_ORDERED_NODE_TYPE , null ) . singleNodeValue ;
339+ } catch ( t ) {
340+ return void console . error ( "无效的xpath" ) ;
341+ }
342+ }
343+
344+
345+ function getAllElements ( e , t , r , n , o ) {
346+ let getAllElementsByXpath = function ( e , t , r ) {
347+ return r = r || document , t = t || r , r . evaluate ( e , t , null , XPathResult . ORDERED_NODE_SNAPSHOT_TYPE , null ) ;
348+ }
349+
350+ var i , s = [ ] ;
351+ if ( ! e ) return s ;
352+ if ( r = r || document , n = n || window , o = o || void 0 , t = t || r , "string" == typeof e ) i = 0 === e . search ( / ^ c s s ; / i) ? function getAllElementsByCSS ( e , t ) {
353+ return ( t || document ) . querySelectorAll ( e ) ;
354+ } ( e . slice ( 4 ) , t ) : getAllElementsByXpath ( e , t , r ) ; else {
355+ if ( ! ( i = e ( r , n , o ) ) ) return s ;
356+ if ( i . nodeType ) return s [ 0 ] = i , s ;
357+ }
358+ return function makeArray ( e ) {
359+ var t , r , n , o = [ ] ;
360+ if ( e . pop ) {
361+ for ( t = 0 , r = e . length ; t < r ; t ++ ) ( n = e [ t ] ) && ( n . nodeType ? o . push ( n ) : o = o . concat ( makeArray ( n ) ) ) ;
362+ return a ( ) ( o ) ;
363+ }
364+ if ( e . item ) {
365+ for ( t = e . length ; t ; ) o [ -- t ] = e [ t ] ;
366+ return o ;
367+ }
368+ if ( e . iterateNext ) {
369+ for ( t = e . snapshotLength ; t ; ) o [ -- t ] = e . snapshotItem ( t ) ;
370+ return o ;
371+ }
372+ } ( i ) ;
373+ }
374+ } ) ( ) ;
0 commit comments