Narrow_DynamicHeader

小説家になろうのヘッダーを便利にする

  1. // ==UserScript==
  2. // @name Narrow_DynamicHeader
  3. // @namespace phodra
  4. // @description 小説家になろうのヘッダーを便利にする
  5. // @version 1.2
  6. // @include http://ncode.syosetu.com/*
  7. // @include http://novelcom.syosetu.com/*
  8. // @include http://novel18.syosetu.com/*
  9. // @include http://novelcom18.syosetu.com/*
  10. // ==/UserScript==
  11.  
  12.  
  13. (function (){
  14. // ユーザーID
  15. // 自分のIDを調べて書き換えてください。
  16. // 通常版のユーザーIDは、ホーム画面の右上に以下のように表示されています。
  17. //  (ユーザー名)[ID:000000]でログイン中
  18. // 18禁サイト用のユーザーIDは、Xホーム右側の
  19. // 「評価をつけた作品一覧」のURLから得ることが出来ます。
  20. const MY_USER_ID = 0;
  21. const MY_XUSER_ID = 0;
  22.  
  23.  
  24.  
  25. // ※そのまま移動させると元の場所が埋め立てられるので、
  26. // ※クローンを作成したのちにオリジナルは visibility = hidden で隠す。
  27. // ヘッダー(オリジナル)
  28. var $header_org = $("#novel_header");
  29. // ヘッダー(クローン)
  30. var $header = $header_org.clone(true);
  31. $header.css( 'position', 'relative');
  32. // オリジナルを隠す
  33. $header_org.css( 'visibility', 'hidden');
  34. // header Height
  35. var headerH = $header.outerHeight();
  36. // ヘッダーの親になるボックス
  37. var $box = $("<div id='box'>");
  38. $box.css( 'position', 'fixed');
  39. $box.append($header);
  40.  
  41. // マウス検知エリア
  42. var $detect = $("<div id='detect_area' />");
  43. $detect.css(
  44. {
  45. 'position': 'fixed',
  46. 'height': headerH,
  47. 'top': 0
  48. }
  49. );
  50. // ヘッダーボックスに乗せる、外す
  51. $detect.hover(
  52. function()
  53. {
  54. _boxon = true;
  55. if( scTop>0 ) HeaderShow(true);
  56. else _show = 2;
  57. },
  58. function()
  59. {
  60. _boxon = false;
  61. if( scTop>0 ) HeaderHide( !_lockB && !_lockM );
  62. else _show = 0;
  63. }
  64. );
  65. $detect.append($box);
  66. $("body").append($detect);
  67.  
  68.  
  69.  
  70. // 章タイトルとサブタイトルのラベル
  71. if( $(".margin_r20").size() ){
  72. // コンテンツinfoのクローン
  73. var $info_org = $(".contents1");
  74. var $info = $info_org.clone(true);
  75. $info_org.css( 'visibility', 'hidden');
  76. $info.css(
  77. {
  78. 'left': 0,
  79. 'right': 0,
  80. 'margin': 'auto',
  81. 'opacity': '0.8'
  82. }
  83. );
  84. var $label = $("<div />");
  85. var $cp_title = $info.children(".chapter_title");
  86. $cp_title.hide();
  87. if( $cp_title.size() )
  88. {
  89. var $cp_title2 = $("<span class='label_chapter' />");
  90. $cp_title2.append($(".chapter_title").text());
  91. $label.append($cp_title2);
  92. $label.append(" - ");
  93. }
  94. // サブタイトル用のエレメント
  95. var $label_sub = $("<span class='label_subtitle' />");
  96. $label_sub.append($(".novel_subtitle").text());
  97. $label.append($label_sub);
  98. $info.append($label);
  99. $box.append($info);
  100. }
  101. var boxheight = $box.height();
  102.  
  103.  
  104. // 移動ボタン群
  105. const NDH_BTN = 'ndh_button';
  106. var move_a = "<a class='"+ NDH_BTN +"' />";
  107. var move_div = "<div class='"+ NDH_BTN +"' />";
  108.  
  109. // 前ページ
  110. var bn_p = $("a:contains('<<')");
  111. if( bn_p.size() )
  112. {
  113. var $preEp = $(move_a);
  114. $preEp.text("<");
  115. $preEp.attr(
  116. {
  117. 'alt': 'Prev Episode',
  118. 'href': bn_p.attr('href')
  119. }
  120. );
  121. $preEp.css('left', '15px');
  122. $header.append($preEp);
  123. }
  124.  
  125. // 次ページ
  126. var bn_n = $("a:contains('>>')");
  127. if( bn_n.size() )
  128. {
  129. var $nxtEp = $(move_a);
  130. $nxtEp.text(">");
  131. $nxtEp.attr(
  132. {
  133. 'alt': 'Next Episode',
  134. 'href': bn_n.attr('href')
  135. }
  136. );
  137. $nxtEp.css( 'left', '55px');
  138. $header.append($nxtEp);
  139. }
  140. $("#pageBottom").remove();
  141. $("#pageTop").remove();
  142.  
  143. // 最上部へ移動
  144. var $ptop = $(move_div);
  145. $ptop.attr( 'alt', 'Scroll Top');
  146. $ptop.css( 'right', '55px');
  147. $ptop.text("↑");
  148. $ptop.click( function(e)
  149. {
  150. $("html,body").animate(
  151. { 'scroll-top': 0
  152. }, 500
  153. );
  154. }
  155. );
  156. $header.append($ptop);
  157. // 最下部へ移動
  158. var $pbtm = $(move_div);
  159. $pbtm.text("↓");
  160. $pbtm.attr( 'alt', 'Scroll Bottom');
  161. $pbtm.css( 'right', '15px');
  162. $pbtm.click( function(e)
  163. {
  164. $("html,body").animate(
  165. { 'scroll-top':
  166. $(document).height() -$(window).height()
  167. }, 500
  168. );
  169. }
  170. );
  171. $header.append($pbtm);
  172. // AutoPagerizeのページ移動
  173. // ※インストールしていなくても作製して非表示にしておき、
  174. //  AutoPagerizeの初期化イベントで表示させる。
  175. //  AP初期化イベント中ではなくこの時点で作成するのは、
  176. //  高さの計算を簡略化するため。
  177. // 前のページ
  178. var $ap_prev = $(move_div)
  179. $ap_prev.text("△");
  180. $ap_prev.attr( 'alt', 'Scroll Prev Page');
  181. $ap_prev.css(
  182. {
  183. 'display': 'none',
  184. 'right': '140px',
  185. }
  186. );
  187. $ap_prev.click( function(e)
  188. {
  189. $("html,body").animate(
  190. { 'scroll-top':
  191. scTop==ap.seam[ap.page]?
  192. ap.seam[ap.page-1]: ap.seam[ap.page]
  193. }, 500
  194. );
  195. }
  196. );
  197. $header.append($ap_prev);
  198.  
  199. // 次のページ
  200. var $ap_next = $(move_div);
  201. $ap_next.text("▽");
  202. $ap_next.attr( 'alt', 'Scroll Next Page');
  203. $ap_next.css(
  204. {
  205. 'display': 'none',
  206. 'right': '100px',
  207. }
  208. );
  209. $ap_next.click( function(e)
  210. {
  211. $("html,body").animate(
  212. { 'scroll-top':
  213. ap.page+1<ap.seam.length?
  214. ap.seam[ap.page+1]:
  215. $(document).height() -$(window).height()
  216. }, 500
  217. );
  218. }
  219. );
  220. $header.append($ap_next);
  221.  
  222. // 移動ボタンのスタイル
  223. var NDH_BTN_Style =
  224. "." + NDH_BTN +"{ \
  225. box-sizing: border-box; \
  226. border: solid 1px transparent; \
  227. }" +
  228. "." + NDH_BTN + ":hover{ \
  229. border: outset 1px black; \
  230. }" +
  231. "." + NDH_BTN + ":active{ \
  232. border: inset 1px black; \
  233. background-color: #fafafa; \
  234. }";
  235. var mb_style = $("<style type='text/css' />");
  236. mb_style.append(NDH_BTN_Style);
  237. $("head").append(mb_style);
  238. // 追加したボタンのスタイルをまとめて設定
  239. $( "." +NDH_BTN ).css(
  240. {
  241. 'cursor': 'pointer',
  242. 'position': 'absolute',
  243. 'top': 0,
  244. 'bottom': 0,
  245. 'margin': '4px',
  246. 'padding': '0px 10px',
  247. 'line-height': function()
  248. {
  249. return $(this).height()+'px';
  250. }
  251. }
  252. );
  253.  
  254.  
  255.  
  256. // フラグ
  257. var _boxon = false;
  258. var _lockB = 0, _lockM = 0;
  259. var _show = 0;
  260. // 表示設定
  261. var $navi_box = $("#novelnavi_right");
  262. if( $navi_box.size() )
  263. {
  264. $header_org.find("#novelnavi_right").remove();
  265. $header.find("#novelnavi_right").hide();
  266. var $navi = $("<div id='navi_button' class='" + NDH_BTN + "' />");
  267. $navi.css(
  268. {
  269. 'position': 'absolute',
  270. 'display': 'block',
  271. 'margin': 0,
  272. 'top': 16,
  273. 'right': 3,
  274. 'height': 28,
  275. 'width': 10
  276. }
  277. );
  278. $navi.click( function()
  279. {
  280. if( _lockM )
  281. {
  282. $("#menu_off").click();
  283. _lockM = false;
  284. }else
  285. {
  286. $("#menu_on").click();
  287. _lockM = true;
  288. }
  289. }
  290. );
  291. $header.append($navi);
  292.  
  293. var $navi_menu = $(".novelview_navi");
  294. $navi_menu.css(
  295. {
  296. 'top': headerH,
  297. 'right': 0
  298. }
  299. );
  300. $header.append($navi_menu);
  301.  
  302. $("input[name='fix_menu_bar']").prop(
  303. {
  304. 'disabled': true,
  305. 'checked': false,
  306. }
  307. );
  308. $("#menu_off_2").click(
  309. function(e){ $navi.click(); }
  310. );
  311. }
  312.  
  313.  
  314.  
  315. // AutoPagerize 互換
  316. var ap;
  317. // AP用変数とボタンを初期化
  318. var AP_Init = function()
  319. {
  320. ap =
  321. {
  322. 'seam': [0],
  323. 'page': 0,
  324. };
  325. $ap_next.show();
  326. $ap_prev.show();
  327. };
  328. // ページを継ぎ足した時、継ぎ目の位置を記録する
  329. var AP_SeamLine = function()
  330. {
  331. if( ap != null)
  332. {
  333. var $ap_sep = $(".autopagerize_page_separator");
  334. var $ap_sep_last = $ap_sep.eq(-1);
  335. ap.seam[$ap_sep.index($ap_sep_last)+1] =
  336. parseInt($ap_sep_last.offset().top)-boxheight;
  337. }
  338. };
  339. if( window.AutoPagerize )
  340. {
  341. console.log( 'window.AutoPagerize' );
  342. // 初期化
  343. AP_Init();
  344. // 継ぎ足した時
  345. AutoPagerize.addFilter(AP_SeamLine);
  346. }else
  347. {
  348. $(document).on(
  349. {
  350. 'GM_AutoPagerizeLoaded': function(){
  351. AP_Init();
  352. },
  353. 'GM_AutoPagerizeNextPageLoaded': function(){
  354. AP_SeamLine();
  355. }
  356. }
  357. );
  358. }
  359.  
  360. var scTop = $(window).scrollTop();
  361. // ヘッダーを表示させる
  362. var HeaderShow = function(bool)
  363. {
  364. if( _show<1 && bool )
  365. {
  366. _show = 1;
  367. // 消えている最中でもすぐにまた表示させる
  368. $box.stop();
  369. // ヘッダーを表示させるアニメ
  370. $box.animate(
  371. { 'top': 0},
  372. { 'duration': 'fast',
  373. 'easing' : 'swing',
  374. 'complete': function()
  375. { _show = 2; }
  376. }
  377. );
  378. }
  379. }
  380. // ヘッダーを隠す
  381. var HeaderHide = function(bool)
  382. {
  383. if( _show>0 && bool )
  384. {
  385. _show = -1;
  386. $box.stop();
  387. // ヘッダーを非表示にするアニメ
  388. $box.animate(
  389. { 'top': -boxheight},
  390. {
  391. 'duration': 'normal',
  392. 'easing' : 'linear',
  393. 'complete': function()
  394. { _show = 0; },
  395. 'progress' : function(e)
  396. {
  397. if( parseInt($(this).css('top')) <= -scTop )
  398. {
  399. $box.stop(false,true);
  400. $box.css( 'top', -scTop);
  401. }
  402. }
  403. }
  404. );
  405. }
  406. }
  407. // スクロール位置によってヘッダー位置を調整
  408. // ※上端でチラ見えしてるときは絶対座標っぽくずらす
  409. //  そうでなければ、画面のすぐ上で待機させる
  410. var HeaderPosSet = function()
  411. {
  412. if( scTop <= boxheight ||
  413. parseInt($box.css('top')) != -boxheight )
  414. {
  415. $box.stop();
  416. $box.css( 'top', scTop<=boxheight? -scTop: -boxheight );
  417. }
  418. }
  419. HeaderPosSet();
  420. var novel_title = $(".margin_r20:first").text();
  421. $(window).on(
  422. {
  423. 'ready resize': function()
  424. {
  425. $detect.width($(window).width());
  426. $box.width($(window).width());
  427. },
  428. 'scroll': function()
  429. {
  430. scTop = $(window).scrollTop();
  431. // ヘッダーを追従させる
  432. if( _show==0 && !_lockB && !_lockM ) HeaderPosSet();
  433.  
  434. // AutoPagerize
  435. if( ap != null )
  436. {
  437. for( var i=ap.seam.length-1; i>=0; i-- )
  438. {
  439. if( scTop >= ap.seam[i]-1 )
  440. {
  441. if( ap.page != i )
  442. {
  443. ap.page = i;
  444. if( $("#novel_honbun").size() )
  445. {
  446. $label_sub.text(
  447. $(".novel_subtitle").eq(i).text());
  448. document.title =
  449. novel_title + " - " + $label_sub.text();
  450. }
  451. }
  452. break;
  453. }
  454. }
  455. }
  456. if( $(".novel_hyouka,#novel_footer,#footer").offset().top
  457. < scTop + $(window).height() )
  458. {
  459. if( !_lockB )
  460. {
  461. HeaderShow(true);
  462. _lockB = true;
  463. }
  464. }else
  465. {
  466. if( _lockB ) _lockB = false;
  467. HeaderHide( !_lockM && !_boxon );
  468. }
  469. }
  470. }
  471. );
  472.  
  473.  
  474.  
  475. var href;
  476. // ヘッダーに「目次」を追加
  477. var $index_li = $("<li />");
  478. var $index_node = $("<a />");
  479. var index_href = $("#contents_main>a:first").attr('href');
  480. if( index_href==null ){
  481. // 携帯用のアドレスから生成
  482. var handheld = $("link[media='handheld']").attr('href');
  483. index_href = handheld.match(/\/n\d+?\w+?\//);
  484. }
  485. $index_node.text("目次");
  486. $index_node.attr( 'href', index_href);
  487. $index_li.append($index_node);
  488. $header.find("li:contains('感想')").before($index_li);
  489.  
  490. var userid;
  491. // ノベルフッターから作者コードを抜く
  492. var user_href = $(".undernavi a:contains('マイページ')").attr('href');
  493. if( user_href==null ){
  494. // よくわからんけど作者コードっぽいので引っこ抜く
  495. var atom = $("link[title='Atom']").attr('href');
  496. if( atom ){
  497. userid = atom.match(/(\d+|x\d+[^\.]+?)/)[0] + "/";
  498. user_href = "http://mypage.syosetu.com/" + userid;
  499. }else{
  500. userid=null;
  501. user_href=null;
  502. }
  503. }else{
  504. userid = user_href.match(/(\d+\/|x\d+.+)/)[0];
  505. }
  506. // ヘッダーに「作者マイページ」を追加
  507. var $user_li = $("<li />");
  508. var $user_node = $("<a />");
  509. $user_node.text("作者");
  510. if( userid ){
  511. $user_node.attr( 'href', user_href);
  512. }else{
  513. $user_node.css( 'cssText', 'color: rgba(200,200,200,0.3) !important;');
  514. }
  515. $user_li.append($user_node);
  516. $header.find("li:contains('感想')").before($user_li);
  517.  
  518. // ヘッダーに「メール」を追加
  519. var $mail_li = $("<li />");
  520. var $mail_node = $("<a />");
  521. $mail_node.text("メール");
  522. if( userid && userid[0]!="x" ){
  523. $mail_node.attr( 'href', 'http://syosetu.com/message/sendinput/to/' + userid);
  524. }else{
  525. $mail_node.css( 'cssText', 'color: rgba(200,200,200,0.3) !important;');
  526. }
  527. $mail_li.append($mail_node);
  528. $header.find("li:contains('感想')").before($mail_li);
  529.  
  530. // N2コードを取得
  531. var dlurl, n2code;
  532. dlurl = $(".undernavi li:contains('ダウンロード')>a");
  533. if( dlurl.size() ){
  534. n2code = dlurl.attr('href').match(/\d+\//);
  535. }
  536. var bm_config = userid && userid[0]=="x"?
  537. "http://syosetu.com/favnovelmain18/updateinput/xidfavncode/" + MY_XUSER_ID:
  538. "http://syosetu.com/favnovelmain/updateinput/useridfavncode/" + MY_USER_ID;
  539. bm_config += "_" + n2code;
  540. var dl_prm = {
  541. 'hankaku': '0',
  542. 'code': 'utf-8',
  543. 'kaigyo': 'crlf'
  544. };
  545. // テキストダウンロードボタンを追加
  546. var num = location.href.match( /\d+(?=\/$)/ );
  547. var $down_li = $("<li />");
  548. var $down_node = $("<a />");
  549. $down_node.text("DL");
  550. if( n2code && num ){
  551. var txtdl_url = userid[0]=="x"?
  552. "http://novel18.syosetu.com/txtdownload/dlstart/ncode/":
  553. "http://ncode.syosetu.com/txtdownload/dlstart/ncode/";
  554. $down_node.attr( 'href',
  555. txtdl_url + n2code +
  556. "?hankaku=" + dl_prm.hankaku +
  557. "&code=" + dl_prm.code +
  558. "&kaigyo=" + dl_prm.kaigyo +
  559. "&no=" + location.href.match( /\d+(?=\/$)/ )
  560. );
  561. }else{
  562. $down_node.css( 'cssText', 'color: rgba(200,200,200,0.3) !important;');
  563. $down_node.css( 'pointerEvents', 'none');
  564. }
  565. $down_li.append($down_node);
  566. $header.find("li:contains('レビュー')").after($down_li);
  567. // 「縦書で読む」を消す
  568. $header.find("li a.menu").parent().hide();
  569. // 「ブックマークに追加」/「ブックマークを解除」を改変
  570. var $fav = $("li.booklist,li.booklist_now");
  571. var $fav_a = $fav.children("a");
  572. $fav_a.css(
  573. 'cssText',
  574. "color: #F4FA58 !important;"
  575. );
  576. $fav_a.css( 'font-size', '150%');
  577. if( $("li.booklist").size() ){
  578. $fav_a.text("☆");
  579. $fav_a.attr( 'alt', 'ブックマークに追加');
  580. }else{
  581. $fav_a.text("★");
  582. $fav_a.attr(
  583. {
  584. 'href': bm_config,
  585. 'alt': 'ブックマーク設定'
  586. }
  587. );
  588. }
  589. $fav.attr( 'class', null);
  590. // しおりを挿む/しおり中ボタンの改変
  591. var $bmark_img = $("<img>");
  592. var $bmark = $header.find("li.bookmark,li.bookmark_now");
  593. $bmark.attr(
  594. {
  595. 'id': 'bookmark_icongap',
  596. 'class': null
  597. }
  598. );
  599. var $bmark_a = $bmark.children("a");
  600. if( $bmark_a.size() ){
  601. $bmark_a.text( "挿栞");
  602. $bmark_a.attr( 'alt', 'しおりを挿む');
  603. $bmark_img.attr( 'src', '/novelview/img/bookmarker_now.png');
  604. }else{
  605. $bmark.text("");
  606. $bmark_a = $("<a />");
  607. $bmark_a.attr(
  608. {
  609. 'href': bm_config,
  610. 'alt': 'ブックマーク設定'
  611. }
  612. );
  613. $bmark_a.text("設定");
  614. $bmark.append($bmark_a);
  615. $bmark_img.attr( 'src', '/novelview/img/bookmarker.png');
  616. }
  617.  
  618. var col = $header_org.find("li>a:first").css('border-left-color');
  619. if( $fav.size() ){
  620. $bmark_img.css(
  621. {
  622. 'pointer-events': 'none',
  623. 'position': 'absolute',
  624. 'top': 0,
  625. 'bottom': 0,
  626. 'margin': 'auto',
  627. 'padding': '0px 8px 0px 12px',
  628. }
  629. );
  630. $bmark_a.before($bmark_img);
  631.  
  632. $bmark_a.css(
  633. 'cssText',
  634. // padding-left は画像サイズから手計算。
  635. // ※自動計算はloadイベントを使用しなければならないので、
  636. //  表示の反映に若干ラグが出る(&コードがややこしくなる)。
  637. "padding-left: 33px !important;" +
  638. "border-left: none !important;"
  639. );
  640.  
  641. // <a>要素のテキストを縦中央合わせ
  642. // ※heightが確定していなければ、適切なline-heightを求められない
  643. var Li_Fix = function($li_a){
  644. $li_a.outerHeight(headerH-1);
  645. $li_a.css(
  646. {
  647. 'line-height': $li_a.height()+'px',
  648. 'border-right': '1px solid ' + col,
  649. }
  650. );
  651. $li_a.parent().css('padding',0);
  652. };
  653. Li_Fix($bmark_a);
  654. Li_Fix($fav_a);
  655. }else{
  656. $down_node.css( 'border-right', '1px solid ' + col );
  657. }
  658.  
  659.  
  660.  
  661.  
  662.  
  663. // 感想ページ
  664. if( location.href.indexOf("impression")>0){
  665. // コメントフォームをコメント一覧の上に持っていく
  666. var hyouka = "#hyoukalan";
  667. var $hyouka = $(hyouka);
  668. if( $hyouka.size() )
  669. {
  670. var $target = $("h1:eq(0)");
  671. $target.before($hyouka);
  672. $target.before($("<hr>"));
  673. // 横幅いっぱいにする
  674. $("textarea," + hyouka).css(
  675. {
  676. 'box-sizing': 'border-box',
  677. 'width': '100%'
  678. }
  679. );
  680. // "▽感想を書く"を消去
  681. $(".input").hide();
  682. }
  683. }
  684. })();
  685.  
  686.  
  687.  
  688. // novel_headerのpositionを記録しないようにする
  689. window.changeMenuBar = function(fixMenuBar){}