AnkiDroidでひながいちにち遊ぶのに、ストレスを感じていました。以前の記事(市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編)の方法で辞書からデッキを生成したためカードのサイズが大きく、頻繁にスクロールのためにタッチ操作をしなければいけなかったためです。Ankiを可能な限り高速に単語とその語義を頭に叩き込むゲームとして考えたとき、このUIには改善の余地があると思いました。幸い、カードはHTMLそのものであり、JavaScriptを読みこませれば、カード閲覧時のUIはカスタマイズ可能です。そこで、いくばくかの時間を割いて、UIの改善を試みることにしました。この改善により学習効率が向上すれば、十分にペイすると考えたからです。
開発中のできごと
当初の方針
当初、DOMを走査してページ位置を割り出せるかな、と見込んでいました。可能なら、それを元に仮想的なページ位置を前後できればビューアが完成するはず…でした。
方針転換
DOMの走査は高コストで、かつ、要素によってはオフセット位置を取れなかったりしました。そこで、CSS3 Multiple Columnsを使って、カラム幅 = ウィンドウ幅に設定し、レンダリングの段階でページごとに描写されるようにしました。この方法だと、単にX方向のスクロール位置をウィンドウ幅ぶん前後させるだけで、ページの前後が実現できます。
生JSを諦める
ひとまず動く、というところまでは生JavaScriptを書きましたが、型がないことに耐えられず、途中でScala.jsに切り替えました。初めて触ったのですが、Scala.jsは思っていたより完成度が高く、十分実用になるのではという印象を持ちました。
AnkiDroidにプルリクを送る
- Cookieが永続せず消える
- ユーザーアクションなしでaudio/videoをloadできない
の2点が問題になったため、改善するためのパッチを書いて送りました。無事マージされたので、自前ビルド地獄に落ちずにすみました。Playストアからダウンロードできると楽でいいです。
できあがったもの
最近のAnki環境 pic.twitter.com/vKHWlSShkN
— るびゅ (@ruby_U) 2016年7月18日
全体をスクロールするのではなく、ページ単位の戻り/送りでAnkiのカードを眺めるためのビューアです。シンプルなものですが、これで十分実用になります。
操作方法
操作 | 動作 |
---|---|
左右スワイプ | ページ戻り/送り |
左右タップ | ページ戻り/送り |
左右ロングスワイプ | チャプター戻り/送り |
中央タップ | 音声再生 |
タップ&ホールド | 音声リピート再生 |
成果物
アプリケーションはScalaで記述されており、全部で900行ほど、レポジトリはrubyu/anki-iframe-viewerです。
git
とsbt
がインストールされている環境であれば、以下の手順でコンパイルできます。
$ git clone https://github.com/rubyu/anki-iframe-viewer $ cd anki-iframe-viewer $ sbt prod/dist
現時点で1.0が最新です。リリース一覧からコンパイル済みのjsファイル、cssファイルがダウンロードできます。
- anki-iframe-viewer-opt.js
ビューア本体。このビューアはページを左右にスライドさせることでページ送りを実現しているので、以下のcssの適用が必要です。
- anki-iframe-viewer.css
CSS3 multi-column layout をカードに適用するためのCSS。#container
内にコンテンツが入るようにすると、うまく表示されると思います。
anki-iframe-viewer.css
は標準的な1カラム構成のデザインです。タブレットなどでは、複数カラム構成のデザインのほうが見やすいかもしれません。例えば2カラム構成ではanki-iframe-viewer.css
に続けて、次のコードを適用してください:
#container { -webkit-column-width: 50vw; }
Nexus7は2カラム設定 pic.twitter.com/ZmfhzLLso8
— るびゅ (@ruby_U) 2016年7月18日
注1: コンテンツの横のグラデーションは、このビューアには関係がない、現在どのあたりを読んでいるのかわかりやすくするためのハックです。適当なHTMLに対し、
<div class="outer"> <div class="inner"> ... </div> </div>
.outer { /* berry-juice from http://codepen.io/tumanova/pen/tkvmi?editors=0100 */ background-image: linear-gradient(to bottom, #c5d4d7 6%, #d6b98d 34%, #c99262 57%, #8c5962 80%, #43577e 100%); } .inner { margin: 0 0.5rem 0 0; padding: 0 0.5rem 0 0.5rem; background-color: white; }
などとすれば、最近のブラウザなら再現できるはずです。
注2: anki-iframe-viewer
という名前のアプリケーションですが、 iframe内でしか動作しないわけではありません。後述するiframeデッキに移行する際に導入することを目指して開発を始めたため、この名前になりました。
制約事項
以下の制約は、私が必要としておらず、コスト的にサポートできなかった部分です。
- 画面をタッチして、文字列を選択したりすることができない
- 複数のaudio要素が見つかった場合、最初のものだけを再生してしまう
設定方法
Scala.jsにてApplicationを以下のように定義しています。
@JSExport("AnkiIframeViewerApp") object App extends Logger { ... @JSExport def minLongSwipeSize(d: Double) = { userMinLongSwipeSize = Option(d); this } @JSExport def minSwipeSize(d: Double) = { userMinSwipeSize = Option(d); this } @JSExport def minLongTouchMillis(d: Double) = { userMinLongTouchMillis = Option(d); this } @JSExport def maxGestureMillis(d: Double) = { userMaxGestureMillis = Option(d); this } @JSExport def dispatcherDuplicateEventWindowMillis(d: Double) = { userDispatcherDuplicateEventWindowMillis = Option(d); this } @JSExport def tapCenterRatio(d: Double) = { userTapCenterRatio = Option(d); this } @JSExport def autoPlayAudio(b: Boolean) = { userAutoPlayAudio = Option(b); this } @JSExport def holdReplayAudio(b: Boolean) = { userHoldReplayAudio = Option(b); this } @JSExport def audio(query: String) = { userAudioQueries += query; this } @JSExport def chapter(query: String) = { userChapterQueries += query; this } ... @JSExport def run(): Unit = { ... } }
よって、カード自体は
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <link rel="stylesheet" type="text/css" href="anki-iframe-viewer.css"> <script type="text/javascript" src="anki-iframe-viewer-opt.js"></script> <script type="text/javascript"> AnkiIframeViewerApp() .audio("#chapter-00 audio") .chapter("#chapter-01") .chapter("#chapter-02") .run(); </script> </head> <body> <div id="container"> <div id="chapter-00"> ... </div> <div id="chapter-01"> ... </div> <div id="chapter-02"> ... </div> </div> </body> </html>
のようなHTMLにすべきです。
ここで、AnkiIframeViewerApp()
のaudio()
、chapter()
をコールして設定を行い、run()
でアプリケーションを開始していることに注意してください。anki-iframe-viewer-opt.js
を読み込んだだけではアプリケーションは開始されません。
まずは適当なhtmlファイルを作り、ローカルサーバでビューアが動作することを確認してみてください。正しく動作すれば、それをAnkiに移植してみましょう。
Ankiへの導入方法
Ankiへの導入にはいくつかの問題があり、それらを回避しなければなりません。以下には問題とその対処法を記します。
注1: 何か問題が発生したとき、あるいは何も起こらなかったとき(動きさえしない!)に責任を負うことができないため、実際にデッキにこのビューアを組み込むことは推奨しませんが、なにかの役に立つこともあるだろう、ということで本稿を公開しています。ご注意を。お役に立てたなら嬉しいです。
注2: 2016/07/19現在、このビューアはAnkiDroidでのみ動作確認が取れています。Anki Mobileは不明。Anki Desktopでは動作しません。Anki Desktopについては、組み込んであるPyQt 4.8.4がちょー古いためです。ただし、PyQt 5.5への移行が進行中なので、そのうち動くようになるのではないかと思います。
AnkiDroidとその他の環境で併用したい
動作しない環境では、cssとjsファイルを削除すれば、従来通りのスワイプクロールUIに簡単にフォールバックできます。これは、将来的にこのビューアに問題が発生した場合も、この方法で簡単にフォールバックが行えるということを意味します。
カードのheadをカスタマイズできない
JavaScriptを使って改変できます。カードのテンプレートに、例えば以下のように記述します。
<script> var meta = document.createElement("meta"); meta.setAttribute("name", "viewport"); meta.setAttribute("content", "width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"); document.getElementsByTagName("head")[0].appendChild(meta); </script>
Ankiのデッキサイズに100MBの制約がある
Collections on AnkiWeb are limited to 100MB, not including media.
Are there limits on file sizes on AnkiWeb? / Anki Ecosystem / Knowledge Base - Anki Support
これは実に絶望的な制約で、例えばCOCA60K(6万単語)コーパスからデッキを生成すると、平均して単語あたり1000文字ほどが上限になってしまいます。このための回避策として、カード自体をhtmlファイルに切り出して、カードからはそのhtmlをiframeとして参照するという方法をなんとか編み出しました。以下の手順で、既存のデッキに適用できます。
準備
まず、次の記事を参考に、データを処理する環境を準備してください: 市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編 - ruby-U's blog
次にhttps://github.com/rubyu/wok-scripts/archive/v0.2.zipをダウンロードして、同様に配置してください。
iframeデッキの構築
以下では、チュートリアルで生成したres2.tsv
に手を加えて、カードのデータをhtmlファイルとして切り出します。
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\col-detach.wok -v@str src=1,2,3 -v dst=1 -v@rawstr media=media res2.tsv > res3.tsv
> type res3.tsv "apple" "<iframe src=""4CC39A56AE79DCFDA45E47D76C6C4D94.html""></iframe>" "book" "<iframe src=""1B764C2ED28D05BD3759E0ADAAF56557.html""></iframe>" "car" "<iframe src=""8D5A6E213A3F1BC101840112AABE2D0F.html""></iframe>"
> dir *.html /b media 4CC39A56AE79DCFDA45E47D76C6C4D94.html 1B764C2ED28D05BD3759E0ADAAF56557.html 8D5A6E213A3F1BC101840112AABE2D0F.html
stc
に複数の列が指定されていることに注意してください。これらの列はまとめて一つのhtmlファイルに変換され、dst
でそのhtmlを参照するiframeの出力先の列を指定しています。media
に出力されたhtmlファイルは以下のようなフォーマットになっています。
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="col-detach.css"> <script type="text/javascript" src="col-detach.js"></script> </head> <body> <div id="detached"> <div id="detached-00"> ... </div> <div id="detached-01"> ... </div> <div id="detached-02"> ... </div> </div> </body> </html>
Ankiへの導入
この例の場合、iframeから読み込まれるhtmlは col-detach.js
、col-detach.css
を読み込みます。必要であれば、
anki-iframe-viewer-opt.js
にビューアを起動するためのスクリプトを追記したものをcol-detach.js
、anki-iframe-viewer.css
をcol-detach.css
とリネームし、collection.media
に配置してください。
ひとつのディレクトリに大量のメディアファイルを置くと、メディアスキャンがバッテリードレインを起こすことがある
Anki/AnkiDroidではcollection.media
内にファイルを配置することを推奨していますが、その数が膨大になると、パフォーマンス上の問題が発生します。アプリケーションの動作のみならず、端末自体のパフォーマンスが低下することもあるため、Epwingから生成したデッキには以下の手順を適用するといいかもしれません。
バッテリードレインの例:
本日のxperia Z4くんの様子です pic.twitter.com/pxpBXNyWd3
— るびゅ (@ruby_U) 2016年7月15日
tsvファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-tsv.wok -v idx=1 -v depth=2 -v@rawstr prefix=Foo res3.tsv > Foo.tsv
> type Foo.tsv "apple" "<iframe src=""Foo/4/C/4CC39A56AE79DCFDA45E47D76C6C4D94.html""></iframe>" "book" "<iframe src=""Foo/1/B/1B764C2ED28D05BD3759E0ADAAF56557.html""></iframe>" "car" "<iframe src=""Foo/8/D/8D5A6E213A3F1BC101840112AABE2D0F.html""></iframe>"
htmlファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-html.wok -v depth=2 -v@rawstr target=media
media
で指定されたディレクトリ内のhtmlファイルについて、head内にbaseタグを追加し、また、メディアファイル(mp3, png)のパスを書き換えます。
ここで、depthで指定された数だけparentへと遡るbaseタグが設定されることに注意してください。
mediaディレクトリ内の全てのファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-media.wok -v depth=2 -v@rawstr target=media
> dir /b media 0 1 2 ...
> dir /b media\4 0 1 2 ...
> dir *.html /b media\4\C 4CC39A56AE79DCFDA45E47D76C6C4D94.html
指定のdepthの、ファイル名のprefixから生成されたディレクトリ構造が適用されていることを確認してください。
問題がなければ、media
ディレクトリをFoo
(apply-directory-structure-to-tsv.wok
のprefix
で指定した値)にリネームして、これをcollection.media
にコピーすれば手順は完了です。Foo.tsv
をAnkiに読み込み、動作を確認してください。
この例の場合、iframeから読み込まれるhtmlは Foo/col-detach.js
、Foo/col-detach.css
を参照します。必要であれば、
anki-iframe-viewer-opt.js
にビューアを起動するためのスクリプトを追記したものをcol-detach.js
、anki-iframe-viewer.css
をcol-detach.css
とリネームし、collection.media/Foo
に配置してください。
同様に、iframeから参照されるその他のリソースファイルが存在すれば、それらもcollection.media/Foo
に配置してください。cssファイルから参照されるフォントなど、間接的な参照もこの場合に含まれることに注意してください。
大量のファイルを含むアーカイブファイルを扱えないファイラーがある
前述の手順で生成されたディレクトリをAndroid上にコピーするには、圧縮したあと、Android上でアーカイブファイルを解凍するという手順が簡単かつ高速なのですが、ESファイルエクスプローラーなど有名なアプリでも、大量のファイルを含むアーカイブファイルを扱えない場合があります。RARというアプリでは問題なく展開できるようなので、このような場合には試してみてください。
実際の例
私はCOCA60Kコーパスから、語義がEpwing辞書なデッキを作り、さらに前述の方法で語義をiframeに変換して、ディレクトリ構造を適用したものを実際に運用しています。以下に設定を記載します。
ノートタイプ
ノートタイプは「Front」「Back」の二つからなる、標準のノートタイプです。
テンプレート
表面のテンプレート
<div id="anki-front">{{Front}}</div>
書式
@font-face { font-family: _myfont00; src: url('_Lucida_Sans_Unicode.ttf'); } @font-face { font-family: _myfont01; src: url('_yugothic.ttf'); } @font-face { font-family: _myfont02; src: url('_HanaMinA.ttf'); } @font-face { font-family: _myfont03; src: url('_HanaMinB.ttf'); } html { font-size: 100%; } html body, html body * { padding: 0; margin: 0; } body { overflow: hidden; } #outer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; } iframe { width: 100%; height: 100%; border: none; } #anki-front { margin: 1rem; display: block; font-size: 1rem; font-family: _myfont00, _myfont01, _myfont02, _myfont03, sans-serif, serif; text-align: left; line-height: 1.5rem; color: black; background-color: white; } .win #anki-front { font-size: 200%; }
裏面のテンプレート
<script> document.createElement("meta"); meta.setAttribute("name", "viewport"); meta.setAttribute("content", "width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"); document.getElementsByTagName("head")[0].appendChild(meta); document.addEventListener("touchstart", function(event) { event.preventDefault(); }, false); document.addEventListener("touchmove", function(event) { event.preventDefault(); }, false); document.addEventListener("touchend", function(event) { event.preventDefault(); }, false); document.addEventListener("DOMContentLoaded", function() { document.getElementsByTagName("iframe")[0].focus(); }, false); </script> <div id="outer">{{Back}}</div>
デバイスごとの設定
SO-03G
collection.media/COCA/col-detach.js
anki-iframe-viewer-opt.js
に以下を結合したもの。
AnkiIframeViewerApp() .audio("#detached-02 audio") .chapter("#detached-03") .chapter("#detached-04") .chapter("#detached-05") .chapter("#detached-06") .chapter("#detached-07") .chapter("#detached-08") .chapter("#detached-09") .chapter("#detached-10") .run();
collection.media/COCA/col-detach.css
@font-face { font-family: _myfont00; src: url('_Lucida_Sans_Unicode.ttf'); } @font-face { font-family: _myfont01; src: url('_yugothic.ttf'); } @font-face { font-family: _myfont02; src: url('_HanaMinA.ttf'); } @font-face { font-family: _myfont03; src: url('_HanaMinB.ttf'); } html { overflow: hidden; font-size: 2vmin; } html * { font-size: 1rem; } body { margin: 1rem 0 1rem 0; height: calc(100% - 2rem); font-family: _myfont00, _myfont01, _myfont02, _myfont03, sans-serif, serif; text-align: justify; text-rendering: optimizeLegibility; -webkit-font-smoothing: subpixel–antialiased; -webkit-text-size-adjust: 100%; -webkit-user-select: none; cursor: default; line-height: 1.4; color: black; background-color: white; } #detached { height: 100%; -webkit-column-width: 100vw; -webkit-column-gap: 0; } #horizontal-bar { position: fixed; left: 0; bottom: 0.05rem; width: 100vw; } #horizontal-bar-icon1-A, #horizontal-bar-icon1-B, #horizontal-bar-icon1-C { visibility: hidden; position: absolute; right: 0.5rem; bottom: 0.15rem; width: 1rem; text-align: center; vertical-align: bottom; font-family: san-serif, selif; font-size: 0.5rem; line-height: 0.5; -webkit-filter: grayscale(100%) opacity(70%); } #horizontal-bar-progress-bar { position: absolute; left: 0.5rem; bottom: 0.35rem; width: calc(100vw - 2.5rem); } #horizontal-bar-progress-total { position: absolute; top: 0; width: 100%; border-bottom: 0.05rem solid #f0f0f0; } #horizontal-bar-progress-current { position: absolute; top: 0; width: 0; border-bottom: 0.05rem solid #505050; } #detached-00 { display: inline; float: left; margin: 0 auto 0 0.5rem; font-size: 1.2rem; line-height: 1; } #detached-01 { display: inline; } #detached-01 table { float: right; margin: 0 1rem 0.5rem 0.5rem; max-width: 50vw; border-collapse: collapse; border: 0; } #detached-01 table * { font-weight: normal; line-height: 1.1rem; } #detached-01 td { margin: 2rem 0 2rem 0; text-align: center; min-width: 10vw; } #detached-01 thead th { background-color: #f0f0f0; font-size: 0.4rem; } #detached-01 tbody td { font-size: 0.6rem; } #detached-01 tbody:nth-child(2n) td { background-color: #fefefe; } #detached-01 tbody:nth-child(2n+1) td { background-color: #f9f9f9; } #detached-02 { display: none; } #detached-03 .ebquery:first-child::before { content: "\A"; white-space: pre; } #detached-03:not(:empty), #detached-04:not(:empty), #detached-05:not(:empty), #detached-06:not(:empty), #detached-07:not(:empty), #detached-08:not(:empty), #detached-09:not(:empty), #detached-10:not(:empty) { /* berry-juice from http://codepen.io/tumanova/pen/tkvmi?editors=0100 */ background-image: linear-gradient(to bottom, #c5d4d7 6%, #d6b98d 34%, #c99262 57%, #8c5962 80%, #43577e 100%); } #detached-03 .ebquery:last-child::after, #detached-04 .ebquery:last-child::after, #detached-05 .ebquery:last-child::after, #detached-06 .ebquery:last-child::after, #detached-07 .ebquery:last-child::after, #detached-08 .ebquery:last-child::after, #detached-09 .ebquery:last-child::after, #detached-10 .ebquery:last-child::after { display: block; padding-bottom: 0.2rem; text-align: right; font-size: 0.8rem; } #detached-03 .ebquery:last-child::after { content: "【ランダムハウス英語辞典】"; } #detached-04 .ebquery:last-child::after { content: "【研究社 新英和大辞典】"; } #detached-05 .ebquery:last-child::after { content: "【リーダーズ・プラス】"; } #detached-06 .ebquery:last-child::after { content: "【英辞郎】"; } #detached-07 .ebquery:last-child::after { content: "【ロングマン現代英英辞典】"; } #detached-08 .ebquery:last-child::after { content: "【オックスフォード現代英英辞典】"; } #detached-09 .ebquery:last-child::after { content: "【斎藤和英大辞典】"; } #detached-10 .ebquery:last-child::after { content: "【ロングマン現代英英辞典】"; } #detached-03 .ebquery:last-child, #detached-04 .ebquery:last-child, #detached-05 .ebquery:last-child, #detached-06 .ebquery:last-child, #detached-07 .ebquery:last-child, #detached-08 .ebquery:last-child, #detached-09 .ebquery:last-child, #detached-10 .ebquery:last-child { box-shadow: 0 -5rem 1rem -5rem #909090 inset; } .ebquery:first-child { padding-top: 0.5rem; } .ebquery:not(:last-child) { padding-bottom: 0.2rem; } .ebquery:not(:first-child) { margin-top: 0.1rem; padding-top: 0.3rem; } .ebquery { margin: 0 0.5rem 0 0; padding: 0 0.5rem 0 0.5rem; display: block; word-wrap: break-word; background-color: white; } .ebkw { font-weight: bold; background-color: #f0f0f0; } .ebsb { vertical-align: sub; } .ebsp { vertical-align: super; } .ebec { vertical-align: text-bottom; height: 1.1rem; padding-bottom: 0.25rem; } .ebkw, .pos, .sense1, .sense2, .sense3, .LDOCE5 .sup { padding-left: 0.2rem; padding-right: 0.2rem; line-height: 1.7; } .SRD .sense1 { padding-left: 0.6rem; } .SRD .sense2, .BODY .sense1, .KENE7J5 .sense1, .KENE7J5 .sense2, .KENE7J5 .sense3, .KQNEWEJ6 .sense1, .KQNEWEJ6 .sense2, .KQNEWEJ6 .sense3, .PLUS .sense1, .PLUS .sense2, .eijiro .sense1, .LDOCE5 .sense1, .LDOCE5 .sense2, .OALD7 .sense1 { padding-left: 1.2rem; } .SRD .pos, .KENE7J5 .pos, .KQNEWEJ6 .pos, .PLUS .pos, .eijiro .pos, .BODY .pos { background-color: #e0ffe0; } .LDOCE5 .sup { background-color: #ffead5; } .SRD .sense1, .KENE7J5 .sense2, .KQNEWEJ6 .sense1, .KQNEWEJ6 .sense3, .PLUS .sense1, .eijiro .sense1, .BODY .sense1, .LDOCE5 .sense1, .OALD7 .sense1 { background-color: #ffe8e8; } .SRD .sense2, .KENE7J5 .sense3, .KQNEWEJ6 .sense2, .PLUS .sense2, .LDOCE5 .sense2 { background-color: #e8ffff; } .KENE7J5 .sense1 { background-color: #e8e8ff; } .KQNEWEJ6 .ebul, .PLUS .ebbo, .PLUS .ebul, .PLUS .ebit, .eijiro .ebbo, .eijiro .ebit, .LDOCE5 .ebbo, .OALD7 .ebbo, .BODY .ebbo { font-weight: bold; } html { font-size: 10.5px; }
Nexus7
collection.media/COCA/col-detach.js
SO-03G
と同じ。
collection.media/COCA/col-detach.css
SO-03G
と以下だけ異なる。
html { font-size: 13.5px; } #detached { -webkit-column-width: 50vw; } #detached-00, #detached-01 { display: block; width: 100%; margin-bottom: 0.2rem; } #detached-01 table { float: none; margin: 0 auto 0 auto; } #detached-03 .ebquery:first-child::before { content: ""; white-space: pre; }
PC
collection.media/COCA/col-detach.js
使用しないので空のファイルを配置する。
collection.media/COCA/col-detach.css
SO-03G
と以下だけ異なる。
html { overflow: scroll; font-size: 16px; }