1(function () { 2 "use strict"; 3 4 var COOKIE = { 5 preference: "gamersky_pc_home_version", 6 oldInviteSeen: "gamersky_pc_old_invite_seen", 7 newReturnTipSeen: "gamersky_pc_new_return_tip_seen", 8 surveySeen: "gamersky_pc_new_return_survey_seen" 9 }; 10 var COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 10; 11 var STATISTICS = { 12 oldInviteShown: 2185951, 13 oldInviteExperience: 2185953, 14 newReturnSurveyShown: 2185954 15 }; 16 var SURVEY = { 17 id: 738, 18 serviceUrl: "//router3.gamersky.com/@wenJuan" 19 }; 20 var reportedStatistics = {}; 21 var statisticSequence = 0; 22 var isNewHome = document.documentElement.getAttribute("data-home-version") === "new"; 23 24 function getCookie(name) { 25 var match = document.cookie.match(new RegExp("(?:^|;\\s*)" + name.replace(/[.*+?^${}()|[\]\\\\]/g, "\\$&") + "=([^;]*)")); 26 return match ? decodeURIComponent(match[1]) : ""; 27 } 28 29 function setCookie(name, value) { 30 document.cookie = name + "=" + encodeURIComponent(value) + "; path=/; max-age=" + COOKIE_MAX_AGE + "; samesite=lax"; 31 } 32 33 function getRoute(version) { 34 if (window.location.hostname === "www.gamersky.com") { 35 return version === "new" ? "https://www.gamersky.com/indexbeta/" : "https://www.gamersky.com/"; 36 } 37 return version === "new" ? "../æ°çé¦é¡µ/" : "../æ§çé¦é¡µ/"; 38 } 39 40 // æ§é¦é¡µä»¥åæ¥èæ¬å è½½æ¶ï¼å äºé¡µé¢ä¸»ä½è§£æå®æçæ¬è·³è½¬ï¼é¿å æ§é¦é¡µéªç°ã 41 if (!isNewHome && getCookie(COOKIE.preference) === "new") { 42 window.location.replace(getRoute("new")); 43 return; 44 } 45 46 function chooseVersion(version) { 47 setCookie(COOKIE.preference, version); 48 window.location.assign(getRoute(version)); 49 } 50 51 // 使ç¨ç«ç¹ç»ä¸ JSONP ç»è®¡æ¥å£ï¼é¿å è·¨åéå¶ï¼åä¸é¡µé¢å åä¸äºä»¶åªä¸æ¥ä¸æ¬¡ã 52 function reportStatistic(id, callback) { 53 var callbackName; 54 var script; 55 var timeout; 56 var completed = false; 57 58 function complete() { 59 if (completed) { 60 return; 61 } 62 completed = true; 63 if (timeout) { 64 window.clearTimeout(timeout); 65 } 66 if (script && script.parentNode) { 67 script.parentNode.removeChild(script); 68 } 69 try { 70 delete window[callbackName]; 71 } catch (error) { 72 window[callbackName] = undefined; 73 } 74 if (typeof callback === "function") { 75 callback(); 76 } 77 } 78 79 if (!id || reportedStatistics[id]) { 80 if (typeof callback === "function") { 81 callback(); 82 } 83 return; 84 } 85 86 reportedStatistics[id] = true; 87 callbackName = "gsVersionStatistic_" + new Date().getTime() + "_" + statisticSequence; 88 statisticSequence += 1; 89 script = document.createElement("script"); 90 script.async = true; 91 window[callbackName] = complete; 92 script.onload = complete; 93 script.onerror = complete; 94 script.src = "//click.gamersky.com/Common/GetHits.aspx?callback=" + encodeURIComponent(callbackName) + "&id=" + encodeURIComponent(id) + "&script=3&_=" + new Date().getTime(); 95 timeout = window.setTimeout(complete, 800); 96 (document.head || document.body).appendChild(script); 97 } 98 99 function closeOverlay(overlay) { 100 overlay.hidden = true; 101 document.body.classList.remove("gs-version-overlay-open"); 102 document.documentElement.classList.remove("gs-version-overlay-open"); 103 var trigger = overlay._trigger; 104 if (trigger && typeof trigger.focus === "function") { 105 trigger.focus(); 106 } 107 } 108 109 function openOverlay(overlay, trigger) { 110 overlay._trigger = trigger || null; 111 overlay.hidden = false; 112 document.body.classList.add("gs-version-overlay-open"); 113 document.documentElement.classList.add("gs-version-overlay-open"); 114 var dialog = overlay.querySelector(".gs-version-dialog"); 115 if (dialog) { 116 window.setTimeout(function () { dialog.focus(); }, 0); 117 } 118 } 119 120 function createOverlay(className, label, content) {
121 var overlay = document.createElement("div"); 122 overlay.className = "gs-version-overlay"; 123 overlay.hidden = true; 124 overlay.setAttribute("role", "presentation"); 125 overlay.innerHTML = '<section class="gs-version-dialog ' + className + '" role="dialog" aria-modal="true" aria-label="' + label + '" tabindex="-1">' + content + "</section>"; 126 overlay.addEventListener("click", function (event) { 127 if (event.target === overlay) { 128 closeOverlay(overlay); 129 } 130 }); 131 document.body.appendChild(overlay); 132 return overlay; 133 } 134 135 function closeButton() { 136 return '<button class="gs-version-dialog__close" type="button" data-version-close aria-label="å ³é"><img src="https://image.gamersky.com/webimg13/test/nativeIndex/survey-close.svg" alt="" /></button>'; 137 } 138 139 function initOldHome() { 140 var preference = getCookie(COOKIE.preference); 141 if (preference === "new") { 142 window.location.replace(getRoute("new")); 143 return; 144 } 145 146 if (getCookie(COOKIE.surveySeen) === "pending") { 147 setCookie(COOKIE.surveySeen, "shown"); 148 setCookie(COOKIE.oldInviteSeen, "1"); 149 openReturnSurveyOnOldHome(); 150 return; 151 } 152 153 var invite = createOverlay( 154 "gs-version-invite", 155 "ä½éªæ°çé¦é¡µ", 156 closeButton() + 157 '<div class="gs-version-invite__art"><img src="https://image.gamersky.com/webimg13/test/nativeIndex/invite-header.png" alt="æ°çé¦é¡µä½éªæç»" /></div>' + 158 '<div class="gs-version-invite__body"><h2>游æ°é¦é¡µä¸æ°å¦</h2><p>éè¯·æ¨æ¥ä½éªå¹¶æåºå®è´µå»ºè®®</p><button class="gs-version-primary-button" type="button" data-version-experience>ä½éªæ°ç</button></div>' 159 ); 160 var experienceButton = invite.querySelector("[data-version-experience]"); 161 var close = invite.querySelector("[data-version-close]"); 162 163 experienceButton.addEventListener("click", function () { 164 if (experienceButton.disabled) { 165 return; 166 } 167 experienceButton.disabled = true; 168 reportStatistic(STATISTICS.oldInviteExperience, function () { 169 chooseVersion("new"); 170 }); 171 }); 172 close.addEventListener("click", function () { 173 setCookie(COOKIE.preference, "old"); 174 closeOverlay(invite); 175 }); 176 177 if (!getCookie(COOKIE.oldInviteSeen)) { 178 setCookie(COOKIE.oldInviteSeen, "1"); 179 window.setTimeout(function () { 180 openOverlay(invite); 181 reportStatistic(STATISTICS.oldInviteShown); 182 }, 0); 183 } 184 } 185 186 function escapeHtml(value) { 187 return String(value === undefined || value === null ? "" : value).replace(/[&<>"']/g, function (character) { 188 return { 189 "&": "&", 190 "<": "<", 191 ">": ">", 192 '"': """, 193 "'": "'" 194 }[character]; 195 }); 196 } 197 198 function trimText(value) { 199 return String(value === undefined || value === null ? "" : value).replace(/^\s+|\s+$/g, ""); 200 } 201 202 function parseSurveyId(value) { 203 var numberValue = Number(value); 204 return isNaN(numberValue) ? value : numberValue; 205 } 206 207 function getSurveyUserId() { 208 try { 209 if (window.GSAppJsApi && window.GSAppJsApi.User && typeof window.GSAppJsApi.User.getCurrentUserId === "function") { 210 return window.GSAppJsApi.User.getCurrentUserId() || ""; 211 } 212 } catch (error) { 213 return ""; 214 } 215 return ""; 216 } 217 218 // é®å·é¡µåæºå®ç°ä½¿ç¨çæå¡ï¼ææè¯·æ±å为 JSON POSTï¼å¹¶æºå¸¦æµè§å¨åæ®ã 219 function requestSurvey(path, data, callback) { 220 var request = new XMLHttpRequest(); 221 var completed = false; 222 223 function complete(error, response) { 224 if (completed) { 225 return; 226 } 227 completed = true; 228 callback(error, response); 229 } 230 231 request.open("POST", SURVEY.serviceUrl + path, true); 232 request.withCredentials = true; 233 request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); 234 request.onreadystatechange = function () { 235 var response; 236 if (request.readyState !== 4) { 237 return; 238 } 239 if (request.status < 200 || request.status >= 300) { 240 complete(new Error("é®å·æå¡è¯·æ±å¤±è´¥")); 241 return; 242 } 243 try { 244 response = JSON.parse(request.responseText || "{}"); 245 } catch (error) { 246 complete(error); 247 return; 248 } 249 complete(null, response); 250 }; 251 request.onerror = function () { 252 complete(new Error("é®å·æå¡è¯·æ±å¤±è´¥")); 253 }; 254 request.send(JSON.stringify(data || {})); 255 } 256 257 function getSurveyVisibleQuestions(questionArray) { 258 var questions = []; 259 var index; 260 var question; 261 var answerType; 262 263 for (index = 0; index < questionArray.length; index += 1) { 264 question = questionArray[index]; 265 answerType = Number(question.answerType); 266 if (Number(question.isOpen) === 1 && Number(question.isPagination) === 0 && (answerType === 1 || answerType === 2 || answerType === 3)) { 267 questions.push(question); 268 } 269 } 270 return questions; 271 } 272
273 function renderSurveyQuestion(question, index) { 274 var answerType = Number(question.answerType); 275 var questionId = escapeHtml(question.id); 276 var answerName = "survey-answer-" + questionId; 277 var number = index + 1 < 10 ? "0" + (index + 1) : String(index + 1); 278 var required = Number(question.isMustAnswer) === 1; 279 var title = '<legend class="gs-version-question__title"><span class="gs-version-question__number">' + number + '</span>' + 280 (required ? '<span class="gs-version-question__required">*</span>' : "") + 281 '<span>' + escapeHtml(question.title) + "</span>" + 282 (answerType === 2 ? '<span class="gs-version-question__optional">[å¤éé¢]</span>' : "") + 283 "</legend>"; 284 var options = question.answerModelArray || []; 285 var optionHtml = ""; 286 var optionIndex; 287 var option; 288 var optionId; 289 var isOther; 290 var inputType; 291 292 if (answerType === 3) { 293 return '<fieldset class="gs-version-question" data-survey-question-id="' + questionId + '" data-survey-answer-type="3" data-survey-required="' + (required ? "1" : "0") + '">' + 294 title + 295 '<textarea class="gs-version-textarea" data-survey-text-answer="' + questionId + '" placeholder="请è¾å ¥å 容"></textarea></fieldset>'; 296 } 297 298 inputType = answerType === 2 ? "checkbox" : "radio"; 299 for (optionIndex = 0; optionIndex < options.length; optionIndex += 1) { 300 option = options[optionIndex]; 301 optionId = escapeHtml(option.id); 302 isOther = option.answerType === "user_input_answer"; 303 optionHtml += '<label><input type="' + inputType + '" name="' + answerName + '" value="' + optionId + '" data-survey-option-answer="1" />' + 304 (answerType === 2 ? '<i class="gs-version-checkbox-icon" aria-hidden="true"></i>' : "") + 305 '<span>' + escapeHtml(option.answerText) + "</span></label>"; 306 if (isOther) { 307 optionHtml += '<input class="gs-version-other-input" type="text" data-survey-other-for="' + optionId + '" placeholder="请è¾å ¥å 容" hidden />'; 308 } 309 } 310 311 return '<fieldset class="gs-version-question" data-survey-question-id="' + questionId + '" data-survey-answer-type="' + answerType + '" data-survey-required="' + (required ? "1" : "0") + '">' + 312 title + 313 '<div class="' + (answerType === 2 ? "gs-version-options" : "gs-version-scale") + '">' + optionHtml + "</div></fieldset>"; 314 } 315 316 function setSurveyNotice(survey, message, isError) { 317 var notice = survey.querySelector("[data-version-survey-notice]"); 318 if (!notice) { 319 return; 320 } 321 notice.textContent = message || ""; 322 notice.hidden = !message; 323 notice.className = "gs-version-survey__notice" + (isError ? " gs-version-survey__notice--error" : ""); 324 } 325 326 function setSurveySubmitState(survey, disabled, text) { 327 var button = survey.querySelector("[data-version-survey-submit]"); 328 if (!button) { 329 return; 330 } 331 button.disabled = disabled; 332 if (text) { 333 button.textContent = text; 334 } 335 } 336
337 function renderSurveyQuestions(survey, questions) { 338 var content = survey.querySelector("[data-version-survey-content]"); 339 var html = '<p class="gs-version-survey__notice" data-version-survey-notice hidden></p>'; 340 var index; 341 342 if (!questions.length) { 343 content.innerHTML = '<p class="gs-version-survey__loading">å½åé®å·ææ å¯å±ç¤ºé¢ç®ï¼è¯·ç¨ååè¯ã</p>'; 344 setSurveySubmitState(survey, true, "æäº¤é®å·"); 345 return; 346 } 347 for (index = 0; index < questions.length; index += 1) { 348 html += renderSurveyQuestion(questions[index], index); 349 } 350 content.innerHTML = html; 351 } 352 353 function syncSurveyOtherInputs(form) { 354 var otherInputs = form.querySelectorAll("[data-survey-other-for]"); 355 var index; 356 var input; 357 var selector; 358 var selected; 359 360 for (index = 0; index < otherInputs.length; index += 1) { 361 input = otherInputs[index]; 362 selector = '[data-survey-option-answer][value="' + input.getAttribute("data-survey-other-for") + '"]:checked'; 363 selected = form.querySelector(selector); 364 input.hidden = !selected; 365 if (!selected) { 366 input.value = ""; 367 } 368 } 369 } 370 371 function collectSurveyAnswers(survey, form) { 372 var state = survey._surveyState; 373 var answers = []; 374 var index; 375 var question; 376 var answerType; 377 var selectedInputs; 378 var selectedAnswerIds; 379 var selectedIndex; 380 var otherInput; 381 var otherAnswer; 382 var textInput; 383 384 for (index = 0; index < state.questions.length; index += 1) { 385 question = state.questions[index]; 386 answerType = Number(question.answerType); 387 if (answerType === 3) { 388 textInput = form.querySelector('[data-survey-text-answer="' + question.id + '"]'); 389 otherAnswer = trimText(textInput ? textInput.value : ""); 390 if (Number(question.isMustAnswer) === 1 && !otherAnswer) { 391 return { error: "è¯·å®æâ" + question.title + "â" }; 392 } 393 if (otherAnswer) { 394 answers.push({ questionId: question.id, otherAnswer: otherAnswer, answerSheetId: state.answerSheetId }); 395 } 396 continue; 397 } 398 399 selectedInputs = form.querySelectorAll('input[name="survey-answer-' + question.id + '"]:checked'); 400 if (Number(question.isMustAnswer) === 1 && !selectedInputs.length) { 401 return { error: "è¯·å®æâ" + question.title + "â" }; 402 } 403 if (!selectedInputs.length) { 404 continue; 405 } 406 selectedAnswerIds = []; 407 otherAnswer = ""; 408 for (selectedIndex = 0; selectedIndex < selectedInputs.length; selectedIndex += 1) { 409 selectedAnswerIds.push(parseSurveyId(selectedInputs[selectedIndex].value)); 410 otherInput = form.querySelector('[data-survey-other-for="' + selectedInputs[selectedIndex].value + '"]'); 411 if (otherInput) { 412 otherAnswer = trimText(otherInput.value); 413 if (!otherAnswer) { 414 return { error: "请补å âå ¶ä»âå 容" }; 415 } 416 } 417 } 418 answers.push({ questionId: question.id, selectedAnswerId: selectedAnswerIds, otherAnswer: otherAnswer, answerSheetId: state.answerSheetId }); 419 } 420 return { answers: answers }; 421 } 422 423 function loadReturnSurvey(survey) { 424 var state = survey._surveyState; 425 var content = survey.querySelector("[data-version-survey-content]"); 426 427 content.innerHTML = '<p class="gs-version-survey__loading">é®å·å è½½ä¸...</p>'; 428 setSurveySubmitState(survey, true, "æäº¤é®å·"); 429 requestSurvey("/questionnaire/getQuestionnaireDetail", { 430 questionnaireId: SURVEY.id, 431 answerSheetId: 0 432 }, function (detailError, detail) { 433 var questionnaire; 434 if (detailError || !detail || detail.errorDescription || !detail.questionnaireModel) { 435 content.innerHTML = '<p class="gs-version-survey__loading">é®å·å 载失败ï¼è¯·å ³éåéè¯ã</p>'; 436 return; 437 } 438 questionnaire = detail.questionnaireModel; 439 state.questions = getSurveyVisibleQuestions(questionnaire.questionArray || []); 440 renderSurveyQuestions(survey, state.questions); 441 if (!state.questions.length) { 442 return; 443 } 444 requestSurvey("/answersheet/createAnswerSheet", { 445 questionnaireId: SURVEY.id, 446 userId: getSurveyUserId() 447 }, function (createError, createResult) { 448 var answerSheet = createResult && createResult.answerSheetModel; 449 if (createError || !answerSheet || !answerSheet.id) { 450 setSurveyNotice(survey, "é®å·åå§å失败ï¼è¯·å ³éåéè¯ã", true); 451 return; 452 } 453 state.answerSheetId = answerSheet.id; 454 state.ready = true; 455 setSurveySubmitState(survey, false, "æäº¤é®å·"); 456 }); 457 }); 458 } 459 460 function submitReturnSurvey(survey, form) { 461 var state = survey._surveyState; 462 var collected; 463 var userId; 464 465 if (!state.ready) { 466 setSurveyNotice(survey, "é®å·æ£å¨åå§åï¼è¯·ç¨ååè¯ã", true); 467 return; 468 } 469 syncSurveyOtherInputs(form); 470 collected = collectSurveyAnswers(survey, form); 471 if (collected.error) { 472 setSurveyNotice(survey, collected.error, true); 473 return; 474 } 475 setSurveyNotice(survey, "", false); 476 setSurveySubmitState(survey, true, "æäº¤ä¸..."); 477 userId = getSurveyUserId(); 478 requestSurvey("/answersheet/saveAnswerSheet", { 479 model: { 480 questionnaireId: SURVEY.id, 481 status: 1, 482 answers: collected.answers, 483 answerSheetId: state.answerSheetId, 484 userId: "", 485 channel: "", 486 user: { 487 gamerskyUserId: userId, 488 deviceInfo: "", 489 surveyEnvironment: "PCWap" 490 } 491 } 492 }, function (saveError, saveResult) { 493 if (saveError || !saveResult || saveResult.msg !== "ä¿åæå") { 494 setSurveyNotice(survey, "æäº¤å¤±è´¥ï¼è¯·ç¨åéè¯ã", true); 495 setSurveySubmitState(survey, false, "æäº¤é®å·"); 496 return; 497 } 498 closeOverlay(survey); 499 }); 500 } 501 502 function createSurvey() { 503 var body = closeButton() + 504 '<div class="gs-version-survey__art"><img class="gs-version-survey__header" src="https://image.gamersky.com/webimg13/test/nativeIndex/survey-header.png" alt="æ°çé¦é¡µä½éªè°ç æç»" /></div>' + 505 '<form class="gs-version-survey__body" data-version-survey><div class="gs-version-survey__title"><h2>请æ¨å婿¹è¿æ°çé¦é¡µ</h2><p>æ¨çæååå»ºè®®æ¯æä»¬æç»æ¹è¿çç®æ åæ¹åï¼</p></div><div class="gs-version-survey__content" data-version-survey-content></div><button class="gs-version-primary-button gs-version-survey__submit" type="submit" data-version-survey-submit disabled>æäº¤é®å·</button></form>'; 506 var survey = createOverlay("gs-version-survey", "æ°çé¦é¡µä½éªè°ç ", body); 507 survey._surveyState = { 508 answerSheetId: 0, 509 questions: [], 510 ready: false 511 }; 512 return survey; 513 } 514 515 function openReturnSurveyOnOldHome() { 516 var survey = createSurvey(); 517 var close = survey.querySelector("[data-version-close]"); 518 var form = survey.querySelector("[data-version-survey]"); 519 520 close.addEventListener("click", function () { 521 closeOverlay(survey); 522 }); 523 form.addEventListener("change", function () { 524 syncSurveyOtherInputs(form); 525 }); 526 form.addEventListener("submit", function (event) { 527 event.preventDefault(); 528 submitReturnSurvey(survey, form); 529 }); 530 openOverlay(survey); 531 reportStatistic(STATISTICS.newReturnSurveyShown); 532 loadReturnSurvey(survey); 533 } 534 535 function initNewHome() { 536 /* è¿å ¥æ°çå³è§ä¸ºéæ©æ°çï¼ä¸ä¾èµå¼¹çªãå³ä¸è§å ¥å£æç´æ¥è®¿é®çæ¥æºã */
537 setCookie(COOKIE.preference, "new"); 538 539 var navigationGrid = document.querySelector(".top-nav__secondary"); 540 var returnButton = navigationGrid && navigationGrid.querySelector(".gs-version-return"); 541 var returnTip = null; 542 var updateReturnTipPosition = null; 543 544 if (!returnButton) { 545 returnButton = document.createElement("button"); 546 returnButton.className = "gs-version-return"; 547 returnButton.type = "button"; 548 returnButton.innerHTML = '<img class="gs-version-return__icon" src="https://image.gamersky.com/webimg13/test/nativeIndex/return-old-home.svg" alt="" /><span class="gs-version-return__label"><span class="gs-version-return__label-text">è¿åæ§ç</span><span class="gs-version-return__label-line" aria-hidden="true"></span></span>'; 549 returnButton.setAttribute("aria-label", "è¿åæ§çé¦é¡µ"); 550 (navigationGrid || document.body).appendChild(returnButton); 551 } 552 553 function clearReturnTip() { 554 if (!returnTip) { 555 return; 556 } 557 returnTip.remove(); 558 returnTip = null; 559 if (updateReturnTipPosition) { 560 window.removeEventListener("resize", updateReturnTipPosition); 561 window.removeEventListener("scroll", updateReturnTipPosition, true); 562 updateReturnTipPosition = null; 563 } 564 } 565 566 returnButton.addEventListener("click", function () { 567 if (returnTip) { 568 clearReturnTip(); 569 } 570 if (getCookie(COOKIE.surveySeen) === "shown" || getCookie(COOKIE.surveySeen) === "1") { 571 chooseVersion("old"); 572 return; 573 } 574 setCookie(COOKIE.surveySeen, "pending"); 575 chooseVersion("old"); 576 }); 577 578 if (!getCookie(COOKIE.newReturnTipSeen)) { 579 var tip = document.createElement("span"); 580 tip.className = "gs-version-return__tip"; 581 tip.textContent = "ç¹å»æ¤å¤è¿åæ§ç"; 582 returnTip = tip; 583 document.body.appendChild(tip); 584 updateReturnTipPosition = function () { 585 var rect = returnButton.getBoundingClientRect(); 586 tip.style.left = (rect.left + rect.width / 2) + "px"; 587 tip.style.top = (rect.bottom + 14) + "px"; 588 }; 589 updateReturnTipPosition(); 590 window.addEventListener("resize", updateReturnTipPosition); 591 window.addEventListener("scroll", updateReturnTipPosition, true); 592 setCookie(COOKIE.newReturnTipSeen, "1"); 593 window.setTimeout(function () { 594 if (returnTip === tip) { 595 clearReturnTip(); 596 } 597 }, 5000); 598 } 599 } 600 601 document.addEventListener("keydown", function (event) { 602 if (event.key !== "Escape") { 603 return; 604 } 605 var overlay = document.querySelector(".gs-version-overlay:not([hidden])"); 606 if (overlay) { 607 closeOverlay(overlay); 608 } 609 }); 610 611 document.addEventListener("DOMContentLoaded", function () { 612 if (isNewHome) { 613 initNewHome(); 614 } else { 615 initOldHome(); 616 } 617 }); 618})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.