TextPage.vue 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245
  1. <template>
  2. <div ref="main" class="main">
  3. <div class="layout back" @wheel.prevent.stop="onMouseWheel">
  4. <div class="absolute" v-html="background"></div>
  5. <div class="absolute" v-html="pageDivider"></div>
  6. </div>
  7. <div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
  8. <div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
  9. <div v-html="page1"></div>
  10. </div>
  11. </div>
  12. <div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
  13. <div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
  14. <div v-html="page2"></div>
  15. </div>
  16. </div>
  17. <div v-show="showStatusBar" ref="statusBar" class="layout">
  18. <div v-html="statusBar"></div>
  19. </div>
  20. <div v-show="clickControl" ref="layoutEvents" class="layout events"
  21. oncontextmenu="return false;"
  22. @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
  23. @wheel.prevent.stop="onMouseWheel"
  24. @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
  25. >
  26. <div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
  27. @click.prevent.stop="onStatusBarClick"
  28. v-html="statusBarClickable"
  29. ></div>
  30. </div>
  31. <div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
  32. @mousedown.prevent.stop @touchstart.stop
  33. @click.prevent.stop="onStatusBarClick"
  34. v-html="statusBarClickable"
  35. >
  36. </div>
  37. <!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
  38. <canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
  39. <div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
  40. </div>
  41. </template>
  42. <script>
  43. //-----------------------------------------------------------------------------
  44. import vueComponent from '../../vueComponent.js';
  45. import {loadCSS} from 'fg-loadcss';
  46. import _ from 'lodash';
  47. import './TextPage.css';
  48. import * as utils from '../../../share/utils';
  49. import bookManager from '../share/bookManager';
  50. import DrawHelper from './DrawHelper';
  51. import rstore from '../../../store/modules/reader';
  52. import {clickMap} from '../share/clickMap';
  53. const minLayoutWidth = 100;
  54. const componentOptions = {
  55. watch: {
  56. bookPos: function() {
  57. this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
  58. this.draw();
  59. },
  60. bookPosSeen: function() {
  61. this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
  62. },
  63. settings: function() {
  64. this.debouncedLoadSettings();
  65. },
  66. toggleLayout: function() {
  67. this.updateLayout();
  68. },
  69. inAnimation: function() {
  70. this.updateLayout();
  71. },
  72. },
  73. };
  74. class TextPage {
  75. _options = componentOptions;
  76. toggleLayout = false;
  77. showStatusBar = false;
  78. clickControl = true;
  79. background = null;
  80. pageDivider = null;
  81. page1 = null;
  82. page2 = null;
  83. statusBar = null;
  84. statusBarClickable = null;
  85. lastBook = null;
  86. bookPos = 0;
  87. bookPosSeen = null;
  88. fontStyle = null;
  89. fontSize = null;
  90. fontName = null;
  91. fontWeight = null;
  92. inAnimation = false;
  93. meta = null;
  94. created() {
  95. this.drawHelper = new DrawHelper();
  96. this.commit = this.$store.commit;
  97. this.dispatch = this.$store.dispatch;
  98. this.config = this.$store.state.config;
  99. this.reader = this.$store.state.reader;
  100. this.debouncedStartClickRepeat = _.debounce((x, y) => {
  101. this.startClickRepeat(x, y);
  102. }, 800);
  103. this.debouncedPrepareNextPage = _.debounce(() => {
  104. this.prepareNextPage();
  105. }, 100);
  106. this.debouncedDrawStatusBar = _.throttle(() => {
  107. this.drawStatusBar();
  108. }, 60);
  109. this.debouncedDrawPageDividerAndOrnament = _.throttle(() => {
  110. this.drawPageDividerAndOrnament();
  111. }, 65);
  112. this.debouncedLoadSettings = _.debounce(() => {
  113. this.loadSettings();
  114. }, 50);
  115. this.debouncedUpdatePage = _.debounce(async(lines) => {
  116. if (!this.pageChangeAnimation)
  117. this.toggleLayout = !this.toggleLayout;
  118. else {
  119. this.page2 = this.page1;
  120. this.toggleLayout = true;
  121. }
  122. if (this.toggleLayout)
  123. this.page1 = this.drawHelper.drawPage(lines);
  124. else
  125. this.page2 = this.drawHelper.drawPage(lines);
  126. await this.doPageAnimation();
  127. }, 10);
  128. this.$root.addEventHook('resize', async() => {
  129. this.$nextTick(this.onResize);
  130. await utils.sleep(500);
  131. this.$nextTick(this.onResize);
  132. });
  133. }
  134. mounted() {
  135. this.context = this.$refs.offscreenCanvas.getContext('2d');
  136. }
  137. hex2rgba(hex, alpha = 1) {
  138. const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
  139. return `rgba(${r},${g},${b},${alpha})`;
  140. }
  141. calcDrawProps() {
  142. const wideLetter = 'Щ';
  143. //preloaded fonts
  144. this.fontList = [`12px '${this.fontName}'`];
  145. //widths
  146. this.realWidth = this.$refs.main.clientWidth;
  147. this.realHeight = this.$refs.main.clientHeight;
  148. this.$refs.layoutEvents.style.width = this.realWidth + 'px';
  149. this.$refs.layoutEvents.style.height = this.realHeight + 'px';
  150. const dual = (this.dualPageMode ? 2 : 1);
  151. this.boxW = this.realWidth - 2*this.indentLR;
  152. this.w = this.boxW/dual - (this.dualPageMode ? 2*this.dualIndentLR : 0);
  153. this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
  154. this.h = this.scrollHeight - 2*this.indentTB;
  155. this.lineHeight = this.fontSize + this.lineInterval;
  156. this.pageRowsCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
  157. this.pageLineCount = (this.dualPageMode ? this.pageRowsCount*2 : this.pageRowsCount)
  158. //stuff
  159. this.currentAnimation = '';
  160. this.pageChangeDirectionDown = true;
  161. this.fontShift = this.fontVertShift/100;
  162. this.textShift = this.textVertShift/100 + this.fontShift;
  163. //statusBar
  164. this.$refs.statusBar.style.left = '0px';
  165. this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
  166. const sbColor = (this.statusBarColorAsText ? this.textColor : this.statusBarColor);
  167. this.statusBarRgbaColor = this.hex2rgba(sbColor || '#000000', this.statusBarColorAlpha);
  168. const ddColor = (this.dualDivColorAsText ? this.textColor : this.dualDivColor);
  169. this.dualDivRgbaColor = this.hex2rgba(ddColor || '#000000', this.dualDivColorAlpha);
  170. //drawHelper
  171. this.drawHelper.realWidth = this.realWidth;
  172. this.drawHelper.realHeight = this.realHeight;
  173. this.drawHelper.lastBook = this.lastBook;
  174. this.drawHelper.book = this.book;
  175. this.drawHelper.parsed = this.parsed;
  176. this.drawHelper.pageRowsCount = this.pageRowsCount;
  177. this.drawHelper.pageLineCount = this.pageLineCount;
  178. this.drawHelper.dualPageMode = this.dualPageMode;
  179. this.drawHelper.dualIndentLR = this.dualIndentLR;
  180. /*this.drawHelper.dualDivWidth = this.dualDivWidth;
  181. this.drawHelper.dualDivHeight = this.dualDivHeight;
  182. this.drawHelper.dualDivRgbaColor = this.dualDivRgbaColor;
  183. this.drawHelper.dualDivStrokeFill = this.dualDivStrokeFill;
  184. this.drawHelper.dualDivStrokeGap = this.dualDivStrokeGap;
  185. this.drawHelper.dualDivShadowWidth = this.dualDivShadowWidth;*/
  186. this.drawHelper.backgroundColor = this.backgroundColor;
  187. this.drawHelper.statusBarRgbaColor = this.statusBarRgbaColor;
  188. this.drawHelper.fontStyle = this.fontStyle;
  189. this.drawHelper.fontWeight = this.fontWeight;
  190. this.drawHelper.fontSize = this.fontSize;
  191. this.drawHelper.fontName = this.fontName;
  192. this.drawHelper.fontShift = this.fontShift;
  193. this.drawHelper.textColor = this.textColor;
  194. this.drawHelper.textShift = this.textShift;
  195. this.drawHelper.p = this.p;
  196. this.drawHelper.boxW = this.boxW;
  197. this.drawHelper.w = this.w;
  198. this.drawHelper.h = this.h;
  199. this.drawHelper.indentLR = this.indentLR;
  200. this.drawHelper.textAlignJustify = this.textAlignJustify;
  201. this.drawHelper.lineHeight = this.lineHeight;
  202. this.drawHelper.context = this.context;
  203. //альтернатива context.measureText
  204. if (!this.context.measureText(wideLetter).width) {
  205. const ctx = this.$refs.measureWidth;
  206. this.drawHelper.measureText = function(text, style) {
  207. ctx.innerText = text;
  208. ctx.style.font = this.fontByStyle(style);
  209. return ctx.clientWidth;
  210. };
  211. this.drawHelper.measureTextFont = function(text, font) {
  212. ctx.innerText = text;
  213. ctx.style.font = font;
  214. return ctx.clientWidth;
  215. }
  216. }
  217. //statusBar
  218. this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
  219. //parsed
  220. if (this.parsed) {
  221. let wideLine = wideLetter;
  222. if (!this.drawHelper.measureText(wideLine, {}))
  223. throw new Error('Ошибка measureText');
  224. while (this.drawHelper.measureText(wideLine, {}) < this.w) wideLine += wideLetter;
  225. this.parsed.setSettings({
  226. p: this.p,
  227. w: this.w,
  228. font: this.font,
  229. fontSize: this.fontSize,
  230. wordWrap: this.wordWrap,
  231. cutEmptyParagraphs: this.cutEmptyParagraphs,
  232. addEmptyParagraphs: this.addEmptyParagraphs,
  233. maxWordLength: wideLine.length - 1,
  234. lineHeight: this.lineHeight,
  235. showImages: this.showImages,
  236. showInlineImagesInCenter: this.showInlineImagesInCenter,
  237. imageHeightLines: this.imageHeightLines,
  238. imageFitWidth: this.imageFitWidth,
  239. compactTextPerc: this.compactTextPerc,
  240. testWidth: 0,
  241. measureText: this.drawHelper.measureText.bind(this.drawHelper),
  242. });
  243. }
  244. //scrolling page
  245. const pageSpace = this.scrollHeight - this.pageRowsCount*this.lineHeight;
  246. let top = pageSpace/2;
  247. if (this.showStatusBar)
  248. top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
  249. let page1 = this.$refs.scrollBox1.style;
  250. let page2 = this.$refs.scrollBox2.style;
  251. page1.perspective = page2.perspective = '3072px';
  252. page1.width = page2.width = this.boxW + this.indentLR + 'px';
  253. page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
  254. page1.top = page2.top = top + 'px';
  255. page1.left = page2.left = this.indentLR + 'px';
  256. page1 = this.$refs.scrollingPage1.style;
  257. page2 = this.$refs.scrollingPage2.style;
  258. page1.width = page2.width = this.boxW + this.indentLR + 'px';
  259. page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
  260. }
  261. async checkLoadedFonts() {
  262. let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
  263. if (loaded.some(r => !r)) {
  264. await Promise.all(this.fontList.map(font => document.fonts.load(font)));
  265. }
  266. }
  267. async loadFonts() {
  268. this.fontsLoading = true;
  269. let close = null;
  270. (async() => {
  271. await utils.sleep(500);
  272. if (this.fontsLoading)
  273. close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
  274. })();
  275. if (!this.fontsLoaded)
  276. this.fontsLoaded = {};
  277. //загрузка дин.шрифта
  278. const loaded = this.fontsLoaded[this.fontCssUrl];
  279. if (this.fontCssUrl && !loaded) {
  280. loadCSS(this.fontCssUrl);
  281. this.fontsLoaded[this.fontCssUrl] = 1;
  282. }
  283. try {
  284. await this.checkLoadedFonts();
  285. } catch (e) {
  286. this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
  287. }
  288. this.fontsLoading = false;
  289. if (close)
  290. close();
  291. }
  292. getSettings() {
  293. const settings = this.settings;
  294. for (let prop in rstore.settingDefaults) {
  295. this[prop] = settings[prop];
  296. }
  297. const wf = this.webFontName;
  298. const i = _.findIndex(rstore.webFonts, ['name', wf]);
  299. if (wf && i >= 0) {
  300. this.fontName = wf;
  301. this.fontCssUrl = rstore.webFonts[i].css;
  302. this.fontVertShift = settings.fontShifts[wf] || 0;
  303. }
  304. }
  305. async calcPropsAndLoadFonts(omitLoadFonts) {
  306. this.calcDrawProps();
  307. this.setBackground();
  308. if (!omitLoadFonts)
  309. await this.loadFonts();
  310. if (omitLoadFonts) {
  311. this.draw();
  312. } else {
  313. // ширина шрифта некоторое время выдается неверно,
  314. // не удалось событийно отловить этот момент, поэтому костыль
  315. while (this.checkingFont) {
  316. this.stopCheckingFont = true;
  317. await utils.sleep(100);
  318. }
  319. this.checkingFont = true;
  320. this.stopCheckingFont = false;
  321. try {
  322. const parsed = this.parsed;
  323. let i = 0;
  324. const t = 'Это тестовый текст. Его ширина выдается системой неправильно некоторое время.';
  325. let twprev = 0;
  326. //5 секунд проверяем изменения шрифта
  327. while (!this.stopCheckingFont && i++ < 50 && this.parsed === parsed) {
  328. const tw = this.drawHelper.measureText(t, {});
  329. if (tw !== twprev) {
  330. this.parsed.setSettings({testWidth: tw});
  331. this.draw();
  332. twprev = tw;
  333. }
  334. await utils.sleep(100);
  335. }
  336. } finally {
  337. this.checkingFont = false;
  338. }
  339. }
  340. }
  341. loadSettings() {
  342. (async() => {
  343. let fontName = this.fontName;
  344. this.getSettings();
  345. await this.calcPropsAndLoadFonts(fontName == this.fontName);
  346. })();
  347. }
  348. showBook() {
  349. this.$refs.main.focus();
  350. this.toggleLayout = false;
  351. this.updateLayout();
  352. this.book = null;
  353. this.meta = null;
  354. this.parsed = null;
  355. this.linesUp = null;
  356. this.linesDown = null;
  357. this.statusBarMessage = '';
  358. this.getSettings();
  359. this.calcDrawProps();
  360. this.draw();// пока не загрузили, очистим канвас
  361. if (this.lastBook) {
  362. (async() => {
  363. try {
  364. //подождем ленивый парсинг
  365. this.stopLazyParse = true;
  366. while (this.doingLazyParse) await utils.sleep(10);
  367. const isParsed = await bookManager.hasBookParsed(this.lastBook);
  368. if (!isParsed) {
  369. return;
  370. }
  371. this.book = await bookManager.getBook(this.lastBook);
  372. this.meta = bookManager.metaOnly(this.book);
  373. const bt = utils.getBookTitle(this.meta.fb2);
  374. this.title = bt.fullTitle;
  375. this.$root.setAppTitle(this.title);
  376. this.parsed = this.book.parsed;
  377. this.page1 = null;
  378. this.page2 = null;
  379. this.statusBar = null;
  380. await this.stopTextScrolling();
  381. await this.calcPropsAndLoadFonts();
  382. this.refreshTime();
  383. if (this.lazyParseEnabled)
  384. this.lazyParsePara();
  385. } catch (e) {
  386. this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
  387. }
  388. })();
  389. }
  390. }
  391. updateLayout() {
  392. if (this.inAnimation) {
  393. this.$refs.scrollBox1.style.visibility = 'visible';
  394. this.$refs.scrollBox2.style.visibility = 'visible';
  395. } else if (this.toggleLayout) {
  396. this.$refs.scrollBox1.style.visibility = 'visible';
  397. this.$refs.scrollBox2.style.visibility = 'hidden';
  398. } else {
  399. this.$refs.scrollBox1.style.visibility = 'hidden';
  400. this.$refs.scrollBox2.style.visibility = 'visible';
  401. }
  402. }
  403. setBackground() {
  404. if (this.wallpaperIgnoreStatusBar) {
  405. this.background = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
  406. ` background-color: ${this.backgroundColor}">` +
  407. `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
  408. `top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
  409. `</div>` +
  410. `</div>`;
  411. } else {
  412. this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
  413. ` background-color: ${this.backgroundColor}"></div>`;
  414. }
  415. }
  416. async onResize() {
  417. try {
  418. this.calcDrawProps();
  419. this.setBackground();
  420. this.draw();
  421. } catch (e) {
  422. //
  423. }
  424. }
  425. get settings() {
  426. return this.$store.state.reader.settings;
  427. }
  428. get font() {
  429. return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
  430. }
  431. onPage1TransitionEnd() {
  432. if (this.resolveTransition1Finish)
  433. this.resolveTransition1Finish();
  434. }
  435. onPage2TransitionEnd() {
  436. if (this.resolveTransition2Finish)
  437. this.resolveTransition2Finish();
  438. }
  439. startSearch(needle) {
  440. this.drawHelper.needle = needle;
  441. this.drawHelper.searching = true;
  442. this.draw();
  443. }
  444. stopSearch() {
  445. this.drawHelper.searching = false;
  446. this.draw();
  447. }
  448. generateWaitingFunc(waitingHandlerName, stopPropertyName) {
  449. const func = (timeout) => {
  450. return new Promise((resolve, reject) => { (async() => {
  451. this[waitingHandlerName] = resolve;
  452. let wait = (timeout + 201)/100;
  453. while (wait > 0 && !this[stopPropertyName]) {
  454. wait--;
  455. await utils.sleep(100);
  456. }
  457. resolve();
  458. })().catch(reject); });
  459. };
  460. return func;
  461. }
  462. async startTextScrolling() {
  463. if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
  464. this.linesDown.length <= this.pageLineCount || this.dualPageMode) {
  465. this.doStopScrolling();
  466. return;
  467. }
  468. //ждем анимацию
  469. while (this.inAnimation) await utils.sleep(10);
  470. this.stopScrolling = false;
  471. this.doingScrolling = true;
  472. const transitionFinish = this.generateWaitingFunc('resolveTransition1Finish', 'stopScrolling');
  473. if (!this.toggleLayout)
  474. this.page1 = this.page2;
  475. this.toggleLayout = true;
  476. await this.$nextTick();
  477. await utils.sleep(50);
  478. this.cachedPos = -1;
  479. this.draw();
  480. const page = this.$refs.scrollingPage1;
  481. let i = 0;
  482. while (!this.stopScrolling) {
  483. page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
  484. page.style.transform = `translateY(-${this.lineHeight}px)`;
  485. if (i > 0) {
  486. this.doDown();
  487. if (this.linesDown.length <= this.pageLineCount + 1) {
  488. this.stopScrolling = true;
  489. }
  490. }
  491. await transitionFinish(this.scrollingDelay);
  492. page.style.transition = '';
  493. page.style.transform = 'none';
  494. page.offsetHeight;
  495. i++;
  496. }
  497. this.resolveTransition1Finish = null;
  498. this.doingScrolling = false;
  499. this.doStopScrolling();
  500. this.draw();
  501. }
  502. async stopTextScrolling() {
  503. this.stopScrolling = true;
  504. const page = this.$refs.scrollingPage1;
  505. page.style.transition = '';
  506. page.style.transform = 'none';
  507. page.offsetHeight;
  508. while (this.doingScrolling) await utils.sleep(10);
  509. }
  510. draw() {
  511. //scrolling
  512. if (this.doingScrolling) {
  513. this.currentAnimation = '';
  514. if (this.cachedPos == this.bookPos) {
  515. this.linesDown = this.linesCached.linesDown;
  516. this.linesUp = this.linesCached.linesUp;
  517. this.page1 = this.pageCached;
  518. } else {
  519. const lines = this.getLines(this.bookPos);
  520. this.linesDown = lines.linesDown;
  521. this.linesUp = lines.linesUp;
  522. this.page1 = this.drawHelper.drawPage(lines.linesDown, true);
  523. }
  524. //caching next
  525. if (this.cachedPageTimer)
  526. clearTimeout(this.cachedPageTimer);
  527. this.cachedPageTimer = setTimeout(() => {
  528. if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
  529. this.cachedPos = this.linesDown[1].begin;
  530. this.linesCached = this.getLines(this.cachedPos);
  531. this.pageCached = this.drawHelper.drawPage(this.linesCached.linesDown, true);
  532. }
  533. this.cachedPageTimer = null;
  534. }, 20);
  535. this.debouncedDrawStatusBar();
  536. return;
  537. }
  538. //check
  539. if (this.w < minLayoutWidth) {
  540. this.page1 = null;
  541. this.page2 = null;
  542. this.statusBar = null;
  543. return;
  544. }
  545. if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
  546. this.doEnd(true);
  547. return;
  548. }
  549. //fast draw prepared
  550. if (!this.pageChangeAnimation && this.pageChangeDirectionDown && this.pagePrepared && this.bookPos == this.bookPosPrepared) {
  551. this.toggleLayout = !this.toggleLayout;
  552. this.linesDown = this.linesDownNext;
  553. this.linesUp = this.linesUpNext;
  554. } else {//normal debounced draw
  555. const lines = this.getLines(this.bookPos);
  556. this.linesDown = lines.linesDown;
  557. this.linesUp = lines.linesUp;
  558. this.debouncedUpdatePage(lines.linesDown);
  559. }
  560. this.pagePrepared = false;
  561. if (!this.pageChangeAnimation)
  562. this.debouncedPrepareNextPage();
  563. this.debouncedDrawStatusBar();
  564. this.debouncedDrawPageDividerAndOrnament();
  565. if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
  566. this.doEnd(true);
  567. return;
  568. }
  569. }
  570. onPage1AnimationEnd() {
  571. if (this.resolveAnimation1Finish)
  572. this.resolveAnimation1Finish();
  573. }
  574. onPage2AnimationEnd() {
  575. if (this.resolveAnimation2Finish)
  576. this.resolveAnimation2Finish();
  577. }
  578. async doPageAnimation() {
  579. if (this.currentAnimation && !this.inAnimation) {
  580. this.inAnimation = true;
  581. const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
  582. const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation');
  583. const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation');
  584. const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
  585. const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100));
  586. let page1 = this.$refs.scrollingPage1;
  587. let page2 = this.$refs.scrollingPage2;
  588. switch (this.currentAnimation) {
  589. case 'thaw':
  590. await this.drawHelper.doPageAnimationThaw(page1, page2,
  591. duration, this.pageChangeDirectionDown, animation1Finish);
  592. break;
  593. case 'blink':
  594. await this.drawHelper.doPageAnimationBlink(page1, page2,
  595. duration, this.pageChangeDirectionDown, animation1Finish, animation2Finish);
  596. break;
  597. case 'rightShift':
  598. await this.drawHelper.doPageAnimationRightShift(page1, page2,
  599. duration, this.pageChangeDirectionDown, transition1Finish);
  600. break;
  601. case 'downShift':
  602. page1.style.height = this.scrollHeight + 'px';
  603. page2.style.height = this.scrollHeight + 'px';
  604. await this.drawHelper.doPageAnimationDownShift(page1, page2,
  605. duration, this.pageChangeDirectionDown, transition1Finish);
  606. page1.style.height = this.scrollHeight + this.lineHeight + 'px';
  607. page2.style.height = this.scrollHeight + this.lineHeight + 'px';
  608. break;
  609. case 'rotate':
  610. await this.drawHelper.doPageAnimationRotate(page1, page2,
  611. duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish);
  612. break;
  613. case 'flip':
  614. await this.drawHelper.doPageAnimationFlip(page1, page2,
  615. duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish, this.backgroundColor);
  616. break;
  617. }
  618. this.resolveAnimation1Finish = null;
  619. this.resolveAnimation2Finish = null;
  620. this.resolveTransition1Finish = null;
  621. this.resolveTransition2Finish = null;
  622. page1.style.animation = '';
  623. page2.style.animation = '';
  624. page1.style.transition = '';
  625. page1.style.transform = 'none';
  626. page1.offsetHeight;
  627. page2.style.transition = '';
  628. page2.style.transform = 'none';
  629. page2.offsetHeight;
  630. this.currentAnimation = '';
  631. this.pageChangeDirectionDown = false;//true только если PgDown
  632. this.inAnimation = false;
  633. this.stopAnimation = false;
  634. }
  635. }
  636. getLines(bookPos) {
  637. if (!this.parsed || this.pageLineCount < 1)
  638. return {};
  639. return {
  640. linesDown: this.parsed.getLines(bookPos, 2*this.pageLineCount),
  641. linesUp: this.parsed.getLines(bookPos, -2*this.pageLineCount)
  642. };
  643. }
  644. drawStatusBar(message) {
  645. if (this.w < minLayoutWidth) {
  646. this.statusBar = null;
  647. return;
  648. }
  649. if (this.showStatusBar && this.linesDown && this.pageLineCount > 0) {
  650. const lines = this.linesDown;
  651. let i = this.pageLineCount;
  652. if (this.keepLastToFirst)
  653. i--;
  654. i = (i > lines.length - 1 ? lines.length - 1 : i);
  655. if (i >= 0) {
  656. if (!message)
  657. message = this.statusBarMessage;
  658. if (!message)
  659. message = this.title;
  660. //check image num
  661. let imageNum = 0;
  662. const len = (lines.length > 2 ? 2 : lines.length);
  663. loop:
  664. for (let j = 0; j < len; j++) {
  665. const line = lines[j];
  666. for (const part of line.parts) {
  667. if (part.image) {
  668. imageNum = part.image.num;
  669. break loop;
  670. }
  671. }
  672. }
  673. //drawing
  674. this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
  675. lines[i].end, this.parsed.textLength, message, imageNum, this.parsed.images.length);
  676. this.bookPosSeen = lines[i].end;
  677. }
  678. } else {
  679. this.statusBar = '';
  680. }
  681. }
  682. drawPageDividerAndOrnament() {
  683. if (this.dualPageMode) {
  684. this.pageDivider = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
  685. `top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
  686. `<div class="fit row justify-center items-center no-wrap">` +
  687. `<div style="height: ${Math.round(this.scrollHeight*this.dualDivHeight/100)}px; width: ${this.dualDivWidth}px; ` +
  688. `box-shadow: 0 0 ${this.dualDivShadowWidth}px ${this.dualDivRgbaColor}; ` +
  689. `background-image: url(&quot;data:image/svg+xml;utf8,<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'>` +
  690. `<line x1='${this.dualDivWidth/2}' y1='0' x2='${this.dualDivWidth/2}' y2='100%' stroke='${this.dualDivRgbaColor}' ` +
  691. `stroke-width='${this.dualDivWidth}' stroke-dasharray='${this.dualDivStrokeFill} ${this.dualDivStrokeGap}'/>` +
  692. `</svg>&quot;);">` +
  693. `</div>` +
  694. `</div>` +
  695. `</div>`;
  696. } else {
  697. this.pageDivider = null;
  698. }
  699. }
  700. blinkCachedLoadMessage(state) {
  701. if (state === 'finish') {
  702. this.statusBarMessage = '';
  703. } else if (state) {
  704. this.statusBarMessage = 'Книга загружена из кэша';
  705. } else {
  706. this.statusBarMessage = ' ';
  707. }
  708. this.drawStatusBar();
  709. }
  710. async lazyParsePara() {
  711. if (!this.parsed || this.doingLazyParse)
  712. return;
  713. this.doingLazyParse = true;
  714. let j = 0;
  715. let k = 0;
  716. let prevPerc = 0;
  717. this.stopLazyParse = false;
  718. for (let i = 0; i < this.parsed.para.length; i++) {
  719. j++;
  720. if (j > 1) {
  721. await utils.sleep(1);
  722. j = 0;
  723. }
  724. if (this.stopLazyParse)
  725. break;
  726. this.parsed.parsePara(i);
  727. k++;
  728. if (k > 100) {
  729. let perc = Math.round(i/this.parsed.para.length*100);
  730. if (perc != prevPerc)
  731. this.drawStatusBar(`Обработка текста ${perc}%`);
  732. prevPerc = perc;
  733. k = 0;
  734. }
  735. }
  736. this.drawStatusBar();
  737. this.doingLazyParse = false;
  738. }
  739. async refreshTime() {
  740. if (!this.timeRefreshing) {
  741. this.timeRefreshing = true;
  742. await utils.sleep(60*1000);
  743. if (this.book && this.parsed.textLength) {
  744. this.debouncedDrawStatusBar();
  745. }
  746. this.timeRefreshing = false;
  747. this.refreshTime();
  748. }
  749. }
  750. prepareNextPage() {
  751. // подготовка следующей страницы заранее
  752. if (!this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1)
  753. return;
  754. let i = this.pageLineCount;
  755. if (this.keepLastToFirst)
  756. i--;
  757. if (i >= 0 && this.linesDown.length > i) {
  758. this.bookPosPrepared = this.linesDown[i].begin;
  759. const lines = this.getLines(this.bookPosPrepared);
  760. this.linesDownNext = lines.linesDown;
  761. this.linesUpNext = lines.linesUp;
  762. if (this.toggleLayout)
  763. this.page2 = this.drawHelper.drawPage(lines.linesDown);//наоборот
  764. else
  765. this.page1 = this.drawHelper.drawPage(lines.linesDown);
  766. this.pagePrepared = true;
  767. }
  768. }
  769. doDown() {
  770. if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
  771. this.bookPos = this.linesDown[1].begin;
  772. }
  773. }
  774. doUp() {
  775. if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
  776. this.bookPos = this.linesUp[1].begin;
  777. }
  778. }
  779. doPageDown() {
  780. if (this.linesDown && this.pageLineCount > 0) {
  781. let i = this.pageLineCount;
  782. if (this.keepLastToFirst)
  783. i--;
  784. if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
  785. this.currentAnimation = this.pageChangeAnimation;
  786. this.pageChangeDirectionDown = true;
  787. this.bookPos = this.linesDown[i].begin;
  788. } else
  789. this.doEnd();
  790. }
  791. }
  792. doPageUp() {
  793. if (this.linesUp && this.pageLineCount > 0) {
  794. let i = this.pageLineCount;
  795. if (this.keepLastToFirst)
  796. i--;
  797. i = (i > this.linesUp.length - 1 ? this.linesUp.length - 1 : i);
  798. if (i >= 0 && this.linesUp.length > i) {
  799. this.currentAnimation = this.pageChangeAnimation;
  800. this.pageChangeDirectionDown = false;
  801. this.bookPos = this.linesUp[i].begin;
  802. }
  803. }
  804. }
  805. doHome() {
  806. this.currentAnimation = this.pageChangeAnimation;
  807. this.pageChangeDirectionDown = false;
  808. this.bookPos = 0;
  809. }
  810. doEnd(noAni) {
  811. if (this.parsed.para.length && this.pageLineCount > 0) {
  812. let i = this.parsed.para.length - 1;
  813. let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
  814. const lines = this.parsed.getLines(lastPos, -this.pageLineCount);
  815. if (lines) {
  816. i = this.pageLineCount - 1;
  817. i = (i > lines.length - 1 ? lines.length - 1 : i);
  818. if (!noAni)
  819. this.currentAnimation = this.pageChangeAnimation;
  820. this.pageChangeDirectionDown = true;
  821. this.bookPos = lines[i].begin;
  822. }
  823. }
  824. }
  825. doToolBarToggle(event) {
  826. this.$emit('do-action', {action: 'switchToolbar', event});
  827. }
  828. doScrollingToggle() {
  829. this.$emit('do-action', {action: 'scrolling', event});
  830. }
  831. doFullScreenToggle() {
  832. this.$emit('do-action', {action: 'fullScreen', event});
  833. }
  834. doStopScrolling() {
  835. this.$emit('do-action', {action: 'stopScrolling', event});
  836. }
  837. async doFontSizeInc() {
  838. if (!this.settingsChanging) {
  839. this.settingsChanging = true;
  840. const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
  841. this.commit('reader/setSettings', {fontSize: newSize});
  842. await utils.sleep(50);
  843. this.settingsChanging = false;
  844. }
  845. }
  846. async doFontSizeDec() {
  847. if (!this.settingsChanging) {
  848. this.settingsChanging = true;
  849. const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
  850. this.commit('reader/setSettings', {fontSize: newSize});
  851. await utils.sleep(50);
  852. this.settingsChanging = false;
  853. }
  854. }
  855. async doScrollingSpeedUp() {
  856. if (!this.settingsChanging) {
  857. this.settingsChanging = true;
  858. const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
  859. this.commit('reader/setSettings', {scrollingDelay: newDelay});
  860. await utils.sleep(50);
  861. this.settingsChanging = false;
  862. }
  863. }
  864. async doScrollingSpeedDown() {
  865. if (!this.settingsChanging) {
  866. this.settingsChanging = true;
  867. const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
  868. this.commit('reader/setSettings', {scrollingDelay: newDelay});
  869. await utils.sleep(50);
  870. this.settingsChanging = false;
  871. }
  872. }
  873. async startClickRepeat(pointX, pointY) {
  874. this.repX = pointX;
  875. this.repY = pointY;
  876. if (!this.repInit && this.repDoing) {
  877. this.repInit = true;
  878. let delay = 400;
  879. while (this.repDoing) {
  880. this.handleClick(pointX, pointY);
  881. await utils.sleep(delay);
  882. if (delay > 15)
  883. delay *= 0.8;
  884. }
  885. this.repInit = false;
  886. }
  887. }
  888. endClickRepeat() {
  889. this.repDoing = false;
  890. }
  891. onTouchStart(event) {
  892. if (!this.$root.isMobileDevice)
  893. return;
  894. this.endClickRepeat();
  895. if (event.touches.length == 1) {
  896. const touch = event.touches[0];
  897. const rect = event.target.getBoundingClientRect();
  898. const x = touch.pageX - rect.left;
  899. const y = touch.pageY - rect.top;
  900. const hc = this.handleClick(x, y, new Set(['Menu']));
  901. if (hc) {
  902. if (hc != 'Menu') {
  903. this.repDoing = true;
  904. this.debouncedStartClickRepeat(x, y);
  905. } else {
  906. this.startTouch = {x, y};
  907. }
  908. }
  909. }
  910. }
  911. onTouchMove(event) {
  912. if (this.startTouch) {
  913. event.preventDefault();
  914. }
  915. }
  916. onTouchEnd(event) {
  917. if (!this.$root.isMobileDevice)
  918. return;
  919. this.endClickRepeat();
  920. if (event.changedTouches.length == 1) {
  921. const touch = event.changedTouches[0];
  922. const rect = event.target.getBoundingClientRect();
  923. const x = touch.pageX - rect.left;
  924. const y = touch.pageY - rect.top;
  925. if (this.startTouch) {
  926. const dy = this.startTouch.y - y;
  927. const dx = this.startTouch.x - x;
  928. const moveDelta = 30;
  929. const touchDelta = 15;
  930. if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
  931. //движение вверх
  932. this.doFullScreenToggle();
  933. } else if (dy < 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
  934. //движение вниз
  935. this.doScrollingToggle();
  936. } else if (dx > 0 && Math.abs(dx) >= moveDelta && Math.abs(dy) < Math.abs(dx)) {
  937. //движение влево
  938. this.doScrollingSpeedDown();
  939. } else if (dx < 0 && Math.abs(dx) >= moveDelta && Math.abs(dy) < Math.abs(dx)) {
  940. //движение вправо
  941. this.doScrollingSpeedUp();
  942. } else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
  943. this.doToolBarToggle(event);
  944. }
  945. this.startTouch = null;
  946. }
  947. }
  948. }
  949. onTouchCancel() {
  950. if (!this.$root.isMobileDevice)
  951. return;
  952. this.endClickRepeat();
  953. }
  954. onMouseDown(event) {
  955. if (this.$root.isMobileDevice)
  956. return;
  957. this.endClickRepeat();
  958. if (event.button == 0) {
  959. const hc = this.handleClick(event.offsetX, event.offsetY);
  960. if (hc && hc != 'Menu') {
  961. this.repDoing = true;
  962. this.debouncedStartClickRepeat(event.offsetX, event.offsetY);
  963. }
  964. } else if (event.button == 1) {
  965. this.doScrollingToggle();
  966. } else if (event.button == 2) {
  967. this.doToolBarToggle(event);
  968. }
  969. }
  970. onMouseUp() {
  971. if (this.$root.isMobileDevice)
  972. return;
  973. this.endClickRepeat();
  974. }
  975. onMouseWheel(event) {
  976. if (this.$root.isMobileDevice)
  977. return;
  978. if (event.deltaY > 0) {
  979. this.doDown();
  980. } else if (event.deltaY < 0) {
  981. this.doUp();
  982. }
  983. }
  984. onStatusBarClick() {
  985. const url = this.meta.url;
  986. if (url && url.indexOf('disk://') != 0) {
  987. window.open(url, '_blank');
  988. } else {
  989. this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
  990. }
  991. }
  992. getClickAction(pointX, pointY) {
  993. const w = pointX/this.realWidth*100;
  994. const h = pointY/this.realHeight*100;
  995. let action = '';
  996. loops: {
  997. for (const x in clickMap) {
  998. for (const y in clickMap[x]) {
  999. if (w < x && h < y) {
  1000. action = clickMap[x][y];
  1001. break loops;
  1002. }
  1003. }
  1004. }
  1005. }
  1006. return action;
  1007. }
  1008. handleClick(pointX, pointY, exclude) {
  1009. const action = this.getClickAction(pointX, pointY);
  1010. if (!exclude || !exclude.has(action)) {
  1011. switch (action) {
  1012. case 'Down' ://Down
  1013. this.doDown();
  1014. break;
  1015. case 'Up' ://Up
  1016. this.doUp();
  1017. break;
  1018. case 'PgDown' ://PgDown
  1019. this.doPageDown();
  1020. break;
  1021. case 'PgUp' ://PgUp
  1022. this.doPageUp();
  1023. break;
  1024. case 'Menu' :
  1025. this.doToolBarToggle();
  1026. break;
  1027. default :
  1028. // Nothing
  1029. }
  1030. }
  1031. return action;
  1032. }
  1033. }
  1034. export default vueComponent(TextPage);
  1035. //-----------------------------------------------------------------------------
  1036. </script>
  1037. <style scoped>
  1038. .main {
  1039. flex: 1;
  1040. margin: 0;
  1041. padding: 0;
  1042. overflow: hidden;
  1043. position: relative;
  1044. min-width: 200px;
  1045. }
  1046. .layout {
  1047. margin: 0;
  1048. padding: 0;
  1049. position: absolute;
  1050. z-index: 10;
  1051. }
  1052. .over-hidden {
  1053. overflow: hidden;
  1054. }
  1055. .on-top {
  1056. z-index: 100;
  1057. }
  1058. .back {
  1059. z-index: 5;
  1060. }
  1061. .events {
  1062. z-index: 20;
  1063. background-color: rgba(0,0,0,0);
  1064. }
  1065. </style>