在AO3作品列表标题旁显示📑图标,点击📑可直接打开文章恢复到上次阅读的位置,支持导出/导入阅读进度
// ==UserScript==
// @name AO3 CtnRead
// @description 在AO3作品列表标题旁显示📑图标,点击📑可直接打开文章恢复到上次阅读的位置,支持导出/导入阅读进度
// @namespace https://greasyfork.org/users/1600410
// @author Forever A
// @version 2.9.3
// @license MIT
// @icon data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text x='10' y='80' font-size='70'>📑</text></svg>
// @icon64 data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 100 100'><text x='10' y='80' font-size='70'>📑</text></svg>
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js
// @include http://*archiveofourown.org/*
// @include https://*archiveofourown.org/*
// ==/UserScript==
(function($) {
'use strict';
if (typeof Storage === 'undefined') {
return;
}
var SETTINGS_KEY = 'kh_continue_reading_settings';
var defaultSettings = { showFloatingIcon: true };
var settings = getSettings();
var menuCommandId = null;
function getSettings() {
try {
var saved = GM_getValue(SETTINGS_KEY);
if (saved) {
return JSON.parse(saved);
}
} catch (e) {}
return JSON.parse(JSON.stringify(defaultSettings));
}
function saveSettings(s) {
GM_setValue(SETTINGS_KEY, JSON.stringify(s));
}
function getAllProgressData() {
var data = {};
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.startsWith('kh_pos_')) {
data[key] = localStorage.getItem(key);
}
}
return data;
}
function importProgressData(data) {
var count = 0;
for (var key in data) {
if (data.hasOwnProperty(key) && key.startsWith('kh_pos_')) {
try {
JSON.parse(data[key]);
localStorage.setItem(key, data[key]);
count++;
} catch (e) {}
}
}
return count;
}
function clearOldData(days) {
var now = Date.now();
var cutoff = now - days * 24 * 60 * 60 * 1000;
var keys = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.startsWith('kh_pos_')) {
try {
var data = JSON.parse(localStorage.getItem(key));
if (data && data.updated && data.updated < cutoff) {
keys.push(key);
}
} catch (e) {}
}
}
keys.forEach(function(k) {
localStorage.removeItem(k);
});
return keys.length;
}
function countOldData(days) {
var now = Date.now();
var cutoff = now - days * 24 * 60 * 60 * 1000;
var count = 0;
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.startsWith('kh_pos_')) {
try {
var data = JSON.parse(localStorage.getItem(key));
if (data && data.updated && data.updated < cutoff) {
count++;
}
} catch (e) {}
}
}
return count;
}
function createFloatingIcon() {
if ($('#kh-floating-icon').length || !settings.showFloatingIcon) {
return;
}
var iconW = 40;
var snapOffset = Math.round(iconW * 0.6);
var edgeMargin = 12;
var isSnapped = false;
var isHovering = false;
var isDragging = false;
var dragThreshold = 8;
var snapData = null;
var hoverTimer = null;
var icon = $('<div id="kh-floating-icon" style="position:fixed;z-index:2147483647;cursor:pointer;font-size:28px;opacity:0.5;transition:opacity 0.3s, left 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), top 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);user-select:none;width:' + iconW + 'px;height:' + iconW + 'px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.85);border-radius:50%;box-shadow:0 2px 10px rgba(0,0,0,0.15);-webkit-touch-callout:none;-webkit-user-select:none;touch-action:none;pointer-events:auto;will-change:transform;">📑</div>');
var savedPos = localStorage.getItem('kh_icon_position');
var currentX;
var currentY;
if (savedPos) {
try {
var pos = JSON.parse(savedPos);
currentX = pos.x;
currentY = pos.y;
icon.css({
left: currentX + 'px',
top: currentY + 'px',
transform: 'translateX(0px) translateY(0px)'
});
} catch (e) {
currentX = window.innerWidth - iconW - edgeMargin;
currentY = window.innerHeight - iconW - edgeMargin;
icon.css({
left: currentX + 'px',
top: currentY + 'px',
transform: 'translateX(0px) translateY(0px)'
});
}
} else {
currentX = window.innerWidth - iconW - edgeMargin;
currentY = window.innerHeight - iconW - edgeMargin;
icon.css({
left: currentX + 'px',
top: currentY + 'px',
transform: 'translateX(0px) translateY(0px)'
});
}
function getIconPos() {
var left = parseInt(icon.css('left'), 10);
var top = parseInt(icon.css('top'), 10);
if (isNaN(left)) {
left = currentX || 0;
}
if (isNaN(top)) {
top = currentY || 0;
}
return { x: left, y: top };
}
function savePosition(x, y) {
localStorage.setItem('kh_icon_position', JSON.stringify({ x: x, y: y }));
currentX = x;
currentY = y;
}
function updateOpacity() {
if (isDragging) {
return;
}
var rect = icon[0].getBoundingClientRect();
var isNearEdge = rect.left < 30 || rect.right > window.innerWidth - 30 ||
rect.top < 30 || rect.bottom > window.innerHeight - 30;
icon.css('opacity', isNearEdge ? '0.5' : '1');
}
function setSnapState(snapped, animate) {
var left = parseInt(icon.css('left'), 10);
var top = parseInt(icon.css('top'), 10);
if (isNaN(left)) {
left = currentX || 0;
}
if (isNaN(top)) {
top = currentY || 0;
}
var duration = animate ? '0.25s' : '0s';
var easing = animate ? 'cubic-bezier(0.34, 1.56, 0.64, 1)' : 'ease';
var atRight = left + iconW > window.innerWidth - 30;
var atLeft = left < 30;
var atTop = top < 30;
var atBottom = top + iconW > window.innerHeight - 30;
if (isHovering || isDragging) {
var targetLeft = left;
var targetTop = top;
if (isSnapped && snapData) {
if (snapData.direction === 'right') {
targetLeft = window.innerWidth - iconW - edgeMargin;
} else if (snapData.direction === 'left') {
targetLeft = edgeMargin;
} else if (snapData.direction === 'top') {
targetTop = edgeMargin;
} else if (snapData.direction === 'bottom') {
targetTop = window.innerHeight - iconW - edgeMargin;
}
}
icon.css({
left: targetLeft + 'px',
top: targetTop + 'px',
transform: 'translateX(0px) translateY(0px)',
transition: 'opacity 0.3s, left ' + duration + ' ' + easing + ', top ' + duration + ' ' + easing + ', transform ' + duration + ' ' + easing
});
isSnapped = false;
snapData = null;
updateOpacity();
return;
}
var tx = 0;
var ty = 0;
var snapX = left;
var snapY = top;
var shouldSnap = false;
var direction = null;
if (atRight) {
tx = (window.innerWidth - iconW + snapOffset) - left;
snapX = left;
shouldSnap = true;
direction = 'right';
} else if (atLeft) {
tx = (-iconW + snapOffset) - left;
snapX = left;
shouldSnap = true;
direction = 'left';
} else if (atTop) {
ty = (-iconW + snapOffset) - top;
snapY = top;
shouldSnap = true;
direction = 'top';
} else if (atBottom) {
ty = (window.innerHeight - iconW + snapOffset) - top;
snapY = top;
shouldSnap = true;
direction = 'bottom';
}
if (shouldSnap && snapped) {
icon.css({
left: snapX + 'px',
top: snapY + 'px',
transform: 'translateX(' + tx + 'px) translateY(' + ty + 'px)',
transition: 'opacity 0.3s, left ' + duration + ' ' + easing + ', top ' + duration + ' ' + easing + ', transform ' + duration + ' ' + easing
});
isSnapped = true;
snapData = { direction: direction, tx: tx, ty: ty };
} else {
icon.css({
transform: 'translateX(0px) translateY(0px)',
transition: 'opacity 0.3s, left ' + duration + ' ' + easing + ', top ' + duration + ' ' + easing + ', transform ' + duration + ' ' + easing
});
isSnapped = false;
snapData = null;
}
updateOpacity();
}
function checkAndSnap(animate) {
if (isDragging) {
return;
}
setSnapState(true, animate);
}
function expandIcon() {
var pos = getIconPos();
var targetLeft = pos.x;
var targetTop = pos.y;
if (isSnapped && snapData) {
if (snapData.direction === 'right') {
targetLeft = window.innerWidth - iconW - edgeMargin;
} else if (snapData.direction === 'left') {
targetLeft = edgeMargin;
} else if (snapData.direction === 'top') {
targetTop = edgeMargin;
} else if (snapData.direction === 'bottom') {
targetTop = window.innerHeight - iconW - edgeMargin;
}
}
icon.css({
left: targetLeft + 'px',
top: targetTop + 'px',
transform: 'translateX(0px) translateY(0px)',
transition: 'opacity 0.3s, left 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), top 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)',
opacity: '1'
});
isSnapped = false;
snapData = null;
}
function hidePanel() {
var panel = $('#kh-panel');
if (!panel.length) {
return;
}
panel.css({
opacity: '0',
transform: 'scale(0.9) translateY(8px)',
transition: 'opacity 0.15s ease-out, transform 0.15s ease-out'
});
setTimeout(function() {
panel.hide();
panel.css({
opacity: '1',
transform: 'scale(1) translateY(0px)',
transition: 'none'
});
}, 180);
}
function showPanelWithAnimation() {
var panel = $('#kh-panel');
if (panel.is(':visible')) {
hidePanel();
return;
}
expandIcon();
setTimeout(function() {
var iconRect = icon[0].getBoundingClientRect();
var panelWidth = 140;
var panelHeight = 185;
var margin = 12;
var x = iconRect.right + 8;
var y = iconRect.top + (iconRect.height - panelHeight) / 2;
if (x + panelWidth > window.innerWidth - margin) {
x = iconRect.left - panelWidth - 8;
}
if (x < margin) {
x = iconRect.left + (iconRect.width - panelWidth) / 2;
y = iconRect.bottom + 8;
if (y + panelHeight > window.innerHeight - margin) {
y = iconRect.top - panelHeight - 8;
}
}
x = Math.max(margin, Math.min(window.innerWidth - panelWidth - margin, x));
y = Math.max(margin, Math.min(window.innerHeight - panelHeight - margin, y));
panel.css({
left: x + 'px',
top: y + 'px',
opacity: '0',
transform: 'scale(0.9) translateY(8px)',
transition: 'none'
});
panel.show();
panel[0].offsetHeight;
panel.css({
opacity: '1',
transform: 'scale(1) translateY(0px)',
transition: 'opacity 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)'
});
}, 100);
}
icon.on('mouseenter', function() {
clearTimeout(hoverTimer);
isHovering = true;
expandIcon();
});
icon.on('mouseleave', function() {
clearTimeout(hoverTimer);
hoverTimer = setTimeout(function() {
isHovering = false;
if (!isDragging) {
checkAndSnap(true);
}
}, 150);
});
icon.on('mousedown', function(e) {
if (e.button !== 0) {
return;
}
var pos = getIconPos();
var dragData = {
active: true,
startX: e.clientX,
startY: e.clientY,
offsetX: e.clientX - pos.x,
offsetY: e.clientY - pos.y,
moved: false,
click: true
};
clearTimeout(hoverTimer);
icon.css({
transform: 'translateX(0px) translateY(0px)',
transition: 'none',
opacity: '1',
cursor: 'grabbing'
});
isSnapped = false;
snapData = null;
isDragging = true;
function onMouseMove(ev) {
var dx = ev.clientX - dragData.startX;
var dy = ev.clientY - dragData.startY;
if (!dragData.moved) {
if (Math.abs(dx) > dragThreshold || Math.abs(dy) > dragThreshold) {
dragData.moved = true;
dragData.click = false;
}
}
if (dragData.moved) {
var x = ev.clientX - dragData.offsetX;
var y = ev.clientY - dragData.offsetY;
x = Math.max(-iconW * 0.5, Math.min(window.innerWidth - iconW * 0.5, x));
y = Math.max(-iconW * 0.5, Math.min(window.innerHeight - iconW * 0.5, y));
icon.css({
left: x + 'px',
top: y + 'px',
transform: 'translateX(0px) translateY(0px)',
transition: 'none'
});
}
}
function onMouseUp() {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
if (dragData.moved) {
var pos2 = getIconPos();
savePosition(pos2.x, pos2.y);
setTimeout(function() {
isDragging = false;
if (!isHovering) {
checkAndSnap(true);
}
icon.css('cursor', 'pointer');
icon.css('transition', 'opacity 0.3s, left 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), top 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)');
}, 100);
} else if (dragData.click) {
showPanelWithAnimation();
isDragging = false;
icon.css('cursor', 'pointer');
} else {
isDragging = false;
icon.css('cursor', 'pointer');
}
}
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
e.preventDefault();
});
icon[0].addEventListener('touchstart', function(e) {
var touch = e.touches[0];
if (!touch) {
return;
}
var pos = getIconPos();
var dragData = {
active: true,
startX: touch.clientX,
startY: touch.clientY,
offsetX: touch.clientX - pos.x,
offsetY: touch.clientY - pos.y,
moved: false,
click: true
};
clearTimeout(hoverTimer);
icon.css({
transform: 'translateX(0px) translateY(0px)',
transition: 'none',
opacity: '1',
cursor: 'grabbing'
});
isSnapped = false;
snapData = null;
isDragging = true;
function onTouchMove(ev) {
var t = ev.touches[0];
if (!t) {
return;
}
var dx = t.clientX - dragData.startX;
var dy = t.clientY - dragData.startY;
if (!dragData.moved) {
if (Math.abs(dx) > dragThreshold || Math.abs(dy) > dragThreshold) {
dragData.moved = true;
dragData.click = false;
}
}
if (dragData.moved) {
var x = t.clientX - dragData.offsetX;
var y = t.clientY - dragData.offsetY;
x = Math.max(-iconW * 0.5, Math.min(window.innerWidth - iconW * 0.5, x));
y = Math.max(-iconW * 0.5, Math.min(window.innerHeight - iconW * 0.5, y));
icon.css({
left: x + 'px',
top: y + 'px',
transform: 'translateX(0px) translateY(0px)',
transition: 'none'
});
}
ev.preventDefault();
}
function onTouchEnd() {
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
document.removeEventListener('touchcancel', onTouchEnd);
if (dragData.moved) {
var pos2 = getIconPos();
savePosition(pos2.x, pos2.y);
setTimeout(function() {
isDragging = false;
if (!isHovering) {
checkAndSnap(true);
}
icon.css('cursor', 'pointer');
icon.css('transition', 'opacity 0.3s, left 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), top 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)');
}, 100);
} else if (dragData.click) {
showPanelWithAnimation();
isDragging = false;
icon.css('cursor', 'pointer');
} else {
isDragging = false;
icon.css('cursor', 'pointer');
}
}
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd, { passive: true });
document.addEventListener('touchcancel', onTouchEnd, { passive: true });
}, { passive: true });
icon.on('click', function(e) {
if (dragData && dragData.moved) {
e.stopPropagation();
e.preventDefault();
}
});
$(window).on('resize', function() {
if (icon.length && icon.is(':visible') && !isDragging) {
setTimeout(function() {
checkAndSnap(false);
}, 100);
}
});
setTimeout(function() {
checkAndSnap(false);
}, 300);
$('body').append(icon);
}
function toggleFloatingIcon(show) {
settings.showFloatingIcon = show;
saveSettings(settings);
if (show) {
if (!$('#kh-floating-icon').length) {
createFloatingIcon();
} else {
$('#kh-floating-icon').show();
}
} else {
$('#kh-floating-icon').hide();
$('#kh-panel').hide();
}
updateMenuCommand();
}
function updateMenuCommand() {
if (menuCommandId !== null) {
try {
GM_unregisterMenuCommand(menuCommandId);
} catch (e) {}
}
var label = settings.showFloatingIcon ? '🙈 隐藏悬浮按钮' : '👀 显示悬浮按钮';
menuCommandId = GM_registerMenuCommand(label, function() {
toggleFloatingIcon(!settings.showFloatingIcon);
});
}
function createPanel() {
if ($('#kh-panel').length) {
return;
}
var panel = $('<div id="kh-panel" style="display:none;position:fixed;z-index:2147483647;background:#fff;border:1px solid #ddd;border-radius:8px;padding:10px 12px;min-width:100px;max-width:160px;box-shadow:0 4px 20px rgba(0,0,0,0.15);font-size:12px;color:#333;transform-origin:center center;backface-visibility:hidden;will-change:transform,opacity;"></div>');
panel.on('click', function(e) {
e.stopPropagation();
});
var btnArea = $('<div style="display:flex;flex-direction:column;gap:4px;align-items:center;"></div>');
var exportBtn = $('<button style="width:80px;background:#28a745;color:#fff;border:0;border-radius:4px;padding:5px 10px;cursor:pointer;font-size:12px;text-align:center;touch-action:manipulation;">📤 导出</button>')
.click(function(e) {
e.stopPropagation();
var data = getAllProgressData();
var json = JSON.stringify(data, null, 2);
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'ao3_reading_progress_' + new Date().toISOString().slice(0, 10) + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
hidePanel();
});
var importBtn = $('<button style="width:80px;background:#17a2b8;color:#fff;border:0;border-radius:4px;padding:5px 10px;cursor:pointer;font-size:12px;text-align:center;touch-action:manipulation;">📥 导入</button>')
.click(function(e) {
e.stopPropagation();
var input = $('<input type="file" accept=".json" style="display:none;">');
input.on('change', function() {
var file = this[0].files[0];
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = function(ev) {
try {
var data = JSON.parse(ev.target.result);
var count = importProgressData(data);
alert('成功导入 ' + count + ' 个作品的阅读进度!\n页面即将刷新。');
location.reload();
} catch (err) {
alert('导入失败:文件格式错误\n请确保导入的是有效的 JSON 文件。');
}
};
reader.readAsText(file);
input.remove();
});
input.appendTo('body');
input.click();
hidePanel();
});
var clearLabel = $('<div style="font-size:10px;color:#999;margin-top:3px;text-align:center;">清除超过 N 天未更新的记录</div>');
var clearBtnGroup = $('<div style="display:flex;gap:4px;margin-bottom:2px;"></div>');
function createClearBtn(days, label, color, bgColor) {
return $('<button style="background:' + bgColor + ';color:' + color + ';border:0;border-radius:3px;padding:3px 8px;cursor:pointer;font-size:10px;text-align:center;touch-action:manipulation;">' + label + '</button>')
.click(function(e) {
e.stopPropagation();
var count = countOldData(days);
if (count === 0) {
alert('没有超过 ' + days + ' 天未更新的记录需要清除。');
return;
}
if (!confirm('⚠️ 确定要清除 ' + count + ' 条超过 ' + days + ' 天未更新的记录吗?\n此操作不可撤销!')) {
return;
}
var cleared = clearOldData(days);
alert('已清除 ' + cleared + ' 条超过 ' + days + ' 天未更新的记录。');
hidePanel();
});
}
clearBtnGroup.append(
createClearBtn(30, '30天', '#333', '#ffc107'),
createClearBtn(60, '60天', '#fff', '#ff9800'),
createClearBtn(90, '90天', '#fff', '#f44336')
);
var clearAllBtn = $('<button style="background:#dc3545;color:#fff;border:0;border-radius:4px;padding:5px 10px;cursor:pointer;font-size:12px;text-align:center;touch-action:manipulation;width:80px;">🗑️ 清空全部</button>')
.click(function(e) {
e.stopPropagation();
var keys = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.startsWith('kh_pos_')) {
keys.push(key);
}
}
if (keys.length === 0) {
alert('没有记录需要清除。');
return;
}
if (!confirm('⚠️ 确定要清空全部 ' + keys.length + ' 条阅读进度记录吗?\n此操作不可撤销!')) {
return;
}
keys.forEach(function(k) {
localStorage.removeItem(k);
});
alert('已清空 ' + keys.length + ' 条记录。');
hidePanel();
setTimeout(function() {
location.reload();
}, 300);
});
btnArea.append(exportBtn, importBtn, clearLabel, clearBtnGroup, clearAllBtn);
panel.append(btnArea);
$('body').append(panel);
}
function hidePanel() {
var panel = $('#kh-panel');
if (!panel.length || panel.css('display') === 'none') {
return;
}
panel.css({
opacity: '0',
transform: 'scale(0.9) translateY(8px)',
transition: 'opacity 0.15s ease-out, transform 0.15s ease-out'
});
setTimeout(function() {
panel.hide();
panel.css({
opacity: '1',
transform: 'scale(1) translateY(0px)',
transition: 'none'
});
}, 180);
}
$(document).on('click', function(e) {
var panel = $('#kh-panel');
if (panel.length && panel.is(':visible')) {
var target = e.target;
if (!panel.is(target) && panel.has(target).length === 0) {
hidePanel();
}
}
});
function getLatestChapter(workId) {
var lastChapter = localStorage.getItem('kh_last_chapter_' + workId);
if (lastChapter) {
return parseInt(lastChapter, 10);
}
var posData = localStorage.getItem('kh_pos_' + workId);
if (posData) {
try {
var pos = JSON.parse(posData);
if (pos.chapterNum) {
return pos.chapterNum;
}
} catch(e) {}
}
return null;
}
function addContinueReadingIcons() {
var blurbs = $('li.work.blurb, li.bookmark.blurb').not('.deleted');
blurbs.each(function() {
var blurb = $(this);
var workId = getWorkId(blurb);
if (!workId) {
return;
}
var heading = blurb.find('h4.heading');
if (!heading.length || heading.find('.kh-continue-icon').length) {
return;
}
var posData = localStorage.getItem('kh_pos_' + workId);
if (!posData) {
return;
}
try {
JSON.parse(posData);
} catch (e) {
return;
}
var targetUrl = '/works/' + workId;
var chapterNum = getLatestChapter(workId);
if (chapterNum) {
targetUrl += '/chapters/' + chapterNum;
}
var icon = $('<a href="' + targetUrl + '" title="继续阅读" class="kh-continue-icon" style="cursor:pointer;text-decoration:none;margin-left:8px;font-size:1.1em;display:inline-block;">📑</a>')
.click(function(e) {
e.preventDefault();
e.stopPropagation();
location.href = this.href;
});
heading.append(icon);
});
}
function getWorkId(blurb) {
var blurbId = blurb.attr('id');
if (blurb.hasClass('work')) {
return blurbId.replace('work_', '');
}
if (blurb.hasClass('bookmark')) {
var link = blurb.find('h4 a:first').attr('href');
if (link && link.indexOf('series') === -1 && link.indexOf('external_work') === -1) {
return link.split('/').pop();
}
}
return null;
}
function initScrollMemory(workId) {
setTimeout(function() {
var data = localStorage.getItem('kh_pos_' + workId);
if (data) {
try {
var pos = JSON.parse(data);
var targetChapter = null;
var y = pos.scrollY || 0;
if (pos.chapter) {
var el = document.getElementById(pos.chapter);
if (el) {
targetChapter = $(el);
}
}
if (!targetChapter && pos.chapterNum) {
var chapters = $("div.chapter[id^='chapter-']");
var idx = pos.chapterNum - 1;
if (idx >= 0 && idx < chapters.length) {
targetChapter = chapters[idx];
}
}
if (targetChapter && typeof pos.offset === 'number') {
y = targetChapter[0].offsetTop + targetChapter[0].offsetHeight * pos.offset;
}
window.scrollTo({ top: y, behavior: 'smooth' });
} catch (e) {}
}
}, 600);
var timer;
$(window).on('scroll', function() {
clearTimeout(timer);
timer = setTimeout(function() {
var chapters = $("div.chapter[id^='chapter-']");
var chapId = 'full_work';
var chapNum = null;
if (chapters.length > 1) {
var center = window.innerHeight / 2;
var idx = 0;
chapters.each(function(i) {
if (this.getBoundingClientRect().top <= center) {
chapId = this.id;
idx = i;
}
});
chapNum = idx + 1;
} else {
var urlMatch = location.pathname.match(/\/chapters\/(\d+)/);
if (urlMatch) {
chapId = 'chapter-' + urlMatch[1];
chapNum = parseInt(urlMatch[1], 10);
}
}
var el = document.getElementById(chapId);
var offset = 0;
if (el) {
offset = Math.max(0, Math.min(1, (window.scrollY - el.offsetTop) / (el.offsetHeight || 1)));
}
var data = JSON.stringify({
chapter: chapId,
chapterNum: chapNum,
offset: offset,
scrollY: window.scrollY,
updated: Date.now()
});
localStorage.setItem('kh_pos_' + workId, data);
if (chapNum !== null) {
localStorage.setItem('kh_last_chapter_' + workId, String(chapNum));
}
}, 1500);
});
}
function getWorkIdFromPage() {
var match = location.pathname.match(/\/works\/(\d+)/);
return match ? match[1] : null;
}
updateMenuCommand();
if (settings.showFloatingIcon) {
createFloatingIcon();
createPanel();
}
if ($('li.work.blurb, li.bookmark.blurb').length) {
var observer = new MutationObserver(function() {
addContinueReadingIcons();
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(addContinueReadingIcons, 500);
}
if ($('#workskin').length) {
var workId = getWorkIdFromPage();
if (workId) {
initScrollMemory(workId);
}
}
})(jQuery);