forked from dodying/UserJs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path小说阅读脚本辅助朗读.user.js
More file actions
241 lines (225 loc) · 8.94 KB
/
小说阅读脚本辅助朗读.user.js
File metadata and controls
241 lines (225 loc) · 8.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/* eslint-env browser */
// ==UserScript==
// @name 小说阅读脚本辅助朗读
// @description 小说阅读脚本辅助朗读
// @include *
// @version 1.0.317
// @created 2020-12-11 13:05:42
// @modified 2021-10-03 09:30:52
// @author dodying
// @namespace https://github.com/dodying/UserJs
// @supportURL https://github.com/dodying/UserJs/issues
// @icon https://gitee.com/dodying/userJs/raw/master/Logo.png
// @run-at document-end
// @grant GM_setValue
// @grant GM_getValue
// @noframes
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.js
// ==/UserScript==
/* global GM_setValue GM_getValue */
/* global $ */
/* eslint-disable no-debugger */
(function () {
const config = GM_getValue('config', {
lucky: false,
voice: window.speechSynthesis.getVoices().find((i) => i.default) ? window.speechSynthesis.getVoices().find((i) => i.default).name : null,
volume: 1,
rate: 1,
pitch: 1,
});
const addSpeakButton = () => {
$('<div style="position:fixed;top:10px;right:72px;z-index:1597;font-size:16px;cursor:pointer;">').html('\u23f8').insertBefore('#preferencesBtn').on({ // 暂停按钮
click: (e) => {
if (!window.speechSynthesis.speaking) return;
if (window.speechSynthesis.paused) {
window.speechSynthesis.resume();
$(e.target).html('\u23f8');
} else {
window.speechSynthesis.pause();
$(e.target).html('\u23f5');
}
},
});
$('<div style="position:fixed;top:10px;right:36px;z-index:1597;font-size:16px;cursor:pointer;">').html('\ud83c\udfa4').insertBefore('#preferencesBtn').on({
click: () => (config.lucky ? speakMyNovelReader() : toogleSpeakPanel()),
contextmenu: (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
toogleSpeakPanel();
},
});
};
if ($('body[name="MyNovelReader"]').length) {
addSpeakButton();
} else {
const observer = new window.MutationObserver((mutationsList) => {
if ($('body[name="MyNovelReader"]').length) {
observer.disconnect();
addSpeakButton();
}
});
observer.observe(document.body, { attributes: true });
}
function toogleSpeakPanel() {
const container = $('<div id="speakPanel">').html([
'<style>',
'#speakPanel{position:fixed;top:0;left:0;z-index:1597;width:100vw;height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center;}',
'#speakPanel>div{background:#fff;}',
'#speakPanel div.config>div>:nth-child(1){display:inline-block;width:150px;}',
'#speakPanel div.config>div>input[type="range"]::after{content:attr(value)}',
'</style>',
'<div>',
' <div class="config">',
' <div><input type="checkbox" name="lucky" id="speakLucky"><label for="speakLucky">点击按钮,直接开始朗读</label></div>',
' <div><label for="speakVoice">Voice</label><select name="voice" id="speakVoice"></select></div>',
' <div><label for="speakVolume">音量</label><input type="range" min="0" max="1" step="0.05" name="volume" id="speakVolume"></div>',
' <div><label for="speakRate">语速</label><input type="range" min="0.1" max="10" step="0.1" name="rate" id="speakRate"></div>',
' <div><label for="speakPitch">声调</label><input type="range" min="0" max="2" step="0.1" name="pitch" id="speakPitch"></div>',
' </div>',
' <hr>',
' <div>',
' <div><button name="selectText">选择文本</button></div>', // TODO
' <div><button name="startSpeak">开始朗读</button></div>', // TODO
' </div>',
'</div>',
].join('')).insertAfter('body').on({
click: (e) => {
if ($(e.target).is(container)) {
container.remove();
}
},
});
container.find('.config').find('input,select,textarea').on({ // 应用并保存设置 // TODO 朗读时更改设置
change: (e) => {
const elem = e.target;
const key = elem.name;
if (elem.type === 'checkbox') {
config[key] = elem.checked;
} else if (elem.type === 'select-one') {
config[key] = elem.value;
} else if (elem.type === 'range') {
config[key] = elem.value;
elem.setAttribute('value', config[key]);
}
GM_setValue('config', config);
},
});
loadVoices();
window.speechSynthesis.onvoiceschanged = function (e) {
loadVoices();
};
for (const key in config) { // 读取设置
const elem = $(`.config [name=${key}]`, container).get(0);
if (elem.type === 'checkbox') {
elem.checked = config[key];
} else if (elem.type === 'select-one') {
elem.value = [...elem.options].map((i) => i.value).includes(config[key]) ? config[key] : elem.options[0].value;
} else if (elem.type === 'range') {
elem.value = config[key];
elem.setAttribute('value', config[key]);
}
}
function loadVoices() {
$('.config #speakVoice', container).empty();
window.speechSynthesis.getVoices().forEach((voice) => {
$('<option>').text(voice.name).val(voice.name).attr('selected', voice.default)
.appendTo('.config #speakVoice', container);
});
}
}
function speakMyNovelReader() {
console.log('lucky');
// let stack = 0;
let cancel; let cancelCompleted; let
interval;
let currentElem;
const readThis = function (elem) {
if (!$(elem).is('#mynovelreader-content>article>:not(.chapter-footer-nav)')) return;
if (interval) {
clearInterval(interval);
interval = null;
}
cancel = true;
window.speechSynthesis.cancel();
interval = setInterval(() => {
if (cancelCompleted && !window.speechSynthesis.paused && !window.speechSynthesis.pending && !window.speechSynthesis.speaking) {
clearInterval(interval);
interval = null;
cancelCompleted = false;
cancel = false;
speakTheseElems($(elem).nextAll().addBack());
}
}, 20);
};
$(window).on({
click: (e) => {
readThis(e.target);
},
keydown: (e) => {
if (!e.key.match(/^([zZxX0.]|Insert|Delete)$/i)) return;
const $all = $('#mynovelreader-content>article>:not(.chapter-footer-nav)');
let index = $all.index(currentElem);
if (['z', '0'].includes(e.key)) {
index = index - 1;
} else if (['x', '.'].includes(e.key)) {
index = index + 1;
} else if (['Z', 'Insert'].includes(e.key)) {
index = index - 5;
} else if (['X', 'Delete'].includes(e.key)) {
index = index + 5;
}
if (index < 0) index = 0;
if (index >= $all.length) index = $all.length - 1;
readThis($('#mynovelreader-content>article>:not(.chapter-footer-nav)').get(index));
},
});
speakTheseElems($($('#chapter-list>.active>div').attr('href')).children().toArray());
async function speakTheseElems(elemsToSpeak) {
// const stackNow = stack++;
for (const elem of elemsToSpeak) {
if ($(elem).is('.chapter-footer-nav')) break;
// console.log(stackNow);
currentElem = elem;
elem.style.background = 'white';
elem.scrollIntoViewIfNeeded ? elem.scrollIntoViewIfNeeded() : elem.scrollIntoView();
await new Promise((resolve, reject) => {
const text = elem.textContent.trim();
const utterThis = new window.SpeechSynthesisUtterance(text);
utterThis.voice = window.speechSynthesis.getVoices().find((i) => i.name === config.voice);
utterThis.volume = config.volume;
utterThis.rate = config.rate;
utterThis.pitch = config.pitch;
// Object.assign(utterThis, config);
// utterThis.addEventListener('boundary', (e) => { // 当前朗读字词
// console.log(text.substr(e.charIndex, e.charLength));
// });
utterThis.addEventListener('end', () => {
resolve();
});
// for (const i of ['error', 'mark', 'pause', 'resume', 'start']) {
// utterThis.addEventListener(i, (...args) => {
// console.log(i, args);
// });
// }
window.speechSynthesis.speak(utterThis);
});
elem.style.background = '';
if (cancel) break;
}
if (!cancel && $(elemsToSpeak).filter('.chapter-footer-nav').last().find('.next-page:not([style="color:#666666"])').length) {
const $article = $(elemsToSpeak).filter('.chapter-footer-nav').last().find('.next-page:not([style="color:#666666"])')
.parents('#mynovelreader-content>article');
let interval;
interval = setInterval(() => {
if ($article.next().length) {
clearInterval(interval);
speakTheseElems($article.next().children().toArray());
interval = null;
}
}, 200);
}
cancelCompleted = true;
}
}
}());