├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── css ├── linelikefromstand_4u_.css └── serveronly.css ├── img ├── favicon.svg └── ss.png ├── index.html └── js └── main.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_size = 4 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /data 2 | /*.txt 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hidao80 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINE風LINEバックアップテキストデータビュアー 2 | 3 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE.md) 4 | [![VanillaJS](https://img.shields.io/badge/Framework-VanillaJS-blue.svg)](https://nodejs.org/ja/) 5 | ![Not compatible](https://img.shields.io/badge/IE-Not_compatible-red.svg) 6 | ![hidao quality](https://img.shields.io/badge/hidao-quality-orange.svg) 7 | 8 | 9 | LINEで「トーク履歴を送信」で取得できる味気ないトーク履歴をLINEのトーク画面風の見た目に変換するWebアプリケーションです。 10 | 11 | トーク履歴のテキストファイルをアップロードするとプレビューされるので、「ダウンロード」ボタンを押すと、プレビューされた内容をそのまま単一の HTML ファイルにして保存することができます。 12 | 13 | また、アップロードされるのはご利用のブラウザまでで、サーバにトーク履歴が保存されることはありません。 14 | 15 | ご利用は http://hidao80.github.io/LINEBackupViewer/ からどうぞ。 16 | 17 | ![スクリーンショット](img/ss.png) 18 | 19 | ※画面は開発中のものです 20 | 21 | ## ToDo 22 | 23 | - [x] デフォルト画面のダウンロード(仮)ができる 24 | - [x] ダウンロードボタンをクックすると、画面に描画されているメッセージをHTMLファイルとしてダウンロードする 25 | - [x] ログのアップロードすると、画面にLINE風に描画する 26 | - [x] グループLINEのトークで本人を識別する 27 | - [x] 日付の区切りが入る 28 | - [x] 各投稿時刻を表示する 29 | - [x] OS によるフォントとボタンスタイルの違いを統一 30 | -------------------------------------------------------------------------------- /css/linelikefromstand_4u_.css: -------------------------------------------------------------------------------- 1 | html { 2 | width: 100vw; 3 | font-family: sans-serif; 4 | } 5 | 6 | body { 7 | max-width: 800px; 8 | margin: 0 auto; 9 | background: rgb(113,147,193);/*色は任意*/ 10 | } 11 | 12 | /*吹き出し*/ 13 | .balloon_l { 14 | width: 100%; 15 | margin: 10px 0; 16 | display: flex; 17 | justify-content: flex-start; 18 | } 19 | 20 | .balloon_r { 21 | width: 100%; 22 | margin: 10px 0; 23 | display: flex; 24 | justify-content: flex-end; 25 | } 26 | 27 | .balloon_l .says { 28 | max-width:calc(80% - 50px); /*最大幅は任意*/ 29 | position: relative; 30 | padding: 17px 13px 15px 18px; 31 | border-radius: 12px; 32 | background: white;/*色は任意*/ 33 | line-height:1.5; 34 | font-size: smaller; 35 | display: inline-block; 36 | } 37 | 38 | .balloon_r .says { 39 | max-width:calc(80% - 50px); /*最大幅は任意*/ 40 | position: relative; 41 | padding: 17px 13px 15px 18px; 42 | border-radius: 12px; 43 | background: #85e249;/*色は任意*/ 44 | line-height:1.5; 45 | font-size: smaller; 46 | display: inline-block; 47 | } 48 | 49 | .says p{ 50 | display: inline; 51 | margin:8px 0 0 !important; 52 | } 53 | 54 | .says p:first-child{ 55 | margin-top:0 !important; 56 | } 57 | 58 | .says:after { 59 | content: ""; 60 | position: absolute; 61 | border: 10px solid transparent; 62 | top: 13px; 63 | } 64 | 65 | .balloon_l .says:after { 66 | left: -26px; 67 | border-right: 22px solid white; 68 | } 69 | 70 | .balloon_r .says:after { 71 | right: -26px; 72 | border-left: 22px solid #85e249; 73 | } 74 | 75 | .balloon_l-before { 76 | display: inline-block; 77 | width: 50px; /*任意のサイズ*/ 78 | height: 50px; 79 | margin-right: 25px; 80 | content: ''; 81 | text-align: center; 82 | border-radius: 25px; 83 | } 84 | 85 | .balloon_l-before > div, 86 | .balloon_r-after > div { 87 | color: white; 88 | font-size: 1.25rem; 89 | text-shadow: 0 0 2px black; 90 | display: inline-block; 91 | line-height: 50px; 92 | } 93 | 94 | .balloon_r-after { 95 | display: inline-block; 96 | width: 50px; /*任意のサイズ*/ 97 | height: 50px; 98 | margin-left: 25px; 99 | content: ''; 100 | text-align: center; 101 | vertical-align: top; 102 | border-radius: 25px; 103 | } 104 | 105 | div.prop { 106 | padding-left: 5px; 107 | padding-right: 5px; 108 | color: white; 109 | font-size: xx-small; 110 | align-self: flex-end; /*縦位置を下揃え*/ 111 | display: inline-block; 112 | vertical-align: bottom; 113 | margin-bottom: 1rem; 114 | } 115 | 116 | div.title { 117 | text-align: center; 118 | color: white; 119 | vertical-align: bottom; 120 | } 121 | 122 | div.date { 123 | text-align: center; 124 | color: lightgray; 125 | vertical-align: bottom; 126 | font-size: xx-small; 127 | } 128 | 129 | div.date:before, 130 | div.date:after { 131 | content: ' ─── '; 132 | } 133 | -------------------------------------------------------------------------------- /css/serveronly.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | 5 | #menu_bar { 6 | width : 100%; 7 | display: flex; 8 | } 9 | 10 | #menu_bar>button { 11 | flex : 1; 12 | margin-right: 1px; 13 | } 14 | -------------------------------------------------------------------------------- /img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /img/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hidao80/LINEBackupViewer/128d6ed399a9600a103d731b76e738a76e5bee3d/img/ss.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LINE バックアップビュアー 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 22 |
23 |
24 |

使い方は?

25 |
26 |
27 |

まず、上のメニューの「アップロード」ボタンを押して、LINEトークのバックアップファイルをアップロードします。

28 |
29 |
30 |

ファイル名が「[LINE] <相手の名前>とのトーク.txt」となっているものです。

31 |
32 |
33 |

「ダウンロード」ボタンが有効になったら、「ダウンロード」ボタンをタップします。

34 |
35 |
36 |

バックアップファイルの内容をLINEのトーク画面風に変換したHTMLファイルがダウンロードされます。

37 |
38 |
39 |

ふーん

40 |
41 |
42 |

ありがとう!
使ってみる!

43 |
44 |
45 | 48 | 54 |
55 | 76 | 79 | 82 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * グローバル変数 3 | */ 4 | var global = { 5 | // トークの持ち主名 6 | ownerName: "", 7 | 8 | // ダウンロードファイルのファイル名 9 | title: "", 10 | 11 | // プレビューするかどうかフラグ 12 | noRendring: false, 13 | 14 | // LINEのログからHTMLに変換したデータ 15 | html: "", 16 | }; 17 | 18 | /** 19 | * 定数 20 | */ 21 | 22 | /** 23 | * ID を入れると DOM Element を返す 24 | */ 25 | var _$ = id => { 26 | return document.getElementById(id); 27 | } 28 | 29 | /** 30 | * ID を入れると DOM Element を返す 31 | */ 32 | function getOutputHTML() { 33 | let html = ` 34 | 35 | 36 | 37 | 38 | LINE バックアップビュアー 39 | 43 | 44 | 45 |
46 |
`; 47 | html += global.html; 48 | html += ` 50 |
51 | 52 | `; 53 | 54 | return html; 55 | } 56 | 57 | /** 58 | * LINEログファイル(プレーンテキスト)の選択 59 | */ 60 | function selectLogTextFile(e) { 61 | // ファイルの選択をしてからアップロードする 62 | _$("upload_file").click(); 63 | } 64 | 65 | /** 66 | * 強制的に再描画させるおまじない 67 | */ 68 | async function repaint() { 69 | for (let i = 0; i < 2; i++) { 70 | await new Promise(resolve => requestAnimationFrame(resolve)); 71 | } 72 | }; 73 | 74 | function getAvailableDownloadNotify() { 75 | return ` 76 |
77 |
78 |
報告
79 |
80 |

81 | ダウンロードできるようになりました。
82 | ファイルサイズが大きすぎるため、プレビューできません。 83 |

84 |
85 |
86 |
87 | `; 88 | } 89 | 90 | /** 91 | * LINEログファイル(プレーンテキスト)のアップロード 92 | */ 93 | function uploadLogTextFile(e) { 94 | // ファイル要素から、選択されたファイルを取得する 95 | const files = _$("upload_file").files; 96 | 97 | // ファイルが選択されていなかったら終了 98 | if (files.length === 0) { 99 | return false; 100 | } 101 | 102 | // 1つ目のファイルを取得する 103 | const file = files[0]; 104 | 105 | // ボタン内のスピナーを非表示 106 | _$('spinner').style.display = 'none'; 107 | 108 | // アップロードされたファイルからテキストを抽出 109 | const reader = new FileReader(); 110 | reader.readAsText(file); 111 | reader.onload = () => { 112 | // ユーザ名リストを本人選択モーダルダイアログにセット 113 | setUserNameSelect(reader.result); 114 | 115 | // ダイアログを表示 116 | $('#usersNameModal').modal('show'); 117 | } 118 | 119 | _$('modal-choice').onclick = async (e) => { 120 | // ボタン内のスピナーを表示 121 | _$('spinner').style.display = 'inline-block'; 122 | 123 | // 強制的に再描画する 124 | await repaint(); 125 | 126 | // 選択したユーザの名前を取得。トークの持ち主とする 127 | global.ownerName = _$('userName').value.trim(); 128 | 129 | // ダウンロードファイルのファイル名を作成 130 | // 1対1のトークでは、後で取得するログに書かれているものを優先する 131 | global.title = global.ownerName + "のルーム"; 132 | 133 | // 作業変数の初期化 134 | global.html = _$("contents").textContent = null 135 | global.noRendring = false; 136 | 137 | // ログファイルを読み込んでHTMLに変換 138 | global.html = convertLogTextToHTML(reader.result); 139 | 140 | // 成形したHTMLをプレビュー画面に適応 141 | // レンダリングするデータが大きすぎる場合は、プレビューできないので代替メッセージを表示する 142 | _$("contents").innerHTML = global.noRendring ? getAvailableDownloadNotify() : global.html; 143 | 144 | // ダイアログを非表示 145 | $('#usersNameModal').modal('hide'); 146 | }; 147 | 148 | // Submitイベントをキャンセルする 149 | return false; 150 | } 151 | 152 | /** 153 | * アップロードされたログファイルからHTMLのコンテンツ部分をHTMLに変換する 154 | */ 155 | function convertLogTextToHTML(text) { 156 | let count = 0, res, line = 0, html = "", progress = 0, groups, msg; 157 | const lines = text.split(/\r\n|\n|\r/); 158 | const total = lines.length; 159 | 160 | for (let i in lines) { 161 | // 進捗を表示(デバッグ用) 162 | // progress = Math.floor(++count / total * 100); 163 | // console.log(`${progress}%` + " " + count + "/" + total + " " + lines[i]); 164 | 165 | if ((res = lines[i].match(/(?\d\d?:\d\d)\t(?.+)\t"?(?.+)/)) !== null) { 166 | // メッセージの先頭行の場合 167 | if (msg !== "" && groups !== undefined) { 168 | html += writeMsg(groups, msg); 169 | } 170 | groups = res.groups; 171 | msg = res.groups.msg.replace(/^"/, ""); // 複数行にまたがるときの先頭のダブルクォーテーションを除去 172 | line++; 173 | } else if ((res = lines[i].match(/^(?\d+\˙\/\d +\/\d+\(.+\)$)/)) !== null) { 174 | // 日付行の場合 175 | if (msg !== "" && groups !== undefined) { 176 | html += writeMsg(groups, msg); 177 | } 178 | html += `
${res.groups.date}
`; 179 | 180 | msg = ""; 181 | line++; 182 | } else if ((res = lines[i].match(/\[LINE\]\s(?.+)とのトーク履歴/)) !== null) { 183 | // ファイルヘッダ行の場合 184 | // タイトルを上書き 185 | global.title = res.groups.userName + "とのトーク履歴"; 186 | html += `
${global.title}
`; 187 | 188 | line++; 189 | } else if (lines[i].match(/^保存日時:/) !== null && global.line <= 2) { 190 | // 2行目以降の保存日時行は無視する 191 | line++; 192 | } else if (lines[i].trim() !== "") { 193 | // 空行でなければ、終端のダブルクォーテーションを取り除いて改行してから表示する(複数行メッセージに対応) 194 | msg += "
" + lines[i].replace(/"$/, ""); 195 | line++; 196 | } 197 | 198 | if (html.length > 1_000_000) { 199 | global.noRendring = true; 200 | } 201 | } 202 | 203 | // 最後のメッセージを出力 204 | html += writeMsg(groups, msg); 205 | return html; 206 | } 207 | 208 | function nameToColorCode(name) { 209 | return name.split('').map(char => char.charCodeAt(0)).map(byte => byte.toString(16)).join('').slice(0, 6); 210 | } 211 | 212 | /** 213 | * メッセージを出力文字列に追加 214 | * @param groups string matchの結果 215 | * @param msg string 投稿され内容 216 | * @returns HTML 形式の出力文字列 217 | */ 218 | function writeMsg(groups, msg) { 219 | // すでにあるメッセージをHTMLに成形 220 | return global.ownerName == groups.userName.trim() 221 | ? `
222 |
既読
${groups.at}
223 |

${msg.trim()}

224 |
225 |
${global.ownerName.slice(0, 2)}
226 |
227 |
` 228 | : `
229 |
230 |
${groups.userName.slice(0, 2)}
231 |
232 |

${msg.trim()}

233 |
${groups.at}
234 |
235 | 236 | `; 237 | } 238 | 239 | 240 | function setUserNameSelect(text) { 241 | const lines = text.split('\n'); 242 | const users = {}; 243 | let res; 244 | 245 | for (const item of lines) { 246 | if ((res = item.match(/(?\d\d?:\d\d)\t(?.+)\t(?.+)/)) !== null) { 247 | users[res.groups.userName] = true; 248 | } 249 | } 250 | 251 | _$("modal-body").innerHTML = ``; 252 | for (const name in users) { 253 | _$("userName").innerHTML += ``; 254 | } 255 | } 256 | 257 | /** 258 | * HTMLファイルのダウンロード 259 | */ 260 | function downloadHTMLFile(e) { 261 | // アンカータグの作成 262 | const downLoadLink = document.createElement("a"); 263 | 264 | // ダウンロードするHTML文章の生成 265 | const outputDataString = getOutputHTML(); 266 | const downloadFileName = "[LINE] " + global.title + ".html" 267 | downLoadLink.download = downloadFileName; 268 | downLoadLink.href = URL.createObjectURL(new Blob([outputDataString], { type: "text/html" })); 269 | downLoadLink.dataset.downloadurl = ["text/html", downloadFileName, downLoadLink.href].join(":"); 270 | downLoadLink.click(); 271 | } 272 | 273 | window.onload = () => { 274 | _$("download_button").addEventListener("click", e => downloadHTMLFile(e)); 275 | _$("upload_button").addEventListener("click", e => selectLogTextFile(e)); 276 | _$("upload_file").addEventListener("change", e => uploadLogTextFile(e)); 277 | _$("upload_file").addEventListener("click", e => e.target.value = ""); 278 | } 279 | --------------------------------------------------------------------------------