diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 474f50fa..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: 2 -jobs: - build: - working_directory: ~/circleci-narou - docker: - - image: circleci/ruby:3.0 - environment: - BUNDLE_JOBS: 1 - BUNDLE_RETRY: 3 - BUNDLE_PATH: vendor/bundle - steps: - - checkout - - - run: - name: Install bundler 2.1.4 - command: gem install bundler:2.1.4 - - # Restore bundle cache - - restore_cache: - keys: - - narou-bundle-{{ checksum "Gemfile.lock" }} - - narou-bundle- - - - run: - name: Bundle Install - command: bundle check || bundle install - - # Store bundle cache - - save_cache: - key: narou-bundle-{{ checksum "Gemfile.lock" }} - paths: - - vendor/bundle - - - run: - name: Run rspec - command: | - bundle exec rspec \ - --format RspecJunitFormatter \ - --out test_results/rspec/results.xml \ - --format progress \ - $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) - - # Save test results for timing analysis - - store_test_results: - path: test_results - - - store_artifacts: - path: ./coverage - destination: artifact-file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4b1ec6a6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + branches: [ "**" ] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + env: + BUNDLE_JOBS: "1" + BUNDLE_RETRY: "3" + BUNDLE_PATH: vendor/bundle + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Ruby 3.4 + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + + - name: Install bundler 2.7.2 + run: gem install bundler:2.7.2 + + - name: Cache bundle + uses: actions/cache@v4 + with: + path: vendor/bundle + key: ${{ runner.os }}-narou-bundle-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-narou-bundle- + + - name: Bundle install + run: | + bundle _2.7.2_ config set path "${BUNDLE_PATH}" + bundle _2.7.2_ check || bundle _2.7.2_ install + + - name: Run RSpec + run: | + mkdir -p test_results/rspec + # CircleCI のテスト分割は GHA ではそのまま使えないため、単純実行に置き換え + bundle _2.7.2_ exec rspec \ + --format RspecJunitFormatter \ + --out test_results/rspec/results.xml \ + --format progress + + - name: Upload test results (JUnit XML) + if: always() + uses: actions/upload-artifact@v4 + with: + name: rspec-junit + path: test_results + + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage diff --git a/Gemfile.lock b/Gemfile.lock index 0c0d6410..9e6592cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,146 +1,226 @@ PATH remote: . specs: - narou (3.8.0) - activesupport (>= 6.1, < 8.0) - diff-lcs (~> 1.2, >= 1.2.5) - erubis (~> 2.7) - ffi (~> 1.4, >= 1.4.2) - haml (>= 5.1.2, < 6) - mail (~> 2.6.0, >= 2.6.6) - memoist (~> 0.11.0) + narou (3.9.1.mod.R1) + activesupport (~> 8.0, >= 8.1.0) + bootsnap (~> 1.18, >= 1.18.6) + csv (~> 3.3) + diff-lcs (~> 1.6, >= 1.6.2) + erubi (~> 1.13.1) + ffi (~> 1.17, >= 1.17.2) + haml (>= 5.2.2, < 6) + mail (~> 2.9, >= 2.9.0) + memoist (~> 0.16.2) + nkf (~> 0.2.0) open_uri_redirections (~> 0.2, >= 0.2.1) - pony (~> 1, >= 1.11) - psych (~> 4.0) - rubyzip (~> 2.0, >= 2.0.0) + pony (~> 1, >= 1.13) + psych (~> 5.2) + puma (~> 6.4) + rackup (~> 2.1) + rexml (~> 3.4) + rubyzip (~> 3.2, >= 3.2.0) + sanitize (~> 7.0.0) sassc (~> 2.4) - sinatra (~> 2.0, >= 2.0.8.1) - sinatra-contrib (~> 2.0, >= 2.0.8.1) + sinatra (~> 4.2, >= 4.2.0) + sinatra-contrib (~> 4.2, >= 4.2.0) systemu (~> 2.6, >= 2.6.5) termcolorlight (~> 1.0, >= 1.1.1) - tilt (~> 2.0, >= 2.0.10) - unicode-display_width (~> 1.4) - webrick (~> 1.7) + tilt (~> 2.6, >= 2.6.1) + unicode-display_width (~> 3.2) GEM remote: https://rubygems.org/ specs: - activesupport (6.1.4) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.1.0) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - awesome_print (1.8.0) - byebug (11.1.3) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + awesome_print (1.9.2) + base64 (0.3.0) + bigdecimal (3.3.1) + bigdecimal (3.3.1-java) + bootsnap (1.18.6) + msgpack (~> 1.2) + byebug (12.0.0) coderay (1.1.3) - concurrent-ruby (1.1.9) - diff-lcs (1.4.4) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + crass (1.0.6) + csv (3.3.5) + date (3.4.1) + date (3.4.1-java) + diff-lcs (1.6.2) docile (1.3.5) - erubis (2.7.0) - ffi (1.15.3) - ffi (1.15.3-java) + drb (2.2.3) + erubi (1.13.1) + ffi (1.17.2) + ffi (1.17.2-java) + ffi (1.17.2-x64-mingw-ucrt) haml (5.2.2) temple (>= 0.8.0) tilt i18n (1.8.10) concurrent-ruby (~> 1.0) jar-dependencies (0.4.1) - mail (2.6.6) - mime-types (>= 1.16, < 4) - memoist (0.11.0) + json (2.15.1) + json (2.15.1-java) + logger (1.7.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + memoist (0.16.2) method_source (1.0.0) - mime-types (3.3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2021.0704) + mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (5.14.4) + msgpack (1.8.0) + msgpack (1.8.0-java) multi_json (1.15.0) - mustermann (1.1.1) + mustermann (3.0.4) ruby2_keywords (~> 0.0.1) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.4) + nio4r (2.7.4-java) + nkf (0.2.0) + nkf (0.2.0-java) + nokogiri (1.18.10) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.10-java) + racc (~> 1.4) + nokogiri (1.18.10-x64-mingw-ucrt) + racc (~> 1.4) open_uri_redirections (0.2.1) pony (1.13.1) mail (>= 2.0) - pry (0.13.1) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.13.1-java) + pry (0.15.2-java) coderay (~> 1.1) method_source (~> 1.0) spoon (~> 0.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - psych (4.0.1) - psych (4.0.1-java) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + psych (5.2.6) + date + stringio + psych (5.2.6-java) + date jar-dependencies (>= 0.1.7) - rack (2.2.3.1) - rack-protection (2.2.0) - rack - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + puma (6.6.1) + nio4r (~> 2.0) + puma (6.6.1-java) + nio4r (~> 2.0) + racc (1.8.1) + racc (1.8.1-java) + rack (3.2.3) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rackup (2.2.1) + rack (>= 3) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) + rspec-support (~> 3.13.0) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.10.1) - rspec_junit_formatter (0.4.1) + rspec-support (3.13.6) + rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (3.2.0) + sanitize (7.0.0) + crass (~> 1.0.2) + nokogiri (>= 1.16.8) sassc (2.4.0) ffi (~> 1.9) - simplecov (0.21.2) + securerandom (0.4.1) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.2) - sinatra (2.2.0) - mustermann (~> 1.0) - rack (~> 2.2) - rack-protection (= 2.2.0) + sinatra (4.2.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.2.1) + rack-session (>= 2.0.0, < 3) tilt (~> 2.0) - sinatra-contrib (2.2.0) - multi_json - mustermann (~> 1.0) - rack-protection (= 2.2.0) - sinatra (= 2.2.0) + sinatra-contrib (4.2.1) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.2.1) + sinatra (= 4.2.1) tilt (~> 2.0) spoon (0.0.6) ffi + stringio (3.1.7) systemu (2.6.5) temple (0.8.2) termcolorlight (1.1.1) - tilt (2.0.10) + tilt (2.6.1) timecop (0.9.2) - tzinfo (2.0.4) + timeout (0.4.3) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (1.7.0) - webrick (1.7.0) - zeitwerk (2.4.2) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.0.4) PLATFORMS java ruby + x64-mingw-ucrt DEPENDENCIES - awesome_print (~> 1.8) + awesome_print (~> 1.9) narou! - pry (~> 0.12) - pry-byebug (~> 3.8) - rspec (~> 3.10) + pry (~> 0.15) + pry-byebug (~> 3.11) + rspec (~> 3.13) rspec-retry (~> 0.6) - rspec_junit_formatter (~> 0.4) - simplecov (~> 0.20) + rspec_junit_formatter (~> 0.6) + simplecov (~> 0.22) timecop (~> 0.9) BUNDLED WITH - 2.2.15 + 2.7.2 diff --git a/LICENSE.txt b/LICENSE.txt index daf79ca3..eb1ca36c 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -26,7 +26,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------- /** - * bootbox.js v4.3.0 + * bootbox.js v4.4.0 * * The MIT License * @@ -53,32 +53,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------- -dataTables.colVis.js - -MIT license -Copyright (C) 2008-2014, SpryMedia Ltd. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----------------------------------------------------------------------- - -perfect-scrollbar - -The MIT License (MIT) -Copyright (c) 2012, 2014 Hyunje Alex Jun and other contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----------------------------------------------------------------------- - CSS Toggle Switch This is free and unencumbered software released into the public domain. diff --git a/README.md b/README.md index 2b19a0ea..46ed9c8e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -Narou.rb - 小説家になろうのダウンローダ&縦書き整形&管理アプリ。Kindle(などの電子書籍端末)でなろうを読む場合に超便利です! -=================================================================================== +# Narou.rb_MOD - 小説家になろうのダウンローダ&縦書き整形&管理アプリ。 -[![Gem Version](https://badge.fury.io/rb/narou.svg)](http://badge.fury.io/rb/narou) -[![Join the chat at https://gitter.im/whiteleaf7/narou](https://badges.gitter.im/whiteleaf7/narou.svg)](https://gitter.im/whiteleaf7/narou?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +プロジェクトを作成していただいた [whiteleaf7](https://github.com/whiteleaf7) 氏、カオスの塊だったこのforkに修正と機能追加を行ってくれた [ponponUSA](https://github.com/ponponusa) 氏に感謝を。 + + +## 概要 - Summary -概要 - Summary --------------- このアプリは[小説家になろう](http://syosetu.com/)などで公開されている小説の管理、 及び電子書籍データへの変換を支援します。縦書き用に特化されており、 横書きに最適化されたWEB小説を違和感なく縦書きで読むことが出来るようになります。 @@ -21,24 +20,50 @@ Narou.rb - 小説家になろうのダウンローダ&縦書き整形&管理 + 暁 http://www.akatsuki-novels.com/ (※300話以上ある作品は未対応) + カクヨム https://kakuyomu.jp/ -コンソールで操作するアプリケーションですが、ブラウザを使って直感的に操作することができる WEB UI も搭載!([デモページ](http://whiteleaf7.github.io/narou/demo/)) - 主な機能は小説家になろうの小説のダウンロード、更新管理、テキスト整形、AozoraEpub3・kindlegen連携によるEPUB/MOBI出力です。 その他にも変換したデータを直接電子書籍端末へ送信する機能は、メールで送信する機能などもあります。 -詳細な説明やインストール方法は **[Narou.rb 説明書](https://github.com/whiteleaf7/narou/wiki)** を御覧ください。 +詳細な説明やインストール方法は **[Narou.rb_MOD説明書](https://github.com/ponponusa/narou/wiki)** を御覧ください。 + +## 動作要件 - Requirements + +- Ruby 3.4以上(※元プロジェクトから変更されています) + +## 更新履歴 - ChangeLog + +### > 3.9.1.mod.R1 : 2025-10-28 + +#### <更新内容> ※[Rumia-Channel/narou](https://github.com/Rumia-Channel/narou)からの更新点を記載しています -![WEB UI ScreenCapture](https://raw.github.com/wiki/whiteleaf7/narou/images/webui_cap.png) -![Console ScreenCapture](https://raw.github.com/wiki/whiteleaf7/narou/images/narou_cap.gif) +```md +- テキスト/EPUB変換処理の高速化 + - 主に話数の多い(1000話オーバーなど)小説で顕著に効果があります + - ※小説掲載サイトからの取得ロジックに変更はないため、取得速度は変化はありません(変更予定もなし) +- JavaScriptライブラリの更新、変更 + - update jQuery 1.11.1 -> 3.7.1 + - update datatables.js 1.10.10 -> 2.3.4 + - update bootstrap 3.3.5 -> 3.4.1 + - and more... +- Rubyパッケージの更新、変更 + - supported Ruby version 2.3.0~ -> 3.4.0~ + - add puma/bootsnap/and more... + - update sinatra/ActiveSuport/tilt/and more... +- Digest認証からBasic認証に変更 + - Rack3.1から[Digest認証が非対応](https://github.com/ruby-grape/grape/issues/2294)となったため +- その他、細かな修正 +``` -更新履歴 - ChangeLog --------------------- +## TODO -3.9.1: 2024-09-19 ------------------ -#### 修正内容 -- 小説家になろうの目次修正に対応 #432 @etg-lt +- 外部Webサーバを利用しない形でのHTTPS対応 +- bootstrap5への移行 + - bootstrap3系では、jQuery3系に対応していないため + - jQuery migrateを削除したい +- 小説タイトルの自動整形 +- セキュリティリスクのある実装の修正 +- 変換処理の並列化による高速化 + - 今後の最適化のためにもスレッドセーフにする ---- -「小説家になろう」は株式会社ヒナプロジェクトの登録商標です +「小説家になろう」は株式会社ヒナプロジェクトの登録商標です。 diff --git a/bin/narou b/bin/narou index da47ca1e..34c007a3 100755 --- a/bin/narou +++ b/bin/narou @@ -1,4 +1,4 @@ -#! /usr/bin/env ruby +#! /usr/bin/env ruby --yjit # -*- mode: ruby -*- # -*- coding: utf-8 -*- # diff --git a/lib/command/browser.rb b/lib/command/browser.rb index 3d6fc562..714bcff5 100644 --- a/lib/command/browser.rb +++ b/lib/command/browser.rb @@ -45,7 +45,13 @@ def execute(argv) if @options["vote"] # TODO: 最新話の場所をAPIで取得する data_dir = Downloader.get_novel_data_dir_by_target(data["id"]) - latest_index = YAML.unsafe_load_file(File.join(data_dir, Downloader::TOC_FILE_NAME))["subtitles"].last["index"] + toc_path = File.join(data_dir, Downloader::TOC_FILE_NAME) + begin + latest_index = YAML.unsafe_load_file(toc_path)["subtitles"].last["index"] + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + latest_index = YAML.unsafe_load(File.read(toc_path))["subtitles"].last["index"] + end open_url = "#{toc_url + latest_index}/#my_novelpoint" else open_url = toc_url diff --git a/lib/command/diff.rb b/lib/command/diff.rb index 3dec2d2c..8e590d2f 100644 --- a/lib/command/diff.rb +++ b/lib/command/diff.rb @@ -212,8 +212,20 @@ def create_temp_files(id) cache_sections = [] cache_section_list.each do |path| match_latest_path = File.join(novel_dir, File.basename(path)) - latest_novel_sections << YAML.unsafe_load_file(match_latest_path) if File.exist?(match_latest_path) - cache_sections << YAML.unsafe_load_file(path) + if File.exist?(match_latest_path) + begin + latest_novel_sections << YAML.unsafe_load_file(match_latest_path) + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + latest_novel_sections << YAML.unsafe_load(File.read(match_latest_path)) + end + end + begin + cache_sections << YAML.unsafe_load_file(path) + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + cache_sections << YAML.unsafe_load(File.read(path)) + end end novel_info = Database.instance[id] diff --git a/lib/command/list/novel_decorator.rb b/lib/command/list/novel_decorator.rb index 057e96a9..44a60982 100644 --- a/lib/command/list/novel_decorator.rb +++ b/lib/command/list/novel_decorator.rb @@ -21,7 +21,7 @@ def initialize(novel, parent) def decorate_id disp_id = ((frozen ? "*" : "") + id.to_s).rjust(4) if frozen - disp_id.sub("*", "*") + disp_id.gsub("*", "*") else disp_id end diff --git a/lib/command/setting.rb b/lib/command/setting.rb index dd8c3743..e8d98ffe 100644 --- a/lib/command/setting.rb +++ b/lib/command/setting.rb @@ -642,25 +642,18 @@ def self.get_setting_tab_info invisible: true, tab: :global }, - "server-digest-auth.enable" => { - type: :boolean, help: "WEBサーバでDigest認証を使用するかどうか", + "server-basic-auth.enable" => { + type: :boolean, help: "WEBサーバでBasic認証を使用するかどうか", invisible: true, tab: :global }, - "server-digest-auth.user" => { - type: :string, help: "WEBサーバでDigest認証をするユーザ名", + "server-basic-auth.user" => { + type: :string, help: "WEBサーバでBasic認証をするユーザ名", invisible: true, tab: :global }, - "server-digest-auth.password" => { - type: :string, help: "WEBサーバのDigest認証のパスワード。hashed-passwordも設定した場合はそちらが優先される", - invisible: true, - tab: :global - }, - "server-digest-auth.hashed-password" => { - type: :string, - help: "WEBサーバのDigest認証のパスワードを、Realmを\"narou.rb\"としてハッシュにしたもの。下記のようなコマンドで生成できる\n" \ - "$ ruby -r 'digest/md5' -e 'puts Digest::MD5.hexdigest \"\#{$*[0]}:narou.rb:\#{$*[1]}\"' user password", + "server-basic-auth.password" => { + type: :string, help: "WEBサーバのBasic認証のパスワード", invisible: true, tab: :global }, diff --git a/lib/converterbase.rb b/lib/converterbase.rb index 7e9ec882..9395be67 100644 --- a/lib/converterbase.rb +++ b/lib/converterbase.rb @@ -45,6 +45,7 @@ def initialize(setting, inspector, illustration) @subtitles = nil @data_type = "text" @current_index = 0 + @device = Narou.get_device reset_member_values end @@ -65,7 +66,6 @@ def reset_member_values @num_and_comma_list = {} @force_indent_special_chapter_list = {} @in_author_comment_block = nil - @device = Narou.get_device end def outputs(data = "", force = false) @@ -1332,6 +1332,17 @@ def convert(text, text_type) return data end + # 複数のテキストをまとめて変換する + # pairs: { key1 => [text, text_type], key2 => [text, text_type], ... } + # 戻り値: { key1 => converted_text1, key2 => converted_text2, ... } + def convert_multi(pairs) + results = {} + pairs.each do |key, (text, text_type)| + results[key] = convert(text, text_type) + end + results + end + # # 変換処理本体 # @@ -1368,7 +1379,7 @@ def convert_main(io) @write_fp.write(data) else @read_fp.each_with_index do |line, i| - progressbar.output(i) if progressbar + progressbar.output(i) if progressbar && (i % 50).zero? # 50行ごとに制限 @request_skip_output_line = false zenkaku_rstrip(line) if @request_insert_blank_next_line diff --git a/lib/device.rb b/lib/device.rb index 6c761173..fbaf091d 100644 --- a/lib/device.rb +++ b/lib/device.rb @@ -24,6 +24,9 @@ def get_storage_path end case Helper.determine_os + when :docker + require_relative "device/library/docker" + extend Device::Library::Docker when :windows require_relative "device/library/windows" extend Device::Library::Windows diff --git a/lib/device/ibunko.rb b/lib/device/ibunko.rb index fa0a9410..2c51ba6e 100644 --- a/lib/device/ibunko.rb +++ b/lib/device/ibunko.rb @@ -33,7 +33,11 @@ def create_pure_aozora_zip data = File.read(@converted_txt_path, encoding: Encoding::UTF_8) # EPUB最適化のために混入しうるHTML/タグ類を汎用的に除去 # 先に汎用HTMLを除去してから、青空注記→i文庫カスタムタグへの変換を行う - data.gsub!(%r{]+>}, "") + previous = nil + while data != previous + previous = data + data = data.gsub(%r{]+>}, "") + end # HTMLエンティティは実体に復号 data = Helper.restore_entity(data) # 青空注記 → i文庫HDカスタムタグへ変換 diff --git a/lib/device/library/docker.rb b/lib/device/library/docker.rb new file mode 100644 index 00000000..e7f5d2ef --- /dev/null +++ b/lib/device/library/docker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# +# Copyright 2013 whiteleaf. All rights reserved. +# + +module Device::Library + module Docker + def get_device_root_dir(volume_name) + nil + end + + def eject(volume_name) + raise Device::CantEject + end + end +end diff --git a/lib/device/library/linux.rb b/lib/device/library/linux.rb index 0017be65..446d7519 100644 --- a/lib/device/library/linux.rb +++ b/lib/device/library/linux.rb @@ -32,7 +32,7 @@ def device_mount_info(volume_name) raise Device::CantEject, "端末が接続されていません" unless device_root pattern = %r!^(/dev/[^ ]+) .* #{device_root} .*\Wuhelper=(\w+)! - open("|mount") do |io| + File.open("|mount") do |io| while line = io.gets if line =~ pattern return [$1, $2] diff --git a/lib/downloader.rb b/lib/downloader.rb index 9d9fc0a2..9fb28831 100644 --- a/lib/downloader.rb +++ b/lib/downloader.rb @@ -7,6 +7,7 @@ require "yaml" require "fileutils" require "ostruct" +require "sanitize" require_relative "narou" require_relative "helper" require_relative "sitesetting" @@ -159,7 +160,11 @@ def self.get_data_by_target(target) # toc 読込 # def self.get_toc_data(archive_path) - YAML.unsafe_load_file(File.join(archive_path, TOC_FILE_NAME)) + path = File.join(archive_path, TOC_FILE_NAME) + YAML.unsafe_load_file(path) + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + YAML.unsafe_load(File.read(path)) end def self.get_toc_by_target(target) @@ -419,7 +424,7 @@ def run_download auto_add_tags = Inventory.load("local_setting")["auto-add-tags"] if @setting["tag"] && auto_add_tags - clean_tag = @setting["tag"].gsub(/<[^>]*>/, '').gsub(/キーワードが設定されていません/, '').gsub(/キーワード/, '').gsub(/\"?\(\?\.\+\?\)\"?/, '').gsub(/\(\?\ 0 new_tags = clean_tag.split(/[  ]+| /).uniq old_tags = (record && record["tags"]) ? record["tags"] : [] @@ -644,7 +649,7 @@ def update_database(suspend: false) } auto_add_tags = Inventory.load("local_setting")["auto-add-tags"] if @setting["tag"] && auto_add_tags - clean_tag = @setting["tag"].gsub(/<[^>]*>/, '').gsub(/キーワード/, '').gsub(/\"?\(\?\.\+\?\)\"?/, '').gsub(/\(\?\ 0 tags = clean_tag.split(/[  ]+| /) if record && record["tags"] @@ -1109,7 +1114,12 @@ def sections_download_and_save(subtitles) def different_section?(old_relative_path, new_subtitle_info) path = get_novel_data_dir.join(old_relative_path) return true unless path.exist? - YAML.unsafe_load_file(path)["element"] != new_subtitle_info["element"] + begin + YAML.unsafe_load_file(path)["element"] != new_subtitle_info["element"] + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + YAML.unsafe_load(File.read(path))["element"] != new_subtitle_info["element"] + end end # @@ -1360,6 +1370,11 @@ def load_novel_data(filename) YAML.unsafe_load_file(get_novel_data_dir.join(filename)) rescue Errno::ENOENT nil + rescue SystemCallError => e + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + path = get_novel_data_dir.join(filename) + return nil unless File.exist?(path) + YAML.unsafe_load(File.read(path)) end # diff --git a/lib/extensions/jruby.rb b/lib/extensions/jruby.rb index 29ba08f4..bc70c0a1 100644 --- a/lib/extensions/jruby.rb +++ b/lib/extensions/jruby.rb @@ -10,7 +10,7 @@ module FileUtils # マルチバイト文字を含むパスを認識出来ないため def self.cp(src, dst, opt = nil) - open(src, "rb") do |fp| + File.open(src, "rb") do |fp| File.binwrite(dst, fp.read) end end @@ -19,7 +19,7 @@ def self.cp(src, dst, opt = nil) class File # 何故かエンコーディングエラーが出るため def self.binwrite(path, data) - open(path, "wb") do |fp| + File.open(path, "wb") do |fp| fp.write(data) end end diff --git a/lib/helper.rb b/lib/helper.rb index a2562fca..8c4c7edc 100644 --- a/lib/helper.rb +++ b/lib/helper.rb @@ -18,6 +18,11 @@ module Helper FILENAME_LENGTH_LIMIT = 50 FOLDER_LENGTH_LIMIT = 50 + def in_docker? + return false unless File.exist?('/proc/1/cgroup') + File.readlines('/proc/1/cgroup').any? { |line| line.include?('/docker/') || line.include?('/lxc/') } + end + def os_windows? @@os_is_windows ||= HOST_OS =~ /mswin(?!ce)|mingw|bccwin/i end @@ -32,6 +37,8 @@ def os_cygwin? def determine_os case + when in_docker? + :docker when os_windows? :windows when os_mac? diff --git a/lib/html.rb b/lib/html.rb index 3ed428eb..9bd78091 100644 --- a/lib/html.rb +++ b/lib/html.rb @@ -59,7 +59,12 @@ def to_aozora(pre_html: false) end def delete_tag(text = @string) - text.gsub(/<.+?>/, "") + previous = nil + while text != previous + previous = text + text = text.gsub(/<.+?>/, "") + end + text end def br_to_aozora(text = @string) diff --git a/lib/illustration.rb b/lib/illustration.rb index cc0e450d..74347eb0 100644 --- a/lib/illustration.rb +++ b/lib/illustration.rb @@ -59,7 +59,7 @@ def download_image(url, basename = nil) content_type = fp.meta["content-type"] ext = MIME[content_type] or raise UnknownMIMEType, content_type illust_abs_path = create_illust_path(basename) + "." + ext - open(illust_abs_path, "wb") do |write_fp| + File.open(illust_abs_path, "wb") do |write_fp| write_fp.write(fp.read) end @inspector.info("挿絵「#{File.basename(illust_abs_path)}」を保存しました。") diff --git a/lib/ini.rb b/lib/ini.rb index 15833f34..2ed096cb 100644 --- a/lib/ini.rb +++ b/lib/ini.rb @@ -103,7 +103,7 @@ def save(filename = nil) unless filename raise NoFilenameError end - open(@filename, "w") do |fp| + File.open(@filename, "w") do |fp| @data.each do |section, values| if section != GLOBAL_SECTION fp.puts("[#{section}]") diff --git a/lib/inspector.rb b/lib/inspector.rb index 4b12d61f..78d952ad 100644 --- a/lib/inspector.rb +++ b/lib/inspector.rb @@ -82,7 +82,7 @@ def display(klass = ALL, target = $stdout) def save(path = nil) path = File.join(@setting.archive_path, INSPECT_LOG_NAME) if path.nil? - open(path, "w") do |fp| + File.open(path, "w") do |fp| fp.puts "--- ログ出力 #{Time.now} ---" display(ALL, fp) end diff --git a/lib/inventory.rb b/lib/inventory.rb index 8fae8fcf..4e43849b 100644 --- a/lib/inventory.rb +++ b/lib/inventory.rb @@ -64,7 +64,12 @@ def init(name, scope) error "#{@inventory_file_path} が壊れてるっぽい" raise end - YAML.unsafe_load_file(@inventory_file_path) + begin + YAML.unsafe_load_file(@inventory_file_path) + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + YAML.unsafe_load(File.read(@inventory_file_path)) + end end }) end diff --git a/lib/mailer.rb b/lib/mailer.rb index 418f9669..f704be2a 100644 --- a/lib/mailer.rb +++ b/lib/mailer.rb @@ -23,7 +23,12 @@ def self.create this.clear setting_file_path = File.join(Narou.root_dir, SETTING_FILE) if File.exist?(setting_file_path) - options = YAML.unsafe_load_file(setting_file_path) + begin + options = YAML.unsafe_load_file(setting_file_path) + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + options = YAML.unsafe_load(File.read(setting_file_path)) + end unless options.delete(:complete) raise SettingUncompleteError, "設定ファイルの書き換えが終了していないようです。\n" + "設定ファイルは #{setting_file_path} にあります" diff --git a/lib/narou.rb b/lib/narou.rb index 9aa8dc64..884450ae 100644 --- a/lib/narou.rb +++ b/lib/narou.rb @@ -44,7 +44,7 @@ class << self @@is_web = false def last_commit_year - 2024 + 2025 end def root_dir diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index e9cd97d4..a120df02 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -89,7 +89,7 @@ def self.convert_file(filename, options = {}) setting.author = "" setting.title = File.basename(filename) novel_converter = new(setting, output_filename, options[:display_inspector]) - text = open(filename, "r:BOM|UTF-8") { |fp| fp.read }.gsub("\r", "") + text = File.open(filename, "r:BOM|UTF-8") { |fp| fp.read }.gsub("\r", "") if options[:encoding] text.force_encoding(options[:encoding]).encode!(Encoding::UTF_8) end @@ -259,97 +259,64 @@ def self.txt_to_epub(filename, dst_dir: nil, device: nil, verbose: false, yokoga # def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) return :success if subjects.nil? || subjects.empty? - - # 必要なgemが利用できない場合は警告を出して処理をスキップ if defined?(ZIP_UNAVAILABLE) stream_io.error "dc:subject埋め込み機能を使用するにはrubyzip gemが必要です" return :error end - if defined?(REXML_UNAVAILABLE) - stream_io.error "dc:subject埋め込み機能を使用するにはrexml gemが必要です" - return :error - end - - temp_dir = nil + entries = {} begin - # 一時ディレクトリ作成 - temp_dir = Dir.mktmpdir - - # EPUBファイルを展開 + # EPUBをメモリ上に展開 Zip::File.open(epub_path) do |zip_file| zip_file.each do |entry| - entry_path = File.join(temp_dir, entry.name) - FileUtils.mkdir_p(File.dirname(entry_path)) - entry.extract(entry_path) + entries[entry.name] = entry.get_input_stream.read end end - # standard.opfファイルを探す - opf_path = Dir.glob(File.join(temp_dir, "**", "standard.opf")).first - unless opf_path + # standard.opf 書き換え + opf_name, opf_body = entries.find { |name, _| name.end_with?("standard.opf") } + unless opf_name stream_io.error "standard.opfファイルが見つかりませんでした" return :error end - # 文字列置換でdc:subjectを追加する方式に変更 - # (REXMLの整形では元のフォーマットが崩れるため) - content = File.read(opf_path) - - # 既存のdc:subjectを削除 + content = opf_body.dup content.gsub!(/.*?<\/dc:subject>\s*\n?\s*/m, "") - - # 新しいdc:subjectを生成(XMLエスケープも行う) - dc_subject_lines = subjects.map do |subject| - next if subject.strip.empty? - escaped_subject = subject.strip.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub("\"", """).gsub("'", "'") - " #{escaped_subject}" - end.compact - + dc_subject_lines = subjects.map(&:strip).reject(&:empty?).map { |s| + esc = s.gsub("&","&").gsub("<","<").gsub(">",">").gsub("\"",""").gsub("'","'") + " #{esc}" + } if dc_subject_lines.any? - # の直前にdc:subjectを挿入 dc_subjects_xml = dc_subject_lines.join("\n") + "\n" content.sub!(/(\s*)<\/metadata>/, "\n#{dc_subjects_xml}\\1") end + entries[opf_name] = content - # XMLを書き戻し - File.write(opf_path, content) - - # EPUBファイルを再作成(OutputStreamを使用しmimetypeを無圧縮・先頭に配置) + # 再Zip化 (mimetypeは無圧縮で先頭) File.delete(epub_path) - Zip::OutputStream.open(epub_path) do |zos| - # mimetypeファイルを最初に無圧縮で追加 - mimetype_path = File.join(temp_dir, "mimetype") - unless File.exist?(mimetype_path) + # mimetype必須 + if !entries["mimetype"] stream_io.error "mimetypeファイルが見つかりません" return :error end # 第1引数に名前、第4引数にZip::Entry::STORED を渡す - zos.put_next_entry('mimetype', nil, nil, Zip::Entry::STORED) + zos.put_next_entry("mimetype", nil, nil, Zip::Entry::STORED) + zos.write entries["mimetype"] - zos.write File.read(mimetype_path, mode: "rb") - - # 他のファイルを追加(mimetypeを除く) - Dir.glob(File.join(temp_dir, "**", "*"), File::FNM_DOTMATCH).sort.each do |file_path| - next if File.directory?(file_path) - relative_path = file_path.sub(temp_dir + "/", "") - next if relative_path == "mimetype" # mimetypeは既に追加済み - zos.put_next_entry(relative_path) - zos.write File.read(file_path, mode: "rb") + entries.each do |name, body| + next if name == "mimetype" + zos.put_next_entry(name) + zos.write body end end stream_io.puts "dc:subjectを追加しました: #{subjects.join(', ')}" :success - rescue => e stream_io.error "dc:subject追加中にエラーが発生しました: #{e.message}" :error - ensure - # 一時ディレクトリを削除 - FileUtils.rm_rf(temp_dir) if temp_dir end end @@ -575,9 +542,12 @@ def initialize_event on(:"convert_main.init") do |subtitles| progressbar = ProgressBar.new(subtitles.size, io: stream_io) end + on(:"convert_main.loop") do |i| - progressbar.output(i) if progressbar + # 毎回ではな10件ごとに絞る + progressbar.output(i) if progressbar && (i % 10).zero? end + on(:"convert_main.finish") do progressbar.clear if progressbar end @@ -595,7 +565,13 @@ def display_footer def load_novel_section(subtitle_info, section_save_dir) file_subtitle = subtitle_info["file_subtitle"] || subtitle_info["subtitle"] # 互換性維持のため path = section_save_dir.join("#{subtitle_info["index"]} #{file_subtitle}.yaml") - YAML.unsafe_load_file(path) + begin + YAML.unsafe_load_file(path) + rescue SystemCallError => e + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + raise if e.is_a?(Errno::ENOENT) + YAML.unsafe_load(File.read(path)) + end rescue Errno::ENOENT => e stream_io.puts stream_io.error(<<~MSG.termcolor) @@ -611,13 +587,25 @@ def create_novel_text_by_template(sections, toc, is_hotentry = false, index = ni cover_chuki = create_cover_chuki device = Narou.get_device setting = @setting - toc["title"] = setting.novel_title unless setting.novel_title.empty? + + toc["title"] = setting.novel_title unless setting.novel_title.empty? toc["author"] = setting.novel_author unless setting.novel_author.empty? + processing_title = toc["title"] processing_title += "_#{index}" if index processed_title = decorate_title(processing_title) template_name = (device && device.ibunko? ? NOVEL_TEXT_TEMPLATE_NAME_FOR_IBUNKO : NOVEL_TEXT_TEMPLATE_NAME) - Template.get(template_name, binding, 1.1) + + # テンプレートをキャッシュする + # コンパイル済みERB(またはProc)をキャッシュして binding だけ都度差し込む + @__template_cache ||= {} + compiled = @__template_cache[template_name] + unless compiled + compiled = Template.compile(template_name, 1.1) + @__template_cache[template_name] = compiled + end + + Template.render(compiled, binding) end # @@ -845,6 +833,9 @@ def cut_subtitles(subtitles) # subtitle info から変換処理をする # def subtitles_to_sections(subtitles, html) + # 章データをキャッシュ + @__section_cache ||= {} + sections = [] section_save_dir = Downloader.get_novel_section_save_dir(@setting.archive_path) @@ -853,31 +844,74 @@ def subtitles_to_sections(subtitles, html) subtitles.each_with_index do |subinfo, i| trigger(:"convert_main.loop", i) @converter.current_index = i - section = load_novel_section(subinfo, section_save_dir) - if section["chapter"].length > 0 - section["chapter"] = @converter.convert(section["chapter"], "chapter") + + # YAMLロードをキャッシュ + key = subinfo["index"] + original_section = @__section_cache[key] + unless original_section + original_section = load_novel_section(subinfo, section_save_dir) + @__section_cache[key] = original_section end - @inspector.subtitle = section["subtitle"] - section["subtitle"] = @converter.convert(section["subtitle"], "subtitle") + # キャッシュを壊さないようディープ寄りにdup + # (chapter/subtitle/elementなど後で書き換えるので) + section = original_section.dup + section["element"] = original_section["element"].dup + + # data_type 判定 element = section["element"] data_type = element.delete("data_type") || "text" @converter.data_type = data_type + + # HTML→青空変換が必要なやつを先にプレーンテキスト化 + preprocessed_element_texts = {} element.each do |text_type, elm_text| if data_type != "text" html.string = elm_text elm_text = html.to_aozora(pre_html: data_type == "pre_html") end - element[text_type] = @converter.convert(elm_text, text_type) + preprocessed_element_texts[text_type] = elm_text end + + # まとめてコンバータに渡すためのバッチ入力を作る + batch_inputs = {} + + # chapter + if section["chapter"] && !section["chapter"].empty? + batch_inputs[:chapter] = [section["chapter"], "chapter"] + end + + # subtitle + @inspector.subtitle = section["subtitle"] + batch_inputs[:subtitle] = [section["subtitle"], "subtitle"] + + # element 各種 + preprocessed_element_texts.each do |text_type, body_text| + batch_inputs[[:element, text_type]] = [body_text, text_type] + end + + # 一括変換 + converted = @converter.convert_multi(batch_inputs) + if batch_inputs[:chapter] + section["chapter"] = converted[:chapter] + end + + section["subtitle"] = converted[:subtitle] + + element.keys.each do |text_type| + section["element"][text_type] = converted[[:element, text_type]] + end + sections << section end + @use_dakuten_font = @converter.use_dakuten_font sections ensure trigger(:"convert_main.finish") end + # # テキストデータ先頭二行からタイトルと作者名を取得 # diff --git a/lib/sitesetting.rb b/lib/sitesetting.rb index ff7a13e8..13a64418 100644 --- a/lib/sitesetting.rb +++ b/lib/sitesetting.rb @@ -73,7 +73,12 @@ def load_file(path) def initialize(path) @match_values = {} - @yaml = YAML.unsafe_load_file(path) + begin + @yaml = YAML.unsafe_load_file(path) + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + @yaml = YAML.unsafe_load(File.read(path)) + end @path = path end diff --git a/lib/template.rb b/lib/template.rb index a19161ed..4cfedd81 100644 --- a/lib/template.rb +++ b/lib/template.rb @@ -13,6 +13,57 @@ class Template class LoadError < StandardError; end + # コンパイル済みテンプレートをキャッシュする + # { "novel.txt" => { erb: , binary_version: 1.1, src_filename: "novel.txt" } } + # キャッシュする関係でスレッドセーフにはなっていないので、並列での変換処理を行う場合は対応が必要 + @__compiled_cache = {} + + # + # テンプレートを元にデータを作成 + # + # テンプレートファイルの検索順位 + # 1. root_dir/template + # 2. script_dir/template + # + def self.compile(src_filename, binary_version) + # すでにキャッシュ済みならそのまま返す + cached = @__compiled_cache[src_filename] + return cached if cached + + # ファイル探索(getと同じロジック) + [Narou.root_dir, Narou.script_dir].each do |dir| + path = dir.join(TEMPLATE_DIR, src_filename + ".erb") + next unless path.exist? + + src = Helper::CacheLoader.load(path) + erb = ERB.new(src, trim_mode: "-") + + compiled = { + erb: erb, + binary_version: binary_version, + src_filename: src_filename + } + + @__compiled_cache[src_filename] = compiled + return compiled + end + + raise LoadError, "テンプレートファイルが見つかりません。(#{src_filename}.erb)" + end + + def self.render(compiled, _binding) + # compiled は compile が返した Hash + @@binary_version = compiled[:binary_version] + @@src_filename = compiled[:src_filename] + + # target_binary_version から参照される @@src_version は + # テンプレート内で <%= Template.target_binary_version 1.1 %> みたいに呼ばれる想定 + # なので、ここでは設定しない。テンプレの中から呼ばれた時点で + # @@src_version が更新され、invalid_templace_version? が機能する + + compiled[:erb].result(_binding) + end + # # テンプレートを元にファイルを作成 # @@ -36,24 +87,10 @@ def self.write(src_filename, dest_filepath, _binding, binary_version, overwrite end end - # - # テンプレートを元にデータを作成 - # - # テンプレートファイルの検索順位 - # 1. root_dir/template - # 2. script_dir/template - # + # 既存コード向けのwrap関数 def self.get(src_filename, _binding, binary_version) - @@binary_version = binary_version - @@src_filename = src_filename - [Narou.root_dir, Narou.script_dir].each do |dir| - path = dir.join(TEMPLATE_DIR, src_filename + ".erb") - next unless path.exist? - src = Helper::CacheLoader.load(path) - result = ERB.new(src, trim_mode: "-").result(_binding) - return result - end - raise LoadError, "テンプレートファイルが見つかりません。(#{src_filename}.erb)" + compiled = compile(src_filename, binary_version) + render(compiled, _binding) end def self.invalid_templace_version? diff --git a/lib/version.rb b/lib/version.rb index f8f519e0..39284b15 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,5 +5,5 @@ # module Narou - VERSION = "3.9.1" + VERSION = "3.9.1.mod.R1" end diff --git a/lib/web/appserver.rb b/lib/web/appserver.rb index 4f018ae1..44c90f75 100755 --- a/lib/web/appserver.rb +++ b/lib/web/appserver.rb @@ -11,6 +11,9 @@ require "sinatra/base" require "sinatra/json" require "sinatra/reloader" if $development +require "securerandom" +require "rack/session" +require "rack/protection" # require "better_errors" if $debug require "tilt/erubi" require "tilt/haml" @@ -33,6 +36,7 @@ class Narou::AppServer < Sinatra::Base configure do set :app_file, __FILE__ set :erb, trim: "-" + set :quiet, true enable :protection enable :sessions @@ -41,7 +45,8 @@ class Narou::AppServer < Sinatra::Base end set :environment, :production unless $development - set :server, :webrick + set :server, :puma + set :server_settings, { Silent: true } if $debug use BetterErrors::Middleware @@ -162,19 +167,17 @@ def fill_general_all_no_in_database end # サーバーの認証の設定 - # とりあえずDigest認証のみ + # - Digest認証がRackの機能からオミットされたので、Basic認証に変更 def setup_server_authentication - auth = Inventory.load("global_setting", :global).group("server-digest-auth") + auth = Inventory.load("global_setting", :global).group("server-basic-auth") user = auth.user - hashed = auth.hashed_password - passwd = hashed || auth.password + passwd = auth.password # ハッシュは使わない - # enableかつユーザー名とパスワードが設定されている時のみ認証を有効にする return unless auth.enable && user && passwd self.class.class_exec do - use Rack::Auth::Digest::MD5, { realm: "narou.rb", opaque: "", passwords_hashed: hashed } do |username| - passwd if username == user + use Rack::Auth::Basic, "narou.rb" do |username, password| + username == user && password == passwd end end end @@ -429,7 +432,13 @@ def setup_server_authentication postscripts_count = 0 toc["subtitles"].each do |sub| begin - element = YAML.unsafe_load_file(downloader.section_file_path(sub))["element"] + section_path = downloader.section_file_path(sub) + begin + element = YAML.unsafe_load_file(section_path)["element"] + rescue SystemCallError + # bootsnap on Windows can raise Errno::E01 errors, fallback to standard YAML + element = YAML.unsafe_load(File.read(section_path))["element"] + end data_type = element["data_type"] || "text" introduction = element["introduction"] || "" postscript = element["postscript"] || "" diff --git a/lib/web/public/resources/bootbox.min.js b/lib/web/public/resources/bootbox.min.js deleted file mode 100644 index 134660a2..00000000 --- a/lib/web/public/resources/bootbox.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * bootbox.js v4.3.0 - * - * http://bootboxjs.com/license.txt - */ -!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d(a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:!0,animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback():!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback(!1)},a.buttons.confirm.callback=function(){return a.callback(!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback(null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback(c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var c=b(n.dialog),d=c.find(".modal-dialog"),f=c.find(".modal-body"),i=a.buttons,j="",k={onEscape:a.onEscape};if(g(i,function(a,b){j+="",k[a]=b.callback}),f.find(".bootbox-body").html(a.message),a.animate===!0&&c.addClass("fade"),a.className&&c.addClass(a.className),"large"===a.size&&d.addClass("modal-lg"),"small"===a.size&&d.addClass("modal-sm"),a.title&&f.before(n.header),a.closeButton){var l=b(n.closeButton);a.title?c.find(".modal-header").prepend(l):l.css("margin-top","-10px").prependTo(f)}return a.title&&c.find(".modal-title").html(a.title),j.length&&(f.after(n.footer),c.find(".modal-footer").html(j)),c.on("hidden.bs.modal",function(a){a.target===this&&c.remove()}),c.on("shown.bs.modal",function(){c.find(".btn-primary:first").focus()}),c.on("escape.close.bb",function(a){k.onEscape&&e(a,c,k.onEscape)}),c.on("click",".modal-footer button",function(a){var d=b(this).data("bb-handler");e(a,c,k[d])}),c.on("click",".bootbox-close-button",function(a){e(a,c,k.onEscape)}),c.on("keyup",function(a){27===a.which&&c.trigger("escape.close.bb")}),b(a.container).append(c),c.modal({backdrop:a.backdrop,keyboard:!1,show:!1}),a.show&&c.modal("show"),c},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"OK"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.init=function(c){return a(c||b)},p}); diff --git a/lib/web/public/resources/default-style.css b/lib/web/public/resources/css/default-style.css similarity index 74% rename from lib/web/public/resources/default-style.css rename to lib/web/public/resources/css/default-style.css index ead2e682..606b469d 100644 --- a/lib/web/public/resources/default-style.css +++ b/lib/web/public/resources/css/default-style.css @@ -54,6 +54,27 @@ user-select: none; } +/* DataTables wrapper の背景色設定 */ +.dataTables_wrapper { + background-color: transparent; +} + +#novel-list { + background-color: #fffcef !important; + /* DataTables 2.x のCSS変数を上書きして選択色を黄色系に */ + --dt-row-selected: 241, 230, 178; /* RGB値で指定 */ + --dt-row-selected-text: 51, 51, 51; +} + +#novel-list tbody tr { + background-color: inherit; +} + +/* 選択行のスタイルを強制 */ +#novel-list tbody tr.selected > * { + box-shadow: inset 0 0 0 9999px rgba(255, 212, 0, 0.3) !important; +} + /* より包括的な選択防止 */ body, * { -webkit-user-select: none; diff --git a/lib/web/public/resources/toggle-switch.css b/lib/web/public/resources/css/toggle-switch.css similarity index 100% rename from lib/web/public/resources/toggle-switch.css rename to lib/web/public/resources/css/toggle-switch.css diff --git a/lib/web/public/resources/dataTables.colVis.js b/lib/web/public/resources/dataTables.colVis.js deleted file mode 100644 index 03f09f40..00000000 --- a/lib/web/public/resources/dataTables.colVis.js +++ /dev/null @@ -1,1113 +0,0 @@ -/*! ColVis 1.1.2-dev - * ©2010-2014 SpryMedia Ltd - datatables.net/license - */ - -/** - * @summary ColVis - * @description Controls for column visibility in DataTables - * @version 1.1.2-dev - * @file dataTables.colReorder.js - * @author SpryMedia Ltd (www.sprymedia.co.uk) - * @contact www.sprymedia.co.uk/contact - * @copyright Copyright 2010-2014 SpryMedia Ltd. - * - * This source file is free software, available under the following license: - * MIT license - http://datatables.net/license/mit - * - * This source file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. - * - * For details please refer to: http://www.datatables.net - */ - -(function(window, document, undefined) { - - -var factory = function( $, DataTable ) { -"use strict"; - -/** - * ColVis provides column visibility control for DataTables - * - * @class ColVis - * @constructor - * @param {object} DataTables settings object. With DataTables 1.10 this can - * also be and API instance, table node, jQuery collection or jQuery selector. - * @param {object} ColVis configuration options - */ -var ColVis = function( oDTSettings, oInit ) -{ - /* Santiy check that we are a new instance */ - if ( !this.CLASS || this.CLASS != "ColVis" ) - { - alert( "Warning: ColVis must be initialised with the keyword 'new'" ); - } - - if ( typeof oInit == 'undefined' ) - { - oInit = {}; - } - - var camelToHungarian = $.fn.dataTable.camelToHungarian; - if ( camelToHungarian ) { - camelToHungarian( ColVis.defaults, ColVis.defaults, true ); - camelToHungarian( ColVis.defaults, oInit ); - } - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Public class variables - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * @namespace Settings object which contains customisable information for - * ColVis instance. Augmented by ColVis.defaults - */ - this.s = { - /** - * DataTables settings object - * @property dt - * @type Object - * @default null - */ - "dt": null, - - /** - * Customisation object - * @property oInit - * @type Object - * @default passed in - */ - "oInit": oInit, - - /** - * Flag to say if the collection is hidden - * @property hidden - * @type boolean - * @default true - */ - "hidden": true, - - /** - * Store the original visibility settings so they could be restored - * @property abOriginal - * @type Array - * @default [] - */ - "abOriginal": [] - }; - - - /** - * @namespace Common and useful DOM elements for the class instance - */ - this.dom = { - /** - * Wrapper for the button - given back to DataTables as the node to insert - * @property wrapper - * @type Node - * @default null - */ - "wrapper": null, - - /** - * Activation button - * @property button - * @type Node - * @default null - */ - "button": null, - - /** - * Collection list node - * @property collection - * @type Node - * @default null - */ - "collection": null, - - /** - * Background node used for shading the display and event capturing - * @property background - * @type Node - * @default null - */ - "background": null, - - /** - * Element to position over the activation button to catch mouse events when using mouseover - * @property catcher - * @type Node - * @default null - */ - "catcher": null, - - /** - * List of button elements - * @property buttons - * @type Array - * @default [] - */ - "buttons": [], - - /** - * List of group button elements - * @property groupButtons - * @type Array - * @default [] - */ - "groupButtons": [], - - /** - * Restore button - * @property restore - * @type Node - * @default null - */ - "restore": null - }; - - /* Store global reference */ - ColVis.aInstances.push( this ); - - /* Constructor logic */ - this.s.dt = $.fn.dataTable.Api ? - new $.fn.dataTable.Api( oDTSettings ).settings()[0] : - oDTSettings; - - this._fnConstruct( oInit ); - return this; -}; - - - -ColVis.prototype = { - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Public methods - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * Get the ColVis instance's control button so it can be injected into the - * DOM - * @method button - * @returns {node} ColVis button - */ - button: function () - { - return this.dom.wrapper; - }, - - /** - * Alias of `rebuild` for backwards compatibility - * @method fnRebuild - */ - "fnRebuild": function () - { - this.rebuild(); - }, - - /** - * Rebuild the list of buttons for this instance (i.e. if there is a column - * header update) - * @method fnRebuild - */ - rebuild: function () - { - /* Remove the old buttons */ - for ( var i=this.dom.buttons.length-1 ; i>=0 ; i-- ) { - this.dom.collection.removeChild( this.dom.buttons[i] ); - } - this.dom.buttons.splice( 0, this.dom.buttons.length ); - - if ( this.dom.restore ) { - this.dom.restore.parentNode( this.dom.restore ); - } - - /* Re-add them (this is not the optimal way of doing this, it is fast and effective) */ - this._fnAddGroups(); - this._fnAddButtons(); - - /* Update the checkboxes */ - this._fnDrawCallback(); - }, - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Private methods (they are of course public in JS, but recommended as private) - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * Constructor logic - * @method _fnConstruct - * @returns void - * @private - */ - "_fnConstruct": function ( init ) - { - this._fnApplyCustomisation( init ); - - var that = this; - var i, iLen; - this.dom.wrapper = document.createElement('div'); - this.dom.wrapper.className = "ColVis"; - - this.dom.button = $( '
", + header: + "", + footer: + "", + closeButton: + "", + form: + "
", + inputs: { + text: + "", + textarea: + "", + email: + "", + select: + "", + checkbox: + "
", + date: + "", + time: + "", + number: + "", + password: + "" + } + }; + + var defaults = { + // default language + locale: "en", + // show backdrop or not. Default to static so user has to interact with dialog + backdrop: "static", + // animate the modal in/out + animate: true, + // additional class string applied to the top level dialog + className: null, + // whether or not to include a close button + closeButton: true, + // show the dialog immediately by default + show: true, + // dialog container + container: "body" + }; + + // our public object; augmented after our private API + var exports = {}; + + /** + * @private + */ + function _t(key) { + var locale = locales[defaults.locale]; + return locale ? locale[key] : locales.en[key]; + } + + function processCallback(e, dialog, callback) { + e.stopPropagation(); + e.preventDefault(); + + // by default we assume a callback will get rid of the dialog, + // although it is given the opportunity to override this + + // so, if the callback can be invoked and it *explicitly returns false* + // then we'll set a flag to keep the dialog active... + var preserveDialog = (typeof callback === "function") && callback.call(dialog, e) === false; + + // ... otherwise we'll bin it + if (!preserveDialog) { + dialog.modal("hide"); + } + } + + function getKeyLength(obj) { + // @TODO defer to Object.keys(x).length if available? + var k, t = 0; + for (k in obj) { + t++; + } + return t; + } + + function each(collection, iterator) { + var index = 0; + $.each(collection, function (key, value) { + iterator(key, value, index++); + }); + } + + function sanitize(options) { + var buttons; + var total; + + if (typeof options !== "object") { + throw new Error("Please supply an object of options"); + } + + if (!options.message) { + throw new Error("Please specify a message"); + } + + // make sure any supplied options take precedence over defaults + options = $.extend({}, defaults, options); + + if (!options.buttons) { + options.buttons = {}; + } + + buttons = options.buttons; + + total = getKeyLength(buttons); + + each(buttons, function (key, button, index) { + + if (typeof button === "function") { + // short form, assume value is our callback. Since button + // isn't an object it isn't a reference either so re-assign it + button = buttons[key] = { + callback: button + }; + } + + // before any further checks make sure by now button is the correct type + if (typeof button !== "object") { + throw new Error("button with key " + key + " must be an object"); + } + + if (!button.label) { + // the lack of an explicit label means we'll assume the key is good enough + button.label = key; + } + + if (!button.className) { + if (total <= 2 && index === total - 1) { + // always add a primary to the main option in a two-button dialog + button.className = "btn-primary"; + } else { + button.className = "btn-default"; + } + } + }); + + return options; + } + + /** + * map a flexible set of arguments into a single returned object + * if args.length is already one just return it, otherwise + * use the properties argument to map the unnamed args to + * object properties + * so in the latter case: + * mapArguments(["foo", $.noop], ["message", "callback"]) + * -> { message: "foo", callback: $.noop } + */ + function mapArguments(args, properties) { + var argn = args.length; + var options = {}; + + if (argn < 1 || argn > 2) { + throw new Error("Invalid argument length"); + } + + if (argn === 2 || typeof args[0] === "string") { + options[properties[0]] = args[0]; + options[properties[1]] = args[1]; + } else { + options = args[0]; + } + + return options; + } + + /** + * merge a set of default dialog options with user supplied arguments + */ + function mergeArguments(defaults, args, properties) { + return $.extend( + // deep merge + true, + // ensure the target is an empty, unreferenced object + {}, + // the base options object for this type of dialog (often just buttons) + defaults, + // args could be an object or array; if it's an array properties will + // map it to a proper options object + mapArguments( + args, + properties + ) + ); + } + + /** + * this entry-level method makes heavy use of composition to take a simple + * range of inputs and return valid options suitable for passing to bootbox.dialog + */ + function mergeDialogOptions(className, labels, properties, args) { + // build up a base set of dialog properties + var baseOptions = { + className: "bootbox-" + className, + buttons: createLabels.apply(null, labels) + }; + + // ensure the buttons properties generated, *after* merging + // with user args are still valid against the supplied labels + return validateButtons( + // merge the generated base properties with user supplied arguments + mergeArguments( + baseOptions, + args, + // if args.length > 1, properties specify how each arg maps to an object key + properties + ), + labels + ); + } + + /** + * from a given list of arguments return a suitable object of button labels + * all this does is normalise the given labels and translate them where possible + * e.g. "ok", "confirm" -> { ok: "OK, cancel: "Annuleren" } + */ + function createLabels() { + var buttons = {}; + + for (var i = 0, j = arguments.length; i < j; i++) { + var argument = arguments[i]; + var key = argument.toLowerCase(); + var value = argument.toUpperCase(); + + buttons[key] = { + label: _t(value) + }; + } + + return buttons; + } + + function validateButtons(options, buttons) { + var allowedButtons = {}; + each(buttons, function (key, value) { + allowedButtons[value] = true; + }); + + each(options.buttons, function (key) { + if (allowedButtons[key] === undefined) { + throw new Error("button key " + key + " is not allowed (options are " + buttons.join("\n") + ")"); + } + }); + + return options; + } + + exports.alert = function () { + var options; + + options = mergeDialogOptions("alert", ["ok"], ["message", "callback"], arguments); + + if (options.callback && !(typeof options.callback === "function")) { + throw new Error("alert requires callback property to be a function when provided"); + } + + /** + * overrides + */ + options.buttons.ok.callback = options.onEscape = function () { + if (typeof options.callback === "function") { + return options.callback.call(this); + } + return true; + }; + + return exports.dialog(options); + }; + + exports.confirm = function () { + var options; + + options = mergeDialogOptions("confirm", ["cancel", "confirm"], ["message", "callback"], arguments); + + /** + * overrides; undo anything the user tried to set they shouldn't have + */ + options.buttons.cancel.callback = options.onEscape = function () { + return options.callback.call(this, false); + }; + + options.buttons.confirm.callback = function () { + return options.callback.call(this, true); + }; + + // confirm specific validation + if (!(typeof options.callback === "function")) { + throw new Error("confirm requires a callback"); + } + + return exports.dialog(options); + }; + + exports.prompt = function () { + var options; + var defaults; + var dialog; + var form; + var input; + var shouldShow; + var inputOptions; + + // we have to create our form first otherwise + // its value is undefined when gearing up our options + // @TODO this could be solved by allowing message to + // be a function instead... + form = $(templates.form); + + // prompt defaults are more complex than others in that + // users can override more defaults + // @TODO I don't like that prompt has to do a lot of heavy + // lifting which mergeDialogOptions can *almost* support already + // just because of 'value' and 'inputType' - can we refactor? + defaults = { + className: "bootbox-prompt", + buttons: createLabels("cancel", "confirm"), + value: "", + inputType: "text" + }; + + options = validateButtons( + mergeArguments(defaults, arguments, ["title", "callback"]), + ["cancel", "confirm"] + ); + + // capture the user's show value; we always set this to false before + // spawning the dialog to give us a chance to attach some handlers to + // it, but we need to make sure we respect a preference not to show it + shouldShow = (options.show === undefined) ? true : options.show; + + /** + * overrides; undo anything the user tried to set they shouldn't have + */ + options.message = form; + + options.buttons.cancel.callback = options.onEscape = function () { + return options.callback.call(this, null); + }; + + options.buttons.confirm.callback = function () { + var value; + + switch (options.inputType) { + case "text": + case "textarea": + case "email": + case "select": + case "date": + case "time": + case "number": + case "password": + value = input.val(); + break; + + case "checkbox": + var checkedItems = input.find("input:checked"); + + // we assume that checkboxes are always multiple, + // hence we default to an empty array + value = []; + + each(checkedItems, function (_, item) { + value.push($(item).val()); + }); + break; + } + + return options.callback.call(this, value); + }; + + options.show = false; + + // prompt specific validation + if (!options.title) { + throw new Error("prompt requires a title"); + } + + if (!(typeof options.callback === "function")) { + throw new Error("prompt requires a callback"); + } + + if (!templates.inputs[options.inputType]) { + throw new Error("invalid prompt type"); + } + + // create the input based on the supplied type + input = $(templates.inputs[options.inputType]); + + switch (options.inputType) { + case "text": + case "textarea": + case "email": + case "date": + case "time": + case "number": + case "password": + input.val(options.value); + break; + + case "select": + var groups = {}; + inputOptions = options.inputOptions || []; + + if (!$.isArray(inputOptions)) { + throw new Error("Please pass an array of input options"); + } + + if (!inputOptions.length) { + throw new Error("prompt with select requires options"); + } + + each(inputOptions, function (_, option) { + + // assume the element to attach to is the input... + var elem = input; + + if (option.value === undefined || option.text === undefined) { + throw new Error("given options in wrong format"); + } + + // ... but override that element if this option sits in a group + + if (option.group) { + // initialise group if necessary + if (!groups[option.group]) { + groups[option.group] = $("").attr("label", option.group); + } + + elem = groups[option.group]; + } + + elem.append(""); + }); + + each(groups, function (_, group) { + input.append(group); + }); + + // safe to set a select's value as per a normal input + input.val(options.value); + break; + + case "checkbox": + var values = $.isArray(options.value) ? options.value : [options.value]; + inputOptions = options.inputOptions || []; + + if (!inputOptions.length) { + throw new Error("prompt with checkbox requires options"); + } + + if (!inputOptions[0].value || !inputOptions[0].text) { + throw new Error("given options in wrong format"); + } + + // checkboxes have to nest within a containing element, so + // they break the rules a bit and we end up re-assigning + // our 'input' element to this container instead + input = $("
"); + + each(inputOptions, function (_, option) { + var checkbox = $(templates.inputs[options.inputType]); + + checkbox.find("input").attr("value", option.value); + checkbox.find("label").append(option.text); + + // we've ensured values is an array so we can always iterate over it + each(values, function (_, value) { + if (value === option.value) { + checkbox.find("input").prop("checked", true); + } + }); + + input.append(checkbox); + }); + break; + } + + // @TODO provide an attributes option instead + // and simply map that as keys: vals + if (options.placeholder) { + input.attr("placeholder", options.placeholder); + } + + if (options.pattern) { + input.attr("pattern", options.pattern); + } + + if (options.maxlength) { + input.attr("maxlength", options.maxlength); + } + + // now place it in our form + form.append(input); + + form.on("submit", function (e) { + e.preventDefault(); + // Fix for SammyJS (or similar JS routing library) hijacking the form post. + e.stopPropagation(); + // @TODO can we actually click *the* button object instead? + // e.g. buttons.confirm.click() or similar + dialog.find(".btn-primary").click(); + }); + + dialog = exports.dialog(options); + + // clear the existing handler focusing the submit button... + dialog.off("shown.bs.modal"); + + // ...and replace it with one focusing our input, if possible + dialog.on("shown.bs.modal", function () { + // need the closure here since input isn't + // an object otherwise + input.on("focus"); + }); + + if (shouldShow === true) { + dialog.modal("show"); + } + + return dialog; + }; + + exports.dialog = function (options) { + options = sanitize(options); + + var dialog = $(templates.dialog); + var innerDialog = dialog.find(".modal-dialog"); + var body = dialog.find(".modal-body"); + var buttons = options.buttons; + var buttonStr = ""; + var callbacks = { + onEscape: options.onEscape + }; + + if ($.fn.modal === undefined) { + throw new Error( + "$.fn.modal is not defined; please double check you have included " + + "the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ " + + "for more details." + ); + } + + each(buttons, function (key, button) { + + // @TODO I don't like this string appending to itself; bit dirty. Needs reworking + // can we just build up button elements instead? slower but neater. Then button + // can just become a template too + buttonStr += ""; + callbacks[key] = button.callback; + }); + + body.find(".bootbox-body").html(options.message); + + if (options.animate === true) { + dialog.addClass("fade"); + } + + if (options.className) { + dialog.addClass(options.className); + } + + if (options.size === "large") { + innerDialog.addClass("modal-lg"); + } else if (options.size === "small") { + innerDialog.addClass("modal-sm"); + } + + if (options.title) { + body.before(templates.header); + } + + if (options.closeButton) { + var closeButton = $(templates.closeButton); + + if (options.title) { + dialog.find(".modal-header").prepend(closeButton); + } else { + closeButton.css("margin-top", "-10px").prependTo(body); + } + } + + if (options.title) { + dialog.find(".modal-title").html(options.title); + } + + if (buttonStr.length) { + body.after(templates.footer); + dialog.find(".modal-footer").html(buttonStr); + } + + + /** + * Bootstrap event listeners; used handle extra + * setup & teardown required after the underlying + * modal has performed certain actions + */ + + dialog.on("hidden.bs.modal", function (e) { + // ensure we don't accidentally intercept hidden events triggered + // by children of the current dialog. We shouldn't anymore now BS + // namespaces its events; but still worth doing + if (e.target === this) { + dialog.remove(); + } + }); + + /* + dialog.on("show.bs.modal", function() { + // sadly this doesn't work; show is called *just* before + // the backdrop is added so we'd need a setTimeout hack or + // otherwise... leaving in as would be nice + if (options.backdrop) { + dialog.next(".modal-backdrop").addClass("bootbox-backdrop"); + } + }); + */ + + dialog.on("shown.bs.modal", function () { + dialog.find(".btn-primary:first").on("focus"); + }); + + /** + * Bootbox event listeners; experimental and may not last + * just an attempt to decouple some behaviours from their + * respective triggers + */ + + if (options.backdrop !== "static") { + // A boolean true/false according to the Bootstrap docs + // should show a dialog the user can dismiss by clicking on + // the background. + // We always only ever pass static/false to the actual + // $.modal function because with `true` we can't trap + // this event (the .modal-backdrop swallows it) + // However, we still want to sort of respect true + // and invoke the escape mechanism instead + dialog.on("click.dismiss.bs.modal", function (e) { + // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop + // moved *inside* the outer dialog rather than *alongside* it + if (dialog.children(".modal-backdrop").length) { + e.currentTarget = dialog.children(".modal-backdrop").get(0); + } + + if (e.target !== e.currentTarget) { + return; + } + + dialog.trigger("escape.close.bb"); + }); + } + + dialog.on("escape.close.bb", function (e) { + if (callbacks.onEscape) { + processCallback(e, dialog, callbacks.onEscape); + } + }); + + /** + * Standard jQuery event listeners; used to handle user + * interaction with our dialog + */ + + dialog.on("click", ".modal-footer button", function (e) { + var callbackKey = $(this).data("bb-handler"); + + processCallback(e, dialog, callbacks[callbackKey]); + }); + + dialog.on("click", ".bootbox-close-button", function (e) { + // onEscape might be falsy but that's fine; the fact is + // if the user has managed to click the close button we + // have to close the dialog, callback or not + processCallback(e, dialog, callbacks.onEscape); + }); + + dialog.on("keyup", function (e) { + if (e.which === 27) { + dialog.trigger("escape.close.bb"); + } + }); + + // the remainder of this method simply deals with adding our + // dialogent to the DOM, augmenting it with Bootstrap's modal + // functionality and then giving the resulting object back + // to our caller + + $(options.container).append(dialog); + + dialog.modal({ + backdrop: options.backdrop ? "static" : false, + keyboard: false, + show: false + }); + + if (options.show) { + dialog.modal("show"); + } + + // @TODO should we return the raw element here or should + // we wrap it in an object on which we can expose some neater + // methods, e.g. var d = bootbox.alert(); d.hide(); instead + // of d.modal("hide"); + + /* + function BBDialog(elem) { + this.elem = elem; + } + + BBDialog.prototype = { + hide: function() { + return this.elem.modal("hide"); + }, + show: function() { + return this.elem.modal("show"); + } + }; + */ + + return dialog; + + }; + + exports.setDefaults = function () { + var values = {}; + + if (arguments.length === 2) { + // allow passing of single key/value... + values[arguments[0]] = arguments[1]; + } else { + // ... and as an object too + values = arguments[0]; + } + + $.extend(defaults, values); + }; + + exports.hideAll = function () { + $(".bootbox").modal("hide"); + + return exports; + }; + + + /** + * standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are + * unlikely to be required. If this gets too large it can be split out into separate JS files. + */ + var locales = { + bg_BG: { + OK: "Ок", + CANCEL: "Отказ", + CONFIRM: "Потвърждавам" + }, + br: { + OK: "OK", + CANCEL: "Cancelar", + CONFIRM: "Sim" + }, + cs: { + OK: "OK", + CANCEL: "Zrušit", + CONFIRM: "Potvrdit" + }, + da: { + OK: "OK", + CANCEL: "Annuller", + CONFIRM: "Accepter" + }, + de: { + OK: "OK", + CANCEL: "Abbrechen", + CONFIRM: "Akzeptieren" + }, + el: { + OK: "Εντάξει", + CANCEL: "Ακύρωση", + CONFIRM: "Επιβεβαίωση" + }, + en: { + OK: "OK", + CANCEL: "Cancel", + CONFIRM: "OK" + }, + es: { + OK: "OK", + CANCEL: "Cancelar", + CONFIRM: "Aceptar" + }, + et: { + OK: "OK", + CANCEL: "Katkesta", + CONFIRM: "OK" + }, + fa: { + OK: "قبول", + CANCEL: "لغو", + CONFIRM: "تایید" + }, + fi: { + OK: "OK", + CANCEL: "Peruuta", + CONFIRM: "OK" + }, + fr: { + OK: "OK", + CANCEL: "Annuler", + CONFIRM: "D'accord" + }, + he: { + OK: "אישור", + CANCEL: "ביטול", + CONFIRM: "אישור" + }, + hu: { + OK: "OK", + CANCEL: "Mégsem", + CONFIRM: "Megerősít" + }, + hr: { + OK: "OK", + CANCEL: "Odustani", + CONFIRM: "Potvrdi" + }, + id: { + OK: "OK", + CANCEL: "Batal", + CONFIRM: "OK" + }, + it: { + OK: "OK", + CANCEL: "Annulla", + CONFIRM: "Conferma" + }, + ja: { + OK: "OK", + CANCEL: "キャンセル", + CONFIRM: "確認" + }, + lt: { + OK: "Gerai", + CANCEL: "Atšaukti", + CONFIRM: "Patvirtinti" + }, + lv: { + OK: "Labi", + CANCEL: "Atcelt", + CONFIRM: "Apstiprināt" + }, + nl: { + OK: "OK", + CANCEL: "Annuleren", + CONFIRM: "Accepteren" + }, + no: { + OK: "OK", + CANCEL: "Avbryt", + CONFIRM: "OK" + }, + pl: { + OK: "OK", + CANCEL: "Anuluj", + CONFIRM: "Potwierdź" + }, + pt: { + OK: "OK", + CANCEL: "Cancelar", + CONFIRM: "Confirmar" + }, + ru: { + OK: "OK", + CANCEL: "Отмена", + CONFIRM: "Применить" + }, + sq: { + OK: "OK", + CANCEL: "Anulo", + CONFIRM: "Prano" + }, + sv: { + OK: "OK", + CANCEL: "Avbryt", + CONFIRM: "OK" + }, + th: { + OK: "ตกลง", + CANCEL: "ยกเลิก", + CONFIRM: "ยืนยัน" + }, + tr: { + OK: "Tamam", + CANCEL: "İptal", + CONFIRM: "Onayla" + }, + zh_CN: { + OK: "OK", + CANCEL: "取消", + CONFIRM: "确认" + }, + zh_TW: { + OK: "OK", + CANCEL: "取消", + CONFIRM: "確認" + } + }; + + exports.addLocale = function (name, values) { + $.each(["OK", "CANCEL", "CONFIRM"], function (_, v) { + if (!values[v]) { + throw new Error("Please supply a translation for '" + v + "'"); + } + }); + + locales[name] = { + OK: values.OK, + CANCEL: values.CANCEL, + CONFIRM: values.CONFIRM + }; + + return exports; + }; + + exports.removeLocale = function (name) { + delete locales[name]; + + return exports; + }; + + exports.setLocale = function (name) { + return exports.setDefaults("locale", name); + }; + + exports.init = function (_$) { + return init(_$ || $); + }; + + return exports; +})); \ No newline at end of file diff --git a/lib/web/public/resources/common.ui.js b/lib/web/public/resources/js/common.ui.js similarity index 100% rename from lib/web/public/resources/common.ui.js rename to lib/web/public/resources/js/common.ui.js diff --git a/lib/web/public/resources/jquery.moveto.js b/lib/web/public/resources/js/jquery.moveto.js similarity index 100% rename from lib/web/public/resources/jquery.moveto.js rename to lib/web/public/resources/js/jquery.moveto.js diff --git a/lib/web/public/resources/jquery.outerclick.js b/lib/web/public/resources/js/jquery.outerclick.js similarity index 100% rename from lib/web/public/resources/jquery.outerclick.js rename to lib/web/public/resources/js/jquery.outerclick.js diff --git a/lib/web/public/resources/jquery.slidenavbar.js b/lib/web/public/resources/js/jquery.slidenavbar.js similarity index 100% rename from lib/web/public/resources/jquery.slidenavbar.js rename to lib/web/public/resources/js/jquery.slidenavbar.js diff --git a/lib/web/public/resources/narou.library.js b/lib/web/public/resources/js/narou.library.js similarity index 99% rename from lib/web/public/resources/narou.library.js rename to lib/web/public/resources/js/narou.library.js index e94aaf29..e7a70e3e 100644 --- a/lib/web/public/resources/narou.library.js +++ b/lib/web/public/resources/js/narou.library.js @@ -15,7 +15,7 @@ var Narou = (function () { }); // jQuery はデフォルトだと dataTransfer オブジェクトをコピーしないので - $.event.props.push("dataTransfer"); + $.event.fixHooks['drop'] = { props: [ 'dataTransfer' ] }; /************************************************************************* * ローカルストレージ @@ -917,7 +917,7 @@ var Narou = (function () { } }); download_modal.one("shown.bs.modal", function () { - $("#download-input").focus(); + $("#download-input").on("focus"); }); }); } diff --git a/lib/web/public/resources/narou.queue.js b/lib/web/public/resources/js/narou.queue.js similarity index 100% rename from lib/web/public/resources/narou.queue.js rename to lib/web/public/resources/js/narou.queue.js diff --git a/lib/web/public/resources/narou.ui.js b/lib/web/public/resources/js/narou.ui.js similarity index 94% rename from lib/web/public/resources/narou.ui.js rename to lib/web/public/resources/js/narou.ui.js index b4a046cc..459ef75a 100644 --- a/lib/web/public/resources/narou.ui.js +++ b/lib/web/public/resources/js/narou.ui.js @@ -225,6 +225,88 @@ $(function() { window.action = action; var isInitialLoad = true; // 初回ロードフラグ + /* + * ショートカット設定 + * http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php + */ + var initialize_shortcut = function() { + var options = { + disable_in_input: true, + }; + var add = function() { + var keys = []; + var callback = null; + for (var i = 0; i < arguments.length; i++) { + var value = arguments[i]; + switch (typeof value) { + case "string": + keys.push(value); + break; + case "function": + callback = value; + break; + default: + $.error("invalid arguments: unknow type"); + break; + } + } + if (!callback) $.error("shortcut error: need callback"); + for (var i = 0; i < keys.length; i++) { + shortcut.add(keys[i], callback, options); + } + }; + + var click = function(id) { + return function() { + $(id).trigger("click"); + }; + }; + + add("Ctrl+A", "Meta+A", function(e) { + console.log("[SHORTCUT] Ctrl+A pressed, action object:", window.action); + e.preventDefault(); // ブラウザのデフォルト動作(テキスト全選択)を防ぐ + if (window.action && window.action.selectView) { + console.log("[SHORTCUT] Calling action.selectView()"); + window.action.selectView(); + } else { + console.error("[SHORTCUT] action.selectView is not available!"); + } + }); + add("Shift+A", function(e) { + console.log("[SHORTCUT] Shift+A pressed, action object:", window.action); + e.preventDefault(); // デフォルト動作を防ぐ + if (window.action && window.action.selectAll) { + console.log("[SHORTCUT] Calling action.selectAll()"); + window.action.selectAll(); + } else { + console.error("[SHORTCUT] action.selectAll is not available!"); + } + }); + add("Ctrl+Shift+A", "Meta+Shift+A", function() { window.action.selectClear(); }); + add("ESC", function() { + if ($("#rect-select-menu").is(":visible")) { + close_rect_select_menu_handler(); + } + if (!window.context_menu.closed) { + window.context_menu.close(); + } + else { + window.action.selectClear(); + } + }); + add("S", click("#action-select-mode-single")); + add("R", click("#action-select-mode-rect")); + add("H", click("#action-select-mode-hybrid")); + add("W", click("#action-view-novel-list-wide")); + add("F", click("#action-view-frozen")); + add("Shift+F", click("#action-view-nonfrozen")); + add("T", click("#action-tag-edit")); + add("F5", function() { + console.log("F5キーによる手動強制テーブルリロード"); + Narou.tableReload(true); + }); + }; + function initializeDataTable(serverSortState) { var initialOrder = serverSortState ? [[serverSortState.column, serverSortState.dir]] : [[ 2, "desc" ]]; @@ -276,6 +358,7 @@ $(function() { dom: (touchable_device ? 'lprtpi' : 'Rlprtpi'), stateSave: true, stateDuration: 0, // tableの状態保存を永続化 + stripeClasses: ['odd', 'even'], // DataTables 2.x互換: odd/evenクラスを明示的に指定 stateLoadCallback: function(settings, callback) { try { var storageKey = 'DataTables_' + settings.sInstance + '_' + location.pathname; @@ -678,8 +761,7 @@ $(function() { }, 100); // テーブル初期化完了後にactionを作成 - action = new Narou.Action(table); - window.action = action; + window.action = new Narou.Action($('#novel-list').DataTable()); // ソート変更時に選択状態を復元 table.on('order.dt', function() { @@ -703,6 +785,11 @@ $(function() { datatables_init_completed = true; + // キーボードショートカットを初期化(actionが利用可能になった後) + if (!touchable_device) { + initialize_shortcut(); + } + // 他のコンポーネントを初期化 initializeComponents(); }, @@ -866,125 +953,18 @@ $(function() { }, 350); }); - // Shiftクリックによる意図しないテキスト選択を徹底的に防ぐ + // Shiftクリックによる意図しないテキスト選択を防ぐ(改訂版) - // ユーティリティ関数:許可する要素かチェック - function isAllowedElement(target) { - // 入力フィールドは許可 - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true') { - return true; - } - - // タグ要素は許可 - var tagElement = $(target).closest('.tag[data-tag]:not(.tag-reset)')[0]; - if (tagElement) { - return true; - } - - return false; - } - - // 選択を強制的にクリアする関数 - function clearSelection() { - try { - if (window.getSelection) { - var selection = window.getSelection(); - if (selection.rangeCount > 0) { - selection.removeAllRanges(); - } - } else if (document.selection) { - document.selection.empty(); - } - } catch (e) { - // エラーは無視 - } - } - - // 1. 選択開始イベントを防ぐ - document.addEventListener('selectstart', function(e) { - if (e.shiftKey && !isAllowedElement(e.target)) { - console.log('Preventing selectstart on shift+click for:', e.target); - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - clearSelection(); - return false; - } - }, true); - - // 2. ドラッグ開始を防ぐ - document.addEventListener('dragstart', function(e) { - if (e.shiftKey && !isAllowedElement(e.target)) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - return false; - } - }, true); - - // 3. マウスダウン時のShiftクリックでテキスト選択を防ぐ - document.addEventListener('mousedown', function(e) { - if (e.shiftKey && !isAllowedElement(e.target)) { - console.log('Preventing mousedown on shift+click for:', e.target); - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - clearSelection(); - return false; - } - }, true); - - // 4. マウスアップ時も防ぐ - document.addEventListener('mouseup', function(e) { - if (e.shiftKey && !isAllowedElement(e.target)) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - clearSelection(); - return false; - } - }, true); - - // 5. クリック時も防ぐ - document.addEventListener('click', function(e) { - if (e.shiftKey && !isAllowedElement(e.target)) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - clearSelection(); - return false; - } - }, true); - - // 6. マウス移動時に選択が発生している場合は即座にクリア - document.addEventListener('mousemove', function(e) { - if (e.shiftKey) { - // 選択が発生していたら即座にクリア - setTimeout(clearSelection, 0); - } - }, true); - - // 7. 定期的に選択状態をチェック・クリア - setInterval(function() { - try { - if (window.getSelection) { - var selection = window.getSelection(); - if (selection.rangeCount > 0 && selection.toString().length > 0) { - // 許可されていない選択の場合のみクリア - var range = selection.getRangeAt(0); - var container = range.commonAncestorContainer; - var element = container.nodeType === Node.TEXT_NODE ? container.parentNode : container; - - if (!isAllowedElement(element)) { - console.log('Clearing unexpected selection:', selection.toString()); - clearSelection(); - } - } - } - } catch (e) { - // エラーは無視 - } - }, 100); + // CSSでテキスト選択を制御する方が確実 + // テーブル行とボタン類のみ user-select: none を適用 + var style = document.createElement('style'); + style.textContent = ` + #novel-list tbody tr { user-select: none; -webkit-user-select: none; -moz-user-select: none; } + .btn, button { user-select: none; -webkit-user-select: none; -moz-user-select: none; } + /* 入力フィールドとタグは選択可能に */ + input, textarea, .tag[data-tag]:not(.tag-reset) { user-select: text !important; -webkit-user-select: text !important; -moz-user-select: text !important; } + `; + document.head.appendChild(style); // タグクリック処理(キャプチャフェーズで最優先処理) // 修飾キーによる検索モード: @@ -1448,7 +1428,7 @@ $(function() { table.fireChangeSelect(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); // 解除 $("#rect-select-menu-clear").on("click", function(e) { @@ -1470,7 +1450,7 @@ $(function() { table.fireChangeSelect(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); // 反転 $("#rect-select-menu-reverse").on("click", function(e) { @@ -1497,7 +1477,7 @@ $(function() { table.fireChangeSelect(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); // キャンセル $("#rect-select-menu-cancel").on("click", function(e) { @@ -1505,7 +1485,7 @@ $(function() { e.stopPropagation(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); }; rect_select_menu_initialize(); @@ -1566,7 +1546,7 @@ $(function() { table.fireChangeSelect(); $("#rect-select-menu").hide(); selected_rect_element.hide(); - $(".n-popover").blur(); + $(".n-popover").on("blur"); }; if (not_clicked_yet) { // 範囲選択開始 @@ -1651,34 +1631,82 @@ $(function() { }); /* - * メニュー - * 表示>表示する項目を設定 - */ - var colvis = new $.fn.dataTable.ColVis(table, { - restore: "元に戻す", - showAll: "全ての項目を表示", - showNone: "全て隠す", - bCssPosition: true, - overlayFade: 300, - exclude: [ "title", "frozen", "new_arrivals_date" ], + * メニュー + * 表示>表示する項目を設定 + */ + // DataTableのAPIインスタンスを生成 + var dt = (table instanceof $.fn.dataTable.Api) ? table : $(table).DataTable(); + + // Buttonsのcolvisグループを定義 + new $.fn.dataTable.Buttons(dt, { + name: 'colvisGroup', + buttons: [ + { + extend: 'colvis', + text: '表示する項目を設定', + columns: ':not(.title):not(.frozen):not(.new_arrivals_date)', + columnText: function (dtApi, columnIdx, columnTitle) { + return columnTitle || ('列 ' + (columnIdx + 1)); + }, + prefixButtons: [ + { + text: '全ての項目を表示', + action: function (e, api) { + api.columns().visible(true); + } + }, + { + text: '全て隠す', + action: function (e, api) { + api.columns().visible(false); + } + }, + { + extend: 'colvisRestore', + text: '元に戻す' + } + ] + } + ] + }); + + // ButtonsコンテナをDOMに追加 + var $btnContainer = dt.buttons('colvisGroup', null).container(); + $('body').append($btnContainer); + $btnContainer.css({ + visibility: 'hidden', + pointerEvents: 'none', + width: 0, + height: 0, + overflow: 'hidden' }); - $("#action-view-setting").on(click_event_name, function(e) { + + // 表示メニューからcolvisメニューを開く + $('#action-view-setting').on(click_event_name, function(e) { e.preventDefault(); + slideNavbar.slide(); + dt.buttons('.buttons-colvis').trigger(); + var pos = {}; if (touchable_device) { var target = $(this); pos.x = target.offset().left; pos.y = target.offset().top + target.outerHeight(); + } else { + pos = Narou.get_event_position(e); // { x:..., y:... } } - else { - pos = Narou.get_event_position(e); - } - $(colvis.dom.collection).css({ - position: "absolute", - left: pos.x, top: pos.y + + // メニュー(.dt-button-collection)の配置と表示 + var $collection = $btnContainer.find('.dt-button-collection').last(); + $collection.css({ + position: 'absolute', + left: pos.x, + top: pos.y, + display: 'block', + visibility: 'visible', + pointerEvents: 'auto', + zIndex: 2000 }); - slideNavbar.slide(); - colvis._fnCollectionShow(); }); /* @@ -2022,7 +2050,7 @@ Mac スタイル:
メニューが画面外にはみ出そうとしたら、 * ボタンを一時的に非アクティブ化 */ function disableButtonMoment($button) { - $button.blur(); + $button.on("blur"); $button.tooltip("hide"); $button.prop("disabled", true); // 少したったらアクティブに戻す @@ -2278,71 +2306,8 @@ Mac スタイル:
メニューが画面外にはみ出そうとしたら、 }); /* - * ショートカット設定 - * http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php + * ショートカット設定は initializeDataTable 関数の前に移動済み */ - var initialize_shortcut = function() { - var options = { - disable_in_input: true, - }; - var add = function() { - var keys = []; - var callback = null; - for (var i = 0; i < arguments.length; i++) { - var value = arguments[i]; - switch (typeof value) { - case "string": - keys.push(value); - break; - case "function": - callback = value; - break; - default: - $.error("invalid arguments: unknow type"); - break; - } - } - if (!callback) $.error("shortcut error: need callback"); - for (var i = 0; i < keys.length; i++) { - shortcut.add(keys[i], callback, options); - } - }; - - var click = function(id) { - return function() { - $(id).trigger("click"); - }; - }; - - add("Ctrl+A", "Meta+A", function() { action.selectView(); }); - add("Shift+A", function() { action.selectAll(); }); - add("Ctrl+Shift+A", "Meta+Shift+A", function() { action.selectClear(); }); - add("ESC", function() { - if ($("#rect-select-menu").is(":visible")) { - close_rect_select_menu_handler(); - } - if (!window.context_menu.closed) { - window.context_menu.close(); - } - else { - window.action.selectClear(); - } - }); - add("S", click("#action-select-mode-single")); - add("R", click("#action-select-mode-rect")); - add("H", click("#action-select-mode-hybrid")); - add("W", click("#action-view-novel-list-wide")); - add("F", click("#action-view-frozen")); - add("Shift+F", click("#action-view-nonfrozen")); - add("T", click("#action-tag-edit")); - add("F5", function() { - console.log("F5キーによる手動強制テーブルリロード"); - Narou.tableReload(true); - }); - }; - if (!touchable_device) { - initialize_shortcut(); - } /* * disabled なメニューは何もしないように diff --git a/lib/web/public/resources/shortcut.js b/lib/web/public/resources/js/shortcut.js similarity index 100% rename from lib/web/public/resources/shortcut.js rename to lib/web/public/resources/js/shortcut.js diff --git a/lib/web/public/resources/sprintf.js b/lib/web/public/resources/js/sprintf.js similarity index 100% rename from lib/web/public/resources/sprintf.js rename to lib/web/public/resources/js/sprintf.js diff --git a/lib/web/public/resources/perfect-scrollbar.min.css b/lib/web/public/resources/perfect-scrollbar.min.css deleted file mode 100644 index 92e24820..00000000 --- a/lib/web/public/resources/perfect-scrollbar.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! perfect-scrollbar - v0.5.8 -* http://noraesae.github.com/perfect-scrollbar/ -* Copyright (c) 2014 Hyunje Alex Jun; Licensed MIT */ - -.ps-container.ps-active-x>.ps-scrollbar-x-rail,.ps-container.ps-active-y>.ps-scrollbar-y-rail{display:block}.ps-container>.ps-scrollbar-x-rail{display:none;position:absolute;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;border-radius:4px;opacity:0;-ms-filter:"alpha(Opacity=0)";filter:alpha(opacity=0);-webkit-transition:background-color .2s linear,opacity .2s linear;-moz-transition:background-color .2s linear,opacity .2s linear;-o-transition:background-color .2s linear,opacity .2s linear;transition:background-color .2s linear,opacity .2s linear;bottom:3px;height:8px}.ps-container>.ps-scrollbar-x-rail>.ps-scrollbar-x{position:absolute;background-color:#aaa;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;border-radius:4px;-webkit-transition:background-color .2s linear;-moz-transition:background-color .2s linear;-o-transition:background-color .2s linear;transition:background-color .2s linear;bottom:0;height:8px}.ps-container>.ps-scrollbar-x-rail.in-scrolling{background-color:#eee;opacity:.9;-ms-filter:"alpha(Opacity=90)";filter:alpha(opacity=90)}.ps-container>.ps-scrollbar-y-rail{display:none;position:absolute;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;border-radius:4px;opacity:0;-ms-filter:"alpha(Opacity=0)";filter:alpha(opacity=0);-webkit-transition:background-color .2s linear,opacity .2s linear;-moz-transition:background-color .2s linear,opacity .2s linear;-o-transition:background-color .2s linear,opacity .2s linear;transition:background-color .2s linear,opacity .2s linear;right:3px;width:8px}.ps-container>.ps-scrollbar-y-rail>.ps-scrollbar-y{position:absolute;background-color:#aaa;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;border-radius:4px;-webkit-transition:background-color .2s linear;-moz-transition:background-color .2s linear;-o-transition:background-color .2s linear;transition:background-color .2s linear;right:0;width:8px}.ps-container>.ps-scrollbar-y-rail.in-scrolling{background-color:#eee;opacity:.9;-ms-filter:"alpha(Opacity=90)";filter:alpha(opacity=90)}.ps-container:hover>.ps-scrollbar-x-rail,.ps-container:hover>.ps-scrollbar-y-rail{opacity:.6;-ms-filter:"alpha(Opacity=60)";filter:alpha(opacity=60)}.ps-container:hover>.ps-scrollbar-x-rail.in-scrolling,.ps-container:hover>.ps-scrollbar-y-rail.in-scrolling{background-color:#eee;opacity:.9;-ms-filter:"alpha(Opacity=90)";filter:alpha(opacity=90)}.ps-container:hover>.ps-scrollbar-x-rail:hover{background-color:#eee;opacity:.9;-ms-filter:"alpha(Opacity=90)";filter:alpha(opacity=90)}.ps-container:hover>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x{background-color:#999}.ps-container:hover>.ps-scrollbar-y-rail:hover{background-color:#eee;opacity:.9;-ms-filter:"alpha(Opacity=90)";filter:alpha(opacity=90)}.ps-container:hover>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y{background-color:#999} \ No newline at end of file diff --git a/lib/web/public/resources/perfect-scrollbar.min.js b/lib/web/public/resources/perfect-scrollbar.min.js deleted file mode 100644 index ea0340ea..00000000 --- a/lib/web/public/resources/perfect-scrollbar.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! perfect-scrollbar - v0.5.8 -* http://noraesae.github.com/perfect-scrollbar/ -* Copyright (c) 2014 Hyunje Alex Jun; Licensed MIT */ -(function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?e(require("jquery")):e(jQuery)})(function(e){"use strict";function t(e){return"string"==typeof e?parseInt(e,10):~~e}var o={wheelSpeed:1,wheelPropagation:!1,swipePropagation:!0,minScrollbarLength:null,maxScrollbarLength:null,useBothWheelAxes:!1,useKeyboard:!0,suppressScrollX:!1,suppressScrollY:!1,scrollXMarginOffset:0,scrollYMarginOffset:0,includePadding:!1},n=0,r=function(){var e=n++;return function(t){var o=".perfect-scrollbar-"+e;return t===void 0?o:t+o}},l="WebkitAppearance"in document.documentElement.style;e.fn.perfectScrollbar=function(n,i){return this.each(function(){function a(e,o){var n=e+o,r=D-R;j=0>n?0:n>r?r:n;var l=t(j*(Y-D)/(D-R));M.scrollTop(l)}function s(e,o){var n=e+o,r=E-k;W=0>n?0:n>r?r:n;var l=t(W*(C-E)/(E-k));M.scrollLeft(l)}function c(e){return P.minScrollbarLength&&(e=Math.max(e,P.minScrollbarLength)),P.maxScrollbarLength&&(e=Math.min(e,P.maxScrollbarLength)),e}function u(){var e={width:I};e.left=B?M.scrollLeft()+E-C:M.scrollLeft(),N?e.bottom=_-M.scrollTop():e.top=Q+M.scrollTop(),H.css(e);var t={top:M.scrollTop(),height:A};Z?t.right=B?C-M.scrollLeft()-V-J.outerWidth():V-M.scrollLeft():t.left=B?M.scrollLeft()+2*E-C-$-J.outerWidth():$+M.scrollLeft(),G.css(t),U.css({left:W,width:k-z}),J.css({top:j,height:R-et})}function d(){M.removeClass("ps-active-x"),M.removeClass("ps-active-y"),E=P.includePadding?M.innerWidth():M.width(),D=P.includePadding?M.innerHeight():M.height(),C=M.prop("scrollWidth"),Y=M.prop("scrollHeight"),!P.suppressScrollX&&C>E+P.scrollXMarginOffset?(X=!0,I=E-F,k=c(t(I*E/C)),W=t(M.scrollLeft()*(I-k)/(C-E))):(X=!1,k=0,W=0,M.scrollLeft(0)),!P.suppressScrollY&&Y>D+P.scrollYMarginOffset?(O=!0,A=D-tt,R=c(t(A*D/Y)),j=t(M.scrollTop()*(A-R)/(Y-D))):(O=!1,R=0,j=0,M.scrollTop(0)),W>=I-k&&(W=I-k),j>=A-R&&(j=A-R),u(),X&&M.addClass("ps-active-x"),O&&M.addClass("ps-active-y")}function p(){var t,o,n=function(e){s(t,e.pageX-o),d(),e.stopPropagation(),e.preventDefault()},r=function(){H.removeClass("in-scrolling"),e(q).unbind(K("mousemove"),n)};U.bind(K("mousedown"),function(l){o=l.pageX,t=U.position().left,H.addClass("in-scrolling"),e(q).bind(K("mousemove"),n),e(q).one(K("mouseup"),r),l.stopPropagation(),l.preventDefault()}),t=o=null}function f(){var t,o,n=function(e){a(t,e.pageY-o),d(),e.stopPropagation(),e.preventDefault()},r=function(){G.removeClass("in-scrolling"),e(q).unbind(K("mousemove"),n)};J.bind(K("mousedown"),function(l){o=l.pageY,t=J.position().top,G.addClass("in-scrolling"),e(q).bind(K("mousemove"),n),e(q).one(K("mouseup"),r),l.stopPropagation(),l.preventDefault()}),t=o=null}function v(e,t){var o=M.scrollTop();if(0===e){if(!O)return!1;if(0===o&&t>0||o>=Y-D&&0>t)return!P.wheelPropagation}var n=M.scrollLeft();if(0===t){if(!X)return!1;if(0===n&&0>e||n>=C-E&&e>0)return!P.wheelPropagation}return!0}function g(e,t){var o=M.scrollTop(),n=M.scrollLeft(),r=Math.abs(e),l=Math.abs(t);if(l>r){if(0>t&&o===Y-D||t>0&&0===o)return!P.swipePropagation}else if(r>l&&(0>e&&n===C-E||e>0&&0===n))return!P.swipePropagation;return!0}function b(){function e(e){var t=e.originalEvent.deltaX,o=-1*e.originalEvent.deltaY;return(t===void 0||o===void 0)&&(t=-1*e.originalEvent.wheelDeltaX/6,o=e.originalEvent.wheelDeltaY/6),e.originalEvent.deltaMode&&1===e.originalEvent.deltaMode&&(t*=10,o*=10),t!==t&&o!==o&&(t=0,o=e.originalEvent.wheelDelta),[t,o]}function t(t){if(l||!(M.find("select:focus").length>0)){var n=e(t),r=n[0],i=n[1];o=!1,P.useBothWheelAxes?O&&!X?(i?M.scrollTop(M.scrollTop()-i*P.wheelSpeed):M.scrollTop(M.scrollTop()+r*P.wheelSpeed),o=!0):X&&!O&&(r?M.scrollLeft(M.scrollLeft()+r*P.wheelSpeed):M.scrollLeft(M.scrollLeft()-i*P.wheelSpeed),o=!0):(M.scrollTop(M.scrollTop()-i*P.wheelSpeed),M.scrollLeft(M.scrollLeft()+r*P.wheelSpeed)),d(),o=o||v(r,i),o&&(t.stopPropagation(),t.preventDefault())}}var o=!1;window.onwheel!==void 0?M.bind(K("wheel"),t):window.onmousewheel!==void 0&&M.bind(K("mousewheel"),t)}function h(){var t=!1;M.bind(K("mouseenter"),function(){t=!0}),M.bind(K("mouseleave"),function(){t=!1});var o=!1;e(q).bind(K("keydown"),function(n){if((!n.isDefaultPrevented||!n.isDefaultPrevented())&&t){for(var r=document.activeElement?document.activeElement:q.activeElement;r.shadowRoot;)r=r.shadowRoot.activeElement;if(!e(r).is(":input,[contenteditable]")){var l=0,i=0;switch(n.which){case 37:l=-30;break;case 38:i=30;break;case 39:l=30;break;case 40:i=-30;break;case 33:i=90;break;case 32:case 34:i=-90;break;case 35:i=n.ctrlKey?-Y:-D;break;case 36:i=n.ctrlKey?M.scrollTop():D;break;default:return}M.scrollTop(M.scrollTop()-i),M.scrollLeft(M.scrollLeft()+l),o=v(l,i),o&&n.preventDefault()}}})}function w(){function e(e){e.stopPropagation()}J.bind(K("click"),e),G.bind(K("click"),function(e){var o=t(R/2),n=e.pageY-G.offset().top-o,r=D-R,l=n/r;0>l?l=0:l>1&&(l=1),M.scrollTop((Y-D)*l)}),U.bind(K("click"),e),H.bind(K("click"),function(e){var o=t(k/2),n=e.pageX-H.offset().left-o,r=E-k,l=n/r;0>l?l=0:l>1&&(l=1),M.scrollLeft((C-E)*l)})}function m(){function t(){var e=window.getSelection?window.getSelection():document.getSlection?document.getSlection():{rangeCount:0};return 0===e.rangeCount?null:e.getRangeAt(0).commonAncestorContainer}function o(){r||(r=setInterval(function(){return x()?(M.scrollTop(M.scrollTop()+l.top),M.scrollLeft(M.scrollLeft()+l.left),d(),void 0):(clearInterval(r),void 0)},50))}function n(){r&&(clearInterval(r),r=null),H.removeClass("in-scrolling"),G.removeClass("in-scrolling")}var r=null,l={top:0,left:0},i=!1;e(q).bind(K("selectionchange"),function(){e.contains(M[0],t())?i=!0:(i=!1,n())}),e(window).bind(K("mouseup"),function(){i&&(i=!1,n())}),e(window).bind(K("mousemove"),function(e){if(i){var t={x:e.pageX,y:e.pageY},r=M.offset(),a={left:r.left,right:r.left+M.outerWidth(),top:r.top,bottom:r.top+M.outerHeight()};t.xa.right-3?(l.left=5,H.addClass("in-scrolling")):l.left=0,t.ya.top+3-t.y?-5:-20,G.addClass("in-scrolling")):t.y>a.bottom-3?(l.top=5>t.y-a.bottom+3?5:20,G.addClass("in-scrolling")):l.top=0,0===l.top&&0===l.left?n():o()}})}function T(t,o){function n(e,t){M.scrollTop(M.scrollTop()-t),M.scrollLeft(M.scrollLeft()-e),d()}function r(){h=!0}function l(){h=!1}function i(e){return e.originalEvent.targetTouches?e.originalEvent.targetTouches[0]:e.originalEvent}function a(e){var t=e.originalEvent;return t.targetTouches&&1===t.targetTouches.length?!0:t.pointerType&&"mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE?!0:!1}function s(e){if(a(e)){w=!0;var t=i(e);p.pageX=t.pageX,p.pageY=t.pageY,f=(new Date).getTime(),null!==b&&clearInterval(b),e.stopPropagation()}}function c(e){if(!h&&w&&a(e)){var t=i(e),o={pageX:t.pageX,pageY:t.pageY},r=o.pageX-p.pageX,l=o.pageY-p.pageY;n(r,l),p=o;var s=(new Date).getTime(),c=s-f;c>0&&(v.x=r/c,v.y=l/c,f=s),g(r,l)&&(e.stopPropagation(),e.preventDefault())}}function u(){!h&&w&&(w=!1,clearInterval(b),b=setInterval(function(){return x()?.01>Math.abs(v.x)&&.01>Math.abs(v.y)?(clearInterval(b),void 0):(n(30*v.x,30*v.y),v.x*=.8,v.y*=.8,void 0):(clearInterval(b),void 0)},10))}var p={},f=0,v={},b=null,h=!1,w=!1;t&&(e(window).bind(K("touchstart"),r),e(window).bind(K("touchend"),l),M.bind(K("touchstart"),s),M.bind(K("touchmove"),c),M.bind(K("touchend"),u)),o&&(window.PointerEvent?(e(window).bind(K("pointerdown"),r),e(window).bind(K("pointerup"),l),M.bind(K("pointerdown"),s),M.bind(K("pointermove"),c),M.bind(K("pointerup"),u)):window.MSPointerEvent&&(e(window).bind(K("MSPointerDown"),r),e(window).bind(K("MSPointerUp"),l),M.bind(K("MSPointerDown"),s),M.bind(K("MSPointerMove"),c),M.bind(K("MSPointerUp"),u)))}function y(){M.bind(K("scroll"),function(){d()})}function L(){M.unbind(K()),e(window).unbind(K()),e(q).unbind(K()),M.data("perfect-scrollbar",null),M.data("perfect-scrollbar-update",null),M.data("perfect-scrollbar-destroy",null),U.remove(),J.remove(),H.remove(),G.remove(),M=H=G=U=J=X=O=E=D=C=Y=k=W=_=N=Q=R=j=V=Z=$=B=K=null}function S(){d(),y(),p(),f(),w(),m(),b(),(ot||nt)&&T(ot,nt),P.useKeyboard&&h(),M.data("perfect-scrollbar",M),M.data("perfect-scrollbar-update",d),M.data("perfect-scrollbar-destroy",L)}var P=e.extend(!0,{},o),M=e(this),x=function(){return!!M};if("object"==typeof n?e.extend(!0,P,n):i=n,"update"===i)return M.data("perfect-scrollbar-update")&&M.data("perfect-scrollbar-update")(),M;if("destroy"===i)return M.data("perfect-scrollbar-destroy")&&M.data("perfect-scrollbar-destroy")(),M;if(M.data("perfect-scrollbar"))return M.data("perfect-scrollbar");M.addClass("ps-container");var E,D,C,Y,X,k,W,I,O,R,j,A,B="rtl"===M.css("direction"),K=r(),q=this.ownerDocument||document,H=e("
").appendTo(M),U=e("
").appendTo(H),_=t(H.css("bottom")),N=_===_,Q=N?null:t(H.css("top")),z=t(H.css("borderLeftWidth"))+t(H.css("borderRightWidth")),F=t(H.css("marginLeft"))+t(H.css("marginRight")),G=e("
").appendTo(M),J=e("
").appendTo(G),V=t(G.css("right")),Z=V===V,$=Z?null:t(G.css("left")),et=t(G.css("borderTopWidth"))+t(G.css("borderBottomWidth")),tt=t(G.css("marginTop"))+t(G.css("marginBottom")),ot="ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,nt=null!==window.navigator.msMaxTouchPoints;return S(),M})}}); \ No newline at end of file diff --git a/lib/web/settingmessages.rb b/lib/web/settingmessages.rb index beb66515..c4282248 100644 --- a/lib/web/settingmessages.rb +++ b/lib/web/settingmessages.rb @@ -21,9 +21,7 @@ module Narou "no-color" => "コンソールのカラー表示を無効にする\n※要サーバ再起動", "economy" => "容量節約に関する設定", "send.without-freeze" => "一括送信時に凍結された小説は対象外にする。(個別送信時は凍結済みでも送信可能)", - "server-digest-auth.enable" => "%%ORIG%%\n※digest-auth関連の設定を変更した場合サーバの再起動が必要", - "server-digest-auth.hashed-password" => "サーバのDigest認証のパスワードを、Realmを\"narou.rb\"としてハッシュにしたもの。\n" \ - "https://tgws.plus/app/digest/ などで生成できる", + "server-basic-auth.enable" => "%%ORIG%%\n※basic-auth関連の設定を変更した場合サーバの再起動が必要", "concurrency" => "%%ORIG%% ※要サーバ再起動", "logging" => "%%ORIG%%\n※要サーバ再起動", "logging.format-filename" => "%%ORIG%%\n※要サーバ再起動", diff --git a/lib/web/views/layout.haml b/lib/web/views/layout.haml index 845d82e2..edaccda5 100644 --- a/lib/web/views/layout.haml +++ b/lib/web/views/layout.haml @@ -13,19 +13,16 @@ %link{:href => "/theme/#{@bootstrap_theme}/bootstrap.min.css", :rel => "stylesheet"}/ %link{:href => "/theme/#{@bootstrap_theme}/style.css", :rel => "stylesheet"}/ - else - %link{:href => "//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css", :rel => "stylesheet"}/ - %link{:href => "/resources/default-style.css?_=#{Narou::VERSION}", :rel => "stylesheet"}/ - %link{:href => "//cdn.datatables.net/plug-ins/725b2a2115b/integration/bootstrap/3/dataTables.bootstrap.css", :rel => "stylesheet"}/ - %link{:href => "//cdn.datatables.net/colreorder/1.1.2/css/dataTables.colReorder.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css", :rel => "stylesheet"}/ + %link{:href => "/resources/css/default-style.css?_=#{Narou::VERSION}", :rel => "stylesheet"}/ + %link{:href => "//cdn.datatables.net/2.3.4/css/dataTables.bootstrap.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.datatables.net/colreorder/2.1.2/css/colReorder.bootstrap.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.datatables.net/buttons/3.2.5/css/buttons.bootstrap.css", :rel => "stylesheet"} %link{:href => "//cdn.datatables.net/colvis/1.1.1/css/dataTables.colVis.css", :rel => "stylesheet"}/ - %link{:href => "//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.6.5/css/bootstrap-select.min.css", :rel => "stylesheet"}/ - %link{:href => "/resources/perfect-scrollbar.min.css", :rel => "stylesheet"}/ - %link{:href => "/resources/toggle-switch.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.jsdelivr.net/npm/bootstrap-select@1.13.18/dist/css/bootstrap-select.min.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.jsdelivr.net/npm/perfect-scrollbar@1.5.6/css/perfect-scrollbar.min.css", :rel => "stylesheet"}/ + %link{:href => "/resources/css/toggle-switch.css", :rel => "stylesheet"}/ %link{:href => "/style.css?_=#{Narou::VERSION}", :rel => "stylesheet"}/ - / HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries - /[if lt IE 9] - - :javascript var local_initialize_functions = []; var slideNavbar; @@ -40,24 +37,28 @@ &= message != yield = partial :move_to_top - %script{:src => "//code.jquery.com/jquery-1.11.1.min.js"} - %script{:src => "//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"} - %script{:src => "//cdn.datatables.net/1.10.10/js/jquery.dataTables.min.js"} - %script{:src => "//cdn.datatables.net/plug-ins/725b2a2115b/integration/bootstrap/3/dataTables.bootstrap.js"} - %script{:src => "//cdn.datatables.net/colreorder/1.3.0/js/dataTables.colReorder.min.js"} - %script{:src => "//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.6.5/js/bootstrap-select.min.js"} - %script{:src => "//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.6.1/lodash.min.js"} - %script{:src => "/resources/sprintf.js"} - %script{:src => "/resources/dataTables.colVis.js"} - %script{:src => "/resources/bootbox.min.js"} - %script{:src => "/resources/shortcut.js"} - %script{:src => "/resources/perfect-scrollbar.min.js"} - %script{:src => "/resources/jquery.outerclick.js"} - %script{:src => "/resources/jquery.slidenavbar.js"} - %script{:src => "/resources/jquery.moveto.js"} - %script{:src => "/resources/narou.library.js?_=#{Narou::VERSION}"} - %script{:src => "/resources/narou.queue.js?_=#{Narou::VERSION}"} - %script{:src => "/resources/common.ui.js?_=#{Narou::VERSION}"} + %script{:src => "//cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/jquery-migrate@3.5.2/dist/jquery-migrate.js"} + %script{:src => "//cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"} + %script{:src => "//cdn.datatables.net/2.3.4/js/dataTables.min.js"} + %script{:src => "//cdn.datatables.net/2.3.4/js/dataTables.bootstrap.js"} + %script{:src => "//cdn.datatables.net/colreorder/2.1.2/js/dataTables.colReorder.min.js"} + %script{:src => "//cdn.datatables.net/colreorder/2.1.2/js/colReorder.bootstrap.js"} + %script{:src => "//cdn.datatables.net/buttons/3.2.5/js/dataTables.buttons.min.js"} + %script{:src => "//cdn.datatables.net/buttons/3.2.5/js/buttons.bootstrap.js"} + %script{:src => "//cdn.datatables.net/buttons/3.2.5/js/buttons.colVis.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/bootstrap-select@1.13.18/js/bootstrap-select.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/perfect-scrollbar@0.8.1/dist/js/perfect-scrollbar.jquery.min.js"} + %script{:src => "/resources/js/bootbox.js"} + %script{:src => "/resources/js/sprintf.js"} + %script{:src => "/resources/js/shortcut.js"} + %script{:src => "/resources/js/jquery.outerclick.js"} + %script{:src => "/resources/js/jquery.slidenavbar.js"} + %script{:src => "/resources/js/jquery.moveto.js"} + %script{:src => "/resources/js/narou.library.js?_=#{Narou::VERSION}"} + %script{:src => "/resources/js/narou.queue.js?_=#{Narou::VERSION}"} + %script{:src => "/resources/js/common.ui.js?_=#{Narou::VERSION}"} :javascript $(document).ready(function($) { slideNavbar = $(".navbar-collapse").slideNavbar(); @@ -66,5 +67,5 @@ }); }); - if env["PATH_INFO"] == "/" - %script{:src => "/resources/narou.ui.js?_=#{Narou::VERSION}"} + %script{:src => "/resources/js/narou.ui.js?_=#{Narou::VERSION}"} diff --git a/lib/web/views/style.scss b/lib/web/views/style.scss index 70034a02..b9185875 100644 --- a/lib/web/views/style.scss +++ b/lib/web/views/style.scss @@ -101,6 +101,7 @@ table#novel-list { color: $default-color; //border-color: darken($odd-color, 20%); border-color: $novel-list-border-color; + background-color: $even-color; @mixin dataTableSortingHeader($arrow_img_url) { background: url($arrow_img_url) no-repeat center right !important; @@ -110,12 +111,16 @@ table#novel-list { thead { background-color: $thead-background-color; color: #ddd0cc; + th { + background-color: $thead-background-color; + } } td { padding: 4px !important; vertical-align: middle; //border-color: darken($odd-color, 20%); border-color: $novel-list-border-color; + background-color: inherit; } tr { td.text-center { @@ -134,9 +139,28 @@ table#novel-list { background-color: #f8f3e5; } } + + /* DataTables 2.x対応: nth-childベースのストライプ */ + tbody > tr:nth-child(odd) { + background-color: $odd-color; + td.sorting_1 { + background-color: #f0ecde; + } + } + tbody > tr:nth-child(even) { + background-color: $even-color; + td.sorting_1 { + background-color: #f8f3e5; + } + } $selected-color: mix($odd-color, rgba(255,212,0,0.5)); //$selected-color: #90cebc; $selected-hover-color: #ded; + + /* DataTables 2.x のCSS変数を上書き */ + --dt-row-selected: #{$selected-color}; + --dt-row-selected-text: #{$default-color}; + tr.selected { td { background-color: $selected-color !important; @@ -203,10 +227,10 @@ table#novel-list { } thead { .sorting_asc { - @include dataTableSortingHeader("/resources/sort_asc.png"); + @include dataTableSortingHeader("/resources/images/sort_asc.png"); } .sorting_desc { - @include dataTableSortingHeader("/resources/sort_desc.png"); + @include dataTableSortingHeader("/resources/images/sort_desc.png"); } } /* @@ -330,6 +354,8 @@ table#novel-list { // DataTables 共通設定 table.dataTable { + background-color: white; + th { text-align: center; } @@ -341,6 +367,10 @@ table.dataTable { background: inherit; } } + + tbody { + background-color: white; + } } /* diff --git a/narou.gemspec b/narou.gemspec index 0c3a5b6c..7d7a1ac8 100644 --- a/narou.gemspec +++ b/narou.gemspec @@ -41,41 +41,44 @@ Gem::Specification.new do |gem| EOS gem.post_install_message = install_message.gsub("\t", " ") - gem.required_ruby_version = ">=2.3.0" + gem.required_ruby_version = ">=3.4.0" gem.files = `git ls-files`.split("\n").reject { |fn| fn =~ %r!^spec/|^"spec! } << Narou.create_git_commit_version gem.executables = gem.files.grep(%r!^bin/!).map { |f| File.basename(f) } gem.add_runtime_dependency 'termcolorlight', '~> 1.0', '>= 1.1.1' - gem.add_runtime_dependency 'rubyzip', '~> 2.3', '>= 2.3.2' - gem.add_runtime_dependency 'mail', '~> 2.6.0', '>= 2.6.6' - gem.add_runtime_dependency 'pony', '~> 1', '>= 1.11' - gem.add_runtime_dependency 'diff-lcs', '~> 1.2', '>= 1.2.5' - gem.add_runtime_dependency 'sinatra', '~> 2.0', '>= 2.0.8.1' - gem.add_runtime_dependency 'sinatra-contrib', '~> 2.0', '>= 2.0.8.1' - gem.add_runtime_dependency 'tilt', '~> 2.0', '>= 2.0.10' + gem.add_runtime_dependency 'rubyzip', '~> 3.2', '>= 3.2.0' + gem.add_runtime_dependency 'mail', '~> 2.9', '>= 2.9.0' + gem.add_runtime_dependency 'pony', '~> 1', '>= 1.13' + gem.add_runtime_dependency 'diff-lcs', '~> 1.6', '>= 1.6.2' + gem.add_runtime_dependency 'sinatra', '~> 4.2', '>= 4.2.0' + gem.add_runtime_dependency 'sinatra-contrib', '~> 4.2', '>= 4.2.0' + gem.add_runtime_dependency 'rackup', '~> 2.1' + gem.add_runtime_dependency 'puma', '~> 6.4' + gem.add_runtime_dependency 'tilt', '~> 2.6', '>= 2.6.1' gem.add_runtime_dependency 'sassc', '~> 2.4' - gem.add_runtime_dependency 'ffi', '~> 1.4', '>= 1.4.2' - gem.add_runtime_dependency 'haml', '>= 5.1.2', '< 6' - gem.add_runtime_dependency 'memoist', '~> 0.11.0' + gem.add_runtime_dependency 'ffi', '~> 1.17', '>= 1.17.2' + gem.add_runtime_dependency 'haml', '>= 5.2.2', '< 6' + gem.add_runtime_dependency 'memoist', '~> 0.16.2' gem.add_runtime_dependency 'systemu', '~> 2.6', '>= 2.6.5' - gem.add_runtime_dependency 'erubi', '~> 1.13' + gem.add_runtime_dependency 'erubi', '~> 1.13.1' gem.add_runtime_dependency 'open_uri_redirections', '~> 0.2', '>= 0.2.1' - gem.add_runtime_dependency 'activesupport', '>= 6.1', '< 8.0' - gem.add_runtime_dependency 'unicode-display_width', '~> 1.4' - gem.add_runtime_dependency 'webrick', '~> 1.7' - gem.add_runtime_dependency 'psych', '~> 4.0' + gem.add_runtime_dependency 'activesupport', '~> 8.0', '>= 8.1.0' + gem.add_runtime_dependency 'unicode-display_width', '~> 3.2' + gem.add_runtime_dependency 'psych', '~> 5.2' gem.add_runtime_dependency 'nkf', '~> 0.2.0' gem.add_runtime_dependency 'csv', '~> 3.3' - gem.add_runtime_dependency 'rexml', '~> 3.2' + gem.add_runtime_dependency 'rexml', '~> 3.4' + gem.add_runtime_dependency 'sanitize', '~> 7.0.0' + gem.add_runtime_dependency 'bootsnap', '~> 1.18', '>= 1.18.6' - gem.add_development_dependency 'rspec', '~> 3.10' + gem.add_development_dependency 'rspec', '~> 3.13' gem.add_development_dependency 'rspec-retry', '~> 0.6' - gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4' + gem.add_development_dependency 'rspec_junit_formatter', '~> 0.6' gem.add_development_dependency 'timecop', '~> 0.9' - gem.add_development_dependency 'pry', '~> 0.12' - gem.add_development_dependency 'pry-byebug', '~> 3.8' - gem.add_development_dependency 'awesome_print', '~> 1.8' - gem.add_development_dependency 'simplecov', '~> 0.20' + gem.add_development_dependency 'pry', '~> 0.15' + gem.add_development_dependency 'pry-byebug', '~> 3.11' + gem.add_development_dependency 'awesome_print', '~> 1.9' + gem.add_development_dependency 'simplecov', '~> 0.22' end diff --git a/narou.rb b/narou.rb index 87da4c13..db5b7342 100644 --- a/narou.rb +++ b/narou.rb @@ -1,4 +1,4 @@ -#! /usr/bin/env ruby +#! /usr/bin/env ruby --yjit # frozen_string_literal: true # @@ -7,6 +7,17 @@ # Copyright 2013 whiteleaf. All rights reserved. # +require 'bootsnap' +Bootsnap.setup( + cache_dir: 'tmp/cache', # Path to your cache + ignore_directories: [], # Directory names to skip. + development_mode: false, # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc + load_path_cache: true, # Optimize the LOAD_PATH with a cache + compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting. + compile_cache_yaml: true, # Compile YAML into a cache + readonly: true, # Use the caches but don't update them on miss or stale entries. +) + require_relative "lib/extension" require_relative "lib/extensions/monkey_patches" require_relative "lib/backtracer"